spine-rigc 0.21.0 → 0.22.1

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/cli.ts CHANGED
@@ -2411,8 +2411,14 @@ function cmdExplain(flags: Record<string, string>): void {
2411
2411
  }
2412
2412
 
2413
2413
  console.log('\nslots (array order IS the draw order)');
2414
+ // The DEFAULT skin's placeholders, resolved by name. It was `skins[0]` until
2415
+ // issue #541, which is the same thing only because rigc pins `default` at
2416
+ // index 0 — a property of the emitter that this report should not be quietly
2417
+ // relying on. Reading it by name means a change to the skins ORDER cannot turn
2418
+ // this line into a report about some other skin.
2419
+ const defaultSkin = result.skeleton.skins.find((skin) => skin.name === 'default');
2414
2420
  for (const s of result.skeleton.slots) {
2415
- const atts = Object.keys(result.skeleton.skins[0].attachments[s.name] ?? {});
2421
+ const atts = Object.keys(defaultSkin?.attachments[s.name] ?? {});
2416
2422
  console.log(
2417
2423
  ` ${s.name.padEnd(12)} bone=${s.bone.padEnd(12)} setup=${(s.attachment ?? 'null').padEnd(22)} color=${s.color ?? 'ffffffff'} attachments=[${atts.join(', ')}]`,
2418
2424
  );
package/docs/AUTHORING.md CHANGED
@@ -540,6 +540,15 @@ in the skeleton and the size in the atlas cannot drift apart. The **region name
540
540
  PNG's basename**; when your placeholder name differs from it, rigc writes a `path`
541
541
  so the attachment still joins to the region.
542
542
 
543
+ **Every** attachment that names an `image` is measured, whichever skin it sits in.
544
+ The atlas holds **one region per file**, so two skins filling one placeholder from
545
+ two files put two regions in it and each attachment draws its own (§3.4.2), while
546
+ two attachments naming the same file share the one region that file made. What
547
+ cannot be reconciled is two *different* files whose basenames collide — only one of
548
+ them can be region `patch` — so rigc refuses the build and names both paths rather
549
+ than letting one of them silently draw the other's pixels
550
+ ([#555](https://github.com/firejune/rigc/issues/555)).
551
+
543
552
  **R6 — A key carries `ease` or `curve`, never both.** A named easing says "this
544
553
  shape, wherever it is used" and is the recommended path. `curve` is the escape
545
554
  hatch: the absolute `(time, value)` control points, verbatim, for when every key
@@ -612,6 +621,33 @@ only repair was *rename* — the one repair a transcription cannot take. Emittin
612
621
  member of the family instead moves no byte on any set the old rule accepted; it
613
622
  just stops refusing the ones it did.
614
623
 
624
+ **R11 — The `skins` array is written with `default` first and the rest in the
625
+ editor's order, and skin names that have no one order are refused.** The same rule
626
+ as R10, in the collection that was believed exempt from it. A skin is a **name** in
627
+ the JSON half of the format and an **ordinal** in the binary half —
628
+ `skins[readInt()]` for an attachment timeline, `skins[skinIndex]` for a linked mesh
629
+ — so an editor that writes the array in another order repoints every such
630
+ reference, silently, in a file that still parses. Measured: a rig built
631
+ `default, zulu, mike, alpha` exported `default, alpha, mike, zulu`
632
+ ([#541](https://github.com/firejune/rigc/issues/541)).
633
+
634
+ ⚠️ **The refusal here is wider than R10's, and deliberately.** R10 can be narrow
635
+ because #539 measured two animation-name pairs and thereby *refuted* a codepoint
636
+ sort. The skins measurement refutes nothing — `alpha, mike, zulu` is the answer
637
+ codepoint, folding and natural order all give — so the editor's skin comparator is
638
+ **not established**, and rigc refuses any pair those candidates could disagree
639
+ about. In practice that is R10's four rows plus two more: a pair a case fold
640
+ reverses (`Zulu` against `mike`) and two digit runs of unequal width (`mike10`
641
+ against `mike2`). Both of those *build* as animation names and are refused as skin
642
+ names, and the two refusals say which is which.
643
+
644
+ **R12 — A placeholder that more than one skin fills gets a per-skin attachment
645
+ `name`.** rigc writes `"name": "<skin>/<placeholder>"` on each of those entries,
646
+ and restates `path` beside it so the texture still resolves where it did. You do
647
+ not author this and there is nothing to do about it — but it is visible in the
648
+ emitted file, so §3.4.2 says what it is and why. A placeholder only one skin fills
649
+ is emitted exactly as before.
650
+
615
651
  ---
616
652
 
617
653
  ## 3. The rig spec, field by field
@@ -709,7 +745,7 @@ the default `type`:
709
745
  | `type` | `"region"`, or omit |
710
746
  | `image` | **rigc extension.** A PNG relative to the rig's `images` directory; rigc measures it (R5) |
711
747
  | `width`, `height` | required by the format — give them, or give an `image` |
712
- | `path` | the atlas region to resolve; defaults to the attachment's own name. rigc sets it for you when the PNG basename differs from the placeholder |
748
+ | `path` | the atlas region to resolve; defaults to the attachment's own name. rigc sets it for you when the PNG basename differs from the placeholder, and whenever it composes a `name` because more than one skin fills this placeholder (§3.4.2) |
713
749
  | `x`, `y` | offset from the bone, in the bone's local space |
714
750
  | `rotation` | degrees; cancels a rotated bone for a plate authored screen-upright |
715
751
  | `scaleX`, `scaleY`, `color` | as Spine |
@@ -1372,6 +1408,70 @@ beside them is refused rather than ignored (an ignored slot is an attachment tha
1372
1408
  vanishes), and a rig with a *slot* of one of those names is refused too, because
1373
1409
  there the two forms are genuinely ambiguous. Rename the slot.
1374
1410
 
1411
+ #### 3.4.2 Two skins, one placeholder — the `name` rigc writes for you
1412
+
1413
+ Two skins putting different art under one placeholder is what a skin is *for*, and
1414
+ it is the one shape rigc emitted wrongly until
1415
+ [#541](https://github.com/firejune/rigc/issues/541). Without a `name` field an
1416
+ attachment's name **is** its placeholder (`SkeletonJson.ts:526`), so four skins
1417
+ filling `patch` are four different attachments all called `patch`. spine-core never
1418
+ notices — its skin table is keyed by placeholder, so the two never meet — and the
1419
+ whole gate is green. The Spine editor refuses the import outright:
1420
+
1421
+ ```
1422
+ ERROR: Unable to import skeleton.
1423
+ [error] Error reading skeleton: skins
1424
+ Cause: [error] Error reading attachment: patch (MOw)
1425
+ Cause: [error] Multiple attachments have the same name: patch patch
1426
+ ```
1427
+
1428
+ ⇒ rigc now writes each of those entries a name of its own, composed from the two
1429
+ names you already gave it:
1430
+
1431
+ ```json
1432
+ "skins": {
1433
+ "default": { "patch": { "patch": { "image": "patch_a.png" } } },
1434
+ "zulu": { "patch": { "patch": { "image": "patch_a.png", "x": 4 } } }
1435
+ }
1436
+ ```
1437
+
1438
+ emits
1439
+
1440
+ ```json
1441
+ { "name": "default/patch", "path": "patch", "width": 64, "height": 64 }
1442
+ { "name": "zulu/patch", "path": "patch", "width": 64, "height": 64, "x": 4 }
1443
+ ```
1444
+
1445
+ Three things to know about it and nothing to author:
1446
+
1447
+ - **`path` is restated, and it has to be.** `path` defaults to the attachment's
1448
+ **name**, not to its placeholder, so an entry given a name and no path would
1449
+ resolve its texture at `zulu/patch` and find no such region. `A00_ROUNDTRIP_PARSE`
1450
+ says so in the parser's own words if it is ever dropped.
1451
+ - **Only contested placeholders are touched.** One skin filling a placeholder, or
1452
+ two skins filling a slot under *different* placeholders, emit exactly what they
1453
+ always did — every rig in this repository is byte-identical across the change.
1454
+ - **A composed name that collides is a compile error, not a surprise.** If some
1455
+ other placeholder in the same slot is literally called `zulu/patch`, rigc refuses
1456
+ and names both sites rather than emitting two attachments with one name again.
1457
+ (`/` is the separator because it appears in **0** of the 160 placeholder names and
1458
+ 159 atlas region names in `examples/` and `gallery/`, where `-` appears in 85 and
1459
+ `_` in 37.)
1460
+ - **Each skin's art is measured and atlased on its own.** The example above points
1461
+ both skins at one PNG, so there is one region; point them at two and there are
1462
+ two, each attachment's `path` resolving to the file that attachment named and its
1463
+ `width`/`height` measured off that file. Until
1464
+ [#555](https://github.com/firejune/rigc/issues/555) only the first skin's PNG was
1465
+ ever opened, and the second skin's art reached neither the atlas nor the
1466
+ measurement — so name the two files **distinctly**, because the region name is
1467
+ the basename and `a/patch.png` beside `b/patch.png` is refused (R5).
1468
+
1469
+ ⚠️ **The uniqueness scope is the slot, not the skeleton.** `spineboy-pro.json`,
1470
+ which the editor wrote, gives the name `head` to a region in slot `head` and to a
1471
+ bounding box in slot `head-bb`, and reuses `hoverglow-small` across eight slots. So
1472
+ a name shared between slots is normal and rigc leaves it alone; what #541 refused
1473
+ was one slot holding two.
1474
+
1375
1475
  ### 3.5 `constraints` — 4.3's single typed array
1376
1476
 
1377
1477
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -1423,7 +1523,7 @@ curve instead of in the keys.
1423
1523
  | --- | --- |
1424
1524
  | `bones` | at least one, in the order they ride the path |
1425
1525
  | `slot` | **required.** The slot whose path attachment they follow (§3.4) |
1426
- | `positionMode` | default `"Percent"`: `position` is a fraction of the arc length. `"Fixed"` makes it world units |
1526
+ | `positionMode` | default `"Percent"`: `position` is a fraction of the measured `lengths` total — **not** of the arc, see §10.6. `"Fixed"` makes it world units, which is the mode a wrong total is visible in |
1427
1527
  | `spacingMode` | default `"Length"` — `Length`, `Fixed`, `Percent` or `Proportional` |
1428
1528
  | `rotateMode` | default `"Tangent"`: each bone turns to the curve's tangent where it sits. `"Chain"`, `"ChainScale"` |
1429
1529
  | `rotation` | default 0. Degrees added after the path's own rotation |
@@ -3244,7 +3344,7 @@ tell a working traversal from a plausible one.
3244
3344
 
3245
3345
  | Group | `property` | Channels | Note |
3246
3346
  | --- | --- | --- | --- |
3247
- | `path` | `position` | 1 | a fraction of the arc length, or world units under `positionMode: "fixed"` |
3347
+ | `path` | `position` | 1 | a fraction of the measured `lengths` total (§10.6 — not the arc), or world units under `positionMode: "fixed"` |
3248
3348
  | `path` | `spacing` | 1 | in the unit `spacingMode` chose |
3249
3349
  | `path` | `mix` | **3** | `[mixRotate, mixX, mixY]` in one key, so a raw `curve` is 12 numbers |
3250
3350
  | `slider` | `time` | 1 | the bone-less slider's own time. A slider WITH a bone takes its time from the bone and this timeline is not what drives it |
@@ -3365,6 +3465,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3365
3465
  | `the triangles do not tile the outline: …` / `the triangles' outline is not one closed loop: …` | §3.4 — a doubled triangle, an unused vertex, a pinch or a hole in `triangles` |
3366
3466
  | `image "X.png" is not on disk at …` | fix the name, or point `--images` at the right directory |
3367
3467
  | `duplicate region name "X"` | two PNGs share a basename; one part, one page, one name |
3468
+ | `"b/X.png" and the art already atlased as region "X" are two different files — … Rename one of the PNGs.` | R5 — the region name is the basename, so only one of the two can hold it. Rename a file (not a placeholder: the placeholder is free to repeat) |
3469
+ | `the image "X.png" was never added to the atlas, so there is no region "X" …` | nothing in the spec — every image an attachment names is measured, so this says rigc skipped one. Report it ([#555](https://github.com/firejune/rigc/issues/555)) |
3368
3470
  | `motion spec names archetype "A" but the rig spec at … is called "B"` | make `archetype` equal the rig's `name` |
3369
3471
  | `animation "A" declares duration Ns but its last key is at Ms` | R7 — fix whichever of the two you meant |
3370
3472
  | `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` |
@@ -3426,6 +3528,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3426
3528
  | `animation "A" keys "X" as a path constraint, but the rig declares it as a "slider"` | §4.12 — a timeline group resolves by name AND type; use the field named after the constraint's own type |
3427
3529
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
3428
3530
  | `N pair(s) of animation names have no one order: … "turn" / "Turn" (case) — they are one name in two cases, and which of them the editor puts first is not measured; rename one of them so they differ by more than letter case` | **R10** — rename until no pair is left. The kind in brackets says which of the editor comparator's four UNMEASURED choices decides the pair: `case` (a pure case tie), `number` (one number written two ways, or a run of digits against a word) or `separator` (make the first character that differs a letter or a digit). rigc keys `animations` in the editor's own comparator — natural and case-insensitive ([#539](https://github.com/firejune/rigc/issues/539), [#543](https://github.com/firejune/rigc/issues/543)) — so a pair that comparator settles is emitted rather than refused, and only the four choices nobody has measured are a compile error; on those, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
3531
+ | `N pair(s) of skin names have no one order: … "Zulu" / "mike" (case) — folded to one case "Zulu" and "mike" order the other way round, so whether the editor folds SKIN names decides this pair` | **R11** — rename until no pair is left. The same shape as the row above with a **wider** family: #539 measured the editor's comparator for animation names and thereby ruled codepoint out, and nothing has ruled anything out for skin names, so a pair the candidates could disagree about is refused even where the animation rule would emit it. `Zulu`/`mike` and `mike10`/`mike2` build as animation names and are refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) |
3532
+ | `N attachment name collision(s): a placeholder that more than one skin fills is emitted with the name "<skin>/<placeholder>" … slot "patch": skin "default" 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 the composed name is one another entry in the same slot already answers to. Both sites are named; either rename ends it |
3429
3533
 
3430
3534
  ### 5.2 Assertions — the gate
3431
3535
 
@@ -4911,9 +5015,19 @@ frames. Every line is marked with where it comes from:
4911
5015
 
4912
5016
  - 📗 **stated** — quoted or paraphrased from the page linked in the line.
4913
5017
  - 🧩 **inferred** — this guide's reading of those pages. Spine does not say it.
4914
- - 🔬 **observed** — read off the editor's own export of a rigc build in the round
4915
- trip of [issue #285](https://github.com/firejune/rigc/issues/285) (Spine 4.3.23),
4916
- not from a page. Used only where rigc now emits the same thing.
5018
+ - 🔬 **observed** — read off the editor's own export of a rigc build, not from a
5019
+ page. Two round trips stand behind these: [issue
5020
+ #285](https://github.com/firejune/rigc/issues/285) (Spine 4.3.23) and the
5021
+ eight-rig trip of 2026-09-16 (Spine **4.3.26**), whose findings are collected in
5022
+ §10.6. ⚠️ This legend said *"used only where rigc now emits the same thing"*,
5023
+ which was true while every observation had already been adopted; §10.6 then
5024
+ carried one that had **not** been — the path `lengths` disagreement — so an
5025
+ observation is now marked by where it was read, and each says for itself
5026
+ whether rigc agrees with it. ⭐ That outstanding one has since been adopted
5027
+ ([#560](https://github.com/firejune/rigc/issues/560)) and the legend is kept in
5028
+ this shape anyway: the reason to mark an observation by its source rather than
5029
+ by whether rigc follows it is that the second fact goes stale and the first
5030
+ does not.
4917
5031
 
4918
5032
  ### 10.1 Structure
4919
5033
 
@@ -4972,12 +5086,17 @@ low figure as a miss — say in the log that the art did not carry them.
4972
5086
  `default`"* and *"bones are ordered so that the parent always comes before a child
4973
5087
  bone"* — [JSON format](http://esotericsoftware.com/spine-json-format). §3.4.
4974
5088
 
4975
- 🔬 **The editor re-keys every name-keyed OBJECT and leaves every ARRAY alone.**
4976
- Read off its export of a rigc build (Spine 4.3.26, `gallery/look`): the
4977
- `animations` object, a skin's 24 `attachments` slot keys and two animations' 16
4978
- and 2 bone-timeline keys all came back sorted, while the 30 `bones`, 24 `slots`
4979
- and 3 `constraints` — arrays — came back in the build's own order, element for
4980
- element, and each slider kept its place among them.
5089
+ 🔬 **The editor re-keys every name-keyed OBJECT, and leaves `bones`, `slots` and
5090
+ `constraints` alone.** Read off its export of a rigc build (Spine 4.3.26,
5091
+ `gallery/look`): the `animations` object, a skin's 24 `attachments` slot keys and
5092
+ two animations' 16 and 2 bone-timeline keys all came back sorted, while the 30
5093
+ `bones`, 24 `slots` and 3 `constraints` — arrays — came back in the build's own
5094
+ order, element for element, and each slider kept its place among them.
5095
+
5096
+ 🚨 **Not every array: `skins` it re-sorts.** That sentence read *"leaves every
5097
+ ARRAY alone"* for two releases, on those three arrays and nothing else, and
5098
+ `skins` is the one it was wrong about — see the paragraph at the end of this
5099
+ section.
4981
5100
 
4982
5101
  ⚠️ **The order it sorts them into is natural and case-insensitive, not
4983
5102
  codepoint.** This paragraph said codepoint until
@@ -5008,20 +5127,31 @@ firings still resolved **by name** — `0.3 -> mike`, `0.6 -> alpha`, payloads
5008
5127
  intact (#539). So the editor treats `events` and `animations` differently, and
5009
5128
  rigc emits events in the order you declare them.
5010
5129
 
5011
- ⚠️ **Read *"leaves every ARRAY alone"* above as bones, slots and constraints —
5012
- `skins` is the array that round trip was not taken over.** `gallery/look` declares
5013
- one skin, as does every other rig in this repository and all twelve editor exports
5014
- in `examples/`, and a one-element array comes back in order whatever the editor
5015
- does to it — so nothing here is
5016
- evidence about `skins`, and a pull request that once called them *measured
5017
- preserved* was reading a vacuous result ([#544](https://github.com/firejune/rigc/issues/544)).
5018
- It matters because `skins` carries ordinals in the binary half too —
5019
- `skins[readInt()]` for an attachment timeline and a linked mesh's skin index — so
5020
- a re-order there would repoint them the way the `animations` re-key repoints a
5021
- slider. ⛔ And it cannot be measured today: the editor refuses a four-skin rig on
5022
- import without writing a project file or printing a word
5023
- ([#541](https://github.com/firejune/rigc/issues/541)). ⇒ **If you author more than
5024
- one skin, nothing on this page says the editor survives it.**
5130
+ 🚨 **`skins` is re-sorted, with `default` pinned first — measured, and it is the
5131
+ first array measured to move.** A four-skin rig built `default, zulu, mike, alpha`
5132
+ exported `default, alpha, mike, zulu`
5133
+ ([#541](https://github.com/firejune/rigc/issues/541)), and the deform timelines
5134
+ came back keyed `mike, zulu` rather than `zulu, mike` with it. Note `alpha` sorts
5135
+ before `default` under every candidate comparator and still came back second: the
5136
+ default skin is **pinned**, not sorted. It matters for the same reason `animations`
5137
+ does — `skins` carries ordinals in the binary half, `skins[readInt()]` for an
5138
+ attachment timeline and `skins[skinIndex]` for a linked mesh — so this is the
5139
+ `animations` defect (#535) in the collection nobody had checked. ⇒ in rigc: R11.
5140
+
5141
+ ⚠️ **What that measurement does *not* settle is which comparator.** `alpha, mike,
5142
+ zulu` is the order codepoint, case-folding and natural order all produce, so unlike
5143
+ the animations case nothing here refutes anything, and rigc refuses any skin-name
5144
+ pair the candidates could disagree about (R11).
5145
+
5146
+ ✅ **The two readings this replaces.** A pull request once called `skins` *measured
5147
+ preserved*, on the strength of a one-skin rig where a one-element array comes back
5148
+ in order whatever the editor does to it;
5149
+ [#544](https://github.com/firejune/rigc/issues/544) corrected that to *unmeasured*,
5150
+ and added that it could not be measured because the editor refused a four-skin rig
5151
+ on import without printing a word. Both readings were the same generalisation — *an
5152
+ editor does not move arrays* — off the three arrays that were measured, and the
5153
+ "without a word" was rigc's own harness discarding the editor's stderr, which had
5154
+ named the cause all along (§3.4.2).
5025
5155
 
5026
5156
  ### 10.2 Draw order
5027
5157
 
@@ -5476,6 +5606,37 @@ never write any of them by hand — and rigc's own output always carries all
5476
5606
  three, because the editor's *import* treats their absence as an export made
5477
5607
  without the box and rebuilds the hull on its own.
5478
5608
 
5609
+ 🔬 🚨 **And the CLI's default export has that box ON, so for anything driven from
5610
+ the command line the ⇒ above is the exception rather than the case.** The
5611
+ sentence is still true of an export made without the box; what is measured is
5612
+ that `-e json` with no export-settings file does not make one. Every field on the
5613
+ nonessential list came back present, unchanged and not zero on round trip 6
5614
+ (2026-09-16, Spine 4.3.26): the header's `fps` (24 on `fields`, the one rig that
5615
+ declares it) and `images`; a mesh's `width`/`height` (64/48) and `edges`
5616
+ (16 entries, identical); the editor colours of a **bounding box** (`3cff6bff`), a
5617
+ **clipping** polygon (`ff3c6bff`) and a **path** (`ff6b3cff`); and a bone's
5618
+ `icon` (`circle`) with its `color`. All eight exports also carry
5619
+ `"audio": "./audio"` — a field rigc never wrote and none of those rigs has any
5620
+ use for — which is the list's own last member arriving unasked. The runtime says
5621
+ the same thing from the other side: `PathAttachment.js`'s doc comment on `color`
5622
+ reads *"Available only when nonessential data was exported"*, and the colour is
5623
+ there.
5624
+
5625
+ 📗 The CLI page documents the form but not the setting: *"If `json` or `binary` is
5626
+ specified instead of a path to an export settings JSON file, then a JSON or
5627
+ binary export is performed using default settings"* —
5628
+ [Command line interface](https://esotericsoftware.com/spine-command-line-interface).
5629
+ ❓ **Neither that page nor the Export page states whether nonessential is on in
5630
+ those defaults**, so the answer above is measured here and documented nowhere.
5631
+ ⇒ In practice: do not plan around fields being dropped. An export you did not
5632
+ personally make without the box is an export that has them, and the round trip is
5633
+ therefore **richer** than the build rather than poorer — which is why #368's
5634
+ hull-and-edges degradation does not return on a second trip (no import warning in
5635
+ any of the eight `roundtrip.log`s, and a five-vertex mesh's `hull: 4` came back
5636
+ `4` rather than recomputed to `5`). A nonessential-**off** trip would need an
5637
+ export-settings JSON, and ❓ the key name inside that file is not documented
5638
+ either.
5639
+
5479
5640
  ⚠️ **A region's `width`/`height` are not on that list.** They are documented with no
5480
5641
  *"assume … if omitted"* default — the same fact R5 states from the parser's side:
5481
5642
  omit them in raw JSON and every UV collapses, in silence. Name an `image`.
@@ -5486,7 +5647,136 @@ omitted"* — and **rigc deliberately does the opposite** (R1, §2). Writing `x:
5486
5647
  legitimate here. The habit worth carrying over is not *omit defaults*, it is
5487
5648
  *declare only what the shot needs*.
5488
5649
 
5489
- ### 10.6 What this section does not claim
5650
+ ### 10.6 What a round trip gives back
5651
+
5652
+ Everything above is what the editor **does**. This is what it **returns** — which
5653
+ matters to you for one reason: a construct nobody has carried through the editor
5654
+ is a construct that might vanish there, and an agent cannot see that it did.
5655
+
5656
+ 🔬 The source is one run: eight discriminator rigs, each built to isolate a group
5657
+ of fields, compiled by rigc **0.21.0** (emitting 4.3.13), imported into a licensed
5658
+ Spine **4.3.26** through the documented CLI and exported back on 2026-09-16, with
5659
+ the predictions written down before anything was opened. **Seven of the eight came
5660
+ back differing from their build in three header fields and nothing else** —
5661
+ `hash` and `audio`, which the editor adds, and `spine`, which it stamps with its
5662
+ own version. The eighth is the path rig, and it is the last bullet here.
5663
+
5664
+ ⚠️ *"Nothing else"* is under two normalisations, both of which are the exporter
5665
+ being ordinary rather than the editor changing anything: **float spelling** (`48`
5666
+ comes back `48.0`) and **omitted defaults** — the export drops any field equal to
5667
+ its parser default, so the header loses `x: 0` and `y: 0`, a bone loses `x: 0`, and
5668
+ a key at t=0 loses its `"time": 0`. Every name-keyed object is also re-sorted, per
5669
+ §10.1. None of those is a loss of information, and each is worth knowing before
5670
+ you read a `diff`.
5671
+
5672
+ ⚠️ Read every line below as *this construct survived*, never as *this construct is
5673
+ recommended*. §10.1–§10.5 are the recommendations; this subsection is only the
5674
+ evidence that the format will carry what you write.
5675
+
5676
+ - 🔬 **A transform constraint survives whole.** 4.3's `source` plus its
5677
+ `properties` map — including a nested `to` with `offset`, `max` and `scale` —
5678
+ came back field for field, with `localSource`, `localTarget`, `additive`,
5679
+ `clamp`, `mixRotate` and `mixY` beside it.
5680
+ - 🔬 **The `transform` timeline survives** — the group shape that maps a
5681
+ constraint name straight to a key array (§4.10), with its per-key mixes and
5682
+ curves intact.
5683
+ - 🔬 **`shear`, `shearx` and `sheary` bone timelines survive**, paired and
5684
+ single-axis, with both channels of a paired key and all their curve control
5685
+ points.
5686
+ - 🔬 **A bone's setup `scaleX`, `scaleY`, `shearX`, `shearY` and `inherit`
5687
+ survive**, a non-default `inherit` included — `noScale`, `onlyTranslation` and
5688
+ `noRotationOrReflection` were all carried on one rig.
5689
+ - 🔬 ⭐ **A bone's `color` and `icon` survive** — 4.3's bone icons round-trip
5690
+ (`circle`, `ff7f00ff`). They are nonessential data, so this is also a reading of
5691
+ §10.5's caveat.
5692
+ - 🔬 **The `drawOrder` timeline survives, offsets and all — including the empty
5693
+ key.** A key with no `offsets` restores the setup order (§4.7), and it came back
5694
+ **empty** rather than spelled out as an identity permutation, which is the
5695
+ spelling that would have made every later diff read as a change.
5696
+ - 🔬 **Slot `color`, `dark` and `blend` survive**, `blend: multiply` and
5697
+ `blend: additive` included.
5698
+ - 🔬 **A path constraint's non-default modes survive**: `positionMode: fixed`,
5699
+ `spacingMode: proportional`, `rotateMode: chainScale`.
5700
+ - 🔬 **A path attachment's `closed: true` and `constantSpeed: false` survive**, and
5701
+ so do its `position`, `spacing` and three-channel `mix` timelines — all three
5702
+ channels of every `mix` key, and all twelve curve numbers on each key that
5703
+ carries a curve. ⚠️ Its `lengths` did **not**, which is the last bullet — and
5704
+ since [#560](https://github.com/firejune/rigc/issues/560) they do, because rigc
5705
+ now emits the numbers the editor recomputes rather than numbers near them.
5706
+ - 🔬 **`physics.mix` and `physics.reset` timelines survive**, the `reset` key
5707
+ included — a key that carries a time and no value at all — and so does the
5708
+ physics constraint's setup `mix`.
5709
+ - 🔬 **A time-driven slider survives** — one with no `bone`, carrying `loop`, a
5710
+ setup `time` and a setup `mix`, with both its `time` and `mix` timelines.
5711
+ - 🔬 **`boundingbox` and `clipping` attachments survive whole**: `vertexCount`,
5712
+ `vertices`, the clipping `end` slot, `convex`, and both editor colours.
5713
+ - 🔬 **An unweighted mesh survives and its `hull` is kept, not recomputed** — a
5714
+ five-vertex mesh declaring `hull: 4` came back `4`, with its `edges`, `color`,
5715
+ `uvs` and `triangles` unchanged. Beside it, on the same rig: the header's
5716
+ `referenceScale`, a region's `rotation` / `scaleX` / `scaleY` / `color`, and an
5717
+ attachment whose `path` differs from its placeholder (the mechanism of
5718
+ [#552](https://github.com/firejune/rigc/issues/552)) all survive. A `--pack`
5719
+ build round-trips too.
5720
+
5721
+ 🚨 **The one thing that did not come back is a path attachment's `lengths`, and it
5722
+ moved the drawing.** The editor recomputes them at export from the geometry —
5723
+ the imported project holds the numbers it was given — and it measures each curve
5724
+ with the **runtime's own four-sample forward difference**, not with an arbitrarily
5725
+ fine one. `PathConstraint`'s `constantSpeed` re-measure is that same computation:
5726
+ its constants are `0.1875 = 3t²`, `0.09375 = 6t³` and `(cx1 − x1) · 0.75 = 3t` at
5727
+ **t = 1/4**, four `Math.sqrt` terms per curve. A 4-sample chord sum over the same
5728
+ control points reproduces the editor to every digit it prints, on a closed path
5729
+ under 4.3.26 (`[152.7006, 305.4012, 458.1019, 610.8025]`) and on an open one under
5730
+ 4.3.23 (`[430.8389, 838.0142, 1127.736, …]`).
5731
+
5732
+ ⚠️ **That last reading settles the model and cannot settle the spelling.** A
5733
+ 4-sample chord sum agrees with the runtime's forward difference to about **nine
5734
+ significant digits** — *below* what float32 can hold, which is why both spellings
5735
+ reproduce both exports exactly, and *above* the six decimals rigc emits, which is
5736
+ why the file can tell them apart. On both rigs above they round apart on the
5737
+ **last** curve, where the running total has accumulated most: `610.802519` against
5738
+ `610.802520`, `1127.735817` against `1127.735818`. So the editor is the evidence
5739
+ for *what* is computed, and only `PathConstraint` itself is evidence for *how*.
5740
+
5741
+ ⭐ **rigc emits the forward difference itself** since
5742
+ [#560](https://github.com/firejune/rigc/issues/560) — `pathCurveLengths` in
5743
+ [`src/compile.ts`](../src/compile.ts) is those runtime lines transcribed, down to
5744
+ `Math.sqrt(dx * dx + dy * dy)` rather than `Math.hypot` and `0.16666667` rather
5745
+ than `1 / 6`, both of which change the emitted file. All seven entries of the two
5746
+ exports above now come back at the precision the editor prints them, so a path rig
5747
+ built here and one authored in the editor parameterise identically. ⚠️ The
5748
+ consequence for you is a vocabulary one: `lengths` is **not** an arc length. It
5749
+ sits about 0.5 % below the arc by construction, so a physical quantity — how far a
5750
+ wheel rolls, how long a ribbon is — has to be measured off the curve and not read
5751
+ out of the artifact.
5752
+
5753
+ 🔬 **And the editor always writes `vertexCount / 3` entries, computing the
5754
+ wrap-around curve even on an open path** — that open path's fourth entry,
5755
+ `2136.228`, is the closed-chain cumulative. ⚠️ Neither array is wrong: the parser
5756
+ allocates `vertexCount / 3` and copies whatever is there, and `PathConstraint`
5757
+ reads at most `lengths[curveCount]`, so the trailing entry is never read.
5758
+
5759
+ ⇒ **What this means for you.** `lengths` is the one number in a path rig you
5760
+ cannot check by looking: `diff` does not compare it, and `A33` asks only that it
5761
+ strictly increase, which any plausible array does. A total that is a fraction of a
5762
+ percent out was measured at **4.9612 mean MAE** on the one rig of that run with a
5763
+ path constraint, against **0.0000** on the other seven. If you are comparing a
5764
+ path rig against an editor reference and everything structural agrees while the
5765
+ picture does not, this is the first place to look.
5766
+
5767
+ ⚠️ **What decides whether it moves a pixel is the POSITION mode**, and an earlier
5768
+ reading of this paragraph put it on `spacingMode: proportional`, which is the one
5769
+ mode it cannot be: proportional spacing scales *with* the total, and that is
5770
+ exactly what cancels. The rig that drifted is `positionMode: fixed`, where an
5771
+ absolute `position` is compared against a total that moved. Under
5772
+ `positionMode: percent` the position scales with the total too, so a uniform
5773
+ change cancels out of both — measured across #560, `gallery/ride` is
5774
+ percent/percent and every one of its 74 rendered frames came back **byte
5775
+ identical** on an emitted array all three of whose numbers moved. ⇒ Read a
5776
+ `lengths` disagreement as *certainly wrong data, and visible only under
5777
+ `positionMode: fixed`*.
5778
+
5779
+ ### 10.7 What this section does not claim
5490
5780
 
5491
5781
  Conventions that are visible in reference exports but that **no public Spine page
5492
5782
  states** are deliberately absent. A guide that asserted them would be handing you an
@@ -5494,7 +5784,10 @@ answer read off the exports:
5494
5784
 
5495
5785
  - any figure for keys per second, or for how key density scales with frame rate;
5496
5786
  - which curve type any particular example project or studio actually shipped;
5497
- - whether a given export was made with Nonessential data checked;
5787
+ - whether a **corpus** export — one somebody else made, out of the editor's own
5788
+ dialog — was made with Nonessential data checked. ⚠️ §10.5 now answers this for
5789
+ the **CLI's** `-e json`, where it is measured; that measurement says nothing
5790
+ about an export you were handed, and the two must not be read as one;
5498
5791
  - how many bones, slots or timelines a rig of a given size ought to have;
5499
5792
  - whether a shipped rig prefers automatic Bezier handles or hand-placed ones.
5500
5793
 
@@ -597,11 +597,20 @@ with the member's own `skin: true` — either half alone is refused, because `Sk
597
597
  | `mesh` | 🟡 | emits `type`, `uvs`, `triangles`, `vertices` (**weighted encoding only**), `hull`, `width`, `height`, `edges` (`compile.ts:1343`, and part 4's rung-6 entry measures it byte-identical to the reference), `path`, `color`. ❌ `sequence`. Unweighted meshes are 🚫 **A20_MESH_WEIGHTS_COHERENT** (`validate.ts:430-433`) |
598
598
  | `linkedmesh` | ❌ | deliberately deferred — never appears in the corpus (part 3-1) |
599
599
  | `boundingbox` | ✅ | `vertexCount` (required and cross-checked), `vertices` **or** by-name `weights`, `color`. **A33_VERTEX_ATTACHMENT_GEOMETRY** |
600
- | `path` | ✅ | `vertexCount` (required, and checked as a multiple of 3 — the parser's own `vertexCount / 3` takes a fractional size in silence), `vertices` **or** by-name `weights`, `closed`, `constantSpeed`, `color`, and a **measured** `lengths`: the cumulative setup arc length of each curve, taken through each influence's own bone, refused if authored. **A33_VERTEX_ATTACHMENT_GEOMETRY** re-checks the structure and the array's monotonicity. ❌ a `deform` timeline on one |
600
+ | `path` | ✅ | `vertexCount` (required, and checked as a multiple of 3 — the parser's own `vertexCount / 3` takes a fractional size in silence), `vertices` **or** by-name `weights`, `closed`, `constantSpeed`, `color`, and a **measured** `lengths`: the cumulative setup length at the end of each curve, taken through each influence's own bone and measured as `PathConstraint` measures it — its own four-sample forward difference, which is what the editor writes too and is about 0.5 % below the arc (AUTHORING §10.6) — refused if authored. **A33_VERTEX_ATTACHMENT_GEOMETRY** re-checks the structure and the array's monotonicity. ❌ a `deform` timeline on one |
601
601
  | `point` | ❌ | deliberately deferred — never appears in the corpus (part 3-1) |
602
602
  | `clipping` | ✅ under `--profile spine` · 🚫 under `spine-html` | `end` (refused when it names no slot), `convex`, `inverse`, `vertexCount`, geometry, `color`. **A33**, and **A11_NO_CLIPPING_ATTACHMENTS** is the renderer-profile refusal |
603
603
  | `sequence` block | ❌ | |
604
604
 
605
+ Across all five types, rigc emits the attachment's own **`name`** in exactly one case: a placeholder
606
+ that more than one skin fills (`compile.ts`'s `nameSkinAttachment`). Part 1-5 above states why it
607
+ matters — `name` defaults to the placeholder and `path` defaults to `name` — so several skins under
608
+ one placeholder are several attachments with one name, which spine-core accepts and the Spine editor
609
+ refuses on import ([#541](https://github.com/firejune/rigc/issues/541)). The composed name is
610
+ `<skin>/<placeholder>`, `path` is restated beside it so the region still resolves, and a placeholder
611
+ one skin fills is emitted with neither. Nothing else in the tree carries a `name`: of the twelve
612
+ editor exports in `examples/`, **0** attachments do, because all twelve declare one skin.
613
+
605
614
  Mesh geometry is generated by exactly three procedural generators (`mesh.ts`): `buildRingMesh`
606
615
  (three concentric rings + hub, outer two pinned), `buildRibbonMesh` (a two-wide strip along a bone
607
616
  chain) and `buildContourMesh` (marching-squares trace of the part's own alpha → Douglas-Peucker →
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
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": {
@@ -59,6 +59,7 @@
59
59
  "validate": "bun cli.ts validate",
60
60
  "explain": "bun cli.ts explain",
61
61
  "selftest": "bun selftest.ts",
62
+ "smoke": "bun scripts/install_smoke.ts",
62
63
  "typecheck": "bunx tsc --noEmit",
63
64
  "typecheck:runs": "bunx tsc --noEmit -p tsconfig.runs.json",
64
65
  "lint": "bunx eslint .",