spine-rigc 0.20.1 → 0.20.3
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 +2 -1
- package/docs/AUTHORING.md +101 -0
- package/docs/FACE.md +169 -0
- package/docs/MOTION.md +52 -0
- package/package.json +1 -1
- package/src/compile.ts +284 -1
- package/src/types.ts +15 -1
package/README.md
CHANGED
|
@@ -443,7 +443,7 @@ limits: [AUTHORING.md §11](docs/AUTHORING.md). The parts it refuses because
|
|
|
443
443
|
something is drawn over them are `rigc chainfit`'s, once a candidate exists —
|
|
444
444
|
[§12](docs/AUTHORING.md).
|
|
445
445
|
|
|
446
|
-
## The gallery —
|
|
446
|
+
## The gallery — seven complete rigs over art that ships with them
|
|
447
447
|
|
|
448
448
|
Each directory in [`gallery/`](https://github.com/firejune/rigc/tree/main/gallery) is
|
|
449
449
|
one rig spec, one motion spec and the PNGs they name, small enough to read in one
|
|
@@ -460,6 +460,7 @@ was verified, and what writing it cost. Repository material: a clone and
|
|
|
460
460
|
| [`gallery/ride`](https://github.com/firejune/rigc/tree/main/gallery/ride) | `path` attachments + **path constraints** | A trolley coasting down a drawn rail and rolling back, driven by a `position` timeline, with `groups` + `stagger` keying the wheels and the ears |
|
|
461
461
|
| [`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait) | **deform `transform`** + `derive` group tracks | A 2.5D head turn: two meshes and six feature bones all keyed from one stated expression, `dx = x(cos t − 1) − z·sin t`, with the depths in the spec rather than a README |
|
|
462
462
|
| [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) | the **`pitch`** and **`wave`** transform kinds | A head bowing and two lop ears rippling, on three meshes each laid out for the closed form that moves it — a fold angle solved for before authoring, and a shear whose winding no amplitude can reverse |
|
|
463
|
+
| [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) | **`slider` constraints** | A head that turns because a **value** says so: two dials drive two sliders, and the rendered animation moves the needles rather than the face — with a depth map under the face mesh, a soft mask on the cowlick, and a slider range derived from the turn ceiling `build` reports rather than picked by eye |
|
|
463
464
|
|
|
464
465
|
<p align="center">
|
|
465
466
|
<img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/rigc-scene.gif" alt="A portrait rig breathing, glancing aside, then turning its head in 2.5D — hair and features sliding at different depths" width="600" />
|
package/docs/AUTHORING.md
CHANGED
|
@@ -564,6 +564,37 @@ behind it writes literal `x`/`y` instead.
|
|
|
564
564
|
|
|
565
565
|
**R9 — Nothing is written until every assertion is green.**
|
|
566
566
|
|
|
567
|
+
**R10 — The `animations` object is keyed in the editor's order, not in yours, and
|
|
568
|
+
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
|
|
570
|
+
reorders anything you wrote, and it is not cosmetic: a `slider`'s animation is a
|
|
571
|
+
**name** in JSON and an **ordinal** in the format's binary half, so an editor that
|
|
572
|
+
re-sorts the object repoints every slider whose animation moved index — silently,
|
|
573
|
+
in a file that still parses and still gates green (§3.5.2,
|
|
574
|
+
[#535](https://github.com/firejune/rigc/issues/535)). Nothing else moves: each
|
|
575
|
+
animation's own body is byte-identical either way, and every other collection is
|
|
576
|
+
emitted in the order you gave it.
|
|
577
|
+
|
|
578
|
+
⚠️ **Codepoint is not the editor's comparator.** The editor sorts **natural and
|
|
579
|
+
case-insensitive** — measured, two rigs, one axis each:
|
|
580
|
+
`Turn, sweep, wave` came back `sweep, Turn, wave`, and `turn10, turn2, zoom` came
|
|
581
|
+
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:
|
|
585
|
+
|
|
586
|
+
| Do not let two animation names differ | Because | Instead |
|
|
587
|
+
| --- | --- | --- |
|
|
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` |
|
|
590
|
+
| 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
|
+
|
|
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.
|
|
597
|
+
|
|
567
598
|
---
|
|
568
599
|
|
|
569
600
|
## 3. The rig spec, field by field
|
|
@@ -1442,6 +1473,35 @@ parser resolves it in a **second pass** over the constraints array, after the
|
|
|
1442
1473
|
animations are read, and a miss throws `Slider animation not found`; rigc refuses it
|
|
1443
1474
|
where the message can name both files.
|
|
1444
1475
|
|
|
1476
|
+
🔒 **And it is the one field an editor round trip can repoint under you, which is
|
|
1477
|
+
why R10 exists.** In JSON the reference is a name on both sides. In the format's
|
|
1478
|
+
binary half it is an **ordinal** — `constraint.animation = animations[readInt()]`
|
|
1479
|
+
(`SkeletonBinary`) — so an editor holding that ordinal writes back whichever
|
|
1480
|
+
animation now stands at the position. `gallery/look` went into a licensed editor
|
|
1481
|
+
(data version 4.3.26) as `turn, tilt, sweep` with `yaw -> "turn"` and came back
|
|
1482
|
+
`sweep, tilt, turn` with **`yaw -> "sweep"`**: a file that parses, gates green and
|
|
1483
|
+
applies the wrong animation. ⭐ Its second slider is what named the mechanism
|
|
1484
|
+
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
|
|
1486
|
+
moves no index ([#535](https://github.com/firejune/rigc/issues/535)); on the same
|
|
1487
|
+
rig through the same editor that restored `yaw -> "turn"` and took the
|
|
1488
|
+
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.
|
|
1504
|
+
|
|
1445
1505
|
⚠️ **The fields of the model you did not choose are refused, not ignored.** The
|
|
1446
1506
|
parser reads `time` only in the bone-less branch and `property`/`from`/`to`/`scale`/
|
|
1447
1507
|
`max`/`local` only in the other, so the losing half would be data no runtime ever
|
|
@@ -1668,6 +1728,13 @@ rather than 1 s. With `loop: false` that is the last frame and harmless, with
|
|
|
1668
1728
|
one — pick `to`/`scale` so the endpoint lands **inside** the duration rather than
|
|
1669
1729
|
exactly on it.
|
|
1670
1730
|
|
|
1731
|
+
🖼️ **Worked example: [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look)** — a face
|
|
1732
|
+
whose yaw and pitch are two sliders sharing one bone, both `local: true` and both
|
|
1733
|
+
`additive: true`, with each range derived from the turn ceiling `build` reports
|
|
1734
|
+
for that mesh (§3.4) rather than chosen. [`docs/FACE.md`](FACE.md) §8's *The turn
|
|
1735
|
+
as a value rather than a time* states that derivation as a rule, and its §7 is
|
|
1736
|
+
what a slider does to a face's channel allocation.
|
|
1737
|
+
|
|
1671
1738
|
### 3.6 `events` — names the animation can fire
|
|
1672
1739
|
|
|
1673
1740
|
**When you need one:** something outside the skeleton has to happen on a
|
|
@@ -3266,6 +3333,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
3266
3333
|
| `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
|
|
3267
3334
|
| `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 |
|
|
3268
3335
|
| `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)) |
|
|
3269
3337
|
|
|
3270
3338
|
### 5.2 Assertions — the gate
|
|
3271
3339
|
|
|
@@ -4810,6 +4878,39 @@ low figure as a miss — say in the log that the art did not carry them.
|
|
|
4810
4878
|
`default`"* and *"bones are ordered so that the parent always comes before a child
|
|
4811
4879
|
bone"* — [JSON format](http://esotericsoftware.com/spine-json-format). §3.4.
|
|
4812
4880
|
|
|
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.
|
|
4887
|
+
|
|
4888
|
+
⚠️ **The order it sorts them into is natural and case-insensitive, not
|
|
4889
|
+
codepoint.** This paragraph said codepoint until
|
|
4890
|
+
[#539](https://github.com/firejune/rigc/issues/539) measured it: `Turn, sweep,
|
|
4891
|
+
wave` came back `sweep, Turn, wave` and `turn10, turn2, zoom` came back
|
|
4892
|
+
`turn2, turn10, zoom`. The corpus says the same thing and always did — of its 105
|
|
4893
|
+
name-keyed collections, 102 are consistent with a codepoint sort and **3 are
|
|
4894
|
+
not**: `spineboy-pro.json` keys `portal-flare9` *before* `portal-flare10`, which
|
|
4895
|
+
no codepoint sort produces. Every natural comparator reproduces all 105. (The
|
|
4896
|
+
population is every object the format keys by a name and that carries more than
|
|
4897
|
+
one key, deform blocks counted at each of their three levels;
|
|
4898
|
+
[`src/compile.ts`](../src/compile.ts) states it in full beside
|
|
4899
|
+
`editorAnimationOrder`, so the count can be re-taken rather than trusted.)
|
|
4900
|
+
⇒ in rigc: only `animations` is emitted sorted (R10), because it is the one
|
|
4901
|
+
object measured here whose ORDER is also an index space — every reference into
|
|
4902
|
+
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.
|
|
4907
|
+
|
|
4908
|
+
✅ **`events` is re-keyed too, and the references into it survive it.** The same
|
|
4909
|
+
session measured it: `zebra, mike, alpha` came back `alpha, mike, zebra`, and the
|
|
4910
|
+
firings still resolved **by name** — `0.3 -> mike`, `0.6 -> alpha`, payloads
|
|
4911
|
+
intact (#539). So the editor treats `events` and `animations` differently, and
|
|
4912
|
+
rigc emits events in the order you declare them.
|
|
4913
|
+
|
|
4813
4914
|
### 10.2 Draw order
|
|
4814
4915
|
|
|
4815
4916
|
📗 **An overlap change is a draw-order key.** The draw order *"can be keyed"*, and
|
package/docs/FACE.md
CHANGED
|
@@ -40,6 +40,12 @@ hold its results.
|
|
|
40
40
|
measures §5's foreshortening at **0.863–1.176** against the yaw's 0.892–1.064
|
|
41
41
|
below — a wider span at the same 12°, because a face is taller than it is deep.
|
|
42
42
|
Read it after this page, not instead of it
|
|
43
|
+
- **The turn driven by a value instead of played as a time** — the same keys, on
|
|
44
|
+
an axis: §8's *The turn as a value rather than a time* derives the range,
|
|
45
|
+
**AUTHORING §3.5.2** is the `slider` constraint that does it, and
|
|
46
|
+
[`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) is
|
|
47
|
+
the worked case. Read it when the angle has to follow something outside the
|
|
48
|
+
animation — a pointer, a gaze target, a game value
|
|
43
49
|
|
|
44
50
|
🚨 **A `deform` key's winding is gated; how far it moved the geometry is not, and
|
|
45
51
|
that half is the one you have to author around.** Since 2026-09-03
|
|
@@ -72,6 +78,7 @@ internal shape:
|
|
|
72
78
|
| a face that **blinks**, **breathes**, **looks around** | ordinary MOTION.md tracks. A lid, a chest, an iris. Nothing on this page is needed |
|
|
73
79
|
| a face that **turns** | a list of `(x, z)` — every part's position across the screen and its **depth** — plus one line of arithmetic evaluated at each of them |
|
|
74
80
|
| a face that turns **far** (a three-quarter view) | ⛔ a different rig, and §8 says where the line is. Not a format problem: a parts-and-labour problem |
|
|
81
|
+
| a face that turns **by however much something outside it says** — a pointer, a gaze target, a game value | the same list and the same arithmetic, reached by a **value** instead of by a playhead: the turn animation becomes a `slider`'s lookup table (§8's last subsection, AUTHORING §3.5.2). What changes is not the geometry, it is the range — which stops being a choice and becomes a measurement |
|
|
75
82
|
|
|
76
83
|
⭐ **The turn is the only part of a face that is not already MOTION.md's job**,
|
|
77
84
|
and it is 90% of this page. A blink is a translating plate (§6); a gaze is
|
|
@@ -926,6 +933,22 @@ stack (`headroll_idle` under `headroll_layer`) — but both are runtime or rig
|
|
|
926
933
|
decisions the motion spec cannot express, so nothing warns an author that two of
|
|
927
934
|
their animations will fight.**
|
|
928
935
|
|
|
936
|
+
🚨 **A `slider` is a third way for two animations to meet on one property, and it
|
|
937
|
+
is an overwrite rather than a blend — so allocate it in this table too.** An
|
|
938
|
+
animation a slider applies (AUTHORING §3.5.2) is never on a track: the constraint
|
|
939
|
+
applies it every frame, at whatever time its own bone currently points at. At
|
|
940
|
+
`mix: 1` with `additive` left at its default that apply **writes the property
|
|
941
|
+
outright**, which erases both any earlier slider on the same property and the
|
|
942
|
+
**playing** animation on the bones its animation keys — including at its own
|
|
943
|
+
neutral, where it looks switched off. ⇒ Every face axis that shares a target
|
|
944
|
+
declares `"additive": true`, and unlike the two collisions above this one **is**
|
|
945
|
+
gated: `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` names the bone, the property,
|
|
946
|
+
every slider keying it in array order and which one wins today. ⛔ The one case
|
|
947
|
+
`additive` cannot rescue is a **slot colour, an attachment swap, a draw order or
|
|
948
|
+
a sequence**: those timelines ignore the flag entirely, so a fade — §8's way of
|
|
949
|
+
taking a part off the screen before its own ceiling — belongs *inside* the single
|
|
950
|
+
animation one slider applies, never in a second slider beside it.
|
|
951
|
+
|
|
929
952
|
⛔ **And the cost is real, so name it rather than discovering it by shipping.** In
|
|
930
953
|
the worked example `idle` keys **nothing** on the iris, on purpose, even though a
|
|
931
954
|
completely still eye reads as a mannequin. The iris is `gaze`'s channel; an
|
|
@@ -1060,6 +1083,136 @@ authoring concept**: the on-axis pair's shared `cos t` now falls out of the same
|
|
|
1060
1083
|
closed form as everybody else's value, so `axis` is gone from the spec while
|
|
1061
1084
|
`look_l`/`look_r` stay — because those two really are one shared number (§5).
|
|
1062
1085
|
|
|
1086
|
+
### The turn as a value rather than a time
|
|
1087
|
+
|
|
1088
|
+
Everything above prices a turn as an animation somebody plays. The same geometry
|
|
1089
|
+
also runs on an **axis**: a `slider` constraint reads a driving bone, maps that
|
|
1090
|
+
bone's rotation to a time inside the turn animation, and applies the animation
|
|
1091
|
+
there (AUTHORING §3.5.2). Nothing in §1–§5 changes — the keys are still §1's line
|
|
1092
|
+
evaluated at each angle — but what selects among them is a **value** rather than a
|
|
1093
|
+
playhead, so the face follows a number somebody else is holding: a pointer, a gaze
|
|
1094
|
+
target, a game state.
|
|
1095
|
+
|
|
1096
|
+
⭐ **The object offers a dial and does not decide when it turns.** That is the
|
|
1097
|
+
whole of the claim and it is deliberately not a larger one: what the rig
|
|
1098
|
+
guarantees is the axis — its range, its arithmetic, and that every angle on it is
|
|
1099
|
+
sound — and what moves the dial belongs to whoever is using the face. The
|
|
1100
|
+
paragraphs below are the part that is ours.
|
|
1101
|
+
|
|
1102
|
+
#### The range stops being a choice and becomes a measurement
|
|
1103
|
+
|
|
1104
|
+
This is the half an author has no other way to get right. §4.2's fold angle is not
|
|
1105
|
+
a rule of thumb once the angle is a dial position: it is the **top of the dial**,
|
|
1106
|
+
because past it a triangle turns inside out and `A39` refuses the build by name.
|
|
1107
|
+
`build` prints that angle for every depth mesh it compiles (AUTHORING §3.4), so
|
|
1108
|
+
the whole mapping falls out of one reading:
|
|
1109
|
+
|
|
1110
|
+
```
|
|
1111
|
+
ceiling the largest turn this depth mesh admits — printed by `build`, not guessed
|
|
1112
|
+
range the largest whole degree strictly INSIDE the ceiling
|
|
1113
|
+
from -range max +range local true (AUTHORING §3.5.2's circle)
|
|
1114
|
+
scale seconds per degree — the one number here you choose
|
|
1115
|
+
duration 2 x range x scale
|
|
1116
|
+
time to + (degrees - from) x scale the slider's own mapping
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
📐 **Worked, on [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look)**,
|
|
1120
|
+
whose face is a 21 × 9 `grid` over one depth sheet:
|
|
1121
|
+
|
|
1122
|
+
```bash
|
|
1123
|
+
bun cli.ts build --rig gallery/look/rig.json \
|
|
1124
|
+
--motion gallery/look/motion.json \
|
|
1125
|
+
--out gallery/look/build
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
```
|
|
1129
|
+
MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
|
|
1130
|
+
depth "face_depth.png" bf156ea0cfc970a3 near=white zScale=194 z=[0, 194]
|
|
1131
|
+
80 of 189 vertices sample a texel the part image does not draw — their z is the sheet's reading of somewhere the part is not
|
|
1132
|
+
turn ceiling yaw +19.32° / -19.32° pitch +22.92° / -26.94°
|
|
1133
|
+
1st pct yaw +19.32° x1.000 of 80 / -19.32° x1.000 of 80 pitch +22.92° x1.000 of 102 / -26.94° x1.000 of 130
|
|
1134
|
+
first to fold: yaw + at 19.32°, triangle 174 [119,138,139], the sheet steps 28.50 level(s) across it, which is 0.112 of the range this mesh sampled
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
⇒ the ceiling is **±19.32°**, so the range is **19** — `floor(19.32)`, and
|
|
1138
|
+
*strictly inside* is the whole of the rule. At the **0.05 s per degree** that rig
|
|
1139
|
+
chooses, `turn` runs `2 × 19 × 0.05` = **1.9 s** and the map is
|
|
1140
|
+
`time = 0 + (degrees + 19) × 0.05`, which is the constraint as its rig spec
|
|
1141
|
+
declares it: `"from": -19, "to": 0, "scale": 0.05, "max": 19, "local": true,
|
|
1142
|
+
"additive": true`. ⭐ **The only two numbers there that anybody chose are `scale`
|
|
1143
|
+
and `to`** — and `to: 0` says nothing more than *the bottom of the range is the
|
|
1144
|
+
animation's first frame*. `from`, `max` and the duration are all the ceiling.
|
|
1145
|
+
|
|
1146
|
+
🔸 **And `scale` is chosen for the endpoint.** rigc rounds every number it emits
|
|
1147
|
+
to six decimals, so a `scale` that is not exact there moves the top of the dial:
|
|
1148
|
+
`1/60` ships as `0.016667`, and a 60° turn then applies at 1.00002 s rather than
|
|
1149
|
+
1 s — the last frame under `loop: false`, the *first* under `loop: true`
|
|
1150
|
+
(AUTHORING §3.5.2). `0.05` is exact at six decimals, which is the only reason the
|
|
1151
|
+
example can put its endpoint exactly on the duration. Pick a `scale` that is not,
|
|
1152
|
+
and land the endpoint inside the duration instead.
|
|
1153
|
+
|
|
1154
|
+
⚠️ **The ceiling is per mesh, and the face's is not the smallest one on the
|
|
1155
|
+
face.** The same run prints one for every depth mesh, and in this example each
|
|
1156
|
+
sidelock reads `yaw +17.04° / -45.80°`: it folds at **17.04°** on one side, which
|
|
1157
|
+
is *inside* the ±19° the face itself admits, and not until 45.80° on the other.
|
|
1158
|
+
The asymmetry is the sheet's, not a coincidence — each of those sheets is
|
|
1159
|
+
steepest at the edge where the strand curves away, and a yaw folds a pair of
|
|
1160
|
+
vertices only in the direction their depth is rising.
|
|
1161
|
+
|
|
1162
|
+
⇒ **A part whose ceiling is lower than the range has to be gone before the turn
|
|
1163
|
+
reaches it.** That is AUTHORING §3.4's third way to live with a ceiling: fade the
|
|
1164
|
+
slot to alpha 0 **inside the animation the slider applies**, landing the alpha-0
|
|
1165
|
+
key *before* the key that folds rather than on it. §7's paragraph on sliders is
|
|
1166
|
+
why that fade cannot be a second slider.
|
|
1167
|
+
|
|
1168
|
+
⭐ **A lookup table wants linear keys, and that is not a style note.** The slider
|
|
1169
|
+
makes the pose a function of the dial, so an easing curve between two keys makes
|
|
1170
|
+
it a **non-linear** function of a number the consumer may be holding perfectly
|
|
1171
|
+
still — the face would drift and settle while the value sits where it was put.
|
|
1172
|
+
Anticipation and follow-through fail for the same reason, not a weaker one
|
|
1173
|
+
(MOTION §3.6, §3.7): both are functions of time, and there is no time on this
|
|
1174
|
+
axis. [MOTION §0.1](MOTION.md) is that split written out, and it is also where the
|
|
1175
|
+
shaping *does* belong — in whatever animation moves the dial.
|
|
1176
|
+
|
|
1177
|
+
⚠️ **Two axes on one face need `"additive": true` on both.** A `pitch` dial
|
|
1178
|
+
beside the `yaw` is the ordinary case and it is the one the format's default
|
|
1179
|
+
breaks the moment the two share a target — in the worked example both `turn` and
|
|
1180
|
+
`tilt` key `headroll`. §7's paragraph on sliders is the mechanism and
|
|
1181
|
+
`A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` is the refusal.
|
|
1182
|
+
|
|
1183
|
+
✅ **The editor half, measured.** This paragraph said *unknown* until the round
|
|
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:
|
|
1186
|
+
|
|
1187
|
+
- **Both sliders come back, and the parameter axis survives.** `additive`,
|
|
1188
|
+
`local`, `bone`, `property`, `from`, `max` and `scale` are identical field for
|
|
1189
|
+
field, and the two keep their places in the `constraints` array. `mix: 1` and
|
|
1190
|
+
`to: 0` are dropped, and those are the format's own defaults (`SkeletonJson`
|
|
1191
|
+
reads `mix` as 1 and `to` as 0 when absent) — an elision, not a loss.
|
|
1192
|
+
- ⚠️ **The animation each slider *names* did not come back, until rigc changed
|
|
1193
|
+
what it emits.** The editor re-sorts the `animations` object and a slider's
|
|
1194
|
+
animation is an ordinal in the format, so `yaw -> "turn"` returned as
|
|
1195
|
+
`yaw -> "sweep"` — the first animation of the sorted list
|
|
1196
|
+
([#535](https://github.com/firejune/rigc/issues/535)). rigc now emits
|
|
1197
|
+
animations codepoint-ascending; on the same rig through the same editor that
|
|
1198
|
+
restored `yaw -> "turn"` and took the re-rendered mean absolute error from
|
|
1199
|
+
10.4655 / 8.4961 / 8.7140 (`sweep` / `tilt` / `turn`) to
|
|
1200
|
+
0.3035 / 0.0769 / 0.0588, worst drift 16.535 px to 3.947 px.
|
|
1201
|
+
- 🚨 **The physics constraint on the cowlick comes back driving nothing.**
|
|
1202
|
+
`rotate: 1` is absent from the export, and an absent `rotate` parses as **0**
|
|
1203
|
+
(`SkeletonJson`), so the returned file states *drives nothing* rather than
|
|
1204
|
+
omitting a default — which is why `A23_PHYSICS_CONSTRAINT_EFFECTIVE` refuses it
|
|
1205
|
+
by name. Independent of the ordering defect, and open as
|
|
1206
|
+
[#536](https://github.com/firejune/rigc/issues/536).
|
|
1207
|
+
- 🔸 Unexplained: `diff` reports `animations.curve_kinds` moved on **196 of 200**
|
|
1208
|
+
keys in every round trip taken, the clean one included. Visually small once the
|
|
1209
|
+
ordering is fixed — but it is 98% of the keys, and *small* is not *explained*.
|
|
1210
|
+
|
|
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`.
|
|
1215
|
+
|
|
1063
1216
|
---
|
|
1064
1217
|
|
|
1065
1218
|
## 9. Looking at it, and the audit gap
|
|
@@ -1580,6 +1733,22 @@ cost off the keyboard and onto the **parts**: per-eye meshes, a meshed neck, a
|
|
|
1580
1733
|
second art layer for the far cheek (§8). Whether to pay *that* is a project's
|
|
1581
1734
|
decision and this page does not make it.
|
|
1582
1735
|
|
|
1736
|
+
🚫 **No Live2D file is read or written, and none ever will be — a boundary
|
|
1737
|
+
rather than an unbuilt feature, and it runs in both directions.** rigc's inputs
|
|
1738
|
+
are a rig spec and a motion spec; its outputs are Spine 4.3 skeleton data and an
|
|
1739
|
+
atlas. There is no importer, no exporter and no converter for `.moc3`, `.cmo3`,
|
|
1740
|
+
`.model3.json` or anything else in that family, and nothing in this repository
|
|
1741
|
+
claims compatibility with that format in either direction
|
|
1742
|
+
([#399](https://github.com/firejune/rigc/issues/399) is where that was settled).
|
|
1743
|
+
⭐ **What is in scope is an authoring idea, stated on its own terms rather than
|
|
1744
|
+
as anybody's feature: that a face angle can be a value rather than a time.**
|
|
1745
|
+
§8's *The turn as a value rather than a time* is that idea on Spine's own
|
|
1746
|
+
`slider` constraint, and every mechanism under it is Spine's — the arithmetic,
|
|
1747
|
+
the flags, the readers and the failure modes are all in AUTHORING §3.5.2 and all
|
|
1748
|
+
measured against `spine-core`. ⚠️ Nothing on this page is a statement about how
|
|
1749
|
+
any other tool works inside, and nothing above implies one: what this repository
|
|
1750
|
+
has measured is its own format.
|
|
1751
|
+
|
|
1583
1752
|
🚫 **No per-eye mesh recipe.** §8 says the eyes need their own deform meshes past
|
|
1584
1753
|
about 26°, and nobody has built that here. The column-placement arithmetic in
|
|
1585
1754
|
§4.2 applies to any grid over any curved patch, so the tangent limit is the part
|
package/docs/MOTION.md
CHANGED
|
@@ -80,6 +80,52 @@ and the last hop is the one that matters: a `both-unacceptable` tie means **prop
|
|
|
80
80
|
again from a different axis** (§4), not *nudge the same candidate*. §5 has the
|
|
81
81
|
detail.
|
|
82
82
|
|
|
83
|
+
### 0.1 When the axis is not time — an animation a `slider` applies
|
|
84
|
+
|
|
85
|
+
⛔ **One shape of request does not normalise to the table above, and carrying it
|
|
86
|
+
through §3 anyway produces a defect nothing on this page can measure.** A `slider`
|
|
87
|
+
constraint (AUTHORING §3.5.2) applies an animation as a function of a **value**: it
|
|
88
|
+
reads a driving bone's transform property, maps it with
|
|
89
|
+
`time = to + (value − from) × scale`, and applies the animation at that time on
|
|
90
|
+
every frame. What it applies is an ordinary animation in the motion spec — same
|
|
91
|
+
tracks, same keys, the same `duration` — but **nothing plays it**. It is a lookup
|
|
92
|
+
table, and `t` in it is a coordinate on the axis rather than a moment.
|
|
93
|
+
|
|
94
|
+
⇒ **So the constructs in §3 that are functions of time are not available to it,
|
|
95
|
+
and each fails in its own way rather than merely reading oddly:**
|
|
96
|
+
|
|
97
|
+
| §3 construct | On an animation a slider applies |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| §3.4 slow in and slow out | an easing curve makes the pose a **non-linear** function of the dial. The consumer moves the value at one rate and the face moves at another, and at a value held still the pose is still whatever the curve says there |
|
|
100
|
+
| §3.6 anticipation | places a counter-pose at a dial *position*, so the pose runs backwards while the value runs forwards. Nothing anticipates a number |
|
|
101
|
+
| §3.8 overshoot and settle | there is no settle: at a held value the pose is what the table says at that value, indefinitely. An overshoot keyed past the extreme is just a wrong pose at the top of the dial |
|
|
102
|
+
| §3.7 follow-through and the offset table | wants parts to arrive at different **times**. Here they differ by **amount** at every value, which is §3.7.1's construct and not this one |
|
|
103
|
+
| §3.3 timing | the key times are the axis's own coordinates — one per angle, position or level the table states — so spacing them is choosing where to sample, not choosing a rhythm |
|
|
104
|
+
|
|
105
|
+
⭐ **What is still this page's job is whatever moves the dial**, and that is an
|
|
106
|
+
ordinary animation with §3 applying to it unchanged. The split is visible in the
|
|
107
|
+
worked example: in
|
|
108
|
+
[`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) the two
|
|
109
|
+
lookup tables `turn` and `tilt` carry **no easing at all**, and `sweep` — the one
|
|
110
|
+
animation there meant to be played — carries every `ease` in the file.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bun -e 'const m = JSON.parse(await Bun.file("gallery/look/motion.json").text());
|
|
114
|
+
for (const [name, a] of Object.entries(m.animations))
|
|
115
|
+
console.log(name, (JSON.stringify(a).match(/"ease"/g) ?? []).length);'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
⚠️ **And two of §4's candidate axes stop being axes.** *Anticipation* and
|
|
119
|
+
*Termination* are both readings of how a movement is placed in time, so a ballot
|
|
120
|
+
spread on either of them over a slider-applied animation is asking a person to
|
|
121
|
+
choose between two wrong answers. The axes that survive are the ones about
|
|
122
|
+
**amount** — *Part amount*, *Path*, *Key density* — because those are still
|
|
123
|
+
readings of the value.
|
|
124
|
+
|
|
125
|
+
📘 The face case is worked end to end in [FACE.md](FACE.md): its §8 derives the
|
|
126
|
+
slider's range from the turn ceiling `build` reports, which is the one number on
|
|
127
|
+
that axis an author cannot guess.
|
|
128
|
+
|
|
83
129
|
---
|
|
84
130
|
|
|
85
131
|
## 1. Prompt grammar — what a request is made of
|
|
@@ -259,6 +305,12 @@ would not exist even if the user had more pictures of the same two poses. Everyt
|
|
|
259
305
|
below is therefore authored knowledge, and it is sourced the way AUTHORING §10 sourced
|
|
260
306
|
the editor's conventions.
|
|
261
307
|
|
|
308
|
+
⚠️ **All of it assumes the axis is time.** If what you are authoring is an
|
|
309
|
+
animation a `slider` applies — a face angle, a dial, a suspension that compresses
|
|
310
|
+
as the wheel rises — §0.1 is the exception, and it is not a small one: the easing,
|
|
311
|
+
the anticipation and the overshoot below each produce a specific defect there
|
|
312
|
+
rather than merely reading oddly.
|
|
313
|
+
|
|
262
314
|
### 3.1 Where these come from, and how each line is marked
|
|
263
315
|
|
|
264
316
|
Two public bodies of material, and nothing else: **the twelve basic principles of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
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": {
|
package/src/compile.ts
CHANGED
|
@@ -136,6 +136,283 @@ export const SPINE_VERSION = '4.3.13';
|
|
|
136
136
|
|
|
137
137
|
const FRAME = 1 / 60;
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The order the emitted `animations` object is keyed in: **codepoint-ascending
|
|
141
|
+
* by name**, which is the order the Spine editor writes it back out in.
|
|
142
|
+
*
|
|
143
|
+
* ## Why the emitter has an opinion about this at all
|
|
144
|
+
*
|
|
145
|
+
* A `slider` constraint names the animation it applies, and in JSON that is a
|
|
146
|
+
* name on both sides. The **binary** format is where the same reference is an
|
|
147
|
+
* ordinal — `SkeletonBinary.js`: `constraint.animation = animations[readInt()]`
|
|
148
|
+
* — and an editor whose own model holds that ordinal reads the name at import
|
|
149
|
+
* and writes back whatever now stands at that position. Round-tripped through a
|
|
150
|
+
* licensed editor (data version 4.3.26), `gallery/look` went in as
|
|
151
|
+
* `turn, tilt, sweep` with `yaw -> "turn"` and came back as `sweep, tilt, turn`
|
|
152
|
+
* with **`yaw -> "sweep"`** (issue #535). Nothing in the returned file says so;
|
|
153
|
+
* it parses, it validates, and it applies the wrong animation.
|
|
154
|
+
*
|
|
155
|
+
* ⭐ The second slider is the control that names the mechanism rather than a
|
|
156
|
+
* second victim: `tilt` survived because it sat at index 1 in *both* orderings
|
|
157
|
+
* and index 1 is called `tilt` in both.
|
|
158
|
+
*
|
|
159
|
+
* ⇒ Emitting in the editor's own order makes its re-sort a no-op, so no index
|
|
160
|
+
* moves and no reference is repointed. Measured on the same rig through the
|
|
161
|
+
* same editor: mean absolute error over the re-rendered frames fell from
|
|
162
|
+
* 10.4655 / 8.4961 / 8.7140 to 0.3035 / 0.0769 / 0.0588, and `yaw -> "turn"`
|
|
163
|
+
* came back intact.
|
|
164
|
+
*
|
|
165
|
+
* ## Codepoint is NOT the editor's comparator, and this is why it is still what
|
|
166
|
+
* is emitted
|
|
167
|
+
*
|
|
168
|
+
* ⚠️ This comment used to say that codepoint order "is what every editor-authored
|
|
169
|
+
* file on hand is in", and that sentence was false when it was written. The
|
|
170
|
+
* counterexample ships with the tests: `examples/spineboy/export/spineboy-pro.json`
|
|
171
|
+
* keys `portal-flare9` **before** `portal-flare10` — in its default skin's
|
|
172
|
+
* attachment map and in two of `portal`'s timeline maps — and no codepoint sort
|
|
173
|
+
* produces that.
|
|
174
|
+
*
|
|
175
|
+
* 🔢 The survey behind that, stated so it can be re-taken rather than believed:
|
|
176
|
+
* over the 12 skeletons in `examples/`, take **every object the format keys by a
|
|
177
|
+
* NAME that carries more than one key** — `animations` and `events`; each skin's
|
|
178
|
+
* `attachments` map and each per-slot map inside it; each animation's `bones`,
|
|
179
|
+
* `slots`, `ik`, `transform`, `path`, `physics`, `events`, and its deform
|
|
180
|
+
* `attachments` at **all three** of its levels (skin, then slot, then attachment
|
|
181
|
+
* name). Arrays are excluded because the editor leaves them alone. That is
|
|
182
|
+
* **105** collections, of which **102** are consistent with a codepoint sort and
|
|
183
|
+
* **3** are not; all 12 natural comparators and all 8 `Intl.Collator`
|
|
184
|
+
* configurations tried reproduce every one of the 105.
|
|
185
|
+
*
|
|
186
|
+
* ⚠️ The deform clause is the whole of what makes it 105 rather than 104, and it
|
|
187
|
+
* is one collection: `animations.hoverboard.attachments.default` in
|
|
188
|
+
* `spineboy-pro.json`, whose four keys are slot names. Counting the skin level
|
|
189
|
+
* of a deform block and stopping there needs an exception the sentence above
|
|
190
|
+
* cannot state — every level of it is keyed by a name, and the editor re-keys
|
|
191
|
+
* each one — so the rule is "every level", and that collection is in.
|
|
192
|
+
*
|
|
193
|
+
* The editor was then measured directly (issue #539). Two rigs, each varying one
|
|
194
|
+
* axis: `Turn, sweep, wave` came back `sweep, Turn, wave`, and
|
|
195
|
+
* `turn10, turn2, zoom` came back `turn2, turn10, zoom`. The intersection leaves
|
|
196
|
+
* one hypothesis — **natural order, case-insensitive**.
|
|
197
|
+
*
|
|
198
|
+
* ⇒ So why not sort that way? Because "natural, case-insensitive" is a family of
|
|
199
|
+
* comparators rather than one. Leading zeros (`turn01` against `turn1`), a pure
|
|
200
|
+
* case tie (`Turn` against `turn`), whether a digit run sorts before a word, and
|
|
201
|
+
* what a separator is worth are each a free choice, and **the editor's answers to
|
|
202
|
+
* them are not measured**. Writing a comparator means choosing all four, and a
|
|
203
|
+
* chosen-but-unmeasured comparator is exactly how #537 landed.
|
|
204
|
+
*
|
|
205
|
+
* 🔒 It is also the one claim in the emitter that no gate can see. spine-core
|
|
206
|
+
* reads back everything else rigc writes; it does not sort, so the emitted key
|
|
207
|
+
* order has no oracle behind it. What rigc *can* check, with no editor and no
|
|
208
|
+
* comparator, is much smaller and is the whole of what matters: whether the names
|
|
209
|
+
* in front of it are a set on which every candidate comparator agrees. On such a
|
|
210
|
+
* set codepoint **is** the editor's order, whatever the editor's comparator turns
|
|
211
|
+
* out to be — and on every other set the build stops with both names in the
|
|
212
|
+
* message. See `orderTurnsOnTheComparator`.
|
|
213
|
+
*
|
|
214
|
+
* Codepoint also stays locale-independent, which `A18` requires: `localeCompare`
|
|
215
|
+
* would make the emitted bytes a property of the machine.
|
|
216
|
+
*/
|
|
217
|
+
function editorAnimationOrder<T>(animations: Record<string, T>): Record<string, T> {
|
|
218
|
+
// One definition of "the order rigc emits", shared with the check below, so the
|
|
219
|
+
// two cannot drift into checking different things.
|
|
220
|
+
const names = Object.keys(animations).sort(codepoint);
|
|
221
|
+
refuseNamesTheEditorCouldKeyDifferently(names);
|
|
222
|
+
const ordered: Record<string, T> = {};
|
|
223
|
+
for (const name of names) ordered[name] = animations[name];
|
|
224
|
+
return ordered;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The characters whose relative order every candidate comparator agrees on: the
|
|
229
|
+
* digits and, once case is folded, the lower-case ASCII letters. Everything else
|
|
230
|
+
* — `-`, `_`, a space, an accented letter — is worth something different to a
|
|
231
|
+
* collator than to a codepoint compare, so a pair those decide is not settled.
|
|
232
|
+
*/
|
|
233
|
+
const SETTLED_CHARS = /[0-9a-z]/;
|
|
234
|
+
|
|
235
|
+
/** Maximal runs of digits and of non-digits, which is what "natural" compares. */
|
|
236
|
+
const runsOf = (name: string): string[] => name.match(/\d+|\D+/g) ?? [];
|
|
237
|
+
|
|
238
|
+
const sign = (n: number): number => (n < 0 ? -1 : n > 0 ? 1 : 0);
|
|
239
|
+
const codepoint = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
240
|
+
|
|
241
|
+
/** What made a pair's order a matter of opinion, and what the author has to do. */
|
|
242
|
+
interface Ambiguity {
|
|
243
|
+
kind: 'case' | 'number' | 'separator';
|
|
244
|
+
because: string;
|
|
245
|
+
repair: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Whether these two names can be ordered two ways — `null` when every comparator
|
|
250
|
+
* the editor could be using puts them in the order rigc emits them in.
|
|
251
|
+
*
|
|
252
|
+
* ## The predicate, and why it is a certificate rather than a hazard list
|
|
253
|
+
*
|
|
254
|
+
* Issue #539 proposed checking for "two names differing only in case, or sharing
|
|
255
|
+
* a prefix followed by digit runs of unequal length". **That list misses the rig
|
|
256
|
+
* that produced the measurement**: `Turn` and `sweep` differ in much more than
|
|
257
|
+
* case and carry no digits, and they are the pair the editor reordered. A list of
|
|
258
|
+
* hazards is open-ended — one was missing the day it was written — so what is
|
|
259
|
+
* implemented is the other direction: a pair is refused unless the position that
|
|
260
|
+
* decides it is one every candidate comparator must read the same way.
|
|
261
|
+
*
|
|
262
|
+
* There are exactly three ways to lose that, and each is a free choice in some
|
|
263
|
+
* real comparator:
|
|
264
|
+
*
|
|
265
|
+
* - **case** — folding reverses them (`Turn` before `sweep` by codepoint, after it
|
|
266
|
+
* folded), or they fold together and only a tie-break separates them.
|
|
267
|
+
* - **number** — a digit run decides, and reading it as text disagrees with
|
|
268
|
+
* reading it as a number (`turn10` before `turn2`), or two runs are the same
|
|
269
|
+
* number written two ways (`turn01`, `turn1`), or a run meets a word and
|
|
270
|
+
* comparators differ on which sorts first.
|
|
271
|
+
* - **separator** — the deciding character is neither a letter nor a digit. This
|
|
272
|
+
* one covers two adversaries at once: a collator that treats `-` or a space as
|
|
273
|
+
* ignorable, and the plain fact that `_` sits *between* `Z` and `a`, so
|
|
274
|
+
* `x.toUpperCase()` and `x.toLowerCase()` order `wave_x` against `wavea`
|
|
275
|
+
* oppositely. Neither name needs a capital in it for that to bite.
|
|
276
|
+
*
|
|
277
|
+
* ## What it was tested against
|
|
278
|
+
*
|
|
279
|
+
* 22 comparators — 12 hand-rolled naturals (fold up/down × three leading-zero
|
|
280
|
+
* tie-breaks × digits-before-words/after) , 2 plain case-insensitive ones, and 8
|
|
281
|
+
* `Intl.Collator`s (`numeric: true`, four sensitivities × `ignorePunctuation`) —
|
|
282
|
+
* over every pair of names up to 4 characters from `{a B 0 1 2 - _ space}`:
|
|
283
|
+
* **10,948,860 pairs, zero escapes**, where an escape is a pair some comparator
|
|
284
|
+
* reorders that this predicate calls safe. Two earlier drafts did have escapes,
|
|
285
|
+
* and both are why a clause above exists: `ignorePunctuation` found `arc-tracker`
|
|
286
|
+
* against `arcs`, and a terminal digit run found `a0` against `a00`.
|
|
287
|
+
*
|
|
288
|
+
* ⭐ The two-sided result is on real data. Across the same 105 collections the
|
|
289
|
+
* survey above defines, the ones this predicate refuses are **exactly** the 3
|
|
290
|
+
* whose own key order a codepoint sort cannot reproduce — no false positive, no
|
|
291
|
+
* false negative, against names the editor itself wrote.
|
|
292
|
+
*
|
|
293
|
+
* ⚠️ It does over-refuse, and the direction is deliberate: `flare1` against
|
|
294
|
+
* `flare10` is safe under all 22 and refused anyway, because the rule is stated
|
|
295
|
+
* on digit-run width rather than on a comparison, and the repair that fixes the
|
|
296
|
+
* genuinely broken sibling (`flare9` against `flare10`) fixes both.
|
|
297
|
+
*/
|
|
298
|
+
function orderTurnsOnTheComparator(a: string, b: string): Ambiguity | null {
|
|
299
|
+
const caseRepair = 'rename one of them so nothing but case has to be compared';
|
|
300
|
+
const numberRepair = 'pad the digit runs to the same width, or rename so no number decides the order';
|
|
301
|
+
const separatorRepair = 'rename so the first character that differs is a letter or a digit';
|
|
302
|
+
const la = a.toLowerCase();
|
|
303
|
+
const lb = b.toLowerCase();
|
|
304
|
+
if (la === lb) {
|
|
305
|
+
return {
|
|
306
|
+
kind: 'case',
|
|
307
|
+
because: `they are one name in two cases, and which of them the editor puts first is not measured`,
|
|
308
|
+
repair: caseRepair,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
let i = 0;
|
|
312
|
+
while (i < la.length && i < lb.length && la[i] === lb[i]) i++;
|
|
313
|
+
if (i === la.length || i === lb.length) {
|
|
314
|
+
const longer = la.length > lb.length ? a : b;
|
|
315
|
+
const rest = (la.length > lb.length ? la : lb).slice(i);
|
|
316
|
+
if (!SETTLED_CHARS.test(rest)) {
|
|
317
|
+
return {
|
|
318
|
+
kind: 'separator',
|
|
319
|
+
because:
|
|
320
|
+
`"${longer}" is the other name followed by ${JSON.stringify(rest)}, which a comparator that ignores ` +
|
|
321
|
+
'punctuation reads as the same name',
|
|
322
|
+
repair: separatorRepair,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
} else if (!SETTLED_CHARS.test(la[i]) || !SETTLED_CHARS.test(lb[i])) {
|
|
326
|
+
const deciding = SETTLED_CHARS.test(la[i]) ? lb[i] : la[i];
|
|
327
|
+
return {
|
|
328
|
+
kind: 'separator',
|
|
329
|
+
because:
|
|
330
|
+
`the first character that differs is ${JSON.stringify(deciding)}, which is neither a letter nor a digit, ` +
|
|
331
|
+
'and what that is worth is a property of the comparator',
|
|
332
|
+
repair: separatorRepair,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (sign(codepoint(a, b)) !== sign(codepoint(la, lb))) {
|
|
336
|
+
return {
|
|
337
|
+
kind: 'case',
|
|
338
|
+
because: `codepoint puts "${a}" first only because of letter case; folded, "${b}" comes first`,
|
|
339
|
+
repair: caseRepair,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const ra = runsOf(la);
|
|
343
|
+
const rb = runsOf(lb);
|
|
344
|
+
for (let k = 0; k < Math.min(ra.length, rb.length); k++) {
|
|
345
|
+
const x = ra[k];
|
|
346
|
+
const y = rb[k];
|
|
347
|
+
if (x === y) continue;
|
|
348
|
+
const xIsDigits = /^\d/.test(x);
|
|
349
|
+
const yIsDigits = /^\d/.test(y);
|
|
350
|
+
if (xIsDigits && yIsDigits) {
|
|
351
|
+
if (BigInt(x) === BigInt(y)) {
|
|
352
|
+
return {
|
|
353
|
+
kind: 'number',
|
|
354
|
+
because: `"${x}" and "${y}" are the same number written two ways, so only a tie-break separates them`,
|
|
355
|
+
repair: numberRepair,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (x.length === y.length) return null;
|
|
359
|
+
return {
|
|
360
|
+
kind: 'number',
|
|
361
|
+
because:
|
|
362
|
+
`"${x}" and "${y}" are runs of digits of different widths, so reading them as text and reading them as ` +
|
|
363
|
+
`the numbers ${BigInt(x)} and ${BigInt(y)} can disagree`,
|
|
364
|
+
repair: numberRepair,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (xIsDigits !== yIsDigits) {
|
|
368
|
+
return {
|
|
369
|
+
kind: 'number',
|
|
370
|
+
because:
|
|
371
|
+
`one has the digits "${xIsDigits ? x : y}" where the other has "${xIsDigits ? y : x}", and comparators ` +
|
|
372
|
+
'differ on whether a number sorts before a word',
|
|
373
|
+
repair: numberRepair,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** How many pairs a refusal spells out before it starts counting them instead. */
|
|
382
|
+
const PAIRS_SPELLED_OUT = 8;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Refuse a set of animation names the editor could key in an order rigc did not
|
|
386
|
+
* emit — the check that lets `editorAnimationOrder` sort by codepoint without
|
|
387
|
+
* claiming codepoint is the editor's rule.
|
|
388
|
+
*
|
|
389
|
+
* It is deliberately **not** conditional on the rig declaring a slider. A slider
|
|
390
|
+
* is what makes the difference bite today, but the emitted order is a claim about
|
|
391
|
+
* the editor either way, and a check that only ran when a slider was present would
|
|
392
|
+
* make the claim hold for some rigs and not others — with nothing saying which.
|
|
393
|
+
* Adding the slider is then the edit that refuses a rig that built yesterday.
|
|
394
|
+
*/
|
|
395
|
+
function refuseNamesTheEditorCouldKeyDifferently(sorted: readonly string[]): void {
|
|
396
|
+
const found: string[] = [];
|
|
397
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
398
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
399
|
+
const pair = orderTurnsOnTheComparator(sorted[i], sorted[j]);
|
|
400
|
+
if (pair) found.push(`"${sorted[i]}" / "${sorted[j]}" (${pair.kind}) — ${pair.because}; ${pair.repair}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!found.length) return;
|
|
404
|
+
const spelled = found.slice(0, PAIRS_SPELLED_OUT);
|
|
405
|
+
throw new CompileError(
|
|
406
|
+
`${found.length} pair(s) of animation names have no one order: rigc keys the emitted "animations" object ` +
|
|
407
|
+
'codepoint-ascending, which is the Spine editor\'s own order only for names whose order does not turn on ' +
|
|
408
|
+
'case, on a number, or on a separator. The editor sorts natural and case-insensitive (#539), a slider\'s ' +
|
|
409
|
+
'animation is an ORDINAL in the format\'s binary half, and an editor that keys these differently repoints ' +
|
|
410
|
+
'every slider whose animation moves index — silently, in a file that still parses (#535). ' +
|
|
411
|
+
`${spelled.join('. ')}` +
|
|
412
|
+
(found.length > spelled.length ? `. …and ${found.length - spelled.length} more pair(s)` : ''),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
139
416
|
// ---------------------------------------------------------------------------
|
|
140
417
|
// number formatting — deterministic, and free of "-0"
|
|
141
418
|
// ---------------------------------------------------------------------------
|
|
@@ -1810,7 +2087,13 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
1810
2087
|
// conditional spread rather than an assignment after the literal, so the key
|
|
1811
2088
|
// lands in that position instead of at the end.
|
|
1812
2089
|
...(Object.keys(events).length ? { events } : {}),
|
|
1813
|
-
|
|
2090
|
+
// Keyed in the editor's own order rather than the motion spec's, because a
|
|
2091
|
+
// slider's reference to an animation is an ordinal in the format and the
|
|
2092
|
+
// editor re-sorts this object — see `editorAnimationOrder`. The sort is
|
|
2093
|
+
// applied HERE and not to the loop above, so what the compiler reads, the
|
|
2094
|
+
// order it reports durations in, and which animation a CompileError names
|
|
2095
|
+
// first are all still the spec's own; only the emitted key order moves.
|
|
2096
|
+
animations: editorAnimationOrder(animations),
|
|
1814
2097
|
};
|
|
1815
2098
|
if (constraints.length) skeleton.constraints = constraints;
|
|
1816
2099
|
|
package/src/types.ts
CHANGED
|
@@ -835,7 +835,21 @@ export interface SpineSkeletonJson {
|
|
|
835
835
|
}>;
|
|
836
836
|
/**
|
|
837
837
|
* Event definitions, keyed by name (`SkeletonJson.ts:451-464`). An object, not
|
|
838
|
-
* an array — the
|
|
838
|
+
* an array — one of the **two** top-level collections in the format that are,
|
|
839
|
+
* `animations` being the other.
|
|
840
|
+
*
|
|
841
|
+
* ⚠️ This sentence said "the one" until issue #535, and the collection it was
|
|
842
|
+
* overlooking is where the defect that card is about lived. The distinction is
|
|
843
|
+
* not cosmetic: the binary format addresses both of these by ORDINAL
|
|
844
|
+
* (`SkeletonBinary`: `animations[readInt()]` for a slider's animation,
|
|
845
|
+
* `events[readInt()]` for an event key), and an editor round trip was measured
|
|
846
|
+
* to re-key every name-keyed object in codepoint order while returning every
|
|
847
|
+
* ARRAY in the order it was given. So a reference into either of these two is
|
|
848
|
+
* a reference whose ordinal an editor can move, and a reference into
|
|
849
|
+
* `bones` / `slots` / `skins` / `constraints` is not. `animations` is emitted
|
|
850
|
+
* in the editor's order for that reason (`compile.ts`'s
|
|
851
|
+
* `editorAnimationOrder`); whether `events` needs the same is unmeasured,
|
|
852
|
+
* because no editor export on hand carries more than one event.
|
|
839
853
|
*/
|
|
840
854
|
events?: Record<string, SpineEvent>;
|
|
841
855
|
animations: Record<
|