spine-rigc 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/cli.ts +37 -6
- package/docs/AUTHORING.md +381 -43
- package/docs/INGEST.md +64 -16
- package/docs/SPEC_COVERAGE.md +1 -1
- package/package.json +1 -1
- package/src/compile.ts +73 -3
- package/src/ingest.ts +196 -26
- package/src/keyorder.ts +547 -0
- package/src/mesh.ts +54 -17
- package/src/rig.ts +119 -9
- package/src/types.ts +36 -17
- package/src/validate.ts +129 -12
package/docs/INGEST.md
CHANGED
|
@@ -510,7 +510,8 @@ resolved rather than guessed, for §0.2's reason: `spineboy/export` holds two, a
|
|
|
510
510
|
|
|
511
511
|
**What it will not do is invent.** Everything the spec format cannot hold is a
|
|
512
512
|
finding with a code — `BLOCK` for a construct the rebuild will be missing, `JUDGE`
|
|
513
|
-
for
|
|
513
|
+
for what a skeleton does not carry and the rebuild has to state (the two values
|
|
514
|
+
below, and whether a muted constraint is the consumer's), `LOSS` wherever the source's spelling and
|
|
514
515
|
rigc's differ on purpose (a number rigc re-derives, a field the spec has no home for, or
|
|
515
516
|
a default the source left to the format and the rebuild writes out). A blocker exits
|
|
516
517
|
non-zero and still writes both files. **Every code it can print has a row at the end
|
|
@@ -560,6 +561,23 @@ at the policy rather than implying the file was read.
|
|
|
560
561
|
is used, stated in the motion spec's `note`, and recorded as a finding per
|
|
561
562
|
animation. Edit it if you know the real number.
|
|
562
563
|
|
|
564
|
+
🎛️ **And one statement is not in a skeleton either: who turns a muted constraint on**
|
|
565
|
+
([#784](https://github.com/firejune/rigc/issues/784)). An ik or transform constraint
|
|
566
|
+
resting at 0 on every mix it reads, that no animation keys above 0, is either a
|
|
567
|
+
leftover that moves nothing or a dial a game sets from code — and the two export as
|
|
568
|
+
the same bytes. `build` refuses the shape by name (`A47`/`A48`), which is how a
|
|
569
|
+
production skeleton's rebuild was refused over an ik its game switches on at runtime.
|
|
570
|
+
So `ingest` reads it the way under which the file is correct: it writes the
|
|
571
|
+
constraint into the rig spec's `invariants.consumerDrivenMix` ([AUTHORING
|
|
572
|
+
§3.7](AUTHORING.md)), with a `why` saying the entry is `ingest`'s reading, and prints a
|
|
573
|
+
`CONSUMER_DRIVEN_MIX` **judgement** naming it. The rebuild is the same bytes — the
|
|
574
|
+
declaration is a statement to the gate, never emitted — and it gates green, with the
|
|
575
|
+
constraint SKIPped by name rather than measured. It is the only field of `invariants`
|
|
576
|
+
`ingest` ever writes, and the rig spec's `note` says so where it is present.
|
|
577
|
+
⚠️ **None of the twelve exports carries the shape**: every constraint they rest muted
|
|
578
|
+
is keyed up by an animation, spineboy's aim rig being the idiom, and the corpus half
|
|
579
|
+
of the `IG` suite holds that no line and no declaration appear on any of them.
|
|
580
|
+
|
|
563
581
|
And two flags for what the skeleton also does not encode: `--art loose` (the default)
|
|
564
582
|
names an `image` per attachment resolved against loose PNGs, `--art none` states
|
|
565
583
|
`width`/`height` for `build --atlas-in` — and for `explain --atlas-in`, which is the
|
|
@@ -609,12 +627,13 @@ is the one failure a comparison of two sets cannot show you.
|
|
|
609
627
|
| `BONE_FIELD` | `BLOCK` | 1 | a bone field with no rig-spec field, so it is dropped. A 4.0/4.1 export spelling `transform` where 4.3 spells `inherit` lands here; so does a misspelling | check the name against AUTHORING §3 first — a typo and an unsupported field read exactly the same |
|
|
610
628
|
| `BONE_TIMELINE` | `BLOCK` | 1 | a bone timeline the motion spec has no track for. The detail names the eleven it has, read off the table. Since [#733](https://github.com/firejune/rigc/issues/733) carried `inherit` — the eleventh case of the runtime's own bone switch, a stepped mode per key — every bone timeline the runtime plays has a track, so this is reachable only for a name the **parser** throws on too (`Invalid timeline type for a bone`), the position `PHYSICS_TIMELINE` is in | check the spelling; there is no bone timeline left for the rebuild to be missing |
|
|
611
629
|
| `CONSTRAINT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a constraint, with its type named beside it | as `BONE_FIELD` |
|
|
612
|
-
| `CONSTRAINT_KEY_RESTATED` | `LOSS` | 0 | an `ik` or `transform` track whose keys do not all state the same fields. The motion spec takes one field set per track, so a field **any** key states is written on **every** key at the value the parser would have read there | nothing. Same values, larger file — the rebuild plays what the source plays |
|
|
630
|
+
| `CONSTRAINT_KEY_RESTATED` | `LOSS` | 0 | an `ik` or `transform` track whose keys do not all state the same fields. The motion spec takes one field set per track, so a field **any** key states is written on **every** key of the spec at the value the parser would have read there — and the line is printed only for a value the **file** will still carry. Since [#716](https://github.com/firejune/rigc/issues/716) the emitter leaves a value out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so a restated default is the source's own text again and says nothing; what is left is a value the table has no row for, or a transform key's `mixY` the source left out beside a `mixX` that is not 1, which the emitter keeps because that is where the editor writes it (§10.6c's *only at 1*). Measured on the twelve exports: 20 tracks printed this before #716, 0 after | nothing. Same values, and where this prints, a larger file — the rebuild plays what the source plays |
|
|
631
|
+
| `CONSUMER_DRIVEN_MIX` | `JUDGE` | 0 | an `ik` or `transform` constraint resting at 0 on every mix it reads — an ik's `mix`; a transform's mixes for the `to` properties it declares — that no animation keys above 0, reading every key the way `A47`/`A48` do: an omitted mix is the parser's 1 and a Bezier handle above 0 lifts a 0 → 0 pair. Nothing in the file ever switches it on, and the file cannot say whether that is a leftover or a mix a game sets from code, so the rig spec **declares** it in `invariants.consumerDrivenMix` ([#784](https://github.com/firejune/rigc/issues/784)) and the rebuild's gate SKIPs it by name. It is a judgement for `DURATION`'s reason: a statement the skeleton does not carry, made and printed — and not a `LOSS`, because the rebuilt skeleton is the source's bytes. A transform that declares no `to` at all is not a candidate: `A48` refuses it with its own sentence, which no declaration answers | nothing, if a game drives that mix. If it is a leftover, delete the entry and rest a mix it reads above 0 — or remove the constraint — and the gate measures it again |
|
|
613
632
|
| `CONSTRAINT_TYPE` | `BLOCK` | 1 | a constraint whose `type` is none rigc knows, so the whole constraint is dropped rather than approximated | the rebuild has no such constraint; check the spelling before assuming the type is unsupported |
|
|
614
633
|
| `DURATION` | `JUDGE` | 0 | skeleton JSON has no duration field at all. The largest key time is used, which is what a runtime plays to — and wrong for an animation that holds its last pose past its last key | if you know the real number, edit `duration` in the motion spec. It costs nothing: the declared duration is checked against the compiled keys |
|
|
615
634
|
| `GENERATION_UNKNOWN` | `BLOCK` | 1 | `skeleton.spine` names no generation rigc knows, or the header states none at all. A version is read as its LEADING `major.minor` token — a down-export writes `4.0-from-4.1.24`, which is 4.0 data from a 4.1 editor — and it is never rounded to the nearest generation: a catalog that rounded handed 19 skeletons labelled `3.8.99` a 4.2 runtime and every one posed as NaN ([#706](https://github.com/firejune/rigc/issues/706) row 7) | check the string against the file you were handed. A real generation rigc does not list belongs on #706 item 1, with the string beside it |
|
|
616
635
|
| `GENERATION_UNSUPPORTED` | `BLOCK` | 1 | the file is Spine data from another generation and this reader reads 4.3. The detail names the generation, the string it was read from, and what a 4.3 reader loses on **this** file: constraints parked in the top-level `ik` / `transform` / `path` / `physics` / `slider` arrays 4.3 folded into `constraints` and this reader never opens (row 1), bones carrying 4.2's `transform` where 4.3 spells `inherit` (row 6), and physics constraints omitting `inertia` / `damping`, whose default is not the same number in 4.2 as in 4.3 (row 4) | re-export the file as 4.3 from an editor of its own generation, or transcribe it by hand (§2). Reading it with **that generation's** defaults is #706 item 2 and is not in this tool |
|
|
617
|
-
| `HEADER_BOOKKEEPING` | `LOSS` | 0 | a header field the editor writes and the rig spec has no home for — `hash`,
|
|
636
|
+
| `HEADER_BOOKKEEPING` | `LOSS` | 0 | a header field the editor writes and the rig spec has no home for — `hash`, the editor's project hash, which is a value about a file rigc did not write. Dropped, and nothing reads it back. `audio` was on this line until [#716](https://github.com/firejune/rigc/issues/716): the rig spec states it now ([AUTHORING §3.1](AUTHORING.md)) and `ingest` carries it, `null` included | nothing. It is one of the two declared exceptions §2.3's pass line is stated apart from |
|
|
618
637
|
| `HEADER_ORIGIN` | `LOSS` | 0 | the source declares an extent and omits `x`/`y`. Inside a declared extent an omitted origin **is** 0, so the spec states it — and the rebuild then spells two fields the source did not | nothing. Same box, different bytes — which is why byte identity is not the claim for an export that takes this branch |
|
|
619
638
|
| `HEADER_REDERIVED` | `LOSS` | 0 | `skeleton.spine`: the rebuild writes the version of the runtime rigc links. The line says whether that is the same string the source states | nothing — but read the line: a 4.2 export rebuilds as 4.3 in that one field, and a source from another generation raises `GENERATION_UNSUPPORTED` beside it, which is the blocker about the DATA rather than about the string |
|
|
620
639
|
| `IK_KEY_FIELD` | `BLOCK` | 1 | a key field on an `ik` timeline that is not part of its shape | check the spelling; an unknown field is dropped from the rebuilt track |
|
|
@@ -629,7 +648,7 @@ is the one failure a comparison of two sets cannot show you.
|
|
|
629
648
|
| `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. Since [#730](https://github.com/firejune/rigc/issues/730) carried `rgb`, `alpha` and `rgb2` the spec has a track for all six the format has, so what still reaches this line is a name **outside** the format — `sequence` written on a slot rather than an attachment is the likeliest — and the detail says so: the runtime's own reader throws `Invalid timeline type for a slot` on it, so no player loads that file either. The detail names the tracks the spec does have and, when the format has any it lacks, those too, **both read off the tables** rather than listed here: this cell named `rgba2` among the timelines nobody carries until [#690](https://github.com/firejune/rigc/issues/690) made that false, which is what a hand-kept list beside a derived one always comes to. `rgb` and `alpha` are carried under their own names and on their own key times, never folded into one `rgba` — that would state each channel at the other's key times, a value nobody keyed | fix the timeline's name, or accept that the rebuild plays nothing there |
|
|
630
649
|
| `SPEC_REFUSED` | `BLOCK` | 1 | the specs were written and **rigc's own parser refuses one of them** — the detail carries that refusal word for word, after the file and the spec it is about. It is the one finding that is not about a single construct: it is whatever `parseRigSpec` or `parseMotionSpec` names, from a shape the format holds and the spec cannot say (a constraint that is `skinRequired` under no skin) to a defect in this decompiler. Until [#692](https://github.com/firejune/rigc/issues/692) the refusal left through `ingest` itself, so the run exited 1 with no line, no code and no `findings.json` at all | read the quoted sentence against the skeleton: it names the object. Both specs are on disk for exactly that, and `build` will refuse them until the shape has a spelling — [AUTHORING §5.1](AUTHORING.md) is the list of what a parser says |
|
|
631
650
|
| `TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape. On an `inherit` key that includes a `curve`: the parser reads `time` and `inherit` there and nothing else, and `build` refuses a curve on that track by name | check the spelling; the field is dropped from the rebuilt key |
|
|
632
|
-
| `TIMELINE_KEY_RESTATED` | `LOSS` | 0 |
|
|
651
|
+
| `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | an editor omits a channel that equals the parser's default, and the motion spec's `v` is positional, so the omission is written out at that default **in the spec** — and the line is printed only where the **file** will carry it too. It was the commonest line in a real run until [#716](https://github.com/firejune/rigc/issues/716): the emitter now leaves a channel out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so on every key kind with a row the rebuild is the source's own text and nothing is said — measured on the twelve exports, 341 tracks printed it before and 0 after. What still prints it is a timeline whose keys have no row (`shearx`, `alpha`, path `spacing`, …), and an `inherit` key: one that omits the mode is written as `normal` — the parser's default — and one spelled with a capital first letter (`NoScale`) as the editor's `noScale`, the same mode either way | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
|
|
633
652
|
| `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
|
|
634
653
|
|
|
635
654
|
⚠️ **One thing the table cannot carry: the region key is kept where a placeholder is
|
|
@@ -750,24 +769,45 @@ motion you are *authoring* — this is the one case the escape hatch exists for,
|
|
|
750
769
|
### 2.3 What "byte-identical" can and cannot mean
|
|
751
770
|
|
|
752
771
|
State the ambition in the right units, because three different things get called
|
|
753
|
-
"identical" and only
|
|
772
|
+
"identical", and the third is reachable only in a form worth stating exactly.
|
|
754
773
|
|
|
755
774
|
| Ambition | Reachable? | What it costs, and what it proves |
|
|
756
775
|
| --- | --- | --- |
|
|
757
776
|
| **Structural agreement** — same bones, slots, attachments, timelines, key counts, curve kinds | ✅ yes, and `diff` measures it | the 3-timing transcription reads **1.000 on all 49 measures**. Aim here first |
|
|
758
777
|
| **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
|
|
759
|
-
| **Byte-identical JSON** |
|
|
778
|
+
| **Byte-identical JSON** | ✅ **for a rebuild, in canonical form, apart from `hash` and `spine`** — the pass line below | a **rebuild** of an editor export (`ingest`, then `build`) is the export. A **transcription** by hand is not held to it: SPEC_COVERAGE's rung-6 count — 49 benign differences, 39 of them explicit defaults — was taken before [#716](https://github.com/firejune/rigc/issues/716) made the emitter leave a default out, and it is a count about what a person chose to write, not about the emitter |
|
|
779
|
+
|
|
780
|
+
⭐ **The pass line of the byte round trip, stated once:** `build(ingest(x))` of an
|
|
781
|
+
editor export `x` is **identical to `x` in canonical form, apart from `hash` and
|
|
782
|
+
`spine`** — canonical form being `JSON.stringify(JSON.parse(text), null, 2)` of each
|
|
783
|
+
file, which keeps every number as parsed, every key in its order and every omitted key
|
|
784
|
+
omitted, and drops only whitespace and the exponent's spelling (an export setting, not
|
|
785
|
+
a property of the rig). `IG83` holds it on all twelve exports under `examples/` every
|
|
786
|
+
run and prints the first differing path when it does not hold; for a skeleton **rigc**
|
|
787
|
+
emitted, `build(ingest(x))` is byte-identical outright (`IG63`, `IG71`).
|
|
760
788
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
contract `bun run selftest` gates on every run — and it is not, for a skeleton the
|
|
764
|
-
editor emitted. Measured over all twelve corpus exports, a field-by-field walk of the
|
|
765
|
-
rebuild against its source produces differences of exactly two kinds, in every file:
|
|
789
|
+
The two **declared exceptions** are the header keys the editor writes and the rig spec
|
|
790
|
+
has no field for, by design, and each has its finding:
|
|
766
791
|
|
|
767
|
-
|
|
|
792
|
+
| Header key | Example, rebuild vs export | Why it is an exception |
|
|
768
793
|
| --- | --- | --- |
|
|
769
|
-
|
|
|
770
|
-
|
|
|
794
|
+
| `hash` | `undefined` vs `"VFWbaK2UoCM"` | the editor's project hash — a value about a file rigc did not write, so a spec that carried it would be claiming an export it did not come from. `HEADER_BOOKKEEPING` |
|
|
795
|
+
| `spine` | `"4.3.13"` vs `"4.3.75-beta"` | the version of the runtime rigc links, stamped by design and re-checked by `A16`. `HEADER_REDERIVED` |
|
|
796
|
+
|
|
797
|
+
🔢 **Two more rows stood in a table here until the third tranche of #716 landed.**
|
|
798
|
+
The **header's `audio`** (`undefined` vs `null`) is a stated value like `images`, and
|
|
799
|
+
the rig spec now carries it ([AUTHORING §3.1](AUTHORING.md)). **An omitted default
|
|
800
|
+
written out** — `…rotate[0].time: 0 vs undefined`, 2,338 keys over the twelve and
|
|
801
|
+
87 `"name": null` — was rigc writing every key it was given where the editor leaves
|
|
802
|
+
out what the parser reads the same way without it; the emitter now leaves out exactly
|
|
803
|
+
those ([AUTHORING §10.6c](AUTHORING.md)), and `IG81` counts 0 of either. One shape of
|
|
804
|
+
it is left and it is deliberate: a stage at the origin, which the editor writes as
|
|
805
|
+
`width`/`height` with no `x`/`y` and rigc spells whole. The 4.3 JSON reader has no
|
|
806
|
+
default for the origin — an absent one loads as `undefined`, a written `0` as `0` — so
|
|
807
|
+
it is not a key the parser reads the same way without it, and the row is not in the
|
|
808
|
+
table; `LOSS HEADER_ORIGIN` names it
|
|
809
|
+
([#622](https://github.com/firejune/rigc/issues/622)). No file in this corpus takes that
|
|
810
|
+
branch: all twelve declare an origin away from 0.
|
|
771
811
|
|
|
772
812
|
🔢 **A third row stood here until issue #716: the emitted precision** —
|
|
773
813
|
`…curve[0]: 0.066667 vs 0.06666667`, `uvs[0]: 0 vs 2.554152e-7` — because rigc wrote
|
|
@@ -813,10 +853,10 @@ does **not** cover, in the same breath:
|
|
|
813
853
|
|
|
814
854
|
| Still uncovered | Why |
|
|
815
855
|
| --- | --- |
|
|
816
|
-
| `
|
|
856
|
+
| `spine` and `hash` | the rig spec has no field for either, by design, and `ingest` reports both as findings — the declared exceptions above |
|
|
817
857
|
| anything below one float32 step | the parser stores frames, curves and vertices in a `Float32Array`, so a difference it cannot represent is invisible to any reading of the parsed form |
|
|
818
858
|
| a Bezier's handles *as written* | the parser samples them into the curve, so a moved handle arrives as moved samples rather than as the handle it was |
|
|
819
|
-
| how the file is **spelled** |
|
|
859
|
+
| how the file is **spelled** | nothing, on an editor export, but the declared exceptions: since [#716](https://github.com/firejune/rigc/issues/716) the rebuild spells every number as the export does (`IG73`), writes every object's keys in the export's order (`IG76`, [AUTHORING §10.6b](AUTHORING.md)) and leaves out every key the export leaves to the parser (`IG81`, [AUTHORING §10.6c](AUTHORING.md)), and `IG83` reads the whole text. This row stays because the value measure still cannot see spelling — it compares what the parser loaded — and the pass line above is what can |
|
|
820
860
|
| how it **looks** | that is `check`, and `--texture-from` is how its figure is attributed |
|
|
821
861
|
|
|
822
862
|
The geometric row needs a real number, because a naive reading of `check` makes an
|
|
@@ -1001,6 +1041,14 @@ has three possible meanings and the message alone does not separate them:
|
|
|
1001
1041
|
`A00` above);
|
|
1002
1042
|
3. **the rule is stricter than the runtime** — fix the rule, or file it.
|
|
1003
1043
|
|
|
1044
|
+
🎛️ **`A47` / `A48` on a foreign export are a fourth reading, and neither side is
|
|
1045
|
+
wrong.** An ik or transform resting muted that no animation keys up moves nothing *in
|
|
1046
|
+
the file*, and `validate <file>` has no rig spec and so no way to be told a game turns
|
|
1047
|
+
it on from code — it refuses with three doors, the third being that statement. The
|
|
1048
|
+
rebuild route makes it for you: `ingest` declares the constraint consumer-driven and
|
|
1049
|
+
prints a `CONSUMER_DRIVEN_MIX` judgement (§2.0), and `build` then SKIPs it by name
|
|
1050
|
+
([#784](https://github.com/firejune/rigc/issues/784)).
|
|
1051
|
+
|
|
1004
1052
|
⇒ Before changing anybody's export because rigc objected, check case 3: does the file
|
|
1005
1053
|
**parse** (`A00`), **step without NaN** (`A10`), and **render**? If all three, the
|
|
1006
1054
|
runtime is content and the burden is on the rule. Reporting that is a better answer
|
package/docs/SPEC_COVERAGE.md
CHANGED
|
@@ -743,7 +743,7 @@ This is the split Part 4(c) needs. **Spine-validity** = the file is wrong for an
|
|
|
743
743
|
| `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` | validity | a transform constraint whose mixes for the properties it drives are all 0 at setup and on every value a key poses, or one that drives no property ([#765](https://github.com/firejune/rigc/issues/765)) |
|
|
744
744
|
| `A13_MESH_BUDGET` | **renderer-profile** | >4 mesh slots, >80 triangles per mesh |
|
|
745
745
|
| `A14_NO_FULL_FRAME_MESH` | **renderer-profile** | a mesh spanning the whole stage |
|
|
746
|
-
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | **renderer-profile** | an overlay
|
|
746
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | **renderer-profile** | an overlay part none of whose texels is below full alpha — refused unopened when the file has no alpha channel and no `tRNS` chunk, and by its texels otherwise, on a loose page and a shared one alike |
|
|
747
747
|
| `A20_MESH_WEIGHTS_COHERENT` | **renderer-profile / archetype** | **unweighted meshes**, weights not summing to 1, out-of-range bone indices |
|
|
748
748
|
| `A21_MESH_RIM_PINNED` | archetype | ring/ribbon/contour rim not pinned |
|
|
749
749
|
| `A22_MESH_UVS_IN_UNIT_RANGE` | validity-ish | UVs outside 0..1 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
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
|
@@ -39,6 +39,7 @@ import { parseJsonWithPosition } from './json-position.ts';
|
|
|
39
39
|
// of it — the same search serves `refuseUnknownKeys`, and a second copy here with
|
|
40
40
|
// a threshold edited is how such a pair drifts apart.
|
|
41
41
|
import { nearMisses } from './keys.ts';
|
|
42
|
+
import { inEditorKeyOrder, withoutParserDefaults } from './keyorder.ts';
|
|
42
43
|
import { EVERY_GLOBAL_PHYSICS, parseMotionSpec } from './motion.ts';
|
|
43
44
|
import {
|
|
44
45
|
BONE_INHERIT_KNOWN,
|
|
@@ -377,6 +378,51 @@ function editorSkinOrder<T extends { name: string }>(skins: readonly T[]): T[] {
|
|
|
377
378
|
return [...pinned, ...[...rest].sort((a, b) => (rank.get(a.name) ?? 0) - (rank.get(b.name) ?? 0))];
|
|
378
379
|
}
|
|
379
380
|
|
|
381
|
+
/**
|
|
382
|
+
* A skin's `attachments` map keyed in the editor's order of its SLOT names —
|
|
383
|
+
* the comparator `animations` and `skins` use — rather than the draw order the
|
|
384
|
+
* table is built in (issue #716).
|
|
385
|
+
*
|
|
386
|
+
* 🔬 Measured twice, from both directions. The editor's export of a rigc build
|
|
387
|
+
* (`gallery/look`, 4.3.26) returned a skin's 24 slot keys sorted (§10.1 of the
|
|
388
|
+
* guide), and over the twelve exports under `examples/` every skin's slot keys
|
|
389
|
+
* are in this comparator's order — 12 of 12, `spineboy-pro`'s
|
|
390
|
+
* `portal-flare9, portal-flare10` among them, which codepoint would reverse.
|
|
391
|
+
* rigc wrote draw order, and 11 of the twelve rebuilds keyed a skin differently
|
|
392
|
+
* from the export they were rebuilt from.
|
|
393
|
+
*
|
|
394
|
+
* ⚠️ **Unlike `skins` and `animations`, nothing here is refused.** Those two are
|
|
395
|
+
* ordinals in the binary half, so an order rigc cannot certify is a reference it
|
|
396
|
+
* cannot certify. A slot key is read by name (`SkeletonJson` resolves each one
|
|
397
|
+
* with `findSlot`) and is no reference at all, so an order the comparator leaves
|
|
398
|
+
* open is a text rigc cannot promise, not a rig that is wrong — and a key's
|
|
399
|
+
* position is never a verdict. Such a map keeps rigc's order WHOLE, never
|
|
400
|
+
* partly sorted:
|
|
401
|
+
*
|
|
402
|
+
* - a pair the comparator leaves open (`editorNameOrder`'s number, separator and
|
|
403
|
+
* folder cases);
|
|
404
|
+
* - any slot name holding `/`. The folder rule was measured on skin and
|
|
405
|
+
* animation names, which the editor files in folders; slots it files under
|
|
406
|
+
* bones, and no round trip carried a slot name with a `/` in it.
|
|
407
|
+
*
|
|
408
|
+
* The per-slot maps inside are left as built: their order is the rig's own, the
|
|
409
|
+
* twelve rebuilds already match the exports there (0 of 141 differ), and an
|
|
410
|
+
* attachment name holding `/` is the common case the folder question would
|
|
411
|
+
* have to answer first.
|
|
412
|
+
*/
|
|
413
|
+
function editorSlotKeyOrder<T>(attachments: Record<string, T>): Record<string, T> {
|
|
414
|
+
const names = Object.keys(attachments);
|
|
415
|
+
if (names.some((name) => name.includes(NAME_FOLDER))) return attachments;
|
|
416
|
+
for (let i = 0; i < names.length; i++) {
|
|
417
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
418
|
+
if (typeof editorNameOrder(names[i], names[j]) !== 'number') return attachments;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const ordered: Record<string, T> = {};
|
|
422
|
+
for (const name of [...names].sort((a, b) => editorNameOrder(a, b) as number)) ordered[name] = attachments[name];
|
|
423
|
+
return ordered;
|
|
424
|
+
}
|
|
425
|
+
|
|
380
426
|
/** What separates a folder from what it holds, in a skin or an animation name. */
|
|
381
427
|
const NAME_FOLDER = '/';
|
|
382
428
|
|
|
@@ -3172,6 +3218,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3172
3218
|
if (rig.skeleton?.referenceScale !== undefined) header.referenceScale = rig.skeleton.referenceScale;
|
|
3173
3219
|
const imagesPath = skeletonImagesPath(rig.skeleton?.images, opts, outDir, partDirs);
|
|
3174
3220
|
if (imagesPath !== undefined) header.images = imagesPath;
|
|
3221
|
+
if (rig.skeleton?.audio !== undefined) header.audio = rig.skeleton.audio;
|
|
3175
3222
|
|
|
3176
3223
|
// Event definitions. Emitted in the order the rig spec declares them — object
|
|
3177
3224
|
// key order is the spec's, not a set's, so A18 stays a contract.
|
|
@@ -3213,7 +3260,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3213
3260
|
parts!.constraints[key],
|
|
3214
3261
|
]),
|
|
3215
3262
|
),
|
|
3216
|
-
attachments,
|
|
3263
|
+
attachments: editorSlotKeyOrder(attachments),
|
|
3217
3264
|
};
|
|
3218
3265
|
}),
|
|
3219
3266
|
),
|
|
@@ -3235,6 +3282,23 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3235
3282
|
if (!boneNames.has(slot.bone)) throw new CompileError(`slot "${slot.name}" has no bone`);
|
|
3236
3283
|
}
|
|
3237
3284
|
|
|
3285
|
+
// Every object's keys in the order the editor writes them, per kind, from the
|
|
3286
|
+
// one table that says so (`src/keyorder.ts`, issue #716) — applied here, once,
|
|
3287
|
+
// to the finished object and before the text exists, so the constructors above
|
|
3288
|
+
// are free to build in whatever order reads best and none of them states the
|
|
3289
|
+
// order a second time. It moves positions and nothing else: no key is added,
|
|
3290
|
+
// dropped or re-valued, and what the gate and `A18` read is this object's text.
|
|
3291
|
+
//
|
|
3292
|
+
// Before it, and on the same finished object: every key whose value is the
|
|
3293
|
+
// one the 4.3 parser reads in its absence is left out, the way the editor
|
|
3294
|
+
// leaves it out (issue #716 tranche 3). One pass beside the other rather than
|
|
3295
|
+
// folded into it, because each has its own table and the selftest plants a
|
|
3296
|
+
// wrong row into each on its own; both walk the object through
|
|
3297
|
+
// `forEachKindedObject`, so what a kind is stays said in one place. The spec
|
|
3298
|
+
// is untouched — an author may still write `x: 0`, and it is still the record
|
|
3299
|
+
// of what was written — and what the parser loads does not move (`S103`).
|
|
3300
|
+
inEditorKeyOrder(withoutParserDefaults(skeleton));
|
|
3301
|
+
|
|
3238
3302
|
return {
|
|
3239
3303
|
skeleton,
|
|
3240
3304
|
skeletonText: `${JSON.stringify(skeleton, null, 2)}\n`,
|
|
@@ -5590,6 +5654,10 @@ function buildContourAttachment(
|
|
|
5590
5654
|
tolerance: generator.tolerance,
|
|
5591
5655
|
margin,
|
|
5592
5656
|
maxVertices,
|
|
5657
|
+
// The spec's two distances are the drawing's pixels; on a `scale:` page
|
|
5658
|
+
// the trace runs on texels, and the STATED scale is what converts them
|
|
5659
|
+
// (issue #779). Off one, `atlasScale` is absent and nothing is multiplied.
|
|
5660
|
+
...(img.atlasScale === undefined ? {} : { pageScale: img.atlasScale }),
|
|
5593
5661
|
});
|
|
5594
5662
|
} catch (err) {
|
|
5595
5663
|
if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
|
|
@@ -5652,8 +5720,8 @@ function buildContourAttachment(
|
|
|
5652
5720
|
bones: [ctx.anchorBone],
|
|
5653
5721
|
coverage: geometry.contour?.coverage,
|
|
5654
5722
|
// In the drawing's pixels, like the authored fit's (issue #762). The trace
|
|
5655
|
-
//
|
|
5656
|
-
//
|
|
5723
|
+
// runs on the plate's grid with the spec's margin and tolerance converted
|
|
5724
|
+
// onto it by the stated scale (issue #779), and its refusals state both.
|
|
5657
5725
|
...(geometry.contour === undefined ? {} : drawingOvershoot(geometry.contour.overshoot, img.atlasScale)),
|
|
5658
5726
|
holePixels: geometry.contour?.holePixels,
|
|
5659
5727
|
depth: depth?.summary,
|
|
@@ -6367,6 +6435,8 @@ function buildRigInfo(
|
|
|
6367
6435
|
meshSoftBones,
|
|
6368
6436
|
deformMayFold,
|
|
6369
6437
|
editorRoundTrip: rig.invariants?.editorRoundTrip === true,
|
|
6438
|
+
// `parseRigSpec` resolved every entry against the rig's own constraints.
|
|
6439
|
+
consumerDrivenMix: (rig.invariants?.consumerDrivenMix ?? []).map((e) => ({ type: e.type, constraint: e.constraint, why: e.why })),
|
|
6370
6440
|
// `isBase` is set on the manifest route only; every other image says false.
|
|
6371
6441
|
basePlates: images.filter((img) => img.isBase).map((img) => img.region),
|
|
6372
6442
|
meshSlotBudget: rig.invariants?.meshSlots ?? null,
|