rig-c 0.0.0-stage → 2.20.4
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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
package/docs/INGEST.md
ADDED
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
# Working with a skeleton you did not author
|
|
2
|
+
|
|
3
|
+
**Read this when what you were handed is already a skeleton.** It is written for an
|
|
4
|
+
agent holding a `skeleton.json` — plus its `.atlas` and page images — that came out
|
|
5
|
+
of the Spine editor or another tool, and that has been asked to work *with* it:
|
|
6
|
+
understand it, answer a complaint rigc raised about it, re-express it as rigc specs,
|
|
7
|
+
normalise or re-pivot it, or extend it with motion it does not have.
|
|
8
|
+
|
|
9
|
+
[AUTHORING.md](AUTHORING.md) is the file formats, the failure map and the CLI — read
|
|
10
|
+
it first and keep it open; this page never restates a field it documents.
|
|
11
|
+
[MOTION.md](MOTION.md) is what goes *between* two poses. This page is the third
|
|
12
|
+
question neither of them answers: **what the toolchain will and will not do with
|
|
13
|
+
somebody else's file, and what the honest routes through it are.**
|
|
14
|
+
|
|
15
|
+
🚨 **The two numbers this page produces are not grades, and they measure different
|
|
16
|
+
things.** `validate`'s red says *this file breaks a stated rule* — a fact about the
|
|
17
|
+
file, not about your work, and sometimes (§3.2) a fact about the rule. `diff`'s
|
|
18
|
+
ratios say *how much of a particular reference's structure your candidate
|
|
19
|
+
reproduces* — so deliberately extending or renaming a foreign skeleton **lowers
|
|
20
|
+
them**, by design (§4.2, §4.3). Neither one has a pass bar, and this page does not
|
|
21
|
+
invent one.
|
|
22
|
+
|
|
23
|
+
- Formats and the CLI reference: [README.md](../README.md)
|
|
24
|
+
- The rig spec, field by field: **AUTHORING §3**, and [`src/rig.ts`](../src/rig.ts)
|
|
25
|
+
- The motion spec, field by field: **AUTHORING §4**
|
|
26
|
+
- Named failures, and the file each one points at: **AUTHORING §5–§6**
|
|
27
|
+
- The coordinate contract, in one place: **AUTHORING §11.2** and
|
|
28
|
+
[`src/transform.ts`](../src/transform.ts)
|
|
29
|
+
- What the editor does when nobody tells it otherwise: **AUTHORING §10**
|
|
30
|
+
- Why the re-pivot in **§4.1** is shaped the way it is, and the child-bone row it
|
|
31
|
+
warns about worked on a bone that actually has one: [RIGGING.md](RIGGING.md) §3.
|
|
32
|
+
Its §2 is how to tell whether the pivot you are moving *to* is identified at all
|
|
33
|
+
- What the Spine 4.3 format holds, field by field:
|
|
34
|
+
[SPEC_COVERAGE.md](SPEC_COVERAGE.md)
|
|
35
|
+
- If you are the *person operating* an agent rather than the agent:
|
|
36
|
+
[PROMPTING.md](PROMPTING.md)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 0. What the toolchain can do with a file you were handed
|
|
41
|
+
|
|
42
|
+
Two facts decide everything below, and they pull in opposite directions:
|
|
43
|
+
|
|
44
|
+
1. **rigc reads compiled skeleton JSON in more places than you would guess.**
|
|
45
|
+
`validate`, `render`, `preview`, `vote`, `check`, `diff` and `ingest` all take a `skeleton.json` path directly, and none of them needs a rig
|
|
46
|
+
spec to do it.
|
|
47
|
+
2. **rigc cannot EDIT one.** There is no command that opens a skeleton and
|
|
48
|
+
changes it. The only thing that produces a skeleton is `build`, and `build`'s
|
|
49
|
+
input is a rig spec plus a motion spec.
|
|
50
|
+
⇒ **Every route that ends in a changed file goes through the specs** — and
|
|
51
|
+
there are two ways to get them: write them (§2, transcription) or have
|
|
52
|
+
`ingest` write them for you from the file itself (§2.0).
|
|
53
|
+
|
|
54
|
+
### 0.1 The table
|
|
55
|
+
|
|
56
|
+
Every row was executed against the fetched example corpus before it was written down.
|
|
57
|
+
`3-timing-and-spacing` and `spineboy` are the two skeletons this page uses; both carry
|
|
58
|
+
an upstream `license.txt` (Appendix, and [NOTICE.md](../NOTICE.md)).
|
|
59
|
+
|
|
60
|
+
| Command | Takes a foreign `skeleton.json`? | What it needs, and what it gives back |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| **`validate <skeleton.json>`** | ✅ **yes — this is its foreign-data form** | the `.json`, plus one `.atlas` beside it or named with `--atlas`. Runs the assertions and prints `PASS`/`FAIL`/`SKIP`/`PROF` per rule, naming the profile that judged it. §1.1 |
|
|
63
|
+
| **`render --candidate <skeleton.json>`** | ✅ **yes** | PNG frames plus a contact sheet, per animation. ⭐ **It does not gate** — it draws a file `validate` refuses, which is how you tell a red about the file from a red about the rule (§3.2) |
|
|
64
|
+
| **`preview --candidate <skeleton.json>`** | ✅ **yes** | one self-contained `.html` that plays it in the official Spine Web Player. Needs a network the first time it is opened ([NOTICE.md](../NOTICE.md)) |
|
|
65
|
+
| **`vote --candidate <a> --candidate <b>`** | ✅ **yes, on either side** | a ballot page. Pairing a foreign export against your own transcription is a legitimate ballot, and the panes carry no paths |
|
|
66
|
+
| **`check --candidate <skeleton.json> --frames <dir>`** | ✅ **yes** | ⭐ it reads **frames and never a reference skeleton**, so a foreign export enters this one *twice over*: as the candidate, or — via `render` — as the source of the frames. §1.4 |
|
|
67
|
+
| **`diff <candidate.json> <reference.json>`** | ✅ **yes, both sides** | 54 structural measures over bones, slots, attachments, constraints, animations and events. ⛔ **Blind to every coordinate** — §1.3 |
|
|
68
|
+
| **`ingest <skeleton.json> --out <dir>`** | ✅ **yes — and it is the only reader that WRITES specs** | the `.json` alone; no atlas, no art, no project file. Out come `rig.json`, `motion.json` and a findings report, such that `build`ing them reproduces the skeleton it read **byte for byte — for a skeleton rigc emitted**. ⚠️ For an editor export the claim is identity in canonical form apart from `hash` and `spine`, which §2.3 states in full. The seventh reader, and the one that ends §2's hand work — §2.0 and §5 |
|
|
69
|
+
| **`pose --images <dir> --frame <png>`** | ⛔ **not the skeleton** | loose part PNGs and one picture. A packed atlas page is not loose parts, and pointing it at one produces a confident answer about nothing — §5 |
|
|
70
|
+
| **`explain --rig … --motion … --out …`** | ⛔ **no** | rig spec + motion spec. It explains **what you wrote**, which makes it a transcription instrument rather than a reading one — §1.5 |
|
|
71
|
+
| **`build --rig … --motion … --images …`** | ⛔ **no** | specs in, skeleton out. The only writer in the toolchain, and the reason §2 exists |
|
|
72
|
+
| **`build … --atlas-in <file.atlas>`** | ⛔ not the skeleton — **but yes, the foreign *atlas*** | resolves your parts against the regions of a pack somebody else made, joining on region name. The one place a foreign *file* becomes an input to `build`. It applies the page's `scale:`, so an imported size is the drawing's rather than the pack's — §2.3 |
|
|
73
|
+
| **`build … --pack`** | ⛔ **no** | puts your own loose parts onto shared pages instead of one page per part, losslessly. Not foreign input, but it is what makes a transcription's atlas comparable in shape to an export's — §2.3 |
|
|
74
|
+
| **`bench <rung> --candidate …`** | ✅ mechanically | it is the *benchmark's* instrument: it knows which corpus example is which rung and measures against that one. Not an ingest instrument, and named here only so nobody reaches for it. Use `diff` and `check` directly |
|
|
75
|
+
|
|
76
|
+
### 0.2 Two path shapes, and why the directory form is not yours
|
|
77
|
+
|
|
78
|
+
`validate`, `render`, `preview`, `check`, `vote` and `bench` all accept either a
|
|
79
|
+
**directory** or a **`.json` file**. The directory form resolves to `skeleton.json` +
|
|
80
|
+
`skeleton.atlas` — **rigc's own output names**. A foreign export directory is named
|
|
81
|
+
after whatever the editor called the project, so the directory form finds nothing:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
rigc validate examples/3-timing-and-spacing/export
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
rigc validate …/examples/3-timing-and-spacing/export/skeleton.json
|
|
89
|
+
.. atlas …/examples/3-timing-and-spacing/export/skeleton.atlas
|
|
90
|
+
…
|
|
91
|
+
ENOENT: no such file or directory, open '…/examples/3-timing-and-spacing/export/skeleton.json'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
⚠️ **That one comes back as a stack trace, exit 1, rather than a named refusal.** It
|
|
95
|
+
is the only complaint on this page that does — everything in §3.1 is a sentence. Do
|
|
96
|
+
not read the trace as *rigc cannot open this export*; read the two `..` lines above
|
|
97
|
+
it, which name the two files it went looking for. ⇒ **Point every command at the
|
|
98
|
+
`.json` itself.** Every command line on this page does.
|
|
99
|
+
|
|
100
|
+
The atlas then resolves by looking beside the skeleton, and that lookup refuses
|
|
101
|
+
rather than guesses:
|
|
102
|
+
|
|
103
|
+
| What is beside the `.json` | What happens |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| exactly one `.atlas` | it is used, and the report names it |
|
|
106
|
+
| no `.atlas` | ⛔ `no .atlas beside …; name one with --atlas <path>` |
|
|
107
|
+
| two or more | ⛔ refuses and lists them — see below |
|
|
108
|
+
| any of the above, with `--atlas <path>` | `--atlas` wins outright |
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
rigc validate examples/spineboy/export/spineboy-pro.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
rigc: 2 atlases beside …/examples/spineboy/export/spineboy-pro.json (spineboy-run.atlas,
|
|
116
|
+
spineboy.atlas); name the right one with --atlas <path> — guessing by filename is how
|
|
117
|
+
an attachment quietly resolves against the wrong page
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
⭐ **This refusal is worth understanding rather than working around, because the
|
|
121
|
+
obvious heuristic is wrong on this very file.** `spineboy-ess` shares a longer
|
|
122
|
+
filename prefix with `spineboy-run.atlas` than with the `spineboy.atlas` it actually
|
|
123
|
+
uses. Pick by prefix and every attachment resolves against a page that does not
|
|
124
|
+
contain it — and §3.2 shows what that looks like when you get it wrong, which is a
|
|
125
|
+
failure two rules downstream of the choice.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 1. Reading it
|
|
130
|
+
|
|
131
|
+
### 1.1 `validate` — what the file is, and what rigc objects to
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
rigc validate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
rigc validate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
139
|
+
.. atlas …/examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas
|
|
140
|
+
.. profile spine — 8 renderer-policy and 8 archetype assertion(s) do not apply
|
|
141
|
+
PASS A07_ATLAS_TEXT_SHAPE
|
|
142
|
+
PASS A00_ROUNDTRIP_PARSE
|
|
143
|
+
PASS A16_SKELETON_VERSION_4_3
|
|
144
|
+
…
|
|
145
|
+
SKIP A31_DRAW_ORDER_OFFSETS_RESOLVE: no animation carries a drawOrder timeline
|
|
146
|
+
SKIP A32_EVENT_KEYS_RESOLVE: no animation carries an event timeline
|
|
147
|
+
SKIP A34_CONSTRAINT_TIMELINE_TARGETS: no animation carries a constraint timeline
|
|
148
|
+
SKIP A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: no animation carries a deform timeline
|
|
149
|
+
SKIP A04_MESH_TRIANGLES_AND_ENCODING: the skeleton carries no mesh attachment
|
|
150
|
+
SKIP A33_VERTEX_ATTACHMENT_GEOMETRY: the skeleton carries no bounding box, clipping attachment or path
|
|
151
|
+
SKIP A20_MESH_WEIGHTS_COHERENT: the skeleton carries no mesh attachment
|
|
152
|
+
SKIP A22_MESH_UVS_IN_UNIT_RANGE: the skeleton carries no mesh attachment
|
|
153
|
+
SKIP A23_PHYSICS_CONSTRAINT_EFFECTIVE: the skeleton declares no physics constraint
|
|
154
|
+
…
|
|
155
|
+
PROF A12_NO_DARK_COLOR: renderer rule, not in profile "spine"
|
|
156
|
+
PROF A21_MESH_RIM_PINNED: archetype rule, not in profile "spine"
|
|
157
|
+
…
|
|
158
|
+
rigc: green
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Read it as three separate statements, because they answer three different questions:
|
|
162
|
+
|
|
163
|
+
- **`PASS` / `FAIL`** — the rule ran, and this is its verdict.
|
|
164
|
+
- **`SKIP`** — the rule ran and had nothing to measure, and the line says what was
|
|
165
|
+
absent. A `SKIP` is never a pass, and on foreign data the `SKIP` list is also a
|
|
166
|
+
**free inventory of what the skeleton does not contain**. The nine lines above tell
|
|
167
|
+
you, without your having opened the JSON, that this export has no draw-order
|
|
168
|
+
timeline, no event timeline, no constraint timeline, no deform timeline, no mesh
|
|
169
|
+
attachment, no physics constraint, and no bounding box, clipping attachment or
|
|
170
|
+
path. ⭐ What passes over an empty list is the other kind of rule — `A01`, `A02`,
|
|
171
|
+
`A11`, `A12`, `A14` ask *how many of this does the file carry*, and **zero is the
|
|
172
|
+
answer**.
|
|
173
|
+
- **`PROF`** — the rule was excluded by the profile before its body ran. §3.3.
|
|
174
|
+
|
|
175
|
+
⚠️ **A green here is a statement about validity and nothing else.** It does not say
|
|
176
|
+
the skeleton is the one you were meant to be given, that its animations are the ones
|
|
177
|
+
their names suggest, or that anything in it is where the art wants it. The gate cannot
|
|
178
|
+
see a wrong animation (AUTHORING §0), and it certainly cannot see a wrong *file*.
|
|
179
|
+
|
|
180
|
+
### 1.2 `render` and `preview` — look at it
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
rigc render --candidate examples/spineboy/export/spineboy-pro.json \
|
|
184
|
+
--atlas examples/spineboy/export/spineboy.atlas \
|
|
185
|
+
--animation walk --fps 8 --max 200 --out render/sb
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
rigc render
|
|
190
|
+
.. skeleton …/examples/spineboy/export/spineboy-pro.json
|
|
191
|
+
.. atlas …/examples/spineboy/export/spineboy.atlas
|
|
192
|
+
.. poser spine-core — no skeleton.model.json beside …/examples/spineboy/export/spineboy-pro.json — a Spine export, not a rigc build
|
|
193
|
+
.. 200x186px at 8 fps, 1 set(s) -> …/render/sb
|
|
194
|
+
.. walk 9 frame(s), 1.000s + contact.png -> …/render/sb/walk@8fps
|
|
195
|
+
rigc: wrote …/render/sb/frames.json
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Three properties of this command matter more for foreign data than for your own:
|
|
199
|
+
|
|
200
|
+
- ⭐ **It does not gate.** That same `spineboy-pro.json` fails two assertions under the
|
|
201
|
+
default profile (§3.2) and renders all nine `walk` frames anyway. ⇒ **A red
|
|
202
|
+
`validate` is not a reason to stop looking**, and looking is often what tells you
|
|
203
|
+
whether the red matters.
|
|
204
|
+
- **Omitting `--animation` renders every animation**, each into its own directory with
|
|
205
|
+
its own contact sheet. On a file you were handed, that is the cheapest complete
|
|
206
|
+
inventory there is — one image per animation, with spacing visible across the grid.
|
|
207
|
+
- **The frame size is fitted to the skeleton's own extent**, so `--max` is a cap on the
|
|
208
|
+
longest side and not a canvas. A skeleton whose world box is 24 units wide renders
|
|
209
|
+
24 px wide however large you set it; read the size on the `..` line before concluding
|
|
210
|
+
a render came out empty.
|
|
211
|
+
|
|
212
|
+
`preview` writes one HTML file that plays the same data in the official Spine Web
|
|
213
|
+
Player. On foreign data it is also the **interop proof**: if that page plays it, a
|
|
214
|
+
Spine runtime plays it, whatever rigc's own rasteriser or validator thinks.
|
|
215
|
+
|
|
216
|
+
### 1.3 `diff` — and the two things it cannot see
|
|
217
|
+
|
|
218
|
+
`diff` takes two compiled skeletons and reports 54 measures in eight groups, plus two
|
|
219
|
+
blocks that report and gate nothing: the `(reported)` measures beside `attachments`
|
|
220
|
+
and `animations`, and the `skeleton` header block at the top, which measures the header's setup-pose
|
|
221
|
+
bounding box.
|
|
222
|
+
A ninth group of six joins them when something has paired the two sides'
|
|
223
|
+
animations — `--as <candidate>=<reference>`, or one animation each side, which pairs by
|
|
224
|
+
position (§1.3.1). Both sides may be foreign; the interesting pairing during ingest is
|
|
225
|
+
**your transcription against the export it came from**:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
rigc diff work/t3/skeleton.json \
|
|
229
|
+
examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
rigc diff
|
|
234
|
+
candidate …/work/t3/skeleton.json
|
|
235
|
+
reference …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
236
|
+
.. bones=3/3 slots=2/2 skins=1/1 attachments=2/2 constraints=0/0 animations=2/2 events=0/0 (candidate/reference)
|
|
237
|
+
|
|
238
|
+
skeleton (reported) (no mean) over 2 measures — the header's setup-pose bounding box, which no reading of the frames could decide
|
|
239
|
+
1.000 bounds_present 1/1 both headers carry a setup-pose bounding box, or neither does — …
|
|
240
|
+
0.250 bounds_box 1/4 the header carries the same box (x, y, width, height — the extent as stated, an omitted origin as the 0 it means) — …
|
|
241
|
+
|
|
242
|
+
bones mean 1.000 over 8 measures
|
|
243
|
+
1.000 count 3/3 how many bones
|
|
244
|
+
1.000 names 3/3 the bone names themselves
|
|
245
|
+
1.000 parent_by_name 3/3 each bone hangs off the same parent
|
|
246
|
+
1.000 order 3/3 the bones are declared in the same order
|
|
247
|
+
1.000 length_present 3/3 a setup `length` is present or absent alike
|
|
248
|
+
1.000 inherit_present 3/3 a setup `inherit` is present or absent alike
|
|
249
|
+
1.000 depth_histogram 3/3 NAME-AGNOSTIC: as many bones at each depth
|
|
250
|
+
1.000 degree_sequence 3/3 NAME-AGNOSTIC: as many bones with each child count
|
|
251
|
+
|
|
252
|
+
bones (name-agnostic) mean 1.000 over 5 measures — the same two skeletons compared with names thrown away
|
|
253
|
+
…
|
|
254
|
+
animations mean 1.000 over 11 measures
|
|
255
|
+
1.000 count 2/2 how many animations
|
|
256
|
+
1.000 names 2/2 the animation names
|
|
257
|
+
1.000 duration 2/2 each animation runs as long (last key time, within one frame)
|
|
258
|
+
1.000 timeline_kinds 8/8 the same timelines exist
|
|
259
|
+
1.000 key_counts 69/69 those timelines carry as many keys
|
|
260
|
+
1.000 curve_kinds 69/69 as many linear / stepped / bezier keys
|
|
261
|
+
…
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Each pair is **matched / total**, where the total is the larger of the two sides. So a
|
|
265
|
+
count of `2/3` means one side has three of something and only two were matched — and
|
|
266
|
+
it does not say *which* side has three. The `..` line above is where you read that.
|
|
267
|
+
|
|
268
|
+
⛔ **`diff` is blind to every coordinate a bone, an attachment or a key carries.** No
|
|
269
|
+
measure reads a bone's `x`/`y`/`rotation`, an attachment's offset, or a key's value —
|
|
270
|
+
only *presence*, *names*, *counts*, *order* and *kinds*. §4.1 moves a pivot 236.5
|
|
271
|
+
units and every one of the 54 measures still reads **1.000**. ⇒ Never take a green
|
|
272
|
+
`diff` as evidence that a geometric edit did not land, and never take it as evidence
|
|
273
|
+
that one did.
|
|
274
|
+
|
|
275
|
+
⚠️ **`diff` has no value-level measure, and the reason is an input rather than a
|
|
276
|
+
policy** (§2.3 says what a value comparison covers): comparing values means reading
|
|
277
|
+
both files through `spine-core`, and a skeleton whose attachments carry a `sequence`
|
|
278
|
+
cannot be parsed without the atlas that resolves it — so the measure takes two
|
|
279
|
+
skeletons **and two packs**, which `rigc diff <a.json> <b.json>` does not have.
|
|
280
|
+
|
|
281
|
+
⚠️ **The one exception is the header's own box**, and it is an exception to the
|
|
282
|
+
sentence and not to the rule: `skeleton.bounds_box` compares four world numbers — the
|
|
283
|
+
setup-pose bounding box each writer put in its header (rigc's is spine-core's
|
|
284
|
+
`getBounds` on the header's 1e-6 grid at float32 since issue #907; the editor's is its own arithmetic, up to
|
|
285
|
+
0.0071 units from `getBounds` on the twelve examples) — exactly, and the block they
|
|
286
|
+
sit in gates nothing. The measure was called `stage_box` until #907, when `build`
|
|
287
|
+
stopped writing the stage there.
|
|
288
|
+
|
|
289
|
+
⭐ **Declared is not the same as written down, and for the origin it is the
|
|
290
|
+
difference between a true reading and a false finding.** The editor omits a header
|
|
291
|
+
field at its default, so a box sitting at `0,0` exports as a `width` and a `height`
|
|
292
|
+
and no `x`/`y` at all — there is no other spelling for it. The measure reads that
|
|
293
|
+
omission as the `0` it means. The extent is still read exactly as stated: it is what
|
|
294
|
+
decides whether there is a box at all, so a missing `width` is an absent box rather
|
|
295
|
+
than a box of width zero.
|
|
296
|
+
|
|
297
|
+
⛔ **And its ratios are not a score.** [`src/diff.ts`](../src/diff.ts) says so in the
|
|
298
|
+
type itself (*"Unweighted mean of the measures below. NOT a quality score"*), and the
|
|
299
|
+
report repeats it at the foot. It measures *agreement with a particular reference*,
|
|
300
|
+
which is the right question during transcription and the wrong question the moment the
|
|
301
|
+
task is to change something (§4.2, §4.3).
|
|
302
|
+
|
|
303
|
+
⭐ **The name-agnostic groups are the ones to reach for when names are what differs.**
|
|
304
|
+
`bones` and `slots` each get a second pass with names thrown away — depth histogram,
|
|
305
|
+
degree sequence, shape histogram, declaration order of shapes, and for slots the
|
|
306
|
+
attachment types and bone-binding shapes by draw-order position. That is how you tell
|
|
307
|
+
*"the same rig with a different vocabulary"* from *"a different rig"*, and §4.2 is the
|
|
308
|
+
recipe built on it.
|
|
309
|
+
|
|
310
|
+
#### 1.3.1 Animations differ by name too — `--as`
|
|
311
|
+
|
|
312
|
+
`bones` and `slots` are matched name-agnostically by their own shape. Animations have
|
|
313
|
+
none: the candidate's `take01` and the reference's `arcs` are the same shot only
|
|
314
|
+
because somebody says they are. Two things say it —
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
rigc diff work/t6/skeleton.json examples/6-arcs/export/6-arcs-pro.json --as take01=arcs
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
— and, with no flag, **one animation each side**, which pairs by position because there
|
|
321
|
+
is exactly one reading of which shot is which. `--as` is repeatable, one pair each, and
|
|
322
|
+
the candidate's name goes on the left, as it does in `bonedist`'s correspondence file.
|
|
323
|
+
|
|
324
|
+
With the pairing in hand the `animations` section reports two figures like the other
|
|
325
|
+
two sections, the second over the paired shots — `duration`, `timeline_kinds`,
|
|
326
|
+
`key_counts`, `curve_kinds`, `draw_order`, `deform` — and the heading says which pairing
|
|
327
|
+
it used:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
animations mean 0.182 over 11 measures
|
|
331
|
+
1.000 count 1/1 how many animations
|
|
332
|
+
0.000 names 0/2 the animation names
|
|
333
|
+
…
|
|
334
|
+
animations (name-agnostic) mean 1.000 over 6 measures — the same two skeletons compared with names thrown away, paired by position: take01=arcs, the one animation each side carries
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Read that pair exactly as you read `bones`'s: **1.000 beside `names` 0.000** says the
|
|
338
|
+
shot is right and its name is yours. ⛔ `names` never moves into the second block, and
|
|
339
|
+
with two shots on each side and no `--as`, the block is **absent** rather than paired by
|
|
340
|
+
declaration order — a candidate that declares its two shots the other way round would
|
|
341
|
+
then read 0.000 across it and the report would be calling a guess a measurement.
|
|
342
|
+
|
|
343
|
+
⚠️ An `--as` naming an animation a side does not have is **refused** with what that side
|
|
344
|
+
does have, and so is one that pairs the same animation twice. Neither is dropped
|
|
345
|
+
quietly: a typo that measured less than you asked for is a report about a pairing you
|
|
346
|
+
did not state.
|
|
347
|
+
|
|
348
|
+
### 1.4 `check` — the instrument that does see coordinates
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
rigc check --candidate <a compiled skeleton> --frames <a rendered frame set>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`check` never opens a reference skeleton. It renders the candidate onto the frames'
|
|
355
|
+
own pixel grid, fits it there by its own drawn pixels, and compares. For ingest that
|
|
356
|
+
gives you a loop nothing else in the toolchain provides, in two steps:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# 1. turn the foreign export into a reference frame set
|
|
360
|
+
rigc render --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
|
|
361
|
+
--fps 12 --max 256 --out ref3
|
|
362
|
+
|
|
363
|
+
# 2. measure anything at all against it
|
|
364
|
+
rigc check --candidate work/t3 --frames ref3 \
|
|
365
|
+
--texture-from examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
📐 **Establish the positive control first.** Point `check` at the same export the
|
|
369
|
+
frames came from, and it has to read zero. It does:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
rigc check --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
|
|
373
|
+
--frames ref3/light
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**No run reproduces this:** abridged — the head's `atlas`, `frames`, `skin`, `scope`, `reference`, `content` and `in units` lines and its two ⚠️ notes are cut, so the `⤷ fit` line the run prints under `content` stands under `framed to`; the section's `frames` line and every `⤷` note are cut; and so is everything the run prints after `per-frame`
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
rigc check
|
|
380
|
+
candidate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
381
|
+
framed to 256x116px 0.117628 px/unit world x[-573.3 .. 1603.0] y[-81.2 .. 908.9] (frames.json's own box — the candidate measured into it)
|
|
382
|
+
⤷ fit x1.000000 offset +0.00, +0.00 px rms 0.00 px over 84 edge(s) union residual +0.00 x +0.00 px aspect +0.00% (declared, 1 pass(es), settled)
|
|
383
|
+
declared frames.json's own box: TAKEN, coincident — a fit there asks for 0.00 px, under the 1 px that separates a candidate in the frames' coordinates from one in its own, over 21 frame(s).
|
|
384
|
+
|
|
385
|
+
── light — candidate animation "light", 12 fps ──
|
|
386
|
+
MAE mean 0.00 worst 0.00 (exact: none of the 21 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
|
|
387
|
+
slot drift worst 0.4 px "pendulum" at f0012
|
|
388
|
+
per-frame all 20 adjacent pair(s) change by as much as the reference's own frames do
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Two things to take from the control, both of which you need before reading any real
|
|
392
|
+
number: the **framing** resolved to the frames' own box at 0.00 px, so the MAE is a
|
|
393
|
+
comparison of pictures rather than of framings; and **slot drift still reads 0.4 px at
|
|
394
|
+
MAE 0.00**, which is that instrument's own floor rather than a difference.
|
|
395
|
+
|
|
396
|
+
⚠️ **`--texture-from` is not optional on ingest work, and the reason is structural.**
|
|
397
|
+
rigc's default atlas is **one region per page at the art's own resolution**; the editor
|
|
398
|
+
packs many regions onto one page, often at a reduced `scale:`. Two builds of
|
|
399
|
+
geometrically identical data therefore sample differently-scaled texels in every frame,
|
|
400
|
+
and that difference is a constant no key can move. `check` says so unprompted, and
|
|
401
|
+
`--texture-from <the atlas the frames were rendered through>` measures it: same
|
|
402
|
+
geometry, swapped texels. §2.3 is what the resulting number means, and which atlas your
|
|
403
|
+
build should be using in the first place.
|
|
404
|
+
|
|
405
|
+
### 1.5 `explain` — for the specs you write, not the file you were given
|
|
406
|
+
|
|
407
|
+
📌 **`explain` does not read a compiled skeleton, and it is worth knowing that up
|
|
408
|
+
front** so you do not go looking for a reading tool that is not there:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
rigc explain examples/spineboy/export/spineboy-pro.json
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
rigc: give either --cut <name> --cuts <cuts.json>, or --rig/--motion/--out
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
It takes `--rig`, `--motion`, `--out`, and optionally `--manifest`, `--images` and
|
|
419
|
+
`--atlas-in` — `build`'s spec-reading flags, and not the ones that decide what `build`
|
|
420
|
+
*writes* (`--pack`, `--page-size`, `--padding`, `--page-edges`, `--copy-images`) or the one that gates
|
|
421
|
+
(`--profile`). It prints the resolved account
|
|
422
|
+
of **your** two spec files, and never gates. Which makes it a §2 instrument rather
|
|
423
|
+
than a §1 one — the thing you run to compare what you transcribed against the export
|
|
424
|
+
you transcribed it from, by eye:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
rigc explain --rig bench/transcriptions/3-timing-and-spacing/3-timing-and-spacing-ess.rig.json \
|
|
428
|
+
--motion bench/transcriptions/3-timing-and-spacing/3-timing-and-spacing-ess.motion.json \
|
|
429
|
+
--out work/x3
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
stage 945.1005 x 815.4317 (spine 4.3.13)
|
|
434
|
+
|
|
435
|
+
bones (spine world: y up)
|
|
436
|
+
root parent=- x=0 y=0
|
|
437
|
+
square parent=root x=380.7311 y=78.8596
|
|
438
|
+
bone parent=root x=204.753 y=708.0989 rotation=180
|
|
439
|
+
|
|
440
|
+
slots (array order IS the draw order)
|
|
441
|
+
pendulum bone=bone setup=pendulum color=ffffffff attachments=[pendulum]
|
|
442
|
+
square bone=square setup=square color=ffffffff attachments=[square]
|
|
443
|
+
|
|
444
|
+
animations
|
|
445
|
+
heavy declared=5.333333s loop=true
|
|
446
|
+
bone.rotate 20 key(s)
|
|
447
|
+
t=0 value=0 bezier[4]
|
|
448
|
+
t=0.233333 value=3.31738 bezier[4]
|
|
449
|
+
…
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
⚠️ **`--out` is required and nothing is written to it.** The flag is shared with
|
|
453
|
+
`build`'s parser; `explain` prints and exits. Passing a directory that does not exist
|
|
454
|
+
is fine — it is not created.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 2. Getting specs out of a skeleton
|
|
459
|
+
|
|
460
|
+
Everything in §1 reads. To **change** anything you need specs. There are two routes
|
|
461
|
+
to them and you should almost always take the first.
|
|
462
|
+
|
|
463
|
+
### 2.0 `ingest` — let the tool write them
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
rigc ingest examples/spineboy/export/spineboy-ess.json --out specs/ --art none
|
|
467
|
+
rigc build --rig specs/rig.json --motion specs/motion.json --atlas-in examples/spineboy/export/spineboy.atlas --out spine
|
|
468
|
+
rigc diff spine/skeleton.json examples/spineboy/export/spineboy-ess.json
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
`ingest` reads the skeleton — **only** the skeleton — and writes `rig.json`,
|
|
472
|
+
`motion.json` and `findings.json`. The contract is an equality rather than a
|
|
473
|
+
rulebook: `build(ingest(x))` is `x`, byte for byte on `skeleton.json`, and the
|
|
474
|
+
atlas comes back with the same region blocks (as a multiset — the page order is in
|
|
475
|
+
no field of the file).
|
|
476
|
+
|
|
477
|
+
📊 **For an editor export the claim is weaker, and measured.** Every
|
|
478
|
+
`examples/*/export/*.json` ingested with `--art none`, rebuilt through the pack
|
|
479
|
+
beside it, and `diff`ed against the file it was read from: **12 of 12 come back with 0
|
|
480
|
+
blockers and 1.000 on all 54 ratio-bearing measures and all 6 reported ones** — and on
|
|
481
|
+
all **nine value measures** too, over **195,363** compared values. Byte
|
|
482
|
+
identity is not the claim there and the reason is the input, not the round trip — §2.3
|
|
483
|
+
has the pass line, and what the value measures do and do not reach. ⚠️ Which pack is "the one beside it" is
|
|
484
|
+
resolved rather than guessed, for §0.2's reason: `spineboy/export` holds two, and
|
|
485
|
+
`spineboy-run.atlas` covers neither skeleton in it.
|
|
486
|
+
|
|
487
|
+
**What it will not do is invent.** Everything the spec format cannot hold is a
|
|
488
|
+
finding with a code — `BLOCK` for a construct the rebuild will be missing, `JUDGE`
|
|
489
|
+
for what a skeleton does not carry and the rebuild has to state (the two values
|
|
490
|
+
below, and whether a muted constraint is the consumer's), `LOSS` wherever the source's spelling and
|
|
491
|
+
rigc's differ on purpose (a number rigc re-derives, a field the spec has no home for, or
|
|
492
|
+
a default the source left to the format and the rebuild writes out). A blocker exits
|
|
493
|
+
non-zero and still writes both files. **Every code it can print has a row at the end
|
|
494
|
+
of this section**, with its gutter, its effect on the exit code and what to do.
|
|
495
|
+
|
|
496
|
+
⛔ **And it reads one generation.** Spine data is locked to the generation that
|
|
497
|
+
exported it, and a mismatch is silent rather than loud: 4.3 takes constraints from the
|
|
498
|
+
top-level `constraints` array alone, so a 4.0–4.2 file's `ik`/`transform`/`path`/
|
|
499
|
+
`physics` arrays load as nothing at all. So `ingest` reads
|
|
500
|
+
`skeleton.spine` before it reads a field of the file, and a file from another
|
|
501
|
+
generation is a blocker naming that generation and counting, **on that file**, what a
|
|
502
|
+
4.3 reader loses by it. Reading such a file with *that generation's own* defaults is a
|
|
503
|
+
different job, and it is not in this tool, which is why the finding points
|
|
504
|
+
at the policy rather than implying the file was read.
|
|
505
|
+
|
|
506
|
+
**Two values are not in a skeleton**, so `ingest` asks rather than guesses:
|
|
507
|
+
|
|
508
|
+
- **the stage** (`skeleton.width`/`height`) — a file that declares none is **carried as
|
|
509
|
+
declaring none**: the rig spec states `"width": null, "height": null` (§2.1 step 3's
|
|
510
|
+
spelling), the rebuild emits a header with none of `x`/`y`/`width`/`height`, and it
|
|
511
|
+
is the file that was read, byte for byte. No finding is recorded, because nothing
|
|
512
|
+
was lost and nobody decided anything. `--stage x,y,w,h` is how a caller *adds* a box
|
|
513
|
+
to such a file, and that is a `NO_STAGE` **judgement**. All twelve exports in the
|
|
514
|
+
fetched corpus carry a box, and `ingest` reads it straight through into the rebuild's
|
|
515
|
+
stage. A **rigc build**'s stage is read instead from the `skeleton.model.json` beside it,
|
|
516
|
+
when that document's `spine.sha256` is the skeleton's digest — its header is the setup-pose
|
|
517
|
+
bounding box — and an export's from its header box. 🔁 What an export's header carries is its setup-pose bounding box; it is the
|
|
518
|
+
only box the file has, so it becomes the rebuild's stage, and the rebuild's own header
|
|
519
|
+
is computed again — `build` writes the setup-pose bounding box there, never the stage
|
|
520
|
+
(issue #907). The stage cannot be *derived*: it is the working area the art was
|
|
521
|
+
painted in, and no pose of the rig states it. 🔸 **Half a stage is a `NO_STAGE` blocker**: an origin with no
|
|
522
|
+
extent, or one extent without the other, declares no stage and is not the absence
|
|
523
|
+
either, and the rig spec holds a stage as four fields or none. It is also the value that costs least to get wrong: `diff`
|
|
524
|
+
reports the header's box as two measures of its own (`bounds_present`, `bounds_box`) and they are
|
|
525
|
+
`(reported)`, so no score reads them and an absurd box is green nearly everywhere.
|
|
526
|
+
⛔ **The flag is refused beside a box the file states** — two sources for one value,
|
|
527
|
+
both named, and the file is the record of what was measured;
|
|
528
|
+
📦 **`--stage-box <slot>` reads the stage a rig carried in its Spine files** (issue
|
|
529
|
+
#1168): a rig whose spec states `skeleton.stageBox` ([AUTHORING §3.1](AUTHORING.md))
|
|
530
|
+
ships its stage as a bounding box in that slot, and a caller who has only
|
|
531
|
+
`skeleton.json`, the atlas and its pages names the slot. The box's four corners are
|
|
532
|
+
read as the stage in place of the header's box, and the rebuilt spec asks for the same
|
|
533
|
+
box rather than transcribing it, so `build` writes it again from the stage — a rigc
|
|
534
|
+
build carrying one rebuilds byte for byte. Only a box `build` could write back is read:
|
|
535
|
+
a slot the skeleton lacks, a slot on a bone other than an unmoved root, a slot holding
|
|
536
|
+
anything but one bounding box in the `default` skin, a box not four unweighted corners
|
|
537
|
+
of one axis-aligned rectangle — each is refused by name, as is `--stage` beside the
|
|
538
|
+
flag, and a model document beside the skeleton stating another stage. A box `build`
|
|
539
|
+
spells differently (its corners in another order, the editor's `color`) is the lossy
|
|
540
|
+
`STAGE_BOX_REWRITTEN`. Without the flag no slot is read as the stage because of its
|
|
541
|
+
name, and the box is transcribed as the attachment it is;
|
|
542
|
+
- **each animation's duration** — the format has no such field. The largest key time
|
|
543
|
+
is used, stated in the motion spec's `note`, and recorded as a finding per
|
|
544
|
+
animation. Edit it if you know the real number.
|
|
545
|
+
|
|
546
|
+
🎛️ **And one statement is not in a skeleton either: who turns a muted constraint on.**
|
|
547
|
+
An ik or transform constraint
|
|
548
|
+
resting at 0 on every mix it reads, that no animation keys above 0, is either a
|
|
549
|
+
leftover that moves nothing or a dial a game sets from code — and the two export as
|
|
550
|
+
the same bytes. `build` refuses the shape by name (`A47`/`A48`), so without a
|
|
551
|
+
statement the rebuild of a file whose game switches that ik on at runtime is refused.
|
|
552
|
+
So `ingest` reads it the way under which the file is correct: it writes the
|
|
553
|
+
constraint into the rig spec's `invariants.consumerDrivenMix` ([AUTHORING
|
|
554
|
+
§3.7](AUTHORING.md)), with a `why` saying the entry is `ingest`'s reading, and prints a
|
|
555
|
+
`CONSUMER_DRIVEN_MIX` **judgement** naming it. The rebuild is the same bytes — the
|
|
556
|
+
declaration is a statement to the gate, never emitted — and it gates green, with the
|
|
557
|
+
constraint SKIPped by name rather than measured. It is the only field of `invariants`
|
|
558
|
+
`ingest` ever writes, and the rig spec's `note` says so where it is present.
|
|
559
|
+
⚠️ **None of the twelve exports carries the shape**: every constraint they rest muted
|
|
560
|
+
is keyed up by an animation, spineboy's aim rig being the idiom, so `ingest` prints no
|
|
561
|
+
such line and writes no declaration for any of them.
|
|
562
|
+
|
|
563
|
+
And two flags for what the skeleton also does not encode: `--art loose` (the default)
|
|
564
|
+
names an `image` per attachment resolved against loose PNGs, `--art none` states
|
|
565
|
+
`width`/`height` for `build --atlas-in` — and for `explain --atlas-in`, which is the
|
|
566
|
+
same pack read for a report rather than for an artifact: `explain` **poses** the rig
|
|
567
|
+
to print its `DEFORM` block, a pose resolves every attachment against an atlas, and a
|
|
568
|
+
size-only spec carries none of its own, so without the flag that pair is refused by
|
|
569
|
+
name rather than posed; and
|
|
570
|
+
under `loose`, `--images <dir>` writes
|
|
571
|
+
the rig spec's own images directory relative to `--out`, so the rebuild is a plain
|
|
572
|
+
`build --rig … --motion … --out …` rather than one carrying `--images` forever. It is
|
|
573
|
+
refused together with `--art none`, which writes no `image` for a directory to be the
|
|
574
|
+
base of. [AUTHORING §0.3](AUTHORING.md) is the loop in full.
|
|
575
|
+
|
|
576
|
+
📝 Both written specs carry a `note` saying they are decompiled and naming the file
|
|
577
|
+
they came from. Leave it there — §2.4 is why.
|
|
578
|
+
|
|
579
|
+
#### Every finding code, and what to do about it
|
|
580
|
+
|
|
581
|
+
A finding line reads `<gutter> <CODE>: <where> — <detail>`, and the code is the part
|
|
582
|
+
that is the same on every run. This table is the whole set: which gutter it prints
|
|
583
|
+
under, whether it changes the exit code, what it means, and what to do about it.
|
|
584
|
+
**Nothing here refuses the file** — a construct the spec format cannot hold is recorded
|
|
585
|
+
rather than rejected — so an exit of 1 means *a blocker was recorded*, and both specs
|
|
586
|
+
are on disk either way. (The one thing `ingest` does refuse outright is an option that
|
|
587
|
+
contradicts the file, which is not a finding: `--stage` beside a box the skeleton
|
|
588
|
+
declares — and, under `--stage-box <slot>`, a slot that holds no box `build` could
|
|
589
|
+
write back, `--stage` beside it, or a model document beside it stating another
|
|
590
|
+
stage (§2.0's stage box, below).)
|
|
591
|
+
|
|
592
|
+
| code | gutter | exit | what it means | what to do |
|
|
593
|
+
| --- | --- | --- | --- | --- |
|
|
594
|
+
| `ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (§2), or accept that the rebuild does not carry it |
|
|
595
|
+
| `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path, and `point` is the one deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
|
|
596
|
+
| `ATTACHMENT_LINK_GEOMETRY` | `LOSS` | 0 | a **linked mesh** that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by nothing at all and the attachment draws the geometry its `source` names; the rig spec has no home for them either, because `build` refuses geometry on a link by name. The detail lists the keys and the source | nothing. The rebuild is the mesh the runtime was already drawing — and if those keys were the geometry you meant, take `source` off and author it as a mesh of its own. `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` is the same fact at the gate |
|
|
597
|
+
| `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | a `sequence` block — a numbered image series — that the rig spec cannot say **as written**. A well-formed block on a region, mesh or linked mesh is carried field for field, with no `image` on the loose route (the frames `<path><number>` are the art), and a `sequence` timeline with it; what is left here is a block the parser reads into a series other than the one written — no `count` (0 regions), a `setup` past the end (clamped), a fraction — or one on a `boundingbox`, `clipping` or `path`, where the parser never reads it. The detail quotes the block and says which | the rebuild draws the single region the attachment names. Fix the block in the source — a `count` is the usual one — and ingest again |
|
|
598
|
+
| `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline that is neither `deform` nor `sequence`. Both are carried, and `readAnimation` tests an attachment timeline for exactly those two names and ignores anything else (`SkeletonJson.js:1147-1201`) — so what reaches this line is a name outside the format, which no player plays either | fix the timeline's name in the source, or accept that the rebuild does not carry it |
|
|
599
|
+
| `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 |
|
|
600
|
+
| `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. Every bone timeline the runtime plays has a track, `inherit` included — the eleventh case of the runtime's own bone switch, a stepped mode per key — 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 |
|
|
601
|
+
| `CONSTRAINT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a constraint, with its type named beside it | as `BONE_FIELD` |
|
|
602
|
+
| `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. 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: 0 tracks print it | nothing. Same values, and where this prints, a larger file — the rebuild plays what the source plays |
|
|
603
|
+
| `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` 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 |
|
|
604
|
+
| `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 |
|
|
605
|
+
| `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 |
|
|
606
|
+
| `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: rounding `3.8.99` up hands 3.8 data to a 4.2 runtime, and it poses as NaN | check the string against the file you were handed. A real generation rigc does not list is worth reporting, with the string beside it |
|
|
607
|
+
| `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, bones carrying `transform` where 4.3 spells `inherit`, and physics constraints omitting `inertia` / `damping`, whose default is not the same number in 4.2 as in 4.3 | 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 not in this tool |
|
|
608
|
+
| `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` is not on this line: the rig spec states it ([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 |
|
|
609
|
+
| `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 |
|
|
610
|
+
| `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 |
|
|
611
|
+
| `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 |
|
|
612
|
+
| `NO_STAGE` | `BLOCK` `JUDGE` | 1 | the skeleton declares no stage, and one of two things follows. A **judgement** — exit 0 — when `--stage x,y,w,h` supplied a box, because nothing measured the box you gave it. A **blocker** when the header states **half** a stage — an origin with no extent, or one extent without the other — which the rig spec cannot hold; the detail names the fields it states. A header with **none** of the four is not a finding at all: it is carried as `"width": null, "height": null` and rebuilds byte for byte | for the judgement, nothing if the box came from the project the file came from. For the blocker, supply the box with `--stage`, or take the stray field(s) out of the source and the absence is carried. It cannot be derived: posing the rig gives the animated extent, which is a different number |
|
|
613
|
+
| `PATH_TIMELINE` | `BLOCK` | 1 | a path-constraint timeline the motion spec has no track for — it carries position, spacing and mix | transcribe it, or accept that the rebuild plays nothing there |
|
|
614
|
+
| `PHYSICS_DRIVES_NOTHING` | `LOSS` | 0 | a physics constraint none of whose `x`, `y`, `rotate`, `scaleX`, `shearX` is above 0 — absent, or stated at 0 or below. `PhysicsConstraint.update` applies a component only above 0 (`PhysicsConstraint.js:112`), so it moves no bone, and `build` refuses exactly that shape by name at `A23_PHYSICS_CONSTRAINT_EFFECTIVE`. The rig spec **omits** it, together with every timeline keyed to it (a track naming it would be an unknown constraint to the rebuild, refused at compile) and its place on any skin's `physics` list; the detail names each, and the values it did state. Measured on a generated rig through spine-core, posing the source with and without such a constraint differs by **0** on every bone world value — and by at most 9e-8 when it sits on the root, which is the runtime's `modifyWorld` recomputing a local transform it had no reason to, not a component. ⚠️ **One thing does move:** a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation, and the detail says which animation and both lengths | nothing, if it was meant to do nothing. If it was meant to jiggle, the file never said so: give it the component it should drive and it is carried like any other. Where the detail names a shortened animation and the length matters to whatever loops it, key something at the length it had |
|
|
615
|
+
| `PHYSICS_GLOBAL_REACHES_NOTHING` | `LOSS` | 0 | a physics timeline keyed under the **empty** name — the one that names no constraint, which the runtime applies to every physics constraint declaring that property global (`"strengthGlobal": true` for `strength`; `reset` resets every physics constraint and asks no flag) — in a file where no physics constraint the rebuild carries declares it. The motion spec spells that timeline `"physics": "*"` and `build` refuses one that reaches nobody by name, so the rig spec **omits** it: in the source it walked every constraint and wrote into none. A constraint `PHYSICS_DRIVES_NOTHING` omitted counts as not carried — it was the only thing such a timeline could reach, and it moved no bone. ⚠️ As with that row, a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation and the detail says both lengths. An unnamed timeline that **does** reach a constraint is not a finding at all: it is carried as `"*"` and rebuilt under the empty name byte for byte | nothing, if it was meant to do nothing. If it was meant to drive the constraints, the file never said which: set `"<property>Global": true` on them in the rig spec and key it as `"physics": "*"` |
|
|
616
|
+
| `PHYSICS_TIMELINE` | `BLOCK` | 1 | the same for a physics constraint, whose eight the motion spec carries in full — so this is reachable only for a name the **parser** falls through too | as `PATH_TIMELINE` |
|
|
617
|
+
| `SLIDER_TIMELINE` | `BLOCK` | 1 | the same for a slider, which carries time and mix | as `PATH_TIMELINE` |
|
|
618
|
+
| `SLOT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a slot | as `BONE_FIELD` |
|
|
619
|
+
| `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. 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**. `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 |
|
|
620
|
+
| `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 | 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 |
|
|
621
|
+
| `STAGE_BOX_REWRITTEN` | `LOSS` | 0 | under `--stage-box <slot>`: the bounding box read as the stage states its corners in another order than `build` writes them (bottom-left first, counter-clockwise), or a `color` — the editor writes one on every box it exports. The rebuilt spec asks for the box (`skeleton.stageBox`), and `build` writes it from the stage, so the rebuild carries the same rectangle spelled `build`'s way. The detail names what changes | nothing. The stage is the same four numbers; only the box's spelling moves |
|
|
622
|
+
| `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 |
|
|
623
|
+
| `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. The emitter 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, 0 tracks print it. 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 |
|
|
624
|
+
| `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
|
|
625
|
+
|
|
626
|
+
⚠️ **An attachment's `name` has no row, because nothing about it is lost.** `ingest`
|
|
627
|
+
carries a stated `name` verbatim — equal to its key or not, exactly as the source spells
|
|
628
|
+
it — and writes none where the source states none, and `compile` emits exactly what the
|
|
629
|
+
spec states.
|
|
630
|
+
|
|
631
|
+
### Transcription — the route that made a foreign skeleton yours
|
|
632
|
+
|
|
633
|
+
⚠️ **The rest of §2 is transcription by hand, and the reading it produces is the
|
|
634
|
+
right one** — it is what an author does *after*
|
|
635
|
+
`ingest`, and it is what to fall back on for the constructs `ingest` reports as
|
|
636
|
+
blockers. The numbers come out of the JSON into a rig spec and a motion spec by hand,
|
|
637
|
+
and `build` emits a new skeleton from those.
|
|
638
|
+
|
|
639
|
+
What you get for it is that the file becomes editable by declaration — a pivot move
|
|
640
|
+
is two numbers in a spec (§4.1) and a new animation is an added block (§4.3), rather
|
|
641
|
+
than a hand-edit of emitted JSON with nothing checking it. That is what
|
|
642
|
+
`ingest` hands you in one command; the sections below are how to read and change what
|
|
643
|
+
it hands you, and every rule in them applies to a spec `ingest` wrote.
|
|
644
|
+
|
|
645
|
+
A file from another Spine generation is the one case where transcription is not the
|
|
646
|
+
first fallback: migrate it through the editor of its own generation first, as
|
|
647
|
+
[GENERATIONS.md](https://github.com/firejune/rigc/blob/main/docs/GENERATIONS.md) §4
|
|
648
|
+
states.
|
|
649
|
+
|
|
650
|
+
### 2.1 The workflow
|
|
651
|
+
|
|
652
|
+
1. **Read the skeleton first, with `validate` and `render`.** The `SKIP` list is your
|
|
653
|
+
feature inventory (§1.1); the contact sheets are what the animations actually do.
|
|
654
|
+
Knowing there is no deform timeline before you start is worth more than discovering
|
|
655
|
+
it in the eleventh hour of transcribing one.
|
|
656
|
+
2. **Get the loose art, at the size the export declares.** rigc measures PNGs rather
|
|
657
|
+
than trusting a size you typed (AUTHORING R5), so the art has to *be* the right
|
|
658
|
+
size. Take the target from the export's own attachments —
|
|
659
|
+
`3-timing-and-spacing` declares `"width": 745, "height": 212` for `pendulum`, and
|
|
660
|
+
the loose `pendulum.png` beside it is exactly 745×212. ⚠️ Do **not** take it from
|
|
661
|
+
the atlas region bounds: that page carries `scale: 0.5`, so `pendulum`'s bounds read
|
|
662
|
+
`373, 106`. Two numbers for one part, and the attachment's is the one in world
|
|
663
|
+
units. `--atlas-in` does that division for you (§2.3), but it can only land
|
|
664
|
+
within the pack's own rounding — by hand, off the attachment, it is exact.
|
|
665
|
+
3. **Transcribe the rig spec: header, bones, slots, skins.** Bones parents-first; the
|
|
666
|
+
`slots` array *is* the draw order (AUTHORING R4), so its order is data you are
|
|
667
|
+
copying and not a detail. Leave `invariants` out entirely — it describes rigc's own
|
|
668
|
+
formations, and an absent field makes an archetype assertion `SKIP`, never pass
|
|
669
|
+
(AUTHORING §3.7).
|
|
670
|
+
|
|
671
|
+
📌 **Transcribe the export's empty slots too** — the ones no skin fills anywhere.
|
|
672
|
+
Such a slot still holds an index in the array, and everything below it is counted
|
|
673
|
+
from that index. Write it as `{ "name": …, "bone": … }` with no `attachment`, or
|
|
674
|
+
with `"attachment": null` if you prefer to say it out loud; either way it comes
|
|
675
|
+
back. If a transcription's `slots.count` is under 1.000, a missing empty slot is
|
|
676
|
+
the first thing to check.
|
|
677
|
+
|
|
678
|
+
⚠️ **No skeleton in `examples/` has one**: all twelve exports fill every slot they declare from some skin. What they
|
|
679
|
+
*do* carry is the neighbouring shape — a slot a skin DOES fill whose setup pose
|
|
680
|
+
shows nothing (34 of `spineboy-pro`'s 52 slots). Both are written the same way in
|
|
681
|
+
the file: `attachment` simply absent.
|
|
682
|
+
|
|
683
|
+
⚠️ **If the export's `skeleton` block carries no `x`/`y`/`width`/`height`, write
|
|
684
|
+
`"width": null, "height": null` and do not invent one** — which is also what
|
|
685
|
+
`rigc ingest` writes for such a file. A made-up stage is a number no gate refuses;
|
|
686
|
+
`A14` and `A19` measure against the stage you state, and `diff`'s header block
|
|
687
|
+
reports `skeleton.bounds_present` and `skeleton.bounds_box` — your build's bounding
|
|
688
|
+
box, which you do not author, against the source's. Copy the four numbers when they are there;
|
|
689
|
+
state the absence when they are not.
|
|
690
|
+
4. **`explain`, then `build`.** `explain` first, because it prints what you wrote in a
|
|
691
|
+
shape you can compare against the export by eye (§1.5) and it never gates. Then
|
|
692
|
+
`build` under `--profile spine`.
|
|
693
|
+
5. **Transcribe the motion spec, one animation at a time**, and `build` after each.
|
|
694
|
+
6. **Close it with `diff` and `check`.** `diff` for structure; `check` against frames
|
|
695
|
+
rendered from the export for geometry; `--texture-from` to attribute the floor.
|
|
696
|
+
|
|
697
|
+
### 2.2 One feature family at a time
|
|
698
|
+
|
|
699
|
+
📌 **Transcribe by *kind*, not by animation.** All the bones, then all the slots, then
|
|
700
|
+
all the attachments, then one timeline kind across every animation. Two reasons, and
|
|
701
|
+
the second is the one that costs a day:
|
|
702
|
+
|
|
703
|
+
- A whole animation touches every feature the format has, so *"animation 1 of 6 done"*
|
|
704
|
+
means you have hit every unsolved problem at once and solved none of them cleanly.
|
|
705
|
+
- **`build` is all-or-nothing and emits only after green** (AUTHORING §0). A partial
|
|
706
|
+
transcription of one kind still builds; a half-transcribed animation may not build at
|
|
707
|
+
all, and then you are debugging your own incomplete work rather than the format.
|
|
708
|
+
|
|
709
|
+
⚠️ **When a kind turns out not to be expressible, stop and say so — that is a finding,
|
|
710
|
+
not a blocker to route around.** [SURVEY_2026-08-22.md](https://github.com/firejune/rigc/blob/main/docs/SURVEY_2026-08-22.md) is the
|
|
711
|
+
per-skeleton survey of exactly this. ⇒ Check the survey for your feature before
|
|
712
|
+
concluding either way, and if it is genuinely absent, the shape of the answer is *"this export
|
|
713
|
+
uses X, which the motion spec cannot say"* with a pointer — not a silent
|
|
714
|
+
approximation.
|
|
715
|
+
|
|
716
|
+
📎 **An editor export's curves usually need the raw `curve` escape hatch.** A named
|
|
717
|
+
easing is one curve reused; an export carries a different bezier per key per channel,
|
|
718
|
+
which no name can say. The motion spec's raw form takes absolute `(time, value)`
|
|
719
|
+
control points verbatim (AUTHORING §4.5). Named easings stay the right default for
|
|
720
|
+
motion you are *authoring* — this is the one case the escape hatch exists for, and the
|
|
721
|
+
3-timing transcription's own `note` says so.
|
|
722
|
+
|
|
723
|
+
### 2.3 What "byte-identical" can and cannot mean
|
|
724
|
+
|
|
725
|
+
State the ambition in the right units, because three different things get called
|
|
726
|
+
"identical", and the third is reachable only in a form worth stating exactly.
|
|
727
|
+
|
|
728
|
+
| Ambition | Reachable? | What it costs, and what it proves |
|
|
729
|
+
| --- | --- | --- |
|
|
730
|
+
| **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 54 measures**. Aim here first |
|
|
731
|
+
| **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
|
|
732
|
+
| **Byte-identical JSON** | ✅ **for a rebuild, in canonical form, apart from `hash`, `spine` and the header's box** — 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: what differs there is what a person chose to write, not the emitter |
|
|
733
|
+
|
|
734
|
+
⭐ **The pass line of the byte round trip, stated once:** `build(ingest(x))` of an
|
|
735
|
+
editor export `x` is **identical to `x` in canonical form, apart from `hash` and
|
|
736
|
+
`spine`**, and from the header's box (below) — canonical form being `JSON.stringify(JSON.parse(text), null, 2)` of each
|
|
737
|
+
file, which keeps every number as parsed, every key in its order and every omitted key
|
|
738
|
+
omitted, and drops only whitespace and the exponent's spelling (an export setting, not
|
|
739
|
+
a property of the rig). It holds on all twelve exports under `examples/`; for a
|
|
740
|
+
skeleton **rigc** emitted, `build(ingest(x))` is byte-identical outright.
|
|
741
|
+
|
|
742
|
+
The two **declared exceptions** are the header keys the editor writes and the rig spec
|
|
743
|
+
has no field for, by design, and each has its finding:
|
|
744
|
+
|
|
745
|
+
| Header key | Example, rebuild vs export | Why it is an exception |
|
|
746
|
+
| --- | --- | --- |
|
|
747
|
+
| `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` |
|
|
748
|
+
| `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` |
|
|
749
|
+
|
|
750
|
+
🔁 **The header's box is the third difference, and it is not a key the spec lacks** (issue
|
|
751
|
+
#907). The rig spec carries the export's `x`, `y`, `width`, `height` — as the rebuild's
|
|
752
|
+
stage — and the rebuild does not copy them back: its header carries the setup-pose bounding
|
|
753
|
+
box `build` computes, which is spine-core's `getBounds` over the export on the header's grid
|
|
754
|
+
(the rebuild draws the export's vertices to the bit, so its box is the export's box), where
|
|
755
|
+
the editor wrote its own arithmetic, at most 0.0071 units away on the twelve —
|
|
756
|
+
`spineboy-pro`'s rebuild writes `-188.63641, -7.936074, 418.453, 686.19934` against the
|
|
757
|
+
editor's `-188.6338, -7.939564, 418.4499, 686.2023`. The suite holds each rebuild's box to
|
|
758
|
+
`getBounds` of its source at tolerance 0 (`IG97`) and reads every other byte against a copy
|
|
759
|
+
of the export carrying that box.
|
|
760
|
+
|
|
761
|
+
🔢 **The rest of the header, and every omitted default, comes back as the export
|
|
762
|
+
spells it.** The **header's `audio`** is a stated value like `images`, and the rig
|
|
763
|
+
spec carries it ([AUTHORING §3.1](AUTHORING.md)). The emitter leaves out exactly the
|
|
764
|
+
keys the parser reads the same way without them ([AUTHORING §10.6c](AUTHORING.md)).
|
|
765
|
+
One shape is left and it is deliberate: a box at the origin, which the editor writes as
|
|
766
|
+
`width`/`height` with no `x`/`y` and rigc spells whole. The 4.3 JSON reader has no
|
|
767
|
+
default for the origin — an absent one loads as `undefined`, a written `0` as `0` — so
|
|
768
|
+
it is not a key the parser reads the same way without it, and the row is not in the
|
|
769
|
+
table; `LOSS HEADER_ORIGIN` names it. No file in the fetched corpus takes that
|
|
770
|
+
branch: all twelve declare an origin away from 0.
|
|
771
|
+
|
|
772
|
+
🔢 **Numbers are spelled as the editor spells them** — each as the shortest decimal
|
|
773
|
+
naming its float32 — and `ingest` carries every number as the double it parsed. The
|
|
774
|
+
whitespace of an export is not compared at all — it is an export setting,
|
|
775
|
+
pretty-printed in the examples and one line from the command line.
|
|
776
|
+
|
|
777
|
+
⇒ **`diff` at 1.000 is a statement about structure** — counts, names, parentage,
|
|
778
|
+
order, timeline kinds, key counts, curve kinds — and **not the values inside the
|
|
779
|
+
keys**, which is why a moved value is invisible to it.
|
|
780
|
+
|
|
781
|
+
⭐ **The values are measured by a second comparison rather than by `diff`.**
|
|
782
|
+
Structure at 1.000 is silent about the numbers inside it: a decompiler that halved
|
|
783
|
+
every rotation, dropped every bone's `length` or mirrored every vertex would read
|
|
784
|
+
1.000 on all 54 measures and on every `(reported)` one. So the example round trip is
|
|
785
|
+
also compared **value by value**,
|
|
786
|
+
with the format's defaults taken from the parser rather than from a table — both files
|
|
787
|
+
are read through `spine-core` and the parsed forms are compared path by path, under a
|
|
788
|
+
tolerance that is the sum of the 1e-6 grid rigc's closed-form models are evaluated on
|
|
789
|
+
— the one absolute grid it emits on — and one float32 step of the runtime's
|
|
790
|
+
storage. Nine measures — here is the `6-arcs` export's, wrapped to fit this
|
|
791
|
+
page:
|
|
792
|
+
|
|
793
|
+
```
|
|
794
|
+
values: 9/9 measure(s) at 1.000 over 13977 compared value(s); skeleton 1.000 ·
|
|
795
|
+
bones 1.000 · slots 1.000 · attachments 1.000 · constraints 1.000 · events 1.000 ·
|
|
796
|
+
key_times 1.000 · key_values 1.000 · curves 1.000
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Over the twelve exports that is **195,363 values** compared, and all twelve read
|
|
800
|
+
1.000 on all nine.
|
|
801
|
+
|
|
802
|
+
`docs/BENCHMARK.md`'s *The nine value measures* is the full statement. What it still
|
|
803
|
+
does **not** cover, in the same breath:
|
|
804
|
+
|
|
805
|
+
| Still uncovered | Why |
|
|
806
|
+
| --- | --- |
|
|
807
|
+
| `spine` and `hash` | the rig spec has no field for either, by design, and `ingest` reports both as findings — the declared exceptions above |
|
|
808
|
+
| 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 |
|
|
809
|
+
| 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 |
|
|
810
|
+
| how the file is **spelled** | nothing, on an editor export, but the declared exceptions: the rebuild spells every number as the export does, writes every object's keys in the export's order ([AUTHORING §10.6b](AUTHORING.md)) and leaves out every key the export leaves to the parser ([AUTHORING §10.6c](AUTHORING.md)). The value measure cannot see spelling — it compares what the parser loaded — and the pass line above is what can |
|
|
811
|
+
| how it **looks** | that is `check`, and `--texture-from` is how its figure is attributed |
|
|
812
|
+
|
|
813
|
+
The geometric row needs a real number, because a naive reading of `check` makes an
|
|
814
|
+
exact transcription look wrong. Here is the 3-timing transcription against frames
|
|
815
|
+
rendered from the export it was transcribed from:
|
|
816
|
+
|
|
817
|
+
```
|
|
818
|
+
── heavy — candidate animation "heavy", 12 fps ──
|
|
819
|
+
declared frames.json's own box: TAKEN, coincident — a fit there asks for 0.04 px, under the 1 px that separates a candidate in the frames' coordinates from one in its own, over 65 frame(s).
|
|
820
|
+
MAE mean 6.42 worst 7.13 at f0064 (0..255 over the union alpha; over the whole frame, mean 0.27)
|
|
821
|
+
⭐ texture floor 6.42 above it 0.00 (over the reference's own pixels, 6.42 and 0.00)
|
|
822
|
+
slot drift worst 0.7 px "pendulum" at f0017
|
|
823
|
+
per-frame all 64 adjacent pair(s) change by as much as the reference's own frames do
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
⭐ **`above it 0.00` is the whole result.** The MAE is 6.42 and **100 % of it is
|
|
827
|
+
texture**: the same geometry sampled through the reference's own atlas reads zero. The
|
|
828
|
+
transcription is geometrically exact, and the 6.42 is one-region-per-page at full
|
|
829
|
+
resolution meeting a 512×128 page declared at `scale: 0.5`. ⇒ **On ingest work, read
|
|
830
|
+
`above it` before you read the MAE.** Without `--texture-from` there is no way to tell
|
|
831
|
+
6.42-that-is-all-texture from 6.42-that-is-all-rig, and the report warns you of exactly
|
|
832
|
+
that rather than leaving you to find out.
|
|
833
|
+
|
|
834
|
+
**Which atlas your build should use, then.** `build` has three atlas routes, and the
|
|
835
|
+
choice is an ingest decision rather than a detail. All three rows below are the same
|
|
836
|
+
specs, `--frames ref3/light`, against frames rendered from the export — one set, so
|
|
837
|
+
the figures compare:
|
|
838
|
+
|
|
839
|
+
| Route | What the emitted attachments say | `check --frames ref3/light` |
|
|
840
|
+
| --- | --- | --- |
|
|
841
|
+
| **default** — one region per page, pointing at the loose PNGs | `pendulum 745x212`, `square 159x159` | `in units … x1.0001`; MAE **6.36**, texture floor 6.36, **above it 0.00** |
|
|
842
|
+
| **`--pack`** — the same loose parts onto shared pages | `pendulum 745x212`, `square 159x159` | `in units … x1.0001`; MAE **6.36**, texture floor 6.36, **above it 0.00** |
|
|
843
|
+
| **`--atlas-in <the export's own atlas>`** | `pendulum 746x212`, `square 160x160` | `in units candidate 1053.5 x 808.2 reference 1053.5 x 808.7 x0.9997`; MAE **2.03**, texture floor 0.00, **above it 2.03** |
|
|
844
|
+
|
|
845
|
+
📌 **The first two are exact and indistinguishable**, and `--pack` is the one to reach
|
|
846
|
+
for when the deliverable is meant to look like an export: MaxRects onto shared pages,
|
|
847
|
+
byte-for-byte region copies, nothing resampled or rotated (AUTHORING §0.1). It does not
|
|
848
|
+
close the 6.36 — nothing that samples full-resolution texels can, against frames drawn
|
|
849
|
+
from a half-resolution pack — so the floor stays, `--texture-from` stays the way to
|
|
850
|
+
attribute it, and *packing does not change what the figure means.*
|
|
851
|
+
|
|
852
|
+
⭐ **The third row is the mirror image of the first two, and reading it wrong is the
|
|
853
|
+
easy mistake.** Its MAE is the *lowest* of the three because it draws through the
|
|
854
|
+
reference's own texels — floor 0.00 — so what is left is geometry, and 2.03 of geometry
|
|
855
|
+
is the ONE PIXEL the pack cannot give back. `--atlas-in` divides a region's size by the
|
|
856
|
+
page's `scale:` (nine of the ten corpus atlases declare one — eight at 0.5, one at 0.4,
|
|
857
|
+
every file but `spineboy-run.atlas`), and the packer wrote `round(drawing × scale)`, so
|
|
858
|
+
a 373-texel region at `scale: 0.5` is consistent with both a 745- and a 746-pixel
|
|
859
|
+
drawing. rigc states 746, the export says 745, and putting the pixel back by hand takes
|
|
860
|
+
the same build to `x1.0000` / MAE **0.00** — which is how the residual is known to be
|
|
861
|
+
the rounding and nothing else.
|
|
862
|
+
|
|
863
|
+
⇒ **The routes differ in what their MAE is MADE OF rather than in whether they are
|
|
864
|
+
right.** Loose art or `--pack` gives exact geometry through coarser texels; `--atlas-in`
|
|
865
|
+
gives the reference's texels through geometry good to half a texel. Read `above it`
|
|
866
|
+
before the MAE either way, and read the `in units` line first — it is the line that
|
|
867
|
+
catches a whole-figure scale error, and it is the only one that does.
|
|
868
|
+
|
|
869
|
+
### 2.4 The worked precedent, and what to take from it
|
|
870
|
+
|
|
871
|
+
Three transcriptions of official Spine exports live in this repository under
|
|
872
|
+
[`bench/transcriptions/`](https://github.com/firejune/rigc/tree/main/bench/transcriptions/)
|
|
873
|
+
— `3-timing-and-spacing` (3 bones, 2 slots, 2 animations, 69 keys, every curve raw),
|
|
874
|
+
`6-arcs` (weighted meshes, mesh `edges`, four 4.3 transform constraints) and
|
|
875
|
+
`spineboy` in both `ess` and `pro`. They are **worked examples you are meant to read**,
|
|
876
|
+
and the rig spec's own `note` field is the thing to read first:
|
|
877
|
+
|
|
878
|
+
> *"Mechanical transcription of Spine's official 3-timing-and-spacing `ess` export …
|
|
879
|
+
> written to prove that a rig spec can express a foreign skeleton at all. It is NOT an
|
|
880
|
+
> authored rig: the numbers were copied out of the reference, so it says nothing about
|
|
881
|
+
> whether an agent could produce them."*
|
|
882
|
+
|
|
883
|
+
⭐ **Copy that habit, not just the technique.** A transcription's `note` should say what
|
|
884
|
+
it is, what it is not, and where its numbers came from — because the file otherwise
|
|
885
|
+
looks exactly like an authored rig and will be read as one by whoever opens it next.
|
|
886
|
+
The `images` line in those specs points out of the repository into the gitignored
|
|
887
|
+
`examples/` directory for the same reason: the art is fetched, not redistributed
|
|
888
|
+
([NOTICE.md](../NOTICE.md)).
|
|
889
|
+
|
|
890
|
+
⚠️ `bench/` does not ship in the npm package, so from an installed copy those files are
|
|
891
|
+
the link above rather than something on disk. Nor does `scripts/fetch-examples.sh` —
|
|
892
|
+
the example corpus is a repository-checkout facility, and every command line on this
|
|
893
|
+
page was run from one.
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
897
|
+
## 3. Complaints, and what each one means
|
|
898
|
+
|
|
899
|
+
Foreign data meets rigc's refusals in two waves: argument handling, before any rule
|
|
900
|
+
runs, and then the assertions.
|
|
901
|
+
|
|
902
|
+
### 3.1 Before the assertions
|
|
903
|
+
|
|
904
|
+
These three exit 2 with one sentence and no report. Together with §0.2's stack trace
|
|
905
|
+
they are the whole set an ingest task realistically meets.
|
|
906
|
+
|
|
907
|
+
| Complaint | What it means | What to do |
|
|
908
|
+
| --- | --- | --- |
|
|
909
|
+
| `N atlases beside <file> (…); name the right one with --atlas <path>` | the export directory holds more than one atlas and rigc will not guess | look at which regions each atlas declares, and name the one that covers the skeleton's attachments. §0.2 says why the filename heuristic is wrong here |
|
|
910
|
+
| `no .atlas beside <file>; name one with --atlas <path>` | you were handed the skeleton without its atlas, or copied one file out of a directory | get the atlas. Nothing downstream works without it — the attachments resolve through it |
|
|
911
|
+
| `<file> is neither a directory nor a .json skeleton` | the path is a `.skel`, a `.spine`, or anything else | §5 — rigc reads JSON only |
|
|
912
|
+
| *(exit 1, a stack trace ending in `ENOENT: … /skeleton.json`)* | you passed a directory and it is not a rigc output directory | point at the `.json` itself. §0.2 |
|
|
913
|
+
|
|
914
|
+
### 3.2 Assertions a real export fails
|
|
915
|
+
|
|
916
|
+
📊 **All twelve skeletons in the fetched corpus come back green** under the default
|
|
917
|
+
profile with the right atlas named. That is the baseline, and it is the honest headline:
|
|
918
|
+
**a correct editor export passes.** The two sections below are worth reading in full
|
|
919
|
+
because they mean opposite things — the first is a wrong input, and the second is data
|
|
920
|
+
that looks wrong and is not.
|
|
921
|
+
|
|
922
|
+
**`A00_ROUNDTRIP_PARSE` — the atlas does not cover the skeleton.**
|
|
923
|
+
|
|
924
|
+
```bash
|
|
925
|
+
rigc validate examples/spineboy/export/spineboy-ess.json \
|
|
926
|
+
--atlas examples/spineboy/export/spineboy-run.atlas
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
```
|
|
930
|
+
FAIL A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: eye-indifferent (attachment: eye-indifferent)
|
|
931
|
+
rigc: 1 assertion(s) failed
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
**What it means:** the atlas you named is a real atlas and a valid one — it is just not
|
|
935
|
+
this skeleton's. `spineboy-run.atlas` packs only what the `run` animation needs. ⇒
|
|
936
|
+
**Read this as an atlas-choice failure, not as a broken skeleton.** It is the
|
|
937
|
+
downstream shape of guessing at §0.2's refusal, and it is precise about the cost: one
|
|
938
|
+
named attachment, so you can tell "wrong atlas" from "the export is missing a region"
|
|
939
|
+
by whether the missing names are a *coherent subset*. Fix by naming the right atlas —
|
|
940
|
+
`spineboy-ess.json` is green against `spineboy.atlas`.
|
|
941
|
+
|
|
942
|
+
**`A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` — a trimmed deform run is not a defect.**
|
|
943
|
+
|
|
944
|
+
```bash
|
|
945
|
+
rigc validate examples/spineboy/export/spineboy-pro.json \
|
|
946
|
+
--atlas examples/spineboy/export/spineboy.atlas
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
```
|
|
950
|
+
PASS A00_ROUNDTRIP_PARSE
|
|
951
|
+
PASS A10_NO_NAN_AFTER_STEPPING
|
|
952
|
+
…
|
|
953
|
+
PASS A35_DEFORM_KEYS_FIT_THE_ATTACHMENT
|
|
954
|
+
rigc: green
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
`hoverboard-board` is an unweighted mesh with 148 floats; the key carries `offset: 1`
|
|
958
|
+
and 147 values, covering `1..148` — the whole array minus a leading zero the editor
|
|
959
|
+
trimmed. A trim can land on a y component, so an odd offset or an odd-length run is what
|
|
960
|
+
a trimmed run looks like, and Spine's own parser copies the run in at the raw index with
|
|
961
|
+
no alignment requirement anywhere. A35 refuses what does break — a run that does not fit
|
|
962
|
+
inside the deform array, non-finite values, an empty key array, an attachment missing
|
|
963
|
+
from the skin. The over-long run in particular is refused, and it is the quietest
|
|
964
|
+
defect the format has.
|
|
965
|
+
|
|
966
|
+
🚨 **A validity rule stricter than the runtime does not look like a bug — it looks like
|
|
967
|
+
a finding about somebody's file.** A `FAIL` on foreign data has three possible meanings
|
|
968
|
+
and the message alone does not separate them:
|
|
969
|
+
|
|
970
|
+
1. **the data is broken** — fix the data;
|
|
971
|
+
2. **the input was wrong** — wrong atlas, missing page, truncated file (§3.1, and
|
|
972
|
+
`A00` above);
|
|
973
|
+
3. **the rule is stricter than the runtime** — fix the rule, or file it.
|
|
974
|
+
|
|
975
|
+
🎛️ **`A47` / `A48` on a foreign export are a fourth reading, and neither side is
|
|
976
|
+
wrong.** An ik or transform resting muted that no animation keys up moves nothing *in
|
|
977
|
+
the file*, and `validate <file>` has no rig spec and so no way to be told a game turns
|
|
978
|
+
it on from code — it refuses with three doors, the third being that statement. The
|
|
979
|
+
rebuild route makes it for you: `ingest` declares the constraint consumer-driven and
|
|
980
|
+
prints a `CONSUMER_DRIVEN_MIX` judgement (§2.0), and `build` then SKIPs it by name.
|
|
981
|
+
|
|
982
|
+
⇒ Before changing anybody's export because rigc objected, check case 3: does the file
|
|
983
|
+
**parse** (`A00`), **step without NaN** (`A10`), and **render**? If all three, the
|
|
984
|
+
runtime is content and the burden is on the rule. Reporting that is a better answer
|
|
985
|
+
than a quietly edited export.
|
|
986
|
+
|
|
987
|
+
### 3.3 Profile choice, and the sixteen rules that will not fire
|
|
988
|
+
|
|
989
|
+
`--profile spine` is the default and answers *"is this valid Spine 4.3 that any
|
|
990
|
+
runtime plays correctly?"* — and it is the right profile for foreign data, because the
|
|
991
|
+
other one is this project's own renderer and archetype policy.
|
|
992
|
+
|
|
993
|
+
**Sixteen assertions do not run under `spine`, and they come back `PROF`, not
|
|
994
|
+
`SKIP`:**
|
|
995
|
+
|
|
996
|
+
| Excluded as | Rules |
|
|
997
|
+
| --- | --- |
|
|
998
|
+
| **renderer policy** (8) | `A11_NO_CLIPPING_ATTACHMENTS`, `A12_NO_DARK_COLOR`, `A13_MESH_BUDGET`, `A14_NO_FULL_FRAME_MESH`, `A15_IDLE_NO_MESH_BONE_KEYS`, `A19_OVERLAY_PNGS_HAVE_ALPHA`, `A27_REGION_NAME_MATCHES_PAGE_FILENAME`, `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` |
|
|
999
|
+
| **archetype policy** (8) | `A21_MESH_RIM_PINNED`, `A24_AXIS_SPACE_STROKE`, `A25_DETACHED_BONE_PARENTAGE`, `A26_SLOT_DRAW_ORDER`, `A28_RIBBON_ROWS_SHARE_WEIGHTS`, `A29_STROKE_WITHIN_CONTACT_DEPTH`, `A30_STROKE_WITHIN_CAP_CONTAINMENT`, `A39_DEFORM_KEEPS_TRIANGLE_WINDING` |
|
|
1000
|
+
|
|
1001
|
+
Two further rules — **`A06`** and **`A20`** — are *mixed*: their validity clauses run
|
|
1002
|
+
in both profiles and their policy clauses only under `spine-html`. `A06`'s
|
|
1003
|
+
size-vs-PNG check is validity; rotation and premultiplied alpha are policy (two
|
|
1004
|
+
regions over the same texels was a policy clause of `A06` until it became `A49`). `A20`'s weight coherence is validity; requiring a mesh to be
|
|
1005
|
+
weighted at all is policy.
|
|
1006
|
+
|
|
1007
|
+
⚠️ **`--profile spine-html` on foreign data produces a wall of failures that mean
|
|
1008
|
+
nothing about the file.** Same `spineboy-pro.json`, same atlas, one flag changed — the
|
|
1009
|
+
run ends `rigc: 13 assertion(s) failed`, and this is the tally with one real message
|
|
1010
|
+
per rule — `A06` takes a page that is one part covering it exactly *or a tiling of
|
|
1011
|
+
regions*, so a packed atlas passes both profiles and the wall is policy only:
|
|
1012
|
+
|
|
1013
|
+
| Count | Rule | One of its messages |
|
|
1014
|
+
| --- | --- | --- |
|
|
1015
|
+
| **10** | `A15_IDLE_NO_MESH_BONE_KEYS` | `idle keys bone "front-shoulder", which drives a mesh — meshes never idle-skip` |
|
|
1016
|
+
| **2** | `A20_MESH_WEIGHTS_COHERENT` | `mesh "front-shin" is unweighted; the ring tier drives meshes by bones` |
|
|
1017
|
+
| **1** | `A11_NO_CLIPPING_ATTACHMENTS` | `1 clipping attachment(s); the renderer skips them silently` |
|
|
1018
|
+
|
|
1019
|
+
Every one of those is a correct statement about a correct file: a bone *does* key a
|
|
1020
|
+
mesh, a mesh *is* unweighted, a clipping attachment *is* present.
|
|
1021
|
+
⇒ **Do not run `spine-html` against
|
|
1022
|
+
somebody's export unless they asked whether it satisfies this project's renderer
|
|
1023
|
+
policy**, which is a different question from whether their file is valid.
|
|
1024
|
+
|
|
1025
|
+
⚠️ **And do not read the absence of `SKIP` lines as thoroughness.** Under `spine` a
|
|
1026
|
+
foreign skeleton typically produces *no* archetype `SKIP` at all — the profile excludes
|
|
1027
|
+
those rules before their bodies could notice the missing `invariants` block, so they
|
|
1028
|
+
arrive as `PROF`. The `PROF` list is where *"was this held to that rule at all"* gets
|
|
1029
|
+
answered (AUTHORING §7).
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
## 4. Recipes
|
|
1034
|
+
|
|
1035
|
+
Each of these starts from a transcription (§2), and none is an edit to emitted JSON —
|
|
1036
|
+
an edit to emitted JSON is a change nothing in the toolchain checked. All three were
|
|
1037
|
+
run from copies of the stored 3-timing transcription:
|
|
1038
|
+
|
|
1039
|
+
```bash
|
|
1040
|
+
T=bench/transcriptions/3-timing-and-spacing
|
|
1041
|
+
mkdir -p work/repivot
|
|
1042
|
+
cp $T/3-timing-and-spacing-ess.rig.json work/repivot/repivot.rig.json
|
|
1043
|
+
cp $T/3-timing-and-spacing-ess.motion.json work/repivot/repivot.motion.json
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
⚠️ A copied spec's own `images` path is relative to where the spec was, so it breaks
|
|
1047
|
+
on the copy. `--images` overrides it, relative to your working directory, and every
|
|
1048
|
+
`build` below passes it.
|
|
1049
|
+
|
|
1050
|
+
### 4.1 Moving a pivot without moving the art
|
|
1051
|
+
|
|
1052
|
+
The ask: *"the arm should swing from its middle, not its end — don't change the
|
|
1053
|
+
drawing."* This is the recipe the rest of the section is measured against, because its
|
|
1054
|
+
correctness criterion is exact and checkable without rendering anything.
|
|
1055
|
+
|
|
1056
|
+
**What changes in the file:**
|
|
1057
|
+
|
|
1058
|
+
| Object | Change | Why |
|
|
1059
|
+
| --- | --- | --- |
|
|
1060
|
+
| **the bone** | its `x`/`y` move to the new pivot, expressed in its **parent's** local axes | the bone's origin *is* the pivot |
|
|
1061
|
+
| **its attachments** | offsets move by the same vector expressed in the **bone's own** axes, with the opposite sign | an attachment offset is the art's centre relative to the bone origin; the bone origin just moved, so this cancels it |
|
|
1062
|
+
| **its child bones** | every child's `x`/`y` needs the same opposite correction | a child's offset is in this bone's local space, so moving the origin moved every child with it |
|
|
1063
|
+
| **the timelines** | ⛔ **nothing** | `rotate` keys are angles about the origin, and the origin is what you changed. This is the entire point of the edit |
|
|
1064
|
+
|
|
1065
|
+
⚠️ **The child-bone row is the one that gets forgotten**, and it fails quietly: the
|
|
1066
|
+
re-pivoted bone's own art lands correctly and everything hanging off it is displaced by
|
|
1067
|
+
exactly the vector you moved. If the bone has children, correct them in the same edit,
|
|
1068
|
+
or the fix looks half-right in a way no assertion will mention.
|
|
1069
|
+
|
|
1070
|
+
**Worked.** 3-timing's `bone` sits at `x: 204.753, y: 708.0989` with `rotation: 180`
|
|
1071
|
+
and `length: 473`; the `pendulum` attachment is at bone-local
|
|
1072
|
+
`x: 316.79, y: 0.4815389`. Move the pivot half the bone's length along the bone's own
|
|
1073
|
+
+x axis, `d = 236.5`: the bone's rotation is 180° in its parent's frame, so
|
|
1074
|
+
`R(180)·(d, 0) = (-d, 0)`, and this bone has no children.
|
|
1075
|
+
|
|
1076
|
+
```diff
|
|
1077
|
+
{ "name": "bone", "parent": "root", "length": 473,
|
|
1078
|
+
- "x": 204.753, "y": 708.0989, "rotation": 180 }
|
|
1079
|
+
+ "x": -31.747, "y": 708.0989, "rotation": 180 }
|
|
1080
|
+
|
|
1081
|
+
"pendulum": { "pendulum": { "image": "pendulum.png",
|
|
1082
|
+
- "x": 316.79, "y": 0.4815389, "rotation": -179.81934 } }
|
|
1083
|
+
+ "x": 80.29, "y": 0.4815389, "rotation": -179.81934 } }
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
```bash
|
|
1087
|
+
rigc build --rig work/repivot/repivot.rig.json \
|
|
1088
|
+
--motion work/repivot/repivot.motion.json \
|
|
1089
|
+
--images examples/3-timing-and-spacing/images \
|
|
1090
|
+
--out work/t3b
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
**Verify the invariant first, arithmetically.** The art's centre in world coordinates
|
|
1094
|
+
is `bone(x, y) + R(bone.rotation) · att(x, y)`. Computed from the two built skeletons,
|
|
1095
|
+
before and after:
|
|
1096
|
+
|
|
1097
|
+
```
|
|
1098
|
+
original bone [204.753, 708.0989] att [316.79, 0.4816] -> art centre [-112.0370, 707.6174]
|
|
1099
|
+
re-pivot bone [-31.747, 708.0989] att [ 80.29, 0.4816] -> art centre [-112.0370, 707.6174]
|
|
1100
|
+
art centre moved by 2.842e-14 units
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
⭐ **That is the criterion.** If the art's world position at the setup pose moves by
|
|
1104
|
+
more than floating-point noise, the compensation is wrong, and no amount of looking at
|
|
1105
|
+
frames will tell you which of the two numbers to blame.
|
|
1106
|
+
|
|
1107
|
+
**Then confirm the movement did change**, which is the half a pose cannot show. The
|
|
1108
|
+
same displacement evaluated across the bone's own rotation:
|
|
1109
|
+
|
|
1110
|
+
| `rotate` | original art centre | re-pivot art centre | apart |
|
|
1111
|
+
| --- | --- | --- | --- |
|
|
1112
|
+
| 0° | `[-112.04, 707.62]` | `[-112.04, 707.62]` | **0.00 units** |
|
|
1113
|
+
| 15° | `[-101.12, 625.64]` | `[-109.18, 686.85]` | 61.74 |
|
|
1114
|
+
| 45° | `[ -18.91, 483.75]` | `[ -88.18, 650.98]` | 181.01 |
|
|
1115
|
+
| 90° | `[ 205.23, 391.31]` | `[ -31.27, 627.81]` | 334.46 |
|
|
1116
|
+
| 180° | `[ 521.54, 708.57]` | `[ 48.54, 708.58]` | 473.00 |
|
|
1117
|
+
|
|
1118
|
+
**What the instruments say about it** — and this pair is why §1.3 carries its warning:
|
|
1119
|
+
|
|
1120
|
+
- **`diff` sees nothing.** `rigc diff work/t3b/skeleton.json <the export>` reads
|
|
1121
|
+
**1.000 on all 54 measures**: same bones, names, parents, order, slots, draw order,
|
|
1122
|
+
attachments, animations, timelines, key counts, curve kinds. All true, and all silent
|
|
1123
|
+
about a 236.5-unit move.
|
|
1124
|
+
- **`check` sees it loudly**, with the framing pinned so the comparison is of pictures
|
|
1125
|
+
and not of framings:
|
|
1126
|
+
|
|
1127
|
+
```bash
|
|
1128
|
+
rigc check --candidate work/t3b --frames ref3/heavy \
|
|
1129
|
+
--viewport -573.3,-81.2,2176.3,990.1 \
|
|
1130
|
+
--texture-from examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas \
|
|
1131
|
+
--all-frames
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
```
|
|
1135
|
+
MAE mean 106.91 worst 116.67 at f0012 (0..255 over the union alpha; over the whole frame, mean 7.15)
|
|
1136
|
+
⭐ texture floor 3.62 above it 105.79 (over the reference's own pixels, 1.95 and 92.51)
|
|
1137
|
+
slot drift worst 42.4 px "pendulum" at f0028
|
|
1138
|
+
chain slots worst slot drift mean MAE in it share
|
|
1139
|
+
square 1/1 0.4 px "square" f0000 0.3 px 10.20 3.2%
|
|
1140
|
+
bone 1/1 42.4 px "pendulum" f0028 31.3 px 125.70 77.3%
|
|
1141
|
+
(unattributed) — — — — 19.5%
|
|
1142
|
+
|
|
1143
|
+
frame MAE union px Δpx ref Δ worst slot drift how slots note
|
|
1144
|
+
f0000 5.98 1246 — — square 0.4 component 2/2
|
|
1145
|
+
f0001 6.18 1245 19 40 square 0.4 component 2/2
|
|
1146
|
+
f0002 19.58 1280 456 520 pendulum 0.7 component 2/2
|
|
1147
|
+
f0003 46.11 1412 702 815 pendulum 2.1 component 2/2
|
|
1148
|
+
f0004 75.88 1608 808 939 pendulum 4.3 component 2/2
|
|
1149
|
+
f0005 93.32 1776 996 1125 pendulum 8.0 component 2/2
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
⭐ **Read the per-frame column before the mean.** `f0000` is **5.98** — the
|
|
1153
|
+
transcription's own texture floor, i.e. *no difference at all* — and it climbs
|
|
1154
|
+
monotonically from `f0002` as the bone rotates. That is the pivot's whole signature:
|
|
1155
|
+
**invisible in the pose, and everything in the movement**, which is what MOTION §3.9
|
|
1156
|
+
argues from the authoring side and what this measures from the ingest side. The
|
|
1157
|
+
`chains` table puts 77.3 % of the difference on the `bone` chain and 3.2 % on
|
|
1158
|
+
`square`, which is the edit's own blast radius.
|
|
1159
|
+
|
|
1160
|
+
⚠️ **Pin `--viewport` for a re-pivot check.** Drop the flag and the same run reads
|
|
1161
|
+
`MAE mean 120.65` with `slot drift worst 30.3 px "pendulum" at f0000` — and that drift
|
|
1162
|
+
at frame 0 is an artefact, because the re-pivot changed the skeleton's **world extent**,
|
|
1163
|
+
so `frames.json`'s box came back `REFUSED, coordinates` and the framing was fitted
|
|
1164
|
+
instead:
|
|
1165
|
+
|
|
1166
|
+
```
|
|
1167
|
+
framed to 256x116px 0.100498 px/unit world x[-908.6 .. 1638.7] y[-74.6 .. 1079.7] (fitted to the candidate's own drawn pixels)
|
|
1168
|
+
declared frames.json's own box: REFUSED, coordinates — a fit there asks for 31.48 px, past the 11.74 px the extent-spread tolerance reaches — a different origin or a different unit, over 65 frame(s).
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
⇒ On any edit that changes the extent, take the viewport from the reference render's
|
|
1172
|
+
own `world x[…] y[…]` line, or read `check`'s framing lines before its figures.
|
|
1173
|
+
|
|
1174
|
+
### 4.2 Renaming, and the name-agnostic mindset
|
|
1175
|
+
|
|
1176
|
+
The ask: *"give everything our project's names."* Mechanically a rename pass over the
|
|
1177
|
+
rig spec; the discipline is in what you check afterwards.
|
|
1178
|
+
|
|
1179
|
+
**Everything in a rig spec resolves by name, and a miss is refused by name.** A bone's
|
|
1180
|
+
`parent`, a slot's `bone`, a constraint's `bones` and `target`, a draw-order key's
|
|
1181
|
+
`slot`, an authored mesh's vertex `weights` — and, across the two files, the motion
|
|
1182
|
+
spec's `archetype` against the rig spec's `name`. That last one is the first refusal a
|
|
1183
|
+
rename produces, before anything else has a chance to go wrong: a `rigc compile error`
|
|
1184
|
+
naming the motion spec's path, the `archetype` that spec states, the path of the rig spec
|
|
1185
|
+
it was handed, and the `name` that rig actually carries — both sides of the mismatch in
|
|
1186
|
+
one sentence. [`src/compile.ts`](../src/compile.ts) builds it; the renamed copies this
|
|
1187
|
+
section works on are not committed, for the reason the Appendix gives, so the figure is
|
|
1188
|
+
described here rather than transcribed off one.
|
|
1189
|
+
|
|
1190
|
+
⭐ **A rename is therefore mostly safe by construction, and its failures arrive as
|
|
1191
|
+
sentences naming both sides.** That is the reason to do it in the specs rather than in
|
|
1192
|
+
emitted JSON, where the same mistake is a silently unresolved reference.
|
|
1193
|
+
|
|
1194
|
+
**Then check it with the name-agnostic groups**, because after a rename the name-keyed
|
|
1195
|
+
measures are *supposed* to disagree. Renaming `bone`→`arm`, `square`→`block`, the two
|
|
1196
|
+
slots to `arm-art`/`block-art` and their attachments to match:
|
|
1197
|
+
|
|
1198
|
+
```
|
|
1199
|
+
bones mean 0.567 over 8 measures
|
|
1200
|
+
1.000 count 3/3 how many bones
|
|
1201
|
+
0.200 names 1/5 the bone names themselves
|
|
1202
|
+
0.333 parent_by_name 1/3 each bone hangs off the same parent
|
|
1203
|
+
0.333 order 1/3 the bones are declared in the same order
|
|
1204
|
+
0.333 length_present 1/3 a setup `length` is present or absent alike
|
|
1205
|
+
0.333 inherit_present 1/3 a setup `inherit` is present or absent alike
|
|
1206
|
+
1.000 depth_histogram 3/3 NAME-AGNOSTIC: as many bones at each depth
|
|
1207
|
+
1.000 degree_sequence 3/3 NAME-AGNOSTIC: as many bones with each child count
|
|
1208
|
+
|
|
1209
|
+
bones (name-agnostic) mean 1.000 over 5 measures — the same two skeletons compared with names thrown away
|
|
1210
|
+
1.000 count 3/3 how many bones
|
|
1211
|
+
1.000 depth_histogram 3/3 as many bones at each depth
|
|
1212
|
+
1.000 degree_sequence 3/3 as many bones with each child count
|
|
1213
|
+
1.000 shape_histogram 3/3 as many bones of each depth-and-child-count shape (`d1c3` = one hop down, three children)
|
|
1214
|
+
1.000 order_shape 3/3 the bones are declared in the same order of shapes
|
|
1215
|
+
|
|
1216
|
+
slots mean 0.143 over 7 measures
|
|
1217
|
+
1.000 count 2/2 how many slots
|
|
1218
|
+
0.000 names 0/4 the slot names themselves
|
|
1219
|
+
0.000 order 0/2 the slots array IS the draw order, so its order is data
|
|
1220
|
+
0.000 bone 0/2 each slot is bound to the same bone
|
|
1221
|
+
0.000 attachment 0/2 each slot shows the same setup attachment
|
|
1222
|
+
0.000 blend 0/2 each slot uses the same blend mode
|
|
1223
|
+
0.000 color_present 0/2 a tint is present or absent alike
|
|
1224
|
+
|
|
1225
|
+
slots (name-agnostic) mean 1.000 over 4 measures — the same two skeletons compared with names thrown away
|
|
1226
|
+
1.000 count 2/2 how many slots
|
|
1227
|
+
1.000 attachment_types_by_position 2/2 the same kind of attachment sits at each position in the draw order
|
|
1228
|
+
1.000 bone_binding_shape 2/2 as many slots hang off a bone of each shape (`?` = no such bone is declared)
|
|
1229
|
+
1.000 order_shape 2/2 the draw order is the same order of `<attachment type>@<bone shape>`
|
|
1230
|
+
|
|
1231
|
+
attachments mean 0.818 over 11 measures
|
|
1232
|
+
1.000 skins 1/1 the skin names
|
|
1233
|
+
1.000 count 2/2 how many attachments
|
|
1234
|
+
0.000 names 0/4 skin/slot/attachment keys
|
|
1235
|
+
1.000 type_counts 2/2 as many of each attachment type
|
|
1236
|
+
…
|
|
1237
|
+
0.000 refs 0/2 each attachment resolves the same region, clipping end and linked-mesh source — 2 on one side only, which `attachments.names` names
|
|
1238
|
+
1.000 skin_members 1/1 each skin activates the same skin-required bones and constraints
|
|
1239
|
+
…
|
|
1240
|
+
animations mean 0.909 over 11 measures
|
|
1241
|
+
…
|
|
1242
|
+
0.000 targets 0/8 each timeline keys the same bone, slot, constraint or attachment — …
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
Three things to read out of that, in order:
|
|
1246
|
+
|
|
1247
|
+
- **The name-keyed collapse is the task, not a defect.** `bones` 0.567, `slots` 0.143,
|
|
1248
|
+
`attachments` 0.818, `animations` 0.909. Note that several *non*-name measures fall with them —
|
|
1249
|
+
`slots.bone`, `slots.blend`, `bones.parent_by_name` — because they are keyed **by**
|
|
1250
|
+
the name that changed. They are not saying the binding changed.
|
|
1251
|
+
- 🚨 **`bones (name-agnostic)` and `slots (name-agnostic)` must stay 1.000.** They
|
|
1252
|
+
measure the rig with the vocabulary thrown away, so a rename that changed only names
|
|
1253
|
+
leaves them untouched. **A drop there is a structural mistake wearing a rename's
|
|
1254
|
+
clothes**, and it is the only assertion this recipe really has.
|
|
1255
|
+
- **`animations` moves on one measure only, `targets`**, because animation *names*
|
|
1256
|
+
were not part of the ask but every timeline keys a renamed bone — `targets` is keyed
|
|
1257
|
+
by the name that changed, as `slots.bone` is. Every other `animations` measure stays
|
|
1258
|
+
1.000. Under the same edit `bones.names` reads `1/5` — `root` survived, and the total
|
|
1259
|
+
is the union of both vocabularies.
|
|
1260
|
+
|
|
1261
|
+
⛔ And remember §1.3: `diff` reads no coordinates either way. A rename that also moved
|
|
1262
|
+
something is invisible to every measure in that report. Pair it with a `check` against
|
|
1263
|
+
frames rendered from the original.
|
|
1264
|
+
|
|
1265
|
+
⚠️ **Do not rename toward what a rule seems to want.** `A27`'s
|
|
1266
|
+
region-name-matches-page-filename is `spine-html` policy (§3.3): under the default
|
|
1267
|
+
profile it does not fire, and renaming somebody's attachments to satisfy a policy they
|
|
1268
|
+
never opted into is a change with no benefit to them.
|
|
1269
|
+
|
|
1270
|
+
### 4.3 Extending a foreign skeleton with a new animation
|
|
1271
|
+
|
|
1272
|
+
The ask: *"add a `nudge` to this."* There is no append — `build` re-emits the whole
|
|
1273
|
+
skeleton — so the extension is an added block in the motion spec of a transcription
|
|
1274
|
+
that already round-trips.
|
|
1275
|
+
|
|
1276
|
+
1. **Get the transcription to structural agreement first** (§2.3), and record the
|
|
1277
|
+
figure. Extending an unfinished transcription mixes two kinds of difference into
|
|
1278
|
+
every measurement after it.
|
|
1279
|
+
2. **Add the animation to the motion spec.** Named easings here, not raw curves —
|
|
1280
|
+
§2.2's escape hatch is for reproducing an export's own beziers, and this movement
|
|
1281
|
+
has no export behind it. What goes *between* the poses is [MOTION.md](MOTION.md);
|
|
1282
|
+
this page stops at the mechanics.
|
|
1283
|
+
3. **`build`, and read the count line.**
|
|
1284
|
+
|
|
1285
|
+
```bash
|
|
1286
|
+
rigc build --rig work/extend/extend.rig.json \
|
|
1287
|
+
--motion work/extend/extend.motion.json \
|
|
1288
|
+
--images examples/3-timing-and-spacing/images \
|
|
1289
|
+
--out work/t3c
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
```
|
|
1293
|
+
.. pages=2 regions=2 bones=3 slots=2 animations=3 version=4.3.13 regionAttachments=2 meshAttachments=0 physicsConstraints=0 rig=3-timing-and-spacing-ess profile=spine
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
`animations=3` where the export had 2. That line is the cheapest confirmation the
|
|
1297
|
+
block landed at all.
|
|
1298
|
+
4. **Read `diff` knowing what it is about to say.**
|
|
1299
|
+
|
|
1300
|
+
```
|
|
1301
|
+
animations mean 0.821 over 11 measures
|
|
1302
|
+
0.667 count 2/3 how many animations
|
|
1303
|
+
0.667 names 2/3 the animation names
|
|
1304
|
+
0.667 duration 2/3 each animation runs as long (last key time, within one frame)
|
|
1305
|
+
0.889 timeline_kinds 8/9 the same timelines exist
|
|
1306
|
+
0.958 key_counts 69/72 those timelines carry as many keys
|
|
1307
|
+
0.958 curve_kinds 69/72 as many linear / stepped / bezier keys
|
|
1308
|
+
1.000 event_keys 0/0 as many event firings — neither side has any
|
|
1309
|
+
0.667 draw_order 2/3 a draw-order timeline is present or absent alike
|
|
1310
|
+
0.667 deform 2/3 a deform timeline is present or absent alike
|
|
1311
|
+
0.889 targets 8/9 each timeline keys the same bone, slot, constraint or attachment — …
|
|
1312
|
+
1.000 keyed_names 0/0 each key names the same attachment, draw-order slot or event — neither side has any
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
🚨 **Every one of those got worse, and that is the correct result.** The
|
|
1316
|
+
`animations` section went 1.000 → 0.821 because the candidate now has something the
|
|
1317
|
+
reference does not. `diff` measures agreement with a reference; you were asked to
|
|
1318
|
+
*disagree* with it, in one specific way. ⇒ **Check that the drop is confined to the
|
|
1319
|
+
`animations` section and is the size the addition explains** — one animation of
|
|
1320
|
+
three, three keys of seventy-two — and that `bones`, `slots`, `attachments` and
|
|
1321
|
+
`constraints` are all still 1.000. That last part is the real assertion here: *the
|
|
1322
|
+
extension changed nothing it was not supposed to change.*
|
|
1323
|
+
5. **Look at it, then ask.** `render --animation nudge` and open the contact sheet;
|
|
1324
|
+
`vote` the foreign original against your extended build when the question is whether
|
|
1325
|
+
the new movement belongs beside the old ones. Nothing in this toolchain can answer
|
|
1326
|
+
that, and MOTION §4–§5 is how to shape the ballot so the answer informs.
|
|
1327
|
+
|
|
1328
|
+
---
|
|
1329
|
+
|
|
1330
|
+
## 5. Non-goals — stated, so nobody proposes them as gaps
|
|
1331
|
+
|
|
1332
|
+
🚫 **rigc does not read editor project files.** A `.spine` is the editor's own project
|
|
1333
|
+
format, not skeleton data, and no rigc command opens one. The path form refuses it by
|
|
1334
|
+
name:
|
|
1335
|
+
|
|
1336
|
+
```
|
|
1337
|
+
rigc: …/hero.spine is neither a directory nor a .json skeleton
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
The route from a project file to rigc is the one the editor already provides: export
|
|
1341
|
+
it, and start from the export. (The official examples' project files are public
|
|
1342
|
+
domain — [NOTICE.md](../NOTICE.md) — and `scripts/fetch-examples.sh` does not download
|
|
1343
|
+
them, because nothing here could use one.)
|
|
1344
|
+
|
|
1345
|
+
🚫 **Binary `.skel` is not read, and the honest statement has two halves.** rigc's
|
|
1346
|
+
dependency *can* read it and rigc *does not*:
|
|
1347
|
+
|
|
1348
|
+
- `@esotericsoftware/spine-core@4.3.13` exports `SkeletonBinary`, whose
|
|
1349
|
+
`readSkeletonData` is the binary reader.
|
|
1350
|
+
- **rigc's own source names it only in comments — no import, no call.** Every read path goes through
|
|
1351
|
+
`JSON.parse` and `SkeletonJson`, and the path resolver requires a `.json` extension —
|
|
1352
|
+
so a `.skel` is refused by the same sentence a `.spine` is, one layer before any
|
|
1353
|
+
format question arises.
|
|
1354
|
+
|
|
1355
|
+
⇒ Binary support is *reachable* rather than *present*: a plumbing job on a dependency
|
|
1356
|
+
that already has the reader, not a parser to write. But it is not there, and nothing on
|
|
1357
|
+
this page works on a `.skel` today. Re-export as JSON.
|
|
1358
|
+
|
|
1359
|
+
✅ **A skeleton-to-spec decompiler exists: `rigc ingest` (§2.0), and it decides
|
|
1360
|
+
nothing the skeleton does not state.** A bone's setup transform — the pivot — is in
|
|
1361
|
+
the skeleton in full, so it is copied with no decision. `ingest` chooses **no**
|
|
1362
|
+
generator: a generator is a *model* (`src/rig.ts`: *"they encode a deformation model …
|
|
1363
|
+
and a model is not a table of numbers"*), the skeleton holds geometry, and geometry is
|
|
1364
|
+
what the rig spec's authored form takes — so a generator-built mesh comes back as
|
|
1365
|
+
authored geometry and rebuilds **byte-identical**. And it writes no `invariants`: an
|
|
1366
|
+
absent field makes an archetype assertion SKIP, never pass (§2.1 step 3), so a
|
|
1367
|
+
decompiled spec carries no intent and says so instead of certifying something nobody
|
|
1368
|
+
measured. Where the skeleton has no value — the stage — `ingest` writes a stated
|
|
1369
|
+
absence (§2.0). ⚠️ Not to be confused with the *atlas* importer below, which is a
|
|
1370
|
+
different direction and also exists.
|
|
1371
|
+
|
|
1372
|
+
⚠️ **What `ingest` is not.** It reads skeleton JSON and writes two spec files.
|
|
1373
|
+
It does not read a `.spine` project or a binary `.skel` (the two entries above), it
|
|
1374
|
+
does not read the atlas or the art, it does not **edit** a skeleton,
|
|
1375
|
+
and it makes no claim about whether an agent could have *produced* the numbers it
|
|
1376
|
+
copied — only that the spec can carry them and `build` reproduces the file from them.
|
|
1377
|
+
|
|
1378
|
+
✅ **A packer and an importer both exist, so do not report them as gaps.**
|
|
1379
|
+
`build --pack` writes shared pages losslessly and `build --atlas-in` resolves against a
|
|
1380
|
+
pack somebody else made (AUTHORING §0.1–§0.2). One-region-per-page is the **default**,
|
|
1381
|
+
not the only shape. `--atlas-in` applies the page's `scale:`, so an imported pack states
|
|
1382
|
+
the drawing's size rather than the pack's — to within the pack's own rounding, which is
|
|
1383
|
+
§2.3's row.
|
|
1384
|
+
|
|
1385
|
+
🚫 **No CLI unpacker, so `pose` needs loose art.** `pose` reads *loose part PNGs*
|
|
1386
|
+
against one picture. A foreign export hands you a packed page instead, and pointing
|
|
1387
|
+
`pose` at one is worse than useless — it treats the whole page as a single part and
|
|
1388
|
+
answers confidently:
|
|
1389
|
+
|
|
1390
|
+
```bash setup
|
|
1391
|
+
rigc render --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
|
|
1392
|
+
--fps 4 --max 900 --out ref-big
|
|
1393
|
+
mkdir -p packed-only
|
|
1394
|
+
cp examples/3-timing-and-spacing/export/3-timing-and-spacing.png packed-only/
|
|
1395
|
+
rigc pose --images packed-only --frame 'ref-big/light@4fps/f0002.png'
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
```
|
|
1399
|
+
rigc pose
|
|
1400
|
+
.. frame …/ref-big/light@4fps/f0002.png (900x409)
|
|
1401
|
+
.. ground rgb(232, 232, 232) over 100% of the border ring
|
|
1402
|
+
.. parts …/packed-only (1 png)
|
|
1403
|
+
.. search scale 0.5–2 in 7 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
|
|
1404
|
+
PLACE 3-timing-and-spacing.png x= 324.3 y= 188.3 rot= -91.6° scale=0.630 residual=0.2085 unexplained= 30%
|
|
1405
|
+
found on a 14x7 anchor grid, step 4 at 16x reduction
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
⚠️ **`residual=0.2085` is *under* the default 0.25 refusal bar**, so nothing refused
|
|
1409
|
+
it, and `PLACE` rather than `AMBIG` means nothing flagged it either. With the same frame
|
|
1410
|
+
and the two real loose parts, the answer is what it should be:
|
|
1411
|
+
|
|
1412
|
+
```bash
|
|
1413
|
+
rigc pose --images examples/3-timing-and-spacing/images \
|
|
1414
|
+
--frame 'ref-big/light@4fps/f0002.png' --scale 0.3,0.6
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
```
|
|
1418
|
+
rigc pose
|
|
1419
|
+
.. frame …/ref-big/light@4fps/f0002.png (900x409)
|
|
1420
|
+
.. ground rgb(232, 232, 232) over 100% of the border ring
|
|
1421
|
+
.. parts …/examples/3-timing-and-spacing/images (2 png)
|
|
1422
|
+
.. search scale 0.3–0.6 in 4 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
|
|
1423
|
+
PLACE pendulum.png x= 319.3 y= 214.6 rot= -88.9° scale=0.411 residual=0.0428 unexplained= 7%
|
|
1424
|
+
found on a 28x13 anchor grid, step 2 at 16x reduction
|
|
1425
|
+
PLACE square.png x= 437.1 y= 343.1 rot= 0.0° scale=0.410 residual=0.0334 unexplained= 4%
|
|
1426
|
+
found on a 113x51 anchor grid, step 2 at 4x reduction
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
⇒ **Check what is in `--images` before trusting a `pose` report on ingest work.** One
|
|
1430
|
+
PNG where you expected several is the tell, and the `.. parts` line prints the
|
|
1431
|
+
count. AUTHORING §11.4 is the rest of what that command cannot see.
|
|
1432
|
+
|
|
1433
|
+
⭐ **And on ingest work you usually have the thing `pose` is missing.** A skeleton you
|
|
1434
|
+
are transcribing IS a compiled candidate, so the parts `pose` refuses because
|
|
1435
|
+
something is drawn over them are readable through its own draw order and hierarchy —
|
|
1436
|
+
`rigc chainfit --candidate <that skeleton> --images <dir> --frame <png>`, AUTHORING
|
|
1437
|
+
§12. It is the natural second pass here: `pose` reads the trunk of a foreign figure,
|
|
1438
|
+
`chainfit` reads the limbs it hides, and both report placements rather than grades.
|
|
1439
|
+
|
|
1440
|
+
📎 To be exact about what is missing: rigc *can* lift a region's drawing back off a
|
|
1441
|
+
page — `extractRegion` does it, and the contour mesh generator uses it under
|
|
1442
|
+
`--atlas-in` — so what is absent is a **command**, not the capability. That includes a
|
|
1443
|
+
region the pack **turned** (`rotate: 90`, `180`, `270`, or the
|
|
1444
|
+
older `rotate: true`), which a foreign pack routinely is and rigc's own never is: the
|
|
1445
|
+
lift transcribes `MeshAttachment.computeUVs`, the one routine in spine-core that
|
|
1446
|
+
states where a turned region's texels are, so what a generator measures does not
|
|
1447
|
+
depend on how the art was delivered (AUTHORING §0.2).
|
|
1448
|
+
|
|
1449
|
+
🚫 **No `validate --fix`, and no normalisation pass.** Every recipe in §4 is a change
|
|
1450
|
+
you state in a spec and rebuild. A tool that rewrote somebody's export in place would
|
|
1451
|
+
be making decisions on their behalf with nowhere to say it had — and, per §3.2, some of
|
|
1452
|
+
those decisions would be wrong about the rule rather than about the file.
|
|
1453
|
+
|
|
1454
|
+
🚫 **`diff` will not be given coordinate measures to make it a geometry check.**
|
|
1455
|
+
`check` is the geometry instrument, and it works by rendering, which is the only way to
|
|
1456
|
+
compare two rigs that may be authored in different coordinate systems at different
|
|
1457
|
+
scales. A coordinate diff between two skeletons would be arithmetic on numbers that do
|
|
1458
|
+
not compare — `check`'s own report says the two world boxes *"are different coordinate
|
|
1459
|
+
systems and do not compare; the pixel grid does."*
|
|
1460
|
+
|
|
1461
|
+
---
|
|
1462
|
+
|
|
1463
|
+
## Appendix — the corpus this page was verified against
|
|
1464
|
+
|
|
1465
|
+
Every command line above was run from a checkout with `bun run fetch-examples`
|
|
1466
|
+
completed. Two skeletons carry all of it:
|
|
1467
|
+
|
|
1468
|
+
| Example | Files used | Licence |
|
|
1469
|
+
| --- | --- | --- |
|
|
1470
|
+
| **`3-timing-and-spacing`** | `export/3-timing-and-spacing-ess.json`; `export/3-timing-and-spacing.atlas` (one 512×128 page, `scale: 0.5`, 2 regions); `images/pendulum.png` (745×212); `images/square.png` (159×159) | `license.txt` present, © 2021-2025 Esoteric Software |
|
|
1471
|
+
| **`spineboy`** | `export/spineboy-pro.json`, `export/spineboy-ess.json`, `export/spineboy.atlas`, `export/spineboy-run.atlas` | `license.txt` present, © 2013 Esoteric Software LLC |
|
|
1472
|
+
|
|
1473
|
+
The working directories the commands write into — `ref3/`, `ref-big/`, `render/`,
|
|
1474
|
+
`work/`, `packed-only/` — are throwaway and none is committed.
|
|
1475
|
+
|
|
1476
|
+
⚠️ **`7-anticipation` ships no `license.txt` upstream**, so the redistribution grant its
|
|
1477
|
+
siblings carry does not exist for it. No excerpt, figure or image on this page comes
|
|
1478
|
+
from it; the only places it appears at all are the two corpus-wide tallies — §3.2's
|
|
1479
|
+
*twelve of twelve skeletons* and §2.3's *nine of ten atlases* — both of which count
|
|
1480
|
+
every directory the fetch produced.
|
|
1481
|
+
`scripts/fetch-examples.sh` prints a warning naming it; [NOTICE.md](../NOTICE.md) has
|
|
1482
|
+
the per-directory terms.
|
|
1483
|
+
|
|
1484
|
+
The transcription these recipes start from is
|
|
1485
|
+
[`bench/transcriptions/3-timing-and-spacing/`](https://github.com/firejune/rigc/tree/main/bench/transcriptions/3-timing-and-spacing/),
|
|
1486
|
+
unmodified. The re-pivoted, renamed and extended variants in §4 were built from copies
|
|
1487
|
+
of it; none is committed, because each is an illustration of an edit rather than a
|
|
1488
|
+
transcription of anything.
|