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 +7 -1
- package/docs/AUTHORING.md +321 -28
- package/docs/SPEC_COVERAGE.md +10 -1
- package/package.json +2 -1
- package/src/compile.ts +515 -71
- package/src/rig.ts +10 -4
- package/src/types.ts +141 -16
- package/tools/editor_roundtrip.ts +225 -27
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(
|
|
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
|
|
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
|
|
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
|
|
4915
|
-
|
|
4916
|
-
|
|
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
|
|
4976
|
-
Read off its export of a rigc build (Spine 4.3.26,
|
|
4977
|
-
`animations` object, a skin's 24 `attachments` slot keys and
|
|
4978
|
-
and 2 bone-timeline keys all came back sorted, while the 30
|
|
4979
|
-
and 3 `constraints` — arrays — came back in the build's own
|
|
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
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
`skins[
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
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
|
|
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
|
|
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
|
|
package/docs/SPEC_COVERAGE.md
CHANGED
|
@@ -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
|
|
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.
|
|
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 .",
|