spine-rigc 0.20.3 → 0.22.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
@@ -508,9 +508,9 @@ work on any frames you have, and `bench` is a repository workflow that needs a c
508
508
  and `bun run fetch-examples`. The reasoning behind all three is in
509
509
  [the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
510
510
 
511
- `build` and `validate` both default to `--profile spine` — the 26 validity rules, which
511
+ `build` and `validate` both default to `--profile spine` — the 27 validity rules, which
512
512
  ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
513
- adds all 41: the other 15 are one renderer's policy and one canvas budget's, and they
513
+ adds all 42: the other 15 are one renderer's policy and one canvas budget's, and they
514
514
  fire on perfectly correct editor-produced Spine data, so reach for that profile when
515
515
  you are shipping into *that* project rather than to be thorough. A report always names
516
516
  the profile it ran and lists what that profile left out.
@@ -576,7 +576,7 @@ letting `A17` blame the editor for the harness's own doing.
576
576
  | 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
577
577
  | 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
578
578
  | 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
579
- | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 41 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
579
+ | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 42 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
580
580
  | 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
581
581
  | 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 has to mean before the number is claimed — conditions rather than a feature list, because direction here comes from what users hit |
582
582
 
@@ -632,7 +632,7 @@ quality."* All six, with their verdicts, are in
632
632
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
633
633
 
634
634
  The whole dossier — the yardstick, `diff` and `check` and what neither of them can
635
- see, every rung, the run viewer, the 41 assertions and the selftest behind them — is
635
+ see, every rung, the run viewer, the 42 assertions and the selftest behind them — is
636
636
  [docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
637
637
  Live rung status is
638
638
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
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
@@ -163,7 +163,7 @@ What the flags mean:
163
163
  | `--atlas-in` | `build` only: 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:`; the atlas is re-emitted into `--out`, re-anchored — **§0.2** |
164
164
  | `--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) |
165
165
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
166
- | `--profile` | `spine` = the 26 validity rules (**the default**) · `spine-html` = all 41, opt-in |
166
+ | `--profile` | `spine` = the 27 validity rules (**the default**) · `spine-html` = all 42, opt-in |
167
167
  | `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** — repeat it 2–4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
168
168
  | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview`, and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote`, so is a name that only *some* candidates have |
169
169
  | `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
@@ -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
@@ -566,7 +575,8 @@ behind it writes literal `x`/`y` instead.
566
575
 
567
576
  **R10 — The `animations` object is keyed in the editor's order, not in yours, and
568
577
  names that have no one order are refused.** Declare animations in whatever order
569
- reads best; the emit keys them codepoint-ascending. This is the one place rigc
578
+ reads best; the emit keys them the way the Spine editor does — **natural and
579
+ case-insensitive**. This is the one place rigc
570
580
  reorders anything you wrote, and it is not cosmetic: a `slider`'s animation is a
571
581
  **name** in JSON and an **ordinal** in the format's binary half, so an editor that
572
582
  re-sorts the object repoints every slider whose animation moved index — silently,
@@ -575,25 +585,68 @@ in a file that still parses and still gates green (§3.5.2,
575
585
  animation's own body is byte-identical either way, and every other collection is
576
586
  emitted in the order you gave it.
577
587
 
578
- ⚠️ **Codepoint is not the editor's comparator.** The editor sorts **natural and
579
- case-insensitive** — measured, two rigs, one axis each:
588
+ ⚠️ **What is measured about that comparator, and what is not.** The editor sorts
589
+ natural and case-insensitive — measured, two rigs, one axis each:
580
590
  `Turn, sweep, wave` came back `sweep, Turn, wave`, and `turn10, turn2, zoom` came
581
591
  back `turn2, turn10, zoom` ([#539](https://github.com/firejune/rigc/issues/539)).
582
- Codepoint agrees with it on most names and not on all, so rigc emits codepoint and
583
- **refuses the sets where the two could differ**, naming the pair. The rule you have
584
- to hold is therefore about *names*, and it is three things:
592
+ But "natural and case-insensitive" is a **family** of comparators, not one, and
593
+ **four** of its choices have never been measured. rigc emits the order every
594
+ member of that family agrees on, and **refuses the sets where one of the four
595
+ would decide**, naming the pair. So the rule you have to hold is about *names*,
596
+ and it is four things:
585
597
 
586
598
  | Do not let two animation names differ | Because | Instead |
587
599
  | --- | --- | --- |
588
- | by **case** at the character that orders them (`Turn` against `sweep`, or `Turn` against `turn`) | folding the case reverses them, and a pure case tie is settled by a tie-break nobody has measured | pick one case for all of them, or change a letter |
589
- | by a **number** read two ways (`turn2` against `turn10`, `turn01` against `turn1`, `1turn` against `turn`) | as text `turn10` sorts first and as a number it does not; `01` and `1` are one number written twice | pad the digits to the same width — `turn02` beside `turn10` |
600
+ | by **case alone** (`Turn` against `turn`) | they fold together, so only a tie-break separates them, and nobody has measured which way it breaks | pick one case for all of them, or change a letter |
601
+ | by a **number written two ways** (`turn01` against `turn1`) | `01` and `1` are one number twice; shorter-first, longer-first and lexicographic are all real tie-breaks | write the number one way — with leading zeros or without, but not both |
602
+ | by a **digit run against a word** (`1turn` against `turn`) | comparators differ on whether a number sorts before a word | rename so a run of digits is never compared against a word |
590
603
  | by a **separator** — anything that is neither a letter nor a digit (`wave_x` against `wavea`, `wave` against `wave-`) | a collator may treat `-` or a space as ignorable, and `_` sits *between* `Z` and `a`, so folding up and folding down order it oppositely | rename so the first character that differs is a letter or a digit |
591
604
 
592
- ⭐ **Capitals and digits are not what is refused** — only pairs whose order turns
593
- on them. `Sweep, Turn, Wave, Zoom02, Zoom10` builds: every comparator puts those
594
- five in one order, so codepoint *is* the editor's order for them. A set with no
595
- such pair is safe under **every** candidate comparator, which is why rigc does not
596
- have to reproduce the editor's sort to know your rig is safe under it.
605
+ ⭐ **Capitals and numbered series are not what is refused** — only pairs one of
606
+ those four decides. `Sweep, Turn, Wave, Zoom02, Zoom10` builds, and so does
607
+ `shot1 … shot12`: every member of the family puts each of those sets in one
608
+ order, and that order is what rigc emits. A numbered series that crosses 9 → 10 is
609
+ keyed **1, 2, … 9, 10, 11, 12**, which is what the editor does with it — and is
610
+ not what a codepoint sort does.
611
+
612
+ ✅ **This list had two more rows before
613
+ [#543](https://github.com/firejune/rigc/issues/543), and both were artefacts of
614
+ the emit rather than facts about the editor.** rigc used to key `animations`
615
+ **codepoint-ascending** and refuse every pair codepoint and the editor could order
616
+ differently — which refused a pair that folds the other way (`Turn` against
617
+ `sweep`) and a pair of digit runs of unequal width (`turn10` against `turn2`).
618
+ Those are the only two name sets anybody has ever put through the editor and read
619
+ back, so the tool was refusing precisely the pairs it knew the most about, and its
620
+ only repair was *rename* — the one repair a transcription cannot take. Emitting a
621
+ member of the family instead moves no byte on any set the old rule accepted; it
622
+ just stops refusing the ones it did.
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.
597
650
 
598
651
  ---
599
652
 
@@ -692,7 +745,7 @@ the default `type`:
692
745
  | `type` | `"region"`, or omit |
693
746
  | `image` | **rigc extension.** A PNG relative to the rig's `images` directory; rigc measures it (R5) |
694
747
  | `width`, `height` | required by the format — give them, or give an `image` |
695
- | `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) |
696
749
  | `x`, `y` | offset from the bone, in the bone's local space |
697
750
  | `rotation` | degrees; cancels a rotated bone for a plate authored screen-upright |
698
751
  | `scaleX`, `scaleY`, `color` | as Spine |
@@ -1355,6 +1408,70 @@ beside them is refused rather than ignored (an ignored slot is an attachment tha
1355
1408
  vanishes), and a rig with a *slot* of one of those names is refused too, because
1356
1409
  there the two forms are genuinely ambiguous. Rename the slot.
1357
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
+
1358
1475
  ### 3.5 `constraints` — 4.3's single typed array
1359
1476
 
1360
1477
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -1377,6 +1494,14 @@ carrying here:
1377
1494
  - A physics constraint's five components all default to 0, so one that names none of
1378
1495
  them parses cleanly and does nothing at all. rigc refuses it up front, and `A23`
1379
1496
  catches it from the other side.
1497
+ - An **ik** and a **physics** constraint both carry `ScaleYMode` under the key
1498
+ `scaleY`, spelled `"none"`, `"uniform"` or `"volume"`. It is an enum resolved by
1499
+ `Utils.enumValue`, so only the first letter's case is free and an unrecognised
1500
+ name is assigned as `undefined` with no error; rigc checks it, like the three
1501
+ path modes below. ⚠️ `src/rig.ts` called the physics one **`scaleYMode`** until
1502
+ issue #545 — the runtime's field name rather than the format's key — and nothing
1503
+ read it, so a spec that wrote `scaleYMode` set no mode and said nothing. A rig
1504
+ that still writes it is now refused by name, with `scaleY` beside it.
1380
1505
 
1381
1506
  Every constraint may also carry `skin: true`, which makes it run only under the skin
1382
1507
  that lists it — see §3.4.1, and note that the flag alone does nothing.
@@ -1482,25 +1607,32 @@ animation now stands at the position. `gallery/look` went into a licensed editor
1482
1607
  `sweep, tilt, turn` with **`yaw -> "sweep"`**: a file that parses, gates green and
1483
1608
  applies the wrong animation. ⭐ Its second slider is what named the mechanism
1484
1609
  rather than a second casualty — `tilt` survived because it sat at index 1 in both
1485
- orderings. rigc now emits animations codepoint-ascending so the editor's re-sort
1610
+ orderings. rigc now emits animations in the editor's own order so its re-sort
1486
1611
  moves no index ([#535](https://github.com/firejune/rigc/issues/535)); on the same
1487
1612
  rig through the same editor that restored `yaw -> "turn"` and took the
1488
1613
  re-rendered mean absolute error from 10.4655 / 8.4961 / 8.7140 down to
1489
- 0.3035 / 0.0769 / 0.0588.
1490
-
1491
- ⚠️ Codepoint is not the editor's own comparator — it sorts natural and
1492
- case-insensitive ([#539](https://github.com/firejune/rigc/issues/539)) — so the
1493
- emit is only its order for names no comparator can put two ways, and the rest are
1494
- a compile error. **R10** has the three shapes to avoid.
1495
-
1496
- ⚠️ **What that repair does not reach: names a codepoint sort and a friendlier one
1497
- disagree about.** Every animation name in every editor-authored file this
1498
- repository has is lowercase ASCII with `-` or `_`, so nothing measured here
1499
- separates codepoint order from a case-insensitive or digit-aware one. Names
1500
- differing only in case (`Turn` / `turn`) or carrying unpadded digits (`turn2` /
1501
- `turn10`) are where the two could part, and there the hazard returns. Until
1502
- somebody round-trips such a pair, **name animations so that every ordering anyone
1503
- might use agrees** — one case, and digits padded or absent.
1614
+ 0.3035 / 0.0769 / 0.0588. (`look`'s three names are ones a codepoint sort orders
1615
+ identically, which is what rigc emitted when that trip was measured.)
1616
+
1617
+ ⚠️ The editor's comparator is natural and case-insensitive
1618
+ ([#539](https://github.com/firejune/rigc/issues/539)), and four of its choices are
1619
+ unmeasured — so the emit is that family's order for names none of the four
1620
+ decides, and the rest are a compile error. **R10** has the four shapes to avoid.
1621
+
1622
+ ✅ **What that repair does not reach is a compile error now, not a hazard.** This
1623
+ paragraph used to say that names a codepoint sort and a friendlier one disagree
1624
+ about — `Turn` / `turn`, `turn2` / `turn10` — were where *the hazard returns*, and
1625
+ that nobody had round-tripped such a pair. Somebody has: `Turn, sweep, wave` came
1626
+ back `sweep, Turn, wave` and `turn10, turn2, zoom` came back `turn2, turn10, zoom`
1627
+ ([#539](https://github.com/firejune/rigc/issues/539)). ⇒ rigc no longer leaves
1628
+ that to naming discipline — and since
1629
+ [#543](https://github.com/firejune/rigc/issues/543) it does better than refusing
1630
+ those two, because they are the two sets the editor's answer is **known** for:
1631
+ both are emitted in the order it returned. What is still a compile error is the
1632
+ set whose order turns on one of the four unmeasured choices, printed with both
1633
+ names, which of them decides it, and the rename that settles it. What changed is
1634
+ the price of forgetting: a build that stops, rather than a slider that silently
1635
+ applies the wrong animation.
1504
1636
 
1505
1637
  ⚠️ **The fields of the model you did not choose are refused, not ignored.** The
1506
1638
  parser reads `time` only in the bone-less branch and `property`/`from`/`to`/`scale`/
@@ -1770,7 +1902,8 @@ is not an array. Every field is optional and each is the payload a firing
1770
1902
 
1771
1903
  Optional with one exception, and only meaningful for rigc's own formations:
1772
1904
  `meshSlots` and `meshTriangles` (the two halves of the mesh budget `A13` measures
1773
- against), `axisBone`, `massBone`, `detached`, `deformMayFold`. Nothing in skeleton
1905
+ against), `axisBone`, `massBone`, `detached`, `deformMayFold`, `editorRoundTrip`.
1906
+ Nothing in skeleton
1774
1907
  JSON records that a
1775
1908
  bone carries a cut's axis or that a parentage is forbidden, so the rig spec says it
1776
1909
  and the validator's archetype assertions read it. **An assertion whose field is
@@ -1808,6 +1941,33 @@ and the entry is gone. ⇒ An exemption whose `why` reads *"known defect, see
1808
1941
  against a fix, not a fix — and the thing that made it repayable was A39
1809
1942
  measuring the ceiling the art could actually take.
1810
1943
 
1944
+ 🎬 **`editorRoundTrip` is the one field here that names a CONSUMER rather than a
1945
+ shape.** Write `"editorRoundTrip": true` when this rig is authored to come back
1946
+ out of the Spine editor — imported, hand-edited, exported — and
1947
+ `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` refuses a physics constraint driving a
1948
+ component that editor cannot hold. `true` is the only accepted value; a `false`
1949
+ would be a key nothing reads ([#545](https://github.com/firejune/rigc/issues/545)).
1950
+
1951
+ ⚠️ **Leaving it out is not a weaker gate, and this is the part worth reading.**
1952
+ rigc's output is not wrong here: a physics constraint driving `rotate` is valid
1953
+ Spine 4.3 that every runtime plays — a cowlick, a tail, an ear — so refusing it by
1954
+ default would be refusing correct data on behalf of a pipeline nobody declared.
1955
+ What a rig that says nothing gets instead is the **SKIP**, and the SKIP names the
1956
+ constraint and the component:
1957
+
1958
+ ```
1959
+ SKIP A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP: the rig "look" does not declare `invariants.editorRoundTrip`, so nothing here is gated against the Spine editor. What is here: physics "whip" drives rotate, and the editor's physics model holds x and y only, so a round trip returns that constraint driving nothing at all (issue #540)
1960
+ ```
1961
+
1962
+ 📏 **Measured, not inferred** ([#540](https://github.com/firejune/rigc/issues/540)):
1963
+ three rigs, twelve constraints, predictions written before the round trip. A lone
1964
+ `y` came back and `x` + `y` together came back — the rule is membership, not arity
1965
+ — while a lone `rotate`, a lone `scaleX` and a lone `shearX` each came back driving
1966
+ **no component at all**, and neither `scaleY` mode rescues `scaleX`. So the fix for
1967
+ a refusal is to drive the constraint in `x`/`y`, or to drop the declaration if this
1968
+ rig never goes near the editor. ⇒ `A23_PHYSICS_CONSTRAINT_EFFECTIVE` is the same
1969
+ loss seen from the far side: it is what fires on the file the editor hands **back**.
1970
+
1811
1971
  🚫 **Do not reach for it to cover a part you have faded out.** A key whose slot
1812
1972
  draws no pixels at that key's own time is already passed over — `A39` measures
1813
1973
  that and says so (§4.11, and the `skipped` line in the `DEFORM` block). Declaring
@@ -3220,6 +3380,38 @@ compiled, and ask only what the one file in front of them can answer. Every
3220
3380
  one per message. (The rig spec's parser predates the convention and its messages
3221
3381
  are prose, so they sit in the second table with everything else.)
3222
3382
 
3383
+ 🚨 **A key neither format has is refused by name, in both files.** Not a row in
3384
+ the table below, because it is not about one field: every object in a rig spec and
3385
+ in a motion spec is checked against the keys its shape actually owns, and a key
3386
+ outside that set stops the build. Issue #545 is why — before it, such a key was
3387
+ never looked at, never mentioned and never emitted, and the build exited 0 with
3388
+ every assertion green. Four keys planted into one physics constraint all vanished,
3389
+ and no line of output named any of them.
3390
+
3391
+ ```
3392
+ rigc compile error: rig.json: constraint "ctl" (physics) has 4 keys this compiler
3393
+ does not read: "scaleYMode" (did you mean "scaleY", "scaleX"?), "scale" (did you
3394
+ mean "scaleX", "scaleY"?), "wobble", "ROTATE" (did you mean "rotate"?). Nothing
3395
+ reads such a key, so it would be dropped from the emitted skeleton in silence —
3396
+ fix the spelling or remove it. Known here: bone, damping, dampingGlobal, fps, …
3397
+ ```
3398
+
3399
+ Read it as a **repair**, not a rule: every stray key on that object is named at
3400
+ once, the closest known spellings come with it (the search is case-insensitive, so
3401
+ a real key in the wrong case leads the list), and the shape's whole key set is
3402
+ printed after. What it will *not* do is guess — a key four edits from anything gets
3403
+ no suggestion, only the set.
3404
+
3405
+ ⚠️ There is **no forward-compatibility escape**, and no `note` field except where
3406
+ one is listed: a rig spec's root, a motion spec's root, an animation, and a
3407
+ `physics` tuning entry. Prose anywhere else has to go in a document, because a key
3408
+ the compiler tolerates is a key it cannot distinguish from one you meant it to
3409
+ read. (The **cut manifest** is deliberately outside this: it is the record of the
3410
+ pipeline that produced the art as much as an input, it carries fields the compiler
3411
+ states outright that it does not read — `roi` — and the fixtures in this repository
3412
+ already give it `note` and `archetype`. It has no shape parse at all, which is the
3413
+ same hole issue #307 closed for the motion spec.)
3414
+
3223
3415
  | Key | Refused when it is not | Why the shape matters |
3224
3416
  | --- | --- | --- |
3225
3417
  | the file itself | a JSON object | the version row below would otherwise report a missing `spec` tag in a file that has no fields at all |
@@ -3273,6 +3465,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3273
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` |
3274
3466
  | `image "X.png" is not on disk at …` | fix the name, or point `--images` at the right directory |
3275
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)) |
3276
3470
  | `motion spec names archetype "A" but the rig spec at … is called "B"` | make `archetype` equal the rig's `name` |
3277
3471
  | `animation "A" declares duration Ns but its last key is at Ms` | R7 — fix whichever of the two you meant |
3278
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` |
@@ -3333,7 +3527,9 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3333
3527
  | `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
3334
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 |
3335
3529
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
3336
- | `N pair(s) of animation names have no one order: … "Turn" / "sweep" (case) — codepoint puts "Turn" first only because of letter case; folded, "sweep" comes first; rename one of them so nothing but case has to be compared` | **R10** — rename until no pair is left. The kind in brackets is which of the three it is: `case`, `number` (pad the digit runs to the same width) or `separator` (make the first character that differs a letter or a digit). rigc keys `animations` codepoint-ascending and the editor sorts natural and case-insensitive ([#539](https://github.com/firejune/rigc/issues/539)); on names where those can disagree, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
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 |
3337
3533
 
3338
3534
  ### 5.2 Assertions — the gate
3339
3535
 
@@ -3402,6 +3598,7 @@ The report prints one line per assertion:
3402
3598
  | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
3403
3599
  | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
3404
3600
  | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be additive (a slot colour, an attachment swap, a draw order, a sequence), where `"additive": true` is not the fix and one of the two has to go. The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, and which one wins today. Three shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, and two sliders on different properties. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
3601
+ | `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` | both | a physics constraint driving a component the **Spine editor** cannot hold, on a rig that declared `invariants.editorRoundTrip` (§3.7). The editor's physics model holds `x` and `y` only, with no cap on how many at once, so a constraint driving `rotate`, `scaleX` or `shearX` is imported, exported and handed back driving **nothing** — measured over three rigs and twelve constraints with the predictions written first ([#540](https://github.com/firejune/rigc/issues/540)). The detail names the constraint and each component. ⚠️ rigc's own output is correct — every runtime plays a rotation jiggle — so this is opt-in and the default is *not* silence: on a rig that declares nothing it **SKIPs**, and the SKIP names the constraint and the component anyway, so an author learns without having asked. Fix by driving the constraint in `x`/`y`, or by dropping the declaration if the rig never goes near the editor. Disjoint from `A23_PHYSICS_CONSTRAINT_EFFECTIVE` by construction: A23 refuses an **empty** driven set, which is what comes back from the editor, and this refuses a non-empty one that will not survive going in. **SKIP** also when the rig declares the editor and carries no physics constraint at all |
3405
3602
 
3406
3603
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
3407
3604
  clauses are gated by profile.
@@ -3426,6 +3623,7 @@ says so, because a deferral without its reason is a wall rather than a work item
3426
3623
  | constraint `type` of anything else | `constraint type "X" is not one Spine 4.3 knows. The five are: ik, transform, path, physics, slider.` — all five are emitted, so this is a typo, and a typo is what the parser drops in silence |
3427
3624
  | a path attachment's `lengths` | `"lengths" is not authored — rigc measures the setup arc length of each curve off the geometry` (§3.4). Not a deferral: a second copy of a number the vertices already fix |
3428
3625
  | a `deform` timeline on a path attachment | `a path attachment does have a vertex array, and rigc does not key it yet` — the format allows it and an animated track is a real idiom, but a deformed path invalidates the `lengths` a `constantSpeed: false` traversal reads. Move the curve by posing the bones its vertices are bound to |
3626
+ | any key neither format has, anywhere in either file | `<object> has a key this compiler does not read: "x" (did you mean "y"?) … Known here: …` (§5.1). Not a deferral either: a key nothing reads is a value you wrote and the emitted skeleton does not contain |
3429
3627
 
3430
3628
  Two more limits that are not errors but will shape what you can attempt:
3431
3629
 
@@ -4878,12 +5076,17 @@ low figure as a miss — say in the log that the art did not carry them.
4878
5076
  `default`"* and *"bones are ordered so that the parent always comes before a child
4879
5077
  bone"* — [JSON format](http://esotericsoftware.com/spine-json-format). §3.4.
4880
5078
 
4881
- 🔬 **The editor re-keys every name-keyed OBJECT and leaves every ARRAY alone.**
4882
- Read off its export of a rigc build (Spine 4.3.26, `gallery/look`): the
4883
- `animations` object, a skin's 24 `attachments` slot keys and two animations' 16
4884
- and 2 bone-timeline keys all came back sorted, while the 30 `bones`, 24 `slots`
4885
- and 3 `constraints` — arrays — came back in the build's own order, element for
4886
- element, and each slider kept its place among them.
5079
+ 🔬 **The editor re-keys every name-keyed OBJECT, and leaves `bones`, `slots` and
5080
+ `constraints` alone.** Read off its export of a rigc build (Spine 4.3.26,
5081
+ `gallery/look`): the `animations` object, a skin's 24 `attachments` slot keys and
5082
+ two animations' 16 and 2 bone-timeline keys all came back sorted, while the 30
5083
+ `bones`, 24 `slots` and 3 `constraints` — arrays — came back in the build's own
5084
+ order, element for element, and each slider kept its place among them.
5085
+
5086
+ 🚨 **Not every array: `skins` it re-sorts.** That sentence read *"leaves every
5087
+ ARRAY alone"* for two releases, on those three arrays and nothing else, and
5088
+ `skins` is the one it was wrong about — see the paragraph at the end of this
5089
+ section.
4887
5090
 
4888
5091
  ⚠️ **The order it sorts them into is natural and case-insensitive, not
4889
5092
  codepoint.** This paragraph said codepoint until
@@ -4900,10 +5103,13 @@ one key, deform blocks counted at each of their three levels;
4900
5103
  ⇒ in rigc: only `animations` is emitted sorted (R10), because it is the one
4901
5104
  object measured here whose ORDER is also an index space — every reference into
4902
5105
  the re-sorted *other* objects is by name on both sides, so nothing moves when
4903
- they are re-keyed. rigc emits **codepoint** and refuses the name sets on which
4904
- codepoint and the editor's comparator could differ, rather than reproducing a
4905
- comparator whose leading-zero, case-tie, digit-against-word and separator
4906
- behaviour is still unmeasured.
5106
+ they are re-keyed. rigc emits **that comparator's own order** and refuses the name
5107
+ sets on which its leading-zero, case-tie, digit-against-word or separator
5108
+ behaviour — the four choices still unmeasured — would decide a pair. Sorting the
5109
+ 105 collections that way reproduces **105 of 105**, the three codepoint cannot
5110
+ included, and refuses none of them; the codepoint rule that stood until
5111
+ [#543](https://github.com/firejune/rigc/issues/543) reproduced 102 and refused
5112
+ those same 3.
4907
5113
 
4908
5114
  ✅ **`events` is re-keyed too, and the references into it survive it.** The same
4909
5115
  session measured it: `zebra, mike, alpha` came back `alpha, mike, zebra`, and the
@@ -4911,6 +5117,32 @@ firings still resolved **by name** — `0.3 -> mike`, `0.6 -> alpha`, payloads
4911
5117
  intact (#539). So the editor treats `events` and `animations` differently, and
4912
5118
  rigc emits events in the order you declare them.
4913
5119
 
5120
+ 🚨 **`skins` is re-sorted, with `default` pinned first — measured, and it is the
5121
+ first array measured to move.** A four-skin rig built `default, zulu, mike, alpha`
5122
+ exported `default, alpha, mike, zulu`
5123
+ ([#541](https://github.com/firejune/rigc/issues/541)), and the deform timelines
5124
+ came back keyed `mike, zulu` rather than `zulu, mike` with it. Note `alpha` sorts
5125
+ before `default` under every candidate comparator and still came back second: the
5126
+ default skin is **pinned**, not sorted. It matters for the same reason `animations`
5127
+ does — `skins` carries ordinals in the binary half, `skins[readInt()]` for an
5128
+ attachment timeline and `skins[skinIndex]` for a linked mesh — so this is the
5129
+ `animations` defect (#535) in the collection nobody had checked. ⇒ in rigc: R11.
5130
+
5131
+ ⚠️ **What that measurement does *not* settle is which comparator.** `alpha, mike,
5132
+ zulu` is the order codepoint, case-folding and natural order all produce, so unlike
5133
+ the animations case nothing here refutes anything, and rigc refuses any skin-name
5134
+ pair the candidates could disagree about (R11).
5135
+
5136
+ ✅ **The two readings this replaces.** A pull request once called `skins` *measured
5137
+ preserved*, on the strength of a one-skin rig where a one-element array comes back
5138
+ in order whatever the editor does to it;
5139
+ [#544](https://github.com/firejune/rigc/issues/544) corrected that to *unmeasured*,
5140
+ and added that it could not be measured because the editor refused a four-skin rig
5141
+ on import without printing a word. Both readings were the same generalisation — *an
5142
+ editor does not move arrays* — off the three arrays that were measured, and the
5143
+ "without a word" was rigc's own harness discarding the editor's stderr, which had
5144
+ named the cause all along (§3.4.2).
5145
+
4914
5146
  ### 10.2 Draw order
4915
5147
 
4916
5148
  📗 **An overlap change is a draw-order key.** The draw order *"can be keyed"*, and
package/docs/FACE.md CHANGED
@@ -1182,7 +1182,10 @@ breaks the moment the two share a target — in the worked example both `turn` a
1182
1182
 
1183
1183
  ✅ **The editor half, measured.** This paragraph said *unknown* until the round
1184
1184
  trip was taken with `tools/editor_roundtrip.ts` on a licensed editor (data
1185
- version 4.3.26) against a 4.3.13 build of this worked example. What it found:
1185
+ version 4.3.26) against a 4.3.13 build of
1186
+ [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) — this
1187
+ subsection's worked case, not the `gallery/portrait` this page names at the top,
1188
+ which declares no constraints at all and so can carry no slider. What it found:
1186
1189
 
1187
1190
  - **Both sliders come back, and the parameter axis survives.** `additive`,
1188
1191
  `local`, `bone`, `property`, `from`, `max` and `scale` are identical field for
@@ -1194,7 +1197,11 @@ version 4.3.26) against a 4.3.13 build of this worked example. What it found:
1194
1197
  animation is an ordinal in the format, so `yaw -> "turn"` returned as
1195
1198
  `yaw -> "sweep"` — the first animation of the sorted list
1196
1199
  ([#535](https://github.com/firejune/rigc/issues/535)). rigc now emits
1197
- animations codepoint-ascending; on the same rig through the same editor that
1200
+ animations in the editor's own order — natural and case-insensitive
1201
+ ([#539](https://github.com/firejune/rigc/issues/539),
1202
+ [#543](https://github.com/firejune/rigc/issues/543)); this rig's names are ones
1203
+ a codepoint sort orders identically, which is what it emitted at the time of
1204
+ the round trip below. On the same rig through the same editor that
1198
1205
  restored `yaw -> "turn"` and took the re-rendered mean absolute error from
1199
1206
  10.4655 / 8.4961 / 8.7140 (`sweep` / `tilt` / `turn`) to
1200
1207
  0.3035 / 0.0769 / 0.0588, worst drift 16.535 px to 3.947 px.
@@ -1202,16 +1209,55 @@ version 4.3.26) against a 4.3.13 build of this worked example. What it found:
1202
1209
  `rotate: 1` is absent from the export, and an absent `rotate` parses as **0**
1203
1210
  (`SkeletonJson`), so the returned file states *drives nothing* rather than
1204
1211
  omitting a default — which is why `A23_PHYSICS_CONSTRAINT_EFFECTIVE` refuses it
1205
- by name. Independent of the ordering defect, and open as
1212
+ by name. Independent of the ordering defect, and filed as
1206
1213
  [#536](https://github.com/firejune/rigc/issues/536).
1214
+
1215
+ ✅ **Why, measured since.** It is not elision and not a defect in one field:
1216
+ the editor's physics model holds `x` and `y` and nothing else, with no limit on
1217
+ how many at once. Three rigs, twelve constraints, predictions written before the
1218
+ round trip — a lone `y` came back, `x` and `y` together came back, and a lone
1219
+ `rotate`, a lone `scaleX` and a lone `shearX` each came back as **no components
1220
+ at all**, with every constraint's fixed-point `strength` returning exactly so a
1221
+ silent harness failure could not read as a finding
1222
+ ([#540](https://github.com/firejune/rigc/issues/540)). ⇒ **A rotation-driven
1223
+ jiggle does not survive the editor, and no `scaleY` mode substitutes for it.**
1224
+ ⚠️ Still open on #536: whether the loss happens at import or at export. The
1225
+ project file's bytes cannot settle it — it carries derived float32s that no
1226
+ input declares — and the answer is invisible to an author either way.
1227
+
1228
+ ✅ **And the gate now says so before the trip, not after.** A face rig that is
1229
+ authored to come back out of the editor declares
1230
+ `"invariants": { "editorRoundTrip": true }` (AUTHORING §3.7) and
1231
+ `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` refuses the constraint by name at build
1232
+ time, with the fix in the message: drive it in `x`/`y`, or drop the declaration.
1233
+ ⚠️ A rig that declares nothing is **not** silent either — A41 SKIPs and the skip
1234
+ names the constraint and the component, which is the whole reason the rule is
1235
+ opt-in rather than default-off. rigc's own output was never wrong here: a
1236
+ rotation jiggle is valid Spine 4.3 that every runtime plays, and refusing it for
1237
+ everybody would be refusing correct data on behalf of one consumer. ⇒ A23 and
1238
+ A41 are the same loss from opposite sides of the trip: A41 fires on what goes
1239
+ in, A23 on what comes back.
1207
1240
  - 🔸 Unexplained: `diff` reports `animations.curve_kinds` moved on **196 of 200**
1208
1241
  keys in every round trip taken, the clean one included. Visually small once the
1209
1242
  ordering is fixed — but it is 98% of the keys, and *small* is not *explained*.
1210
1243
 
1211
- ⚠️ **What it still does not measure:** a rig carrying more than one skin or more
1212
- than one event, which is where the same shape — an ordinal into an object the
1213
- editor re-keys — could bite next (AUTHORING §10.1). The runtime half was never in
1214
- doubt: every figure above came back through `spine-core`.
1244
+ ✅ **The two things this paragraph said it still did not measure have since been
1245
+ measured, and they came out opposite ways** — [#544](https://github.com/firejune/rigc/issues/544)
1246
+ is the card for having left the sentence standing:
1247
+
1248
+ - **More than one event is safe.** The editor re-keys `events` the way it re-keys
1249
+ `animations` — `zebra, mike, alpha` came back `alpha, mike, zebra` — but every
1250
+ firing resolved **by name**, `0.3 -> mike` and `0.6 -> alpha`, payloads intact
1251
+ ([#539](https://github.com/firejune/rigc/issues/539)). The ordinal shape does
1252
+ *not* bite here, and rigc emits events in the order you declare them.
1253
+ - **More than one skin is worse than unmeasured.** A four-skin rig builds green,
1254
+ parses in `spine-core`, and the editor **refuses to import it** — no project
1255
+ file, no message ([#541](https://github.com/firejune/rigc/issues/541)). So there
1256
+ is no export to read, and every figure on this page was taken on a rig carrying
1257
+ exactly one skin (AUTHORING §10.1).
1258
+
1259
+ The runtime half was never in doubt: every figure above came back through
1260
+ `spine-core`.
1215
1261
 
1216
1262
  ---
1217
1263