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/src/model.ts
ADDED
|
@@ -0,0 +1,1245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compiled model — what `compile` knows about a rig once every name is
|
|
3
|
+
* resolved and every number is on the grid the runtime will read, held apart
|
|
4
|
+
* from the shape any one output format gives it (issue #915, step 1b of #380).
|
|
5
|
+
*
|
|
6
|
+
* ## What it is
|
|
7
|
+
*
|
|
8
|
+
* The record a posing core of rigc's own would read: bones with their inherit
|
|
9
|
+
* modes, slots, every attachment kind rigc builds, skins, constraints, events
|
|
10
|
+
* and animations. It is the neutral side of the census in `docs/COMPILED_MODEL.md`:
|
|
11
|
+
* a value belongs here when any backend posing this rig would need it, and a
|
|
12
|
+
* spelling, a key order or an omission at a parser default belongs to the
|
|
13
|
+
* emitter that writes one format. The Spine emitter (`src/emit_spine.ts`) is its
|
|
14
|
+
* first consumer, and the one that owns every Spine 4.3 byte; the model owns
|
|
15
|
+
* none.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ **The numbers are exactly the numbers the file holds**, and the emitter
|
|
18
|
+
* copies them without touching one. Which decimal a number is spelled with is
|
|
19
|
+
* model content and not formatting: it is the double spine-core's JSON reader
|
|
20
|
+
* keeps, so it moves the pose (census §4, 19 of 19 builds).
|
|
21
|
+
*
|
|
22
|
+
* 🔸 **"On the float32 grid" holds for the values `f32` produced, not for
|
|
23
|
+
* every number here** (issue #931's measurement, corrected by #935). A bone's
|
|
24
|
+
* transform, a region's placement and size and a key's value are `f32`'d. A
|
|
25
|
+
* generated weight, and the bind and vertex coordinates an ingested rig
|
|
26
|
+
* states, are `r6` or the source's own decimals: on the nineteen recipes
|
|
27
|
+
* `tools/emit_hashes.ts` generates, 503 of 516 bind coordinates of
|
|
28
|
+
* `gallery/flex` and 767 of 792 of `spineboy-pro` are not float32 values. The
|
|
29
|
+
* runtime reads a bone's and a region's numbers as the doubles the text
|
|
30
|
+
* spells, and a vertex attachment's `vertices` (weights and bind coordinates
|
|
31
|
+
* included) into a float32 array; so a reader reproducing the runtime's pose
|
|
32
|
+
* reads vertex arrays and weights through `Math.fround` and every other number
|
|
33
|
+
* as written. Read as doubles, the corpus's weighted meshes pose 1 to 3
|
|
34
|
+
* millionths away from spine-core; read through `Math.fround`, exact.
|
|
35
|
+
*
|
|
36
|
+
* ## What it holds
|
|
37
|
+
*
|
|
38
|
+
* `bones` and `setupWorld` (issue #915, cut 1b); the four vertex-attachment
|
|
39
|
+
* kinds — mesh, path, bounding box, clipping — as records whose weighted
|
|
40
|
+
* vertices name their bone (issue #917, cut 1c); and the remaining structural
|
|
41
|
+
* records (issue #919, cut 1d): `slots`, the region and linked-mesh
|
|
42
|
+
* attachments, `skins` holding every attachment as a model record,
|
|
43
|
+
* `constraints` and `events`; the skeleton's `referenceScale` (issue #958),
|
|
44
|
+
* which wind and gravity act over; and `animations` (issue #921, cut 1e), each a
|
|
45
|
+
* `CompiledAnimation` whose timelines hold the keys the timeline compilers
|
|
46
|
+
* build; and every region's atlas rectangle (issue #935, `ModelAtlasRect`). Every other field is `CompileResult`'s own, carried by reference under
|
|
47
|
+
* the same name (`CarriedFromCompileResult`) until its own cut gives it a
|
|
48
|
+
* model-side form; the skeleton object, its text and the atlas text are emitted
|
|
49
|
+
* artifacts and are not part of the model at all.
|
|
50
|
+
*
|
|
51
|
+
* ⭐ **A weighted vertex names its bone.** Spine's run binds a vertex to a bone by
|
|
52
|
+
* its POSITION in the emitted bone array, and until cut 1c every vertex
|
|
53
|
+
* attachment carried that run from the moment it was built, so five later
|
|
54
|
+
* stages decoded the index back into a bone. The model keeps the binding by
|
|
55
|
+
* name (`ModelBinding`); the Spine emitter turns names into positions once, at
|
|
56
|
+
* emission, against `bones`' order (`emitVertices`). A bone inserted ahead of a
|
|
57
|
+
* mesh then moves the emitted indexes and no binding.
|
|
58
|
+
*
|
|
59
|
+
* 🔸 **Every omission at a parser fallback the constructors made inline is the
|
|
60
|
+
* emitter's now, and the model holds the value.** A slot's setup attachment is
|
|
61
|
+
* `null` rather than absent; a manifest region carries its `x`, `y` and
|
|
62
|
+
* `rotation` at 0; a physics constraint carries every component and parameter
|
|
63
|
+
* the spec stated, at 0 and at the parser's default included; a linked mesh
|
|
64
|
+
* carries the skin, slot and `timelines` flag it resolves through, its own
|
|
65
|
+
* slot, `default` and `true` included. The Spine emitter leaves each of those
|
|
66
|
+
* out where the constructor used to (`src/emit_spine.ts`).
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ **One omission is still the builder's: `path`** on a region, a mesh and a
|
|
69
|
+
* linked mesh. It is present exactly when `attachmentPath` in `compile.ts`
|
|
70
|
+
* returns one — a stated `path` always, one derived from `image` only where the
|
|
71
|
+
* basename differs from the name the attachment carries. That is the value
|
|
72
|
+
* semantics cut 1c kept for a mesh, and it is not a rule the emitter could run
|
|
73
|
+
* on the model: a STATED `path` equal to the name is written today, a DERIVED
|
|
74
|
+
* one equal to it is not, and the record cannot tell the two apart. Holding
|
|
75
|
+
* the region an attachment resolves through always, and moving that omission
|
|
76
|
+
* to the emitter with a flag for which was stated, is a later decision.
|
|
77
|
+
*
|
|
78
|
+
* 🔸 **The atlas's two constants are not model content.** `filter: Linear,
|
|
79
|
+
* Linear` and `pma: false` (`writeAtlasText` in `src/atlas.ts`) are a sampling
|
|
80
|
+
* hint and an alpha convention the Spine backend chooses and no input states —
|
|
81
|
+
* the census's two `open` rows (docs/COMPILED_MODEL.md §5), decided at cut 1d:
|
|
82
|
+
* they are the atlas emitter's constants, and the model does not carry them.
|
|
83
|
+
*
|
|
84
|
+
* 📄 **It is written as a document** (issue #922, cut 1f): `modelDocument` below
|
|
85
|
+
* spells it as `rigc-compiled/2` (`/1` until issue #1016 added `pages`), and
|
|
86
|
+
* `build` writes that text into `--out` as `skeleton.model.json` beside the
|
|
87
|
+
* Spine files, after the gate, like them.
|
|
88
|
+
*/
|
|
89
|
+
import { createHash } from 'node:crypto';
|
|
90
|
+
import { parseAtlasText } from './atlas.ts';
|
|
91
|
+
import { CompileError } from './errors.ts';
|
|
92
|
+
import type { BoneTransform } from './transform.ts';
|
|
93
|
+
import type { RigSkinConstraintKey } from './rig.ts';
|
|
94
|
+
import type { CompileResult } from './types.ts';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* One bone, as `buildBone` computes it — a field is present exactly when the rig
|
|
98
|
+
* spec declared it (or, for `x`/`y`/`rotation`, when `from` supplied it), and
|
|
99
|
+
* every number is already `f32`'d.
|
|
100
|
+
*/
|
|
101
|
+
export interface ModelBone {
|
|
102
|
+
name: string;
|
|
103
|
+
parent?: string;
|
|
104
|
+
length?: number;
|
|
105
|
+
/** Local to the parent, y up. Solved from the manifest when the spec says `from`. */
|
|
106
|
+
x?: number;
|
|
107
|
+
y?: number;
|
|
108
|
+
/** Degrees, CCW, y up. */
|
|
109
|
+
rotation?: number;
|
|
110
|
+
scaleX?: number;
|
|
111
|
+
scaleY?: number;
|
|
112
|
+
shearX?: number;
|
|
113
|
+
shearY?: number;
|
|
114
|
+
/**
|
|
115
|
+
* The inherit MODE, as the spec states it (the rig parse admits the runtime's
|
|
116
|
+
* first-letter fold, so `noScale` and `NoScale` both reach here). What key the
|
|
117
|
+
* mode is written under is the emitter's: Spine 4.3 spells it `inherit`.
|
|
118
|
+
*/
|
|
119
|
+
inheritMode?: string;
|
|
120
|
+
/** The bone is inactive unless the applied skin names it. Spine 4.3 spells it `skin`. */
|
|
121
|
+
skinRequired?: boolean;
|
|
122
|
+
/** Editor affordances: no pose reads them. */
|
|
123
|
+
editor?: { color?: string; icon?: string };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The fields of `CompileResult` the model carries as they are today, by
|
|
128
|
+
* reference. A later cut replaces one of these with a model-side form by moving
|
|
129
|
+
* it out of this list.
|
|
130
|
+
*/
|
|
131
|
+
export type CarriedFromCompileResult = Pick<
|
|
132
|
+
CompileResult,
|
|
133
|
+
| 'images'
|
|
134
|
+
| 'pageGrids'
|
|
135
|
+
| 'droppedStates'
|
|
136
|
+
| 'absentParts'
|
|
137
|
+
| 'meshBones'
|
|
138
|
+
| 'meshes'
|
|
139
|
+
| 'physics'
|
|
140
|
+
| 'deformTransforms'
|
|
141
|
+
| 'trackDerivations'
|
|
142
|
+
| 'rig'
|
|
143
|
+
>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* One influence of a weighted vertex: the bone by NAME, the vertex in that bone's
|
|
147
|
+
* local setup space, and the share it carries. Every number is exactly what the
|
|
148
|
+
* emitted run holds — `f32` (and, for a generated mesh, the generator's `r6` on
|
|
149
|
+
* the weight) already applied where the builder applies it.
|
|
150
|
+
*/
|
|
151
|
+
export interface ModelBinding {
|
|
152
|
+
bone: string;
|
|
153
|
+
x: number;
|
|
154
|
+
y: number;
|
|
155
|
+
weight: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A vertex attachment's geometry, in one of the format's two encodings.
|
|
160
|
+
*
|
|
161
|
+
* `weighted` is said outright. Spine's reader chooses the encoding by comparing
|
|
162
|
+
* the run's length with the vertex count and nothing else, so a run of the
|
|
163
|
+
* wrong length is read as the other encoding without a word; the model does not
|
|
164
|
+
* inherit that, and the Spine emitter is the one place the choice becomes a
|
|
165
|
+
* length again.
|
|
166
|
+
*
|
|
167
|
+
* - unweighted: `xy`, one `x, y` per vertex in the attachment's own space (the
|
|
168
|
+
* slot bone's), the numbers exactly as the emitted array carries them;
|
|
169
|
+
* - weighted: `bindings`, per vertex its influences in order, each naming its bone.
|
|
170
|
+
*/
|
|
171
|
+
export type ModelVertices = { weighted: false; xy: number[] } | { weighted: true; bindings: ModelBinding[][] };
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A numbered series of atlas regions an attachment draws in turn: the four
|
|
175
|
+
* fields exactly as the spec stated them, each optional one only when stated.
|
|
176
|
+
* Which of them Spine leaves out at the parser's default is the emitter's
|
|
177
|
+
* (`emitSequenceBlock`).
|
|
178
|
+
*/
|
|
179
|
+
export interface ModelSequence {
|
|
180
|
+
count: number;
|
|
181
|
+
start?: number;
|
|
182
|
+
digits?: number;
|
|
183
|
+
setup?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The rectangle a region draws through, in the atlas's own numbers and under
|
|
188
|
+
* the names the atlas's `bounds:` and `offsets:` lines carry (issue #935): the
|
|
189
|
+
* kept rectangle's `width` and `height`, in the drawing's orientation; the
|
|
190
|
+
* trim's `offsetX` (from the drawing's left) and `offsetY` (from its bottom);
|
|
191
|
+
* and the untrimmed drawing's `originalWidth` and `originalHeight`. All six are
|
|
192
|
+
* in the page's texels, exactly as the atlas states them (`AtlasRegion` in
|
|
193
|
+
* `src/atlas.ts`) — NOT divided by a page's `scale:`, because the pose reads
|
|
194
|
+
* only their ratios to the record's `width` and `height`.
|
|
195
|
+
*
|
|
196
|
+
* ⭐ **Why the model holds it.** A region's four corners are
|
|
197
|
+
* `x1 = -W/2·sx + offsetX·W/originalWidth·sx`, `x2 = x1 + width·W/originalWidth·sx`
|
|
198
|
+
* (and the same in y) before the record's placement and the bone — measured
|
|
199
|
+
* exact on 3000 of 3000 probes against spine-core 4.3.13, and 976 of 3000 with
|
|
200
|
+
* the trim ignored (issue #931). The trim and the original size live only in
|
|
201
|
+
* the atlas, so until this record held them one model document posed two ways:
|
|
202
|
+
* `examples/3-timing-and-spacing` built against two atlases differing only in
|
|
203
|
+
* `square`'s trim wrote byte-identical `skeleton.model.json` and
|
|
204
|
+
* `skeleton.json`, and spine-core moved that region's corners by 11.925 world
|
|
205
|
+
* units. A value any backend posing the rig needs belongs in the model.
|
|
206
|
+
*
|
|
207
|
+
* 🔸 **What it leaves out, and why.** The page, the rectangle's `x`/`y` on it
|
|
208
|
+
* and its `rotate` are WHERE the drawing sits in one arrangement of the pixels,
|
|
209
|
+
* not what the drawing is: `build --pack` repacks every part onto shared pages
|
|
210
|
+
* after this document is spelled, and `--copy-images` renames every page, so
|
|
211
|
+
* those four would state a place the written atlas does not have on two of
|
|
212
|
+
* the three routes that write one. None of them enters a region's corners
|
|
213
|
+
* (#931: the atlas's `rotate` transposed into the corners was exact on 2018 of
|
|
214
|
+
* 3000 probes, ignored on 3000 of 3000). The trim and the original size do not
|
|
215
|
+
* move under either: rigc's packer never trims or rotates (`src/atlas.ts`).
|
|
216
|
+
* The draw, which does need the four — a region's page and page UVs — read
|
|
217
|
+
* them off the atlas written beside this document, as a second input
|
|
218
|
+
* (`src/core/uvs.ts`, issue #967), until issue #1016 gave the document a
|
|
219
|
+
* `pages` section spelled from the atlas text `build` writes (`pagesOfAtlas`
|
|
220
|
+
* below): the four are still not this record's, because they are the written
|
|
221
|
+
* arrangement's, and the section moves with it.
|
|
222
|
+
*/
|
|
223
|
+
export interface ModelAtlasRect {
|
|
224
|
+
width: number;
|
|
225
|
+
height: number;
|
|
226
|
+
offsetX: number;
|
|
227
|
+
offsetY: number;
|
|
228
|
+
originalWidth: number;
|
|
229
|
+
originalHeight: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A region's sequence: the four stated fields, and `atlas`, one rectangle per
|
|
234
|
+
* frame in frame order — frame `i` is the region `<path><start + i>`, padded
|
|
235
|
+
* to `digits`, and the runtime draws each frame through its own rectangle
|
|
236
|
+
* (`Sequence.apply` sets the region the corners are computed from).
|
|
237
|
+
*
|
|
238
|
+
* A parallel array rather than a `frames` list of objects: a frame's region
|
|
239
|
+
* NAME is derived from the four fields, so the rectangle is the one thing per
|
|
240
|
+
* frame the record does not already state. Never `null`: the gather pass
|
|
241
|
+
* atlases every frame of every sequence or refuses the missing one by name
|
|
242
|
+
* before any attachment is built. A mesh's or a linked mesh's sequence does not
|
|
243
|
+
* carry it — a mesh's vertices read no atlas (#931, 400 of 400 meshes exact
|
|
244
|
+
* with none), and neither kind's record carries a rectangle.
|
|
245
|
+
*/
|
|
246
|
+
export interface ModelRegionSequence extends ModelSequence {
|
|
247
|
+
atlas: ModelAtlasRect[];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* A mesh: `buildRigMesh`'s authored geometry, the five generators' output, and a
|
|
252
|
+
* manifest part's ring or ribbon. Fields as the builders compute them today.
|
|
253
|
+
*/
|
|
254
|
+
export interface ModelMeshAttachment {
|
|
255
|
+
kind: 'mesh';
|
|
256
|
+
/** The spec's own `name`, present exactly when stated (issue #796). */
|
|
257
|
+
name?: string;
|
|
258
|
+
/**
|
|
259
|
+
* The atlas region, present exactly when `attachmentPath` in `compile.ts`
|
|
260
|
+
* returns one: stated, or derived from `image` where the basename differs from
|
|
261
|
+
* the name the attachment carries. See the header's ⚠️ — resolving it to the
|
|
262
|
+
* region always is a later decision, and so is moving that omission here.
|
|
263
|
+
*/
|
|
264
|
+
path?: string;
|
|
265
|
+
color?: string;
|
|
266
|
+
uvs: number[];
|
|
267
|
+
triangles: number[];
|
|
268
|
+
vertices: ModelVertices;
|
|
269
|
+
hull: number;
|
|
270
|
+
edges: number[];
|
|
271
|
+
width: number;
|
|
272
|
+
height: number;
|
|
273
|
+
sequence?: ModelSequence;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A bounding box: a polygon and nothing else. */
|
|
277
|
+
export interface ModelBoundingBoxAttachment {
|
|
278
|
+
kind: 'boundingbox';
|
|
279
|
+
name?: string;
|
|
280
|
+
vertexCount: number;
|
|
281
|
+
vertices: ModelVertices;
|
|
282
|
+
/** The editor's display colour; no pose reads it. Spine writes it as `color`. */
|
|
283
|
+
editorColor?: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** A clipping polygon, and the slot its clip ends at. */
|
|
287
|
+
export interface ModelClippingAttachment {
|
|
288
|
+
kind: 'clipping';
|
|
289
|
+
name?: string;
|
|
290
|
+
end?: string;
|
|
291
|
+
convex?: boolean;
|
|
292
|
+
inverse?: boolean;
|
|
293
|
+
vertexCount: number;
|
|
294
|
+
vertices: ModelVertices;
|
|
295
|
+
editorColor?: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** A path: knots and their handles, and the cumulative curve lengths stated or measured on the setup pose. */
|
|
299
|
+
export interface ModelPathAttachment {
|
|
300
|
+
kind: 'path';
|
|
301
|
+
name?: string;
|
|
302
|
+
closed?: boolean;
|
|
303
|
+
constantSpeed?: boolean;
|
|
304
|
+
vertexCount: number;
|
|
305
|
+
vertices: ModelVertices;
|
|
306
|
+
lengths: number[];
|
|
307
|
+
editorColor?: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** The four attachment kinds that carry a vertex array — the ones a deform timeline can key. */
|
|
311
|
+
export type ModelVertexAttachment =
|
|
312
|
+
| ModelMeshAttachment
|
|
313
|
+
| ModelBoundingBoxAttachment
|
|
314
|
+
| ModelClippingAttachment
|
|
315
|
+
| ModelPathAttachment;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A region: one quad of one atlas region (or of a numbered series), placed on
|
|
319
|
+
* the slot's bone. `buildRigRegion` for a rig spec's, `placeRegion` for a
|
|
320
|
+
* manifest part's.
|
|
321
|
+
*
|
|
322
|
+
* `x`, `y`, `rotation`, `scaleX`, `scaleY` are present exactly when the spec
|
|
323
|
+
* stated them — and, for a manifest part, `x`, `y` and `rotation` ALWAYS, since
|
|
324
|
+
* `placeRegion` computes all three and 0 is a placement like any other. The
|
|
325
|
+
* Spine emitter leaves a 0 out (`emitRegion`).
|
|
326
|
+
*/
|
|
327
|
+
export interface ModelRegionAttachment {
|
|
328
|
+
kind: 'region';
|
|
329
|
+
name?: string;
|
|
330
|
+
/** As on a mesh: present exactly when `attachmentPath` returns one (see the header's ⚠️). */
|
|
331
|
+
path?: string;
|
|
332
|
+
x?: number;
|
|
333
|
+
y?: number;
|
|
334
|
+
rotation?: number;
|
|
335
|
+
scaleX?: number;
|
|
336
|
+
scaleY?: number;
|
|
337
|
+
width: number;
|
|
338
|
+
height: number;
|
|
339
|
+
color?: string;
|
|
340
|
+
sequence?: ModelRegionSequence;
|
|
341
|
+
/**
|
|
342
|
+
* The rectangle this region draws through (`ModelAtlasRect`), present exactly
|
|
343
|
+
* when the record has no `sequence` — a sequence's frames carry theirs. It is
|
|
344
|
+
* looked up under the region NAME the runtime asks for — `path`, else the
|
|
345
|
+
* stated `name`, else the placeholder — in the build's one atlas source:
|
|
346
|
+
*
|
|
347
|
+
* - `--atlas-in`: the pack's region of that name, first match (as
|
|
348
|
+
* `TextureAtlas.findRegion` resolves it), its numbers as the pack states
|
|
349
|
+
* them;
|
|
350
|
+
* - otherwise: the part PNG atlased under that name, which is its own page —
|
|
351
|
+
* `width`/`height` and `originalWidth`/`originalHeight` the PNG's size,
|
|
352
|
+
* offsets 0. `build --pack` keeps all six (it never trims);
|
|
353
|
+
* - `null` when the source has no region of that name — an ingested region
|
|
354
|
+
* built without `--atlas-in`, or one naming a region the pack lacks. That
|
|
355
|
+
* is a statement that the build has no source, never a trim of 0, and a
|
|
356
|
+
* green build never carries one: the emitted atlas has no such region
|
|
357
|
+
* either, and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refuses the build.
|
|
358
|
+
*/
|
|
359
|
+
atlas?: ModelAtlasRect | null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* A linked mesh: another mesh's geometry under a region of its own.
|
|
364
|
+
*
|
|
365
|
+
* The link is held IN FULL — `skin`, `slot` and `timelines` as the parser
|
|
366
|
+
* resolves them, the defaults applied: `skin` "default" where none was stated,
|
|
367
|
+
* `slot` the attachment's own slot, `timelines` true unless stated false. The
|
|
368
|
+
* Spine emitter leaves out each one that equals the parser's fallback
|
|
369
|
+
* (`emitLinkedMesh`). Whether the author wrote a key or took its default is a
|
|
370
|
+
* fact only the refusals need, and it stays on `compile.ts`'s `PendingLink`.
|
|
371
|
+
*/
|
|
372
|
+
export interface ModelLinkedMeshAttachment {
|
|
373
|
+
kind: 'linkedmesh';
|
|
374
|
+
name?: string;
|
|
375
|
+
/** As on a mesh: present exactly when `attachmentPath` returns one (see the header's ⚠️). */
|
|
376
|
+
path?: string;
|
|
377
|
+
/** The PLACEHOLDER of the source mesh in `skin`/`slot`. */
|
|
378
|
+
source: string;
|
|
379
|
+
skin: string;
|
|
380
|
+
slot: string;
|
|
381
|
+
/** Whether the link plays its source's deform and sequence timelines. */
|
|
382
|
+
timelines: boolean;
|
|
383
|
+
width: number;
|
|
384
|
+
height: number;
|
|
385
|
+
color?: string;
|
|
386
|
+
sequence?: ModelSequence;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* What a skin table holds per placeholder: a model record of one of the six
|
|
391
|
+
* attachment kinds rigc builds, told apart by `kind`, whose words are Spine's
|
|
392
|
+
* `type` words (a region's `type` is the one the emitter leaves out). rigc
|
|
393
|
+
* emits no point attachment (`DEFERRED_ATTACHMENTS` in `compile.ts`).
|
|
394
|
+
*/
|
|
395
|
+
export type SkinTableEntry = ModelVertexAttachment | ModelRegionAttachment | ModelLinkedMeshAttachment;
|
|
396
|
+
|
|
397
|
+
/** One skin's attachments: slot -> placeholder -> record, in the spec's order. */
|
|
398
|
+
export type SkinTable = Record<string, Record<string, SkinTableEntry>>;
|
|
399
|
+
|
|
400
|
+
/** Whether a skin-table entry is one of the four vertex kinds — the ones a deform timeline can key. */
|
|
401
|
+
export function isModelVertexAttachment(entry: SkinTableEntry): entry is ModelVertexAttachment {
|
|
402
|
+
return entry.kind === 'mesh' || entry.kind === 'boundingbox' || entry.kind === 'clipping' || entry.kind === 'path';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* One slot, as the slot loop computes it. The slot array IS the draw order, so
|
|
407
|
+
* `CompiledModel.slots` is in the rig's declaration order and a draw-order
|
|
408
|
+
* offset counts in it.
|
|
409
|
+
*/
|
|
410
|
+
export interface ModelSlot {
|
|
411
|
+
name: string;
|
|
412
|
+
bone: string;
|
|
413
|
+
/** The setup attachment's placeholder; `null` is "shows nothing", which Spine spells by leaving the key out. */
|
|
414
|
+
setup: string | null;
|
|
415
|
+
/**
|
|
416
|
+
* The setup tint as 8-bit hex channels: `rgbaHex` of the motion spec's setup
|
|
417
|
+
* colour (the rounding to the 8-bit grid is value), or the rig's as stated.
|
|
418
|
+
*/
|
|
419
|
+
color?: string;
|
|
420
|
+
/** The two-colour tint's dark colour, as stated; a slot with none takes no `rgba2`/`rgb2` timeline. */
|
|
421
|
+
dark?: string;
|
|
422
|
+
/**
|
|
423
|
+
* The blend as stated, which `parseRigSpec` has held to a spelling the
|
|
424
|
+
* runtime resolves (`RigSlotBlend`: only the first letter's case is free).
|
|
425
|
+
*/
|
|
426
|
+
blend?: string;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* One skin. `bones` and `constraints` are what it activates (`splitRigSkin`'s
|
|
431
|
+
* member lists, empty for a skin the spec gives none — the manifest's `default`
|
|
432
|
+
* among them); `attachments` is its table, in the spec's order.
|
|
433
|
+
*/
|
|
434
|
+
export interface ModelSkin {
|
|
435
|
+
name: string;
|
|
436
|
+
bones: string[];
|
|
437
|
+
constraints: Record<RigSkinConstraintKey, string[]>;
|
|
438
|
+
attachments: SkinTable;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** The five constraint kinds Spine 4.3 has. */
|
|
442
|
+
export type ModelConstraintKind = 'ik' | 'transform' | 'path' | 'physics' | 'slider';
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* One constraint: its `kind` and `name`, and every field `buildRigConstraint`
|
|
446
|
+
* (for a rig spec's) or the motion spec's physics table computes, under the
|
|
447
|
+
* name it has in the spec, numbers already `f32`'d.
|
|
448
|
+
*
|
|
449
|
+
* `declaredIn` says which of the two built it, and it is here because the
|
|
450
|
+
* bytes differ: the physics table writes a physics constraint's fields in
|
|
451
|
+
* another order than `buildRigConstraint`'s physics branch — `inertia` …
|
|
452
|
+
* `mix`, then `fps`, `limit`, where the builder writes `limit`, `fps` first —
|
|
453
|
+
* and the key-order table lists neither `fps` nor `limit`, so the order
|
|
454
|
+
* survives into the file (`MS05` plants it). A table constraint holds every
|
|
455
|
+
* component and parameter the spec stated, 0 and the parser's default
|
|
456
|
+
* included; which of them Spine leaves out is the emitter's.
|
|
457
|
+
*/
|
|
458
|
+
export type ModelConstraint = {
|
|
459
|
+
kind: ModelConstraintKind;
|
|
460
|
+
name: string;
|
|
461
|
+
declaredIn: 'rig' | 'motion';
|
|
462
|
+
} & Record<string, unknown>;
|
|
463
|
+
|
|
464
|
+
/** An event's payload, as the rig declares it; `float`, `volume`, `balance` `f32`'d. */
|
|
465
|
+
export interface ModelEvent {
|
|
466
|
+
int?: number;
|
|
467
|
+
float?: number;
|
|
468
|
+
string?: string;
|
|
469
|
+
audio?: string;
|
|
470
|
+
volume?: number;
|
|
471
|
+
balance?: number;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* One timeline key, as the timeline compilers in `compile.ts` build it: `time`,
|
|
476
|
+
* then the channels its timeline defines, then `curve` — each in the order the
|
|
477
|
+
* compiler inserted it, which the Spine emitter keeps (a key's field order is a
|
|
478
|
+
* byte wherever the key-order table has no row for its kind).
|
|
479
|
+
*
|
|
480
|
+
* Every number is already on the float32 grid (`keyTime` for `time`, `f32` for
|
|
481
|
+
* the rest), for the reason the header gives.
|
|
482
|
+
*
|
|
483
|
+
* 🔸 **The channel names are Spine's** — `value`, `x`/`y`, `mix`, `mixRotate`,
|
|
484
|
+
* `color`, `offset`/`vertices`, `name`, `mode`/`index`/`delay` — because the
|
|
485
|
+
* motion spec's vocabulary is Spine's (docs/COMPILED_MODEL.md §1.1, *The input
|
|
486
|
+
* vocabulary is already Spine's*; docs/AUTHORING.md, *The vocabulary is
|
|
487
|
+
* Spine's*): a channel is named once, by the format the spec was written
|
|
488
|
+
* against, and a second backend maps from it. A later cut may give the model
|
|
489
|
+
* names of its own; this one does not.
|
|
490
|
+
*
|
|
491
|
+
* 🔸 **`curve` is absolute control points, four per channel, or `'stepped'`.**
|
|
492
|
+
* The points are what `bezierForChannel` computes from an easing's handles (the
|
|
493
|
+
* curve the runtime samples, not the handles an editor shows) or a raw curve's
|
|
494
|
+
* own numbers. `'stepped'` is the format's word for a hold, and the model keeps
|
|
495
|
+
* that word because its one consumer reads the same word: a named easing over a
|
|
496
|
+
* segment whose values do not move is held as `'stepped'` (`easingCurve`,
|
|
497
|
+
* issue #369) — a curve over a flat segment draws nothing, so the two encodings
|
|
498
|
+
* play one animation, and `'stepped'` is the one the editor writes. A later cut
|
|
499
|
+
* may spell the hold another way; this one does not.
|
|
500
|
+
*
|
|
501
|
+
* On an ik key the three flags `bendPositive`, `compress` and `stretch` are
|
|
502
|
+
* the flags IN EFFECT on that key: the key's own where the motion states one,
|
|
503
|
+
* else the constraint's, else the constraint's parser default (issue #273 — the
|
|
504
|
+
* 4.3 parser reads the flags per key without inheriting the constraint's, so
|
|
505
|
+
* which flag a key carries is a value). Which of them Spine writes is the
|
|
506
|
+
* emitter's (`emitAnimations`).
|
|
507
|
+
*/
|
|
508
|
+
export type ModelKey = { time: number; curve?: number[] | 'stepped' } & Record<string, unknown>;
|
|
509
|
+
|
|
510
|
+
/** One target's timelines: timeline name (`rotate`, `rgba`, `mix` …) -> its keys, in the spec's order. */
|
|
511
|
+
export type ModelTimelines = Map<string, ModelKey[]>;
|
|
512
|
+
|
|
513
|
+
/** The two timelines an attachment can carry, `deform` compiled before `sequence`. */
|
|
514
|
+
export interface ModelAttachmentTimelines {
|
|
515
|
+
deform?: ModelKey[];
|
|
516
|
+
sequence?: ModelKey[];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* One animation: every timeline the motion spec's animation compiles to, each
|
|
521
|
+
* collection in the order the spec states its tracks (the order the timelines
|
|
522
|
+
* are built in), and the declared duration the compiler verified against the
|
|
523
|
+
* last key.
|
|
524
|
+
*
|
|
525
|
+
* A collection is empty when the animation keys nothing of its kind — `drawOrder`
|
|
526
|
+
* and `events` included, since the compiler refuses an empty key list for both.
|
|
527
|
+
* Which of them a format writes, in what order, and under what spelling is the
|
|
528
|
+
* emitter's.
|
|
529
|
+
*
|
|
530
|
+
* ⭐ **The physics timeline that names no constraint is held under
|
|
531
|
+
* `EVERY_GLOBAL_PHYSICS` (`'*'`, `src/motion.ts`)** — the motion spec's own
|
|
532
|
+
* name for the target that drives every physics constraint declaring the keyed
|
|
533
|
+
* property global (issue #726). It is a key of `constraints.physics` and not a
|
|
534
|
+
* collection of its own because its POSITION among the named physics
|
|
535
|
+
* constraints is a value: `readAnimation` builds the physics timelines in the
|
|
536
|
+
* order it reads them and applies them in that order, and the file interleaves
|
|
537
|
+
* it — a motion keying `wob_b`, then `*`, then `wob` writes `wob_b, "", wob`.
|
|
538
|
+
* `'*'` cannot name a constraint (the rig parse refuses a physics constraint of
|
|
539
|
+
* that name), so the key is unambiguous. Spine spells it as the empty name;
|
|
540
|
+
* that spelling is the emitter's.
|
|
541
|
+
*/
|
|
542
|
+
export interface CompiledAnimation {
|
|
543
|
+
/** The motion spec's declared duration, verified by the compiler to be the last key's time within a frame. */
|
|
544
|
+
duration: number;
|
|
545
|
+
/** bone -> its timelines. */
|
|
546
|
+
bones: Map<string, ModelTimelines>;
|
|
547
|
+
/** slot -> its timelines. */
|
|
548
|
+
slots: Map<string, ModelTimelines>;
|
|
549
|
+
/**
|
|
550
|
+
* By constraint kind: `ik` and `transform` hold one unnamed timeline per
|
|
551
|
+
* constraint, so a constraint maps straight to its keys; `path`, `physics`
|
|
552
|
+
* and `slider` hold timelines by name, like a bone.
|
|
553
|
+
*/
|
|
554
|
+
constraints: {
|
|
555
|
+
ik: Map<string, ModelKey[]>;
|
|
556
|
+
transform: Map<string, ModelKey[]>;
|
|
557
|
+
path: Map<string, ModelTimelines>;
|
|
558
|
+
physics: Map<string, ModelTimelines>;
|
|
559
|
+
slider: Map<string, ModelTimelines>;
|
|
560
|
+
};
|
|
561
|
+
/** skin -> slot -> attachment placeholder -> its deform and/or sequence keys. */
|
|
562
|
+
attachments: Map<string, Map<string, Map<string, ModelAttachmentTimelines>>>;
|
|
563
|
+
/**
|
|
564
|
+
* Draw-order keys. A key's `offsets` are the moves as the motion spec states
|
|
565
|
+
* them, each resolved and checked; the order the format needs them in (by
|
|
566
|
+
* setup index) is the emitter's. A key with no `offsets` is "back to the setup
|
|
567
|
+
* order".
|
|
568
|
+
*/
|
|
569
|
+
drawOrder: ModelKey[];
|
|
570
|
+
/** Event firings, in the spec's (non-decreasing time) order, each naming a declared event. */
|
|
571
|
+
events: ModelKey[];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* The stage the skeleton declares (issue #1026): its origin `x`, `y` and its
|
|
576
|
+
* extent `width`, `height` — the rig spec's `skeleton.width`/`height` (or the
|
|
577
|
+
* manifest's crop), and its `x`/`y` or 0 beside them (issue #578: four fields
|
|
578
|
+
* or none). `null` where the rig declares no stage.
|
|
579
|
+
*
|
|
580
|
+
* ⭐ **Why the model holds it.** `render` and `check` say whether a candidate
|
|
581
|
+
* declares a stage (issue #714), and `A14`/`A19` read its box; until this field
|
|
582
|
+
* the header of `skeleton.json` was the only place the value was written, so a
|
|
583
|
+
* reader of the document had to open the Spine file beside it.
|
|
584
|
+
*
|
|
585
|
+
* 🔁 **And since issue #907 this is the only place it is written.** The Spine
|
|
586
|
+
* emitter used to copy it into the header's `x`, `y`, `width`, `height`,
|
|
587
|
+
* which the format defines as the setup-pose bounding box; the header now
|
|
588
|
+
* carries that box (`headerBoundsOf` in `src/compile.ts`), and every reader of
|
|
589
|
+
* the stage — `A14`, `A19`, `explain` — reads it here.
|
|
590
|
+
*/
|
|
591
|
+
export interface ModelStage {
|
|
592
|
+
x: number;
|
|
593
|
+
y: number;
|
|
594
|
+
width: number;
|
|
595
|
+
height: number;
|
|
596
|
+
/**
|
|
597
|
+
* Where the stage also travels in the Spine files, when the rig asked for it
|
|
598
|
+
* (`skeleton.stageBox`, issue #1168): the slot and the attachment name of the
|
|
599
|
+
* bounding box `compile` wrote from these four numbers. Absent where the rig
|
|
600
|
+
* did not ask, so no document written before the field existed moves a byte;
|
|
601
|
+
* a `/3` reader that predates it refuses the field by name rather than
|
|
602
|
+
* reading the document without it. `A50_STAGE_BOX_IS_THE_STAGE` reads it on
|
|
603
|
+
* both suppliers.
|
|
604
|
+
*/
|
|
605
|
+
box?: ModelStageBox;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** The stage box a rig asked for (`ModelStage.box`): the slot it is in and its attachment name. */
|
|
609
|
+
export interface ModelStageBox {
|
|
610
|
+
slot: string;
|
|
611
|
+
attachment: string;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The orders the Spine file lists two collections in, which are not the
|
|
616
|
+
* model's (issue #1026): `skins` — each skin's name and its slot keys — and
|
|
617
|
+
* `animations`, both in the EDITOR's order. `compile` computes it from the
|
|
618
|
+
* model with the same three functions it hands the Spine emitter
|
|
619
|
+
* (`editorSkinOrder`, `editorSlotKeyOrder`, `editorAnimationOrder` in
|
|
620
|
+
* `src/compile.ts`), so the order stated here and the order `skeleton.json`
|
|
621
|
+
* keys are one computation over one input.
|
|
622
|
+
*
|
|
623
|
+
* ⭐ **Why it is a statement of its own rather than the model's arrays
|
|
624
|
+
* re-sorted.** The model's `skins`, a skin's table and `animations` are in the
|
|
625
|
+
* spec's order on purpose (each field's own doc), and everything that reads
|
|
626
|
+
* them reads that order: a draw-order offset, a binding and a constraint
|
|
627
|
+
* count in the model's arrays; `A18` is held, by `MD04`, to see an animations
|
|
628
|
+
* map inserted in another order; and the model-side suppliers of #1025 put the
|
|
629
|
+
* spec's order through the emitter's rules themselves. Measured on the 19
|
|
630
|
+
* recipes `tools/emit_hashes.ts` generates, writing the arrays in the editor's
|
|
631
|
+
* order would have rewritten 18 of the 19 documents (a skin's slot keys on
|
|
632
|
+
* 18, the animations on 5, where 6,308 leaves move to another index) and told
|
|
633
|
+
* every one of those readers something else; stating the order beside them
|
|
634
|
+
* moves no leaf (`docs/COMPILED_MODEL.md` §9 has both counts).
|
|
635
|
+
*
|
|
636
|
+
* A placeholder's position inside one slot is not here: the emitter keeps a
|
|
637
|
+
* slot's own map as built, which is the model's order already.
|
|
638
|
+
*/
|
|
639
|
+
export interface ModelEditorOrder {
|
|
640
|
+
/** Every skin, `default` first and the rest by the editor's comparator, each with its table's slot keys in the order the file keys them. */
|
|
641
|
+
skins: Array<{ name: string; slots: string[] }>;
|
|
642
|
+
/** Every animation's name, in the order the file keys `animations`. */
|
|
643
|
+
animations: string[];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export interface CompiledModel extends CarriedFromCompileResult {
|
|
647
|
+
/**
|
|
648
|
+
* The stage the skeleton declares, or `null` (issue #1026, `ModelStage`).
|
|
649
|
+
* Not in the Spine header since issue #907, which carries the setup-pose
|
|
650
|
+
* bounding box there.
|
|
651
|
+
*/
|
|
652
|
+
stage: ModelStage | null;
|
|
653
|
+
/**
|
|
654
|
+
* The editor's orders the Spine file lists skins, their slot keys and
|
|
655
|
+
* animations in (issue #1026, `ModelEditorOrder`) — computed from this
|
|
656
|
+
* model's own `skins` and `animations`, never read back from the file.
|
|
657
|
+
*/
|
|
658
|
+
editorOrder: ModelEditorOrder;
|
|
659
|
+
/**
|
|
660
|
+
* The skeleton's reference scale, which a physics constraint's `wind` and
|
|
661
|
+
* `gravity` act over (issue #958): the rig spec's `skeleton.referenceScale`
|
|
662
|
+
* exactly as stated, or — stating none — the 100 the parser reads a header
|
|
663
|
+
* without one as (`UNSTATED_REFERENCE_SCALE` in `src/emit_spine.ts`). It is
|
|
664
|
+
* the number the Spine file is read as either way: the emitter writes this
|
|
665
|
+
* value into the header and the parser-default pass drops it at 100.
|
|
666
|
+
*
|
|
667
|
+
* ⭐ **Why the model holds it.** A Spine header stating 50 moved 30 of 50
|
|
668
|
+
* wind-and-gravity probes under the stepped oracle, and none without wind
|
|
669
|
+
* or gravity (issue #956); the core read the parser's 100 as a constant
|
|
670
|
+
* until this field. A value any backend posing the rig needs belongs here.
|
|
671
|
+
*
|
|
672
|
+
* 🔸 Not `f32`'d: the runtime reads it as the double the header spells
|
|
673
|
+
* (`SkeletonJson` multiplies it by the loader's `scale` and stores it), so
|
|
674
|
+
* it is the rig's number unchanged, and on the grids `modelDocument`
|
|
675
|
+
* states exactly when the rig spec wrote it on one. It is not a libm
|
|
676
|
+
* result, so no platform moves it; none of the nineteen recipes states one,
|
|
677
|
+
* and all nineteen documents carry 100.
|
|
678
|
+
*/
|
|
679
|
+
referenceScale: number;
|
|
680
|
+
/** Every bone, in the rig's declaration order — parents first, as the runtime requires. */
|
|
681
|
+
bones: ModelBone[];
|
|
682
|
+
/** The setup world transform of every bone, computed from `bones`. Never emitted. */
|
|
683
|
+
setupWorld: Map<string, BoneTransform>;
|
|
684
|
+
/** Every slot, in draw order — the rig's declaration order. */
|
|
685
|
+
slots: ModelSlot[];
|
|
686
|
+
/**
|
|
687
|
+
* Every skin, in the order the spec declares them (`default` first when there
|
|
688
|
+
* is one), each with its attachments as model records in the spec's order —
|
|
689
|
+
* NOT the editor's order the emitter sorts skins and slot keys into.
|
|
690
|
+
*/
|
|
691
|
+
skins: ModelSkin[];
|
|
692
|
+
/** The rig's constraints in declaration order, then the motion spec's physics table's. */
|
|
693
|
+
constraints: ModelConstraint[];
|
|
694
|
+
/** Event definitions, in the rig's declared order. */
|
|
695
|
+
events: Map<string, ModelEvent>;
|
|
696
|
+
/**
|
|
697
|
+
* Every animation, in the motion spec's order — NOT the editor's order the
|
|
698
|
+
* Spine emitter keys them in (`emitAnimations`).
|
|
699
|
+
*/
|
|
700
|
+
animations: Map<string, CompiledAnimation>;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// the document: `rigc-compiled/3` (issue #922, cut 1f; `pages` and `/2`,
|
|
705
|
+
// issue #1016; `stage`, `editorOrder` and each page's `pma` and `scale`, `/3`,
|
|
706
|
+
// issue #1026)
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* The document's `spec` value. `/2` since issue #1016 added the `pages`
|
|
711
|
+
* section: a `/1` reader (`readModel` of rigc 1.6) refuses a section it does
|
|
712
|
+
* not know by name, so the same spec over a new section would be refused by
|
|
713
|
+
* every reader already installed rather than read wrongly — a new spec says so
|
|
714
|
+
* before the first section is opened. `/3` since issue #1026 added `stage`,
|
|
715
|
+
* `editorOrder` and two fields on every page, for the same reason: a `/2`
|
|
716
|
+
* reader refuses each of them by name. `readModel` reads all three.
|
|
717
|
+
*/
|
|
718
|
+
export const MODEL_DOCUMENT_SPEC = 'rigc-compiled/3';
|
|
719
|
+
|
|
720
|
+
/** The file `build` writes the document to, in `--out` beside `skeleton.json` and `skeleton.atlas`. */
|
|
721
|
+
export const MODEL_DOCUMENT_FILE = 'skeleton.model.json';
|
|
722
|
+
|
|
723
|
+
/** A value the document writes: what `JSON.stringify` reproduces exactly. */
|
|
724
|
+
type DocValue = string | number | boolean | null | DocValue[] | { [key: string]: DocValue };
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* `record`'s fields in `keys`' order, each only when present (`undefined` is
|
|
728
|
+
* absent, as `JSON.stringify` reads it). A field `keys` does not list is
|
|
729
|
+
* refused by name rather than dropped: a field added to a model record without
|
|
730
|
+
* a place in the document would otherwise vanish from it in silence.
|
|
731
|
+
*/
|
|
732
|
+
function ordered(record: object, keys: readonly string[], where: string, value: (key: string, v: unknown) => DocValue = (_k, v) => plain(v, `${where}.${_k}`)): { [key: string]: DocValue } {
|
|
733
|
+
const own = record as Record<string, unknown>;
|
|
734
|
+
for (const key of Object.keys(own)) {
|
|
735
|
+
if (!keys.includes(key)) {
|
|
736
|
+
throw new CompileError(`internal: the model document has no place for field "${key}" of ${where}; it writes [${keys.join(', ')}]`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const out: { [key: string]: DocValue } = {};
|
|
740
|
+
for (const key of keys) if (own[key] !== undefined) out[key] = value(key, own[key]);
|
|
741
|
+
return out;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* A value the model holds, as the document writes it: objects in their own
|
|
746
|
+
* key order, arrays in theirs. A number JSON cannot carry exactly is refused
|
|
747
|
+
* by its path — `-0` (written `0`), `NaN` and the infinities (written `null`)
|
|
748
|
+
* — and so are `undefined` inside an array (written `null`), a `Map` or `Set`
|
|
749
|
+
* (written `{}`), and anything that is not plain data. Each of those would
|
|
750
|
+
* make the document state a value the model does not hold.
|
|
751
|
+
*/
|
|
752
|
+
function plain(value: unknown, where: string): DocValue {
|
|
753
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
754
|
+
if (typeof value === 'number') {
|
|
755
|
+
if (!Number.isFinite(value)) throw new CompileError(`internal: the model document cannot carry ${value} at ${where}; JSON writes it as null`);
|
|
756
|
+
if (Object.is(value, -0)) throw new CompileError(`internal: the model document cannot carry -0 at ${where}; JSON writes it as 0`);
|
|
757
|
+
return value;
|
|
758
|
+
}
|
|
759
|
+
if (Array.isArray(value)) {
|
|
760
|
+
return value.map((item, i) => {
|
|
761
|
+
if (item === undefined) throw new CompileError(`internal: the model document cannot carry undefined at ${where}[${i}]; JSON writes it as null`);
|
|
762
|
+
return plain(item, `${where}[${i}]`);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
766
|
+
const out: { [key: string]: DocValue } = {};
|
|
767
|
+
for (const [key, item] of Object.entries(value)) if (item !== undefined) out[key] = plain(item, `${where}.${key}`);
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
throw new CompileError(`internal: the model document cannot carry ${Object.prototype.toString.call(value)} at ${where}; it writes plain data only`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** A `Map` as the array of its entries in the map's order, each `{ name, … }`. */
|
|
774
|
+
function named<V>(map: ReadonlyMap<string, V>, where: string, entry: (value: V, at: string) => { [key: string]: DocValue }): DocValue[] {
|
|
775
|
+
return [...map].map(([name, value]) => ({ name, ...entry(value, `${where}["${name}"]`) }));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Keys, as the model holds them: each key's own field order is the compiler's, and a byte for the emitter. */
|
|
779
|
+
function keysOf(keys: readonly ModelKey[], where: string): DocValue {
|
|
780
|
+
return plain(keys, where);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** One target's timelines: `[{ name, keys }]`, in the model's order. */
|
|
784
|
+
function timelinesOf(timelines: ModelTimelines, where: string): DocValue[] {
|
|
785
|
+
return named(timelines, where, (keys, at) => ({ keys: keysOf(keys, at) }));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** target -> timelines: `[{ name, timelines }]`, in the model's order. */
|
|
789
|
+
function targetsOf(targets: ReadonlyMap<string, ModelTimelines>, where: string): DocValue[] {
|
|
790
|
+
return named(targets, where, (timelines, at) => ({ timelines: timelinesOf(timelines, at) }));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const BONE_FIELDS = ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inheritMode', 'skinRequired', 'editor'] as const;
|
|
794
|
+
const SLOT_FIELDS = ['name', 'bone', 'setup', 'color', 'dark', 'blend'] as const;
|
|
795
|
+
const SEQUENCE_FIELDS = ['count', 'start', 'digits', 'setup', 'atlas'] as const;
|
|
796
|
+
const ATLAS_RECT_FIELDS = ['width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight'] as const;
|
|
797
|
+
const BINDING_FIELDS = ['bone', 'x', 'y', 'weight'] as const;
|
|
798
|
+
const EVENT_FIELDS = ['int', 'float', 'string', 'audio', 'volume', 'balance'] as const;
|
|
799
|
+
const CONSTRAINT_KINDS = ['ik', 'transform', 'path', 'physics', 'slider'] as const;
|
|
800
|
+
const ATTACHMENT_FIELDS: Readonly<Record<SkinTableEntry['kind'], readonly string[]>> = {
|
|
801
|
+
mesh: ['kind', 'name', 'path', 'color', 'uvs', 'triangles', 'vertices', 'hull', 'edges', 'width', 'height', 'sequence'],
|
|
802
|
+
boundingbox: ['kind', 'name', 'vertexCount', 'vertices', 'editorColor'],
|
|
803
|
+
clipping: ['kind', 'name', 'end', 'convex', 'inverse', 'vertexCount', 'vertices', 'editorColor'],
|
|
804
|
+
path: ['kind', 'name', 'closed', 'constantSpeed', 'vertexCount', 'vertices', 'lengths', 'editorColor'],
|
|
805
|
+
region: ['kind', 'name', 'path', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence', 'atlas'],
|
|
806
|
+
linkedmesh: ['kind', 'name', 'path', 'source', 'skin', 'slot', 'timelines', 'width', 'height', 'color', 'sequence'],
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
function verticesOf(vertices: ModelVertices, where: string): DocValue {
|
|
810
|
+
return vertices.weighted
|
|
811
|
+
? ordered(vertices, ['weighted', 'bindings'], where, (key, v) =>
|
|
812
|
+
key === 'bindings'
|
|
813
|
+
? vertices.bindings.map((influences, i) => influences.map((b, j) => ordered(b, BINDING_FIELDS, `${where}.bindings[${i}][${j}]`)))
|
|
814
|
+
: plain(v, `${where}.${key}`),
|
|
815
|
+
)
|
|
816
|
+
: ordered(vertices, ['weighted', 'xy'], where);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function attachmentOf(entry: SkinTableEntry, where: string): DocValue {
|
|
820
|
+
const fields = ATTACHMENT_FIELDS[entry.kind];
|
|
821
|
+
if (fields === undefined) throw new CompileError(`internal: the model document knows no attachment kind "${String(entry.kind)}" at ${where}`);
|
|
822
|
+
if (entry.kind === 'region' && (entry.atlas === undefined) === (entry.sequence === undefined)) {
|
|
823
|
+
throw new CompileError(
|
|
824
|
+
`internal: the region at ${where} carries ${entry.atlas === undefined ? 'neither an atlas rectangle nor a sequence' : 'both an atlas rectangle and a sequence'}; ` +
|
|
825
|
+
'a region states exactly one of the two, and a sequence holds a rectangle per frame (issue #935)',
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return ordered(entry, fields, where, (key, v) => {
|
|
829
|
+
if (key === 'vertices') return verticesOf(v as ModelVertices, `${where}.vertices`);
|
|
830
|
+
if (key === 'sequence') {
|
|
831
|
+
return ordered(v as object, SEQUENCE_FIELDS, `${where}.sequence`, (k, item) =>
|
|
832
|
+
k === 'atlas' ? (item as ModelAtlasRect[]).map((rect, i) => atlasRectOf(rect, `${where}.sequence.atlas[${i}]`)) : plain(item, `${where}.sequence.${k}`),
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (key === 'atlas') return v === null ? null : atlasRectOf(v as ModelAtlasRect, `${where}.atlas`);
|
|
836
|
+
return plain(v, `${where}.${key}`);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** One rectangle, in `ModelAtlasRect`'s order, every field required. */
|
|
841
|
+
function atlasRectOf(rect: ModelAtlasRect, where: string): DocValue {
|
|
842
|
+
for (const key of ATLAS_RECT_FIELDS) {
|
|
843
|
+
if (rect[key] === undefined) throw new CompileError(`internal: the atlas rectangle at ${where} has no ${key}; it states all of [${ATLAS_RECT_FIELDS.join(', ')}]`);
|
|
844
|
+
}
|
|
845
|
+
return ordered(rect, ATLAS_RECT_FIELDS, where);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function skinOf(skin: ModelSkin, where: string): DocValue {
|
|
849
|
+
return ordered(skin, ['name', 'bones', 'constraints', 'attachments'], where, (key, v) => {
|
|
850
|
+
if (key === 'constraints') return ordered(skin.constraints, CONSTRAINT_KINDS, `${where}.constraints`);
|
|
851
|
+
if (key !== 'attachments') return plain(v, `${where}.${key}`);
|
|
852
|
+
const bySlot: { [slot: string]: DocValue } = {};
|
|
853
|
+
for (const [slot, byPlaceholder] of Object.entries(skin.attachments)) {
|
|
854
|
+
const entries: { [placeholder: string]: DocValue } = {};
|
|
855
|
+
for (const [placeholder, entry] of Object.entries(byPlaceholder)) entries[placeholder] = attachmentOf(entry, `${where}.attachments["${slot}"]["${placeholder}"]`);
|
|
856
|
+
bySlot[slot] = entries;
|
|
857
|
+
}
|
|
858
|
+
return bySlot;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** A constraint: `kind`, `name`, `declaredIn`, then every other field in the builder's order, which is a byte for the emitter. */
|
|
863
|
+
function constraintOf(constraint: ModelConstraint, where: string): DocValue {
|
|
864
|
+
const { kind, name, declaredIn, ...rest } = constraint;
|
|
865
|
+
return { kind, name, declaredIn, ...(plain(rest, where) as { [key: string]: DocValue }) };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function animationOf(animation: CompiledAnimation, where: string): { [key: string]: DocValue } {
|
|
869
|
+
return ordered(animation, ['duration', 'bones', 'slots', 'constraints', 'attachments', 'drawOrder', 'events'], where, (key, v) => {
|
|
870
|
+
const at = `${where}.${key}`;
|
|
871
|
+
switch (key) {
|
|
872
|
+
case 'bones':
|
|
873
|
+
case 'slots':
|
|
874
|
+
return targetsOf(v as ReadonlyMap<string, ModelTimelines>, at);
|
|
875
|
+
case 'constraints': {
|
|
876
|
+
const c = animation.constraints;
|
|
877
|
+
return ordered(c, CONSTRAINT_KINDS, at, (kind, byName) =>
|
|
878
|
+
kind === 'ik' || kind === 'transform'
|
|
879
|
+
? named(byName as ReadonlyMap<string, ModelKey[]>, `${at}.${kind}`, (keys, k) => ({ keys: keysOf(keys, k) }))
|
|
880
|
+
: targetsOf(byName as ReadonlyMap<string, ModelTimelines>, `${at}.${kind}`),
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
case 'attachments':
|
|
884
|
+
return named(animation.attachments, at, (bySlot, s) => ({
|
|
885
|
+
slots: named(bySlot, s, (byAttachment, a) => ({
|
|
886
|
+
attachments: named(byAttachment, a, (timelines, t) =>
|
|
887
|
+
ordered(timelines, ['deform', 'sequence'], t, (k, keys) => keysOf(keys as ModelKey[], `${t}.${k}`)),
|
|
888
|
+
),
|
|
889
|
+
})),
|
|
890
|
+
}));
|
|
891
|
+
case 'drawOrder':
|
|
892
|
+
case 'events':
|
|
893
|
+
return keysOf(v as ModelKey[], at);
|
|
894
|
+
default:
|
|
895
|
+
return plain(v, at);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** The stage's fields, in the order the Spine header writes them. */
|
|
901
|
+
export const MODEL_STAGE_FIELDS = ['x', 'y', 'width', 'height'] as const;
|
|
902
|
+
/** The stage box's fields (issue #1168), in `ModelStageBox`'s order. */
|
|
903
|
+
export const MODEL_STAGE_BOX_FIELDS = ['slot', 'attachment'] as const;
|
|
904
|
+
/** What the `stage` section writes: the four numbers, then the box where the rig asked for one. */
|
|
905
|
+
const STAGE_FIELDS: readonly string[] = [...MODEL_STAGE_FIELDS, 'box'];
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* The `editorOrder` section: `{ skins: [{ name, slots }], animations }`,
|
|
909
|
+
* refused by name where it is not a permutation of what the model holds — an
|
|
910
|
+
* order naming a skin, a slot key or an animation the model does not have, or
|
|
911
|
+
* leaving one out, would state an order for another rig.
|
|
912
|
+
*/
|
|
913
|
+
function editorOrderOf(model: CompiledModel): DocValue {
|
|
914
|
+
const order = model.editorOrder;
|
|
915
|
+
const problems: string[] = [];
|
|
916
|
+
const same = (what: string, stated: readonly string[], held: readonly string[]): void => {
|
|
917
|
+
if (JSON.stringify([...stated].sort()) !== JSON.stringify([...held].sort())) problems.push(`${what} lists [${stated.join(', ')}], the model holds [${held.join(', ')}]`);
|
|
918
|
+
};
|
|
919
|
+
same('editorOrder.skins', order.skins.map((s) => s.name), model.skins.map((s) => s.name));
|
|
920
|
+
for (const [i, entry] of order.skins.entries()) {
|
|
921
|
+
const skin = model.skins.find((s) => s.name === entry.name);
|
|
922
|
+
if (skin !== undefined) same(`editorOrder.skins[${i}] "${entry.name}".slots`, entry.slots, Object.keys(skin.attachments));
|
|
923
|
+
}
|
|
924
|
+
same('editorOrder.animations', order.animations, [...model.animations.keys()]);
|
|
925
|
+
if (problems.length > 0) throw new CompileError(`internal: the model's editor order is not its own: ${problems.join('; ')}`);
|
|
926
|
+
return {
|
|
927
|
+
skins: order.skins.map((entry, i) => ordered(entry, ['name', 'slots'], `editorOrder.skins[${i}]`)),
|
|
928
|
+
animations: plain(order.animations, 'editorOrder.animations'),
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** The model's fields the document writes, after `spec`, in its key order. */
|
|
933
|
+
const MODEL_DOCUMENT_FIELDS: readonly string[] = [
|
|
934
|
+
'referenceScale', 'stage', 'bones', 'slots', 'skins', 'constraints', 'events', 'animations', 'editorOrder',
|
|
935
|
+
'images', 'pageGrids', 'droppedStates', 'absentParts', 'meshBones', 'meshes', 'physics', 'deformTransforms', 'trackDerivations', 'rig',
|
|
936
|
+
];
|
|
937
|
+
|
|
938
|
+
/** The model's fields the document leaves out — see `modelDocument`. */
|
|
939
|
+
const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* The compiled model as a document: `rigc-compiled/3`, `JSON.stringify(doc,
|
|
943
|
+
* null, 2)` and a newline — the text `build` writes to `skeleton.model.json`,
|
|
944
|
+
* and the record rigc's own posing core reads (`readModel` in
|
|
945
|
+
* `src/core/index.ts`, issue #380, step 2). Its cost in a build is measured in
|
|
946
|
+
* docs/COMPILED_MODEL.md §6 (issue #926).
|
|
947
|
+
*
|
|
948
|
+
* **Key order.** `spec`, then the model's fields in the order this file
|
|
949
|
+
* declares them — `referenceScale` (issue #958), `stage` (issue #1026, the
|
|
950
|
+
* header's four fields or `null`), `bones`, `slots`, `skins`, `constraints`,
|
|
951
|
+
* `events`, `animations`, `editorOrder` (issue #1026, `{ skins: [{ name,
|
|
952
|
+
* slots }], animations }`) — then the fields carried from `CompileResult` in
|
|
953
|
+
* `CarriedFromCompileResult`'s order: `images`, `pageGrids`, `droppedStates`,
|
|
954
|
+
* `absentParts`, `meshBones`, `meshes`, `physics`, `deformTransforms`,
|
|
955
|
+
* `trackDerivations`, `rig`; then `pages`, where each region sits on its page
|
|
956
|
+
* in the atlas written beside it, and each page's `pma` and `scale` (issue
|
|
957
|
+
* #1026) (`pagesOfAtlas`, issue #1016), which is why
|
|
958
|
+
* the atlas text is the third argument; and last `spine`, the digest of the
|
|
959
|
+
* `skeleton.json` written beside it (`spineFileSha256`, issue #968), which is
|
|
960
|
+
* why the Spine text is the second argument. Inside a model record, its interface's field
|
|
961
|
+
* order (`ModelBone`, `ModelSlot`, `ModelSkin`, each attachment kind,
|
|
962
|
+
* `ModelBinding`, `ModelSequence`, `ModelAtlasRect`, `ModelEvent`, `CompiledAnimation`), each
|
|
963
|
+
* field only when the record carries it, and a field the interface does not
|
|
964
|
+
* list refused by name. Three kinds of record keep their own order, because
|
|
965
|
+
* there the order is the value: a constraint's fields after `kind`, `name`,
|
|
966
|
+
* `declaredIn` (the builder's order, which the emitter keeps and which reaches
|
|
967
|
+
* the file where the key-order table has no row), a timeline key's fields
|
|
968
|
+
* (`time`, its channels, `curve`, as the compiler inserted them), and the
|
|
969
|
+
* carried reports, written as `compile` builds them.
|
|
970
|
+
*
|
|
971
|
+
* **Collections.** Every `Map` is an array of `{ name, … }` in the map's
|
|
972
|
+
* order, and that order is the model's own: bones parents first (a weighted
|
|
973
|
+
* binding's index counts in it), slots the draw order (a draw-order offset
|
|
974
|
+
* counts in it), skins, constraints, events and animations in the spec's
|
|
975
|
+
* declared order, timelines and keys in the order they are applied. A skin's
|
|
976
|
+
* attachment table stays an object keyed slot -> placeholder, as the model
|
|
977
|
+
* holds it. An animation writes `bones` and `slots` as `[{ name, timelines:
|
|
978
|
+
* [{ name, keys }] }]`, its `ik` and `transform` constraints as `[{ name, keys
|
|
979
|
+
* }]` and the other three kinds like a bone, and `attachments` as `[{ name:
|
|
980
|
+
* skin, slots: [{ name, attachments: [{ name, deform?, sequence? }] }] }]`;
|
|
981
|
+
* the physics timeline that names no constraint keeps the model's name for it,
|
|
982
|
+
* `*` (`EVERY_GLOBAL_PHYSICS`), not the empty name Spine spells it with.
|
|
983
|
+
*
|
|
984
|
+
* **Numbers** are written exactly as the model holds them, so nothing is
|
|
985
|
+
* re-rounded here; which of them are float32 values, and how a reader reads
|
|
986
|
+
* the rest, is the header's 🔸. A number JSON cannot carry exactly — `-0`,
|
|
987
|
+
* `NaN`, an infinity — is refused by its path (`plain`).
|
|
988
|
+
*
|
|
989
|
+
* 🔒 **Every number the document spells is on one of two grids**: a fixed
|
|
990
|
+
* point of the compiler's float32 spelling (`f32(x) === x` — the shortest
|
|
991
|
+
* decimal naming a float, which is not the float's own double) or of the
|
|
992
|
+
* six-decimal grid (`Math.round(x·1e6)/1e6 === x`). `MX01` in `selftest.ts`
|
|
993
|
+
* holds it over every document the tree's recipes build, with a full double
|
|
994
|
+
* planted to turn it red by its path. One field is the rig spec's number
|
|
995
|
+
* carried unchanged rather than computed, `referenceScale` (issue #958): the
|
|
996
|
+
* runtime reads the header's double, so rounding it here would pose another
|
|
997
|
+
* skeleton; it is on a grid when the spec wrote it on one. A number on neither grid is a full
|
|
998
|
+
* double whose last digits are the platform libm's, which is how the rule was
|
|
999
|
+
* found (issue #942): `meshes[].depth.ceiling` carried `turnCeiling`'s fold
|
|
1000
|
+
* figures (`src/depth.ts`) as full doubles from `Math.atan` and the depth
|
|
1001
|
+
* tone, and `gallery/look`'s document differed between macOS and the Linux
|
|
1002
|
+
* runner by exactly `/meshes/0/depth/ceiling/pitch/negative/{degrees,p1}`,
|
|
1003
|
+
* `26.935130523311` against `26.935130523311003`, while its Spine files were
|
|
1004
|
+
* identical. Those figures are reported on the six-decimal grid now, and a
|
|
1005
|
+
* one-ulp `Math.atan` perturbation either way moves no byte of that document.
|
|
1006
|
+
*
|
|
1007
|
+
* ⚠️ A grid absorbs a difference in a value, not in a choice. Perturbing
|
|
1008
|
+
* `Math.pow` by one ulp moved `gallery/look`'s document until issue #949: the
|
|
1009
|
+
* depth tone reaches every `z`, two triangles whose fold angles round to the
|
|
1010
|
+
* same six-decimal value traded places as the minimum, and the fold named the
|
|
1011
|
+
* other triangle, with its own `depthStep` and `stepShare`. The minimum is
|
|
1012
|
+
* now chosen on the six-decimal grid with the lowest triangle ordinal taking
|
|
1013
|
+
* a tie (`src/depth.ts`'s `foldPrecedes`), and a one-ulp perturbation of
|
|
1014
|
+
* sixteen libm functions, either way, moves no leaf of any gallery document
|
|
1015
|
+
* (`TB02`). What is left is a value within one ulp of a rounding boundary.
|
|
1016
|
+
*
|
|
1017
|
+
* **Left out, and why.** Each is something the model holds that is not a
|
|
1018
|
+
* statement about the rig:
|
|
1019
|
+
*
|
|
1020
|
+
* - `setupWorld` — computed from `bones` by `computeWorldTransforms`
|
|
1021
|
+
* (`src/transform.ts`), so it states nothing `bones` does not, and a core
|
|
1022
|
+
* is to compute it rather than read it; and it is the one field that can
|
|
1023
|
+
* hold a `-0` (a bone's `c` is `sin 0 · scaleX`, which is `-0` at a
|
|
1024
|
+
* `scaleX` of −1), which JSON cannot spell. (A root's `b` was `-sin 0`
|
|
1025
|
+
* until issue #1021; under the runtime's arithmetic it is `cos 90°` at
|
|
1026
|
+
* pi = 3.1415927, −2.3e-8.)
|
|
1027
|
+
* - `images[].absPath` — where the part was on this machine's disk, for the
|
|
1028
|
+
* size assertions.
|
|
1029
|
+
* - `droppedStates[].why` — the sentence names the `--atlas-in` file by its
|
|
1030
|
+
* absolute path.
|
|
1031
|
+
*
|
|
1032
|
+
* What the Spine emitter adds (`emitSkeleton`'s header, its spellings and
|
|
1033
|
+
* omissions) is not in the model and so not here — except the two things the
|
|
1034
|
+
* Spine files were the only place of until issue #1026, which the model now
|
|
1035
|
+
* holds and the emitter reads from it: the stage, and the orders the editor
|
|
1036
|
+
* lists skins, slot keys and animations in (`editorOrder`, computed by the
|
|
1037
|
+
* functions the emitter is handed). The header's `spine`, `fps`, `images` and
|
|
1038
|
+
* `audio` are still the emitter's alone.
|
|
1039
|
+
*/
|
|
1040
|
+
export function modelDocument(model: CompiledModel, skeletonText: string, atlasText: string): string {
|
|
1041
|
+
for (const key of Object.keys(model)) {
|
|
1042
|
+
if (!MODEL_DOCUMENT_FIELDS.includes(key) && !MODEL_DOCUMENT_LEFT_OUT.includes(key)) {
|
|
1043
|
+
throw new CompileError(`internal: the model document has no place for the model's field "${key}"; it writes [${MODEL_DOCUMENT_FIELDS.join(', ')}] and leaves out [${MODEL_DOCUMENT_LEFT_OUT.join(', ')}]`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const doc: { [key: string]: DocValue } = {
|
|
1047
|
+
spec: MODEL_DOCUMENT_SPEC,
|
|
1048
|
+
referenceScale: plain(model.referenceScale, 'referenceScale'),
|
|
1049
|
+
stage: model.stage === null ? null : ordered(model.stage, STAGE_FIELDS, 'stage', (key, v) => (key === 'box' ? ordered(v as object, MODEL_STAGE_BOX_FIELDS, 'stage.box') : plain(v, `stage.${key}`))),
|
|
1050
|
+
bones: model.bones.map((bone, i) =>
|
|
1051
|
+
ordered(bone, BONE_FIELDS, `bones[${i}]`, (key, v) => (key === 'editor' ? ordered(v as object, ['color', 'icon'], `bones[${i}].editor`) : plain(v, `bones[${i}].${key}`))),
|
|
1052
|
+
),
|
|
1053
|
+
slots: model.slots.map((slot, i) => ordered(slot, SLOT_FIELDS, `slots[${i}]`)),
|
|
1054
|
+
skins: model.skins.map((skin, i) => skinOf(skin, `skins[${i}]`)),
|
|
1055
|
+
constraints: model.constraints.map((constraint, i) => constraintOf(constraint, `constraints[${i}]`)),
|
|
1056
|
+
events: named(model.events, 'events', (event, at) => ordered(event, EVENT_FIELDS, at)),
|
|
1057
|
+
animations: named(model.animations, 'animations', (animation, at) => animationOf(animation, at)),
|
|
1058
|
+
editorOrder: editorOrderOf(model),
|
|
1059
|
+
images: plain(model.images.map(({ absPath: _absPath, ...image }) => image), 'images'),
|
|
1060
|
+
pageGrids: plain(model.pageGrids, 'pageGrids'),
|
|
1061
|
+
droppedStates: plain(model.droppedStates.map(({ why: _why, ...state }) => state), 'droppedStates'),
|
|
1062
|
+
absentParts: plain(model.absentParts, 'absentParts'),
|
|
1063
|
+
meshBones: plain(model.meshBones, 'meshBones'),
|
|
1064
|
+
meshes: plain(model.meshes, 'meshes'),
|
|
1065
|
+
physics: plain(model.physics, 'physics'),
|
|
1066
|
+
deformTransforms: plain(model.deformTransforms, 'deformTransforms'),
|
|
1067
|
+
trackDerivations: plain(model.trackDerivations, 'trackDerivations'),
|
|
1068
|
+
rig: plain(model.rig, 'rig'),
|
|
1069
|
+
pages: plain(pagesOfAtlas(atlasText), 'pages'),
|
|
1070
|
+
spine: { sha256: spineFileSha256(skeletonText) },
|
|
1071
|
+
};
|
|
1072
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// --- #1016 where each region sits on its page: begin ---
|
|
1076
|
+
/**
|
|
1077
|
+
* One region of a page, as the document's `pages` section states it: the
|
|
1078
|
+
* region's name exactly as the atlas line spells it (untrimmed — the core finds
|
|
1079
|
+
* a region by that spelling, `./core/uvs.ts`), its rectangle's top-left `x`,
|
|
1080
|
+
* `y` on the page (y down) and `width`, `height` in the drawing's orientation,
|
|
1081
|
+
* the trim (`offsetX` from the drawing's left, `offsetY` from its bottom) and
|
|
1082
|
+
* the untrimmed size, its turn in `degrees`, and its `index:` field — every
|
|
1083
|
+
* number in the page's texels, exactly as `parseAtlasText` reads it.
|
|
1084
|
+
*/
|
|
1085
|
+
export interface ModelPageRegion {
|
|
1086
|
+
name: string;
|
|
1087
|
+
x: number;
|
|
1088
|
+
y: number;
|
|
1089
|
+
width: number;
|
|
1090
|
+
height: number;
|
|
1091
|
+
offsetX: number;
|
|
1092
|
+
offsetY: number;
|
|
1093
|
+
originalWidth: number;
|
|
1094
|
+
originalHeight: number;
|
|
1095
|
+
degrees: number;
|
|
1096
|
+
index: number;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* One page: its name (a path from the directory `build` writes into, trimmed),
|
|
1101
|
+
* its `size`, its `pma` and `scale` (issue #1026), and its regions in file
|
|
1102
|
+
* order.
|
|
1103
|
+
*
|
|
1104
|
+
* `pma` and `scale` are present on every page `pagesOfAtlas` spells and on
|
|
1105
|
+
* every page a `rigc-compiled/3` document states; a `rigc-compiled/2`
|
|
1106
|
+
* document's pages carry neither, which is why they are optional here.
|
|
1107
|
+
*/
|
|
1108
|
+
export interface ModelPage {
|
|
1109
|
+
name: string;
|
|
1110
|
+
width: number;
|
|
1111
|
+
height: number;
|
|
1112
|
+
/** Whether the page's texels are premultiplied, as the runtime reads the page's `pma:` line (absent: false). */
|
|
1113
|
+
pma?: boolean;
|
|
1114
|
+
/**
|
|
1115
|
+
* The number the page's own `scale:` line states, or `null` where the page
|
|
1116
|
+
* has none. Not defaulted to the 1 a page without the line is read as: the
|
|
1117
|
+
* line is an importer's instruction ("these texels are this much smaller than
|
|
1118
|
+
* the drawings"), `check` reports a declared one (issue #171), and a default
|
|
1119
|
+
* would state a line the atlas does not have.
|
|
1120
|
+
*/
|
|
1121
|
+
scale?: number | null;
|
|
1122
|
+
regions: ModelPageRegion[];
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** The fields of a page and of a region, in the order the document writes them — `readModel` mirrors both lists. */
|
|
1126
|
+
export const MODEL_PAGE_FIELDS = ['name', 'width', 'height', 'pma', 'scale', 'regions'] as const;
|
|
1127
|
+
export const MODEL_PAGE_REGION_FIELDS = ['name', 'x', 'y', 'width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight', 'degrees', 'index'] as const;
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* The document's `pages` section (issue #1016): every page of `atlasText` and
|
|
1131
|
+
* every region on it, in file order, with the numbers the draw reads — where
|
|
1132
|
+
* each drawing sits, which `./core/uvs.ts` turns into page UVs. `atlasText`
|
|
1133
|
+
* is the atlas `build` writes beside the document, the one that run's gate
|
|
1134
|
+
* last read: after `--pack` the packed text, after `--copy-images` the text
|
|
1135
|
+
* with the copies' page names.
|
|
1136
|
+
*
|
|
1137
|
+
* ⭐ **Why it is a function of the written atlas rather than a model field.**
|
|
1138
|
+
* Issue #939 left the page, `x`, `y` and `rotate` out of `ModelAtlasRect`
|
|
1139
|
+
* because `--pack` moves them after `compile` returns and `--copy-images`
|
|
1140
|
+
* renames every page — a value the model held from `compile` would state a
|
|
1141
|
+
* place the written atlas does not have. The section is spelled from the
|
|
1142
|
+
* atlas text instead, so it moves exactly when that text does, and `build`
|
|
1143
|
+
* spells the document from the text it writes (`cli.ts`): the document a pack
|
|
1144
|
+
* writes is spelled from the packed text and gated by the packed pass, where
|
|
1145
|
+
* `A18` compares it with a second compile's document spelled from a second,
|
|
1146
|
+
* independent pack.
|
|
1147
|
+
*
|
|
1148
|
+
* 🔸 **What it leaves out.** The page's `format`, `filter` and `repeat`
|
|
1149
|
+
* lines: nothing that draws or checks reads them (the rasteriser samples one
|
|
1150
|
+
* way, `src/render.ts`'s header; the pose reads only the ratios the trimmed
|
|
1151
|
+
* and original sizes make, #939's decision 2). `pma` and `scale` were left out
|
|
1152
|
+
* with them until issue #1026: they are not placement either, but `A06` reads
|
|
1153
|
+
* `pma` and `check`'s texture note reads `scale:`, and the atlas was the only
|
|
1154
|
+
* place either was written. Every region of every page is written, drawn or
|
|
1155
|
+
* not: which regions a rig draws is the core's lookup rule (`./core/uvs.ts`,
|
|
1156
|
+
* *Which region, on which page*), and a writer choosing a subset would
|
|
1157
|
+
* restate it.
|
|
1158
|
+
*/
|
|
1159
|
+
export function pagesOfAtlas(atlasText: string): ModelPage[] {
|
|
1160
|
+
const parsed = parseAtlasText(atlasText);
|
|
1161
|
+
return parsed.pages.map((page) => ({
|
|
1162
|
+
name: page.name,
|
|
1163
|
+
width: page.width,
|
|
1164
|
+
height: page.height,
|
|
1165
|
+
pma: page.pma,
|
|
1166
|
+
scale: statedPageScale(parsed.lines, page.nameLine),
|
|
1167
|
+
regions: page.regions.map((r) => ({
|
|
1168
|
+
name: r.name,
|
|
1169
|
+
x: r.x,
|
|
1170
|
+
y: r.y,
|
|
1171
|
+
width: r.width,
|
|
1172
|
+
height: r.height,
|
|
1173
|
+
offsetX: r.offsetX,
|
|
1174
|
+
offsetY: r.offsetY,
|
|
1175
|
+
originalWidth: r.originalWidth,
|
|
1176
|
+
originalHeight: r.originalHeight,
|
|
1177
|
+
degrees: r.degrees,
|
|
1178
|
+
index: r.index,
|
|
1179
|
+
})),
|
|
1180
|
+
}));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* A `scale:` line, as `check` has always read one (`atlasScales` in
|
|
1185
|
+
* `src/render.ts` reads every such line in a text with this pattern): an
|
|
1186
|
+
* indented `scale:` entry and one number. Shared so the two readings — every
|
|
1187
|
+
* line of a text, and the lines of one page — cannot drift into two patterns.
|
|
1188
|
+
*/
|
|
1189
|
+
export const ATLAS_SCALE_LINE = /^[ \t]+scale:[ \t]*([0-9.eE+-]+)[ \t]*$/;
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* The number a page's own `scale:` line states, or `null` (issue #1026). The
|
|
1193
|
+
* page's entries are the lines after its name up to the first line that is
|
|
1194
|
+
* blank or carries no colon — where `TextureAtlasReader` stops reading a page's
|
|
1195
|
+
* fields, and where `parseAtlasText` stops too. A line `ATLAS_SCALE_LINE`
|
|
1196
|
+
* matches and whose number is finite is the statement; the last one wins, as a
|
|
1197
|
+
* repeated entry does in `parseAtlasText`. Unlike `AtlasPage.scale`, nothing is
|
|
1198
|
+
* defaulted and a non-positive number is stated as written, because this is
|
|
1199
|
+
* the line `check` reports, not the ratio an importer divides by.
|
|
1200
|
+
*/
|
|
1201
|
+
function statedPageScale(lines: readonly string[], nameLine: number): number | null {
|
|
1202
|
+
let stated: number | null = null;
|
|
1203
|
+
for (let at = nameLine + 1; at < lines.length; at++) {
|
|
1204
|
+
const line = lines[at];
|
|
1205
|
+
const trimmed = line.trim();
|
|
1206
|
+
if (trimmed.length === 0 || !trimmed.includes(':')) break;
|
|
1207
|
+
const m = ATLAS_SCALE_LINE.exec(line);
|
|
1208
|
+
if (m === null) continue;
|
|
1209
|
+
const value = Number(m[1]);
|
|
1210
|
+
if (Number.isFinite(value)) stated = value;
|
|
1211
|
+
}
|
|
1212
|
+
return stated;
|
|
1213
|
+
}
|
|
1214
|
+
// --- #1016 where each region sits on its page: end ---
|
|
1215
|
+
|
|
1216
|
+
// --- #968 the Spine file the document was written beside: begin ---
|
|
1217
|
+
/**
|
|
1218
|
+
* The document's last section, `spine`: `{ "sha256": "<64 lowercase hex>" }`,
|
|
1219
|
+
* the SHA-256 of the exact bytes `build` writes to `skeleton.json` in the same
|
|
1220
|
+
* run (the UTF-8 of `skeletonText`, as `writeFileSync` writes it).
|
|
1221
|
+
*
|
|
1222
|
+
* ⭐ Why it exists (issue #968). `build` writes the Spine pair and this document
|
|
1223
|
+
* as one output, and `rigc render` poses the document through rigc's own core
|
|
1224
|
+
* when it finds one beside the skeleton. Nothing else ties the two files: a
|
|
1225
|
+
* `skeleton.json` edited by hand after the build, beside the document it was
|
|
1226
|
+
* built with, was drawn from the document — the build's rig, not the file the
|
|
1227
|
+
* render was pointed at (the `RF89`/`RF91`/`RF93` plants, exit 0 where the
|
|
1228
|
+
* Spine file refuses). The render takes the core only when the file beside the
|
|
1229
|
+
* document hashes to this value, and names the mismatch otherwise.
|
|
1230
|
+
*
|
|
1231
|
+
* Named `spine.sha256` rather than a top-level `skeletonSha256`: the section
|
|
1232
|
+
* is the document's statement about the Spine output it was written with, a
|
|
1233
|
+
* digest of it and nothing about the rig, so it sits apart from the model's
|
|
1234
|
+
* fields and after them; and an object leaves the atlas's digest a field to
|
|
1235
|
+
* add, not a second section. The file's NAME is not recorded — `build` always
|
|
1236
|
+
* writes `skeleton.json` beside this document, and a copied pair keeps both.
|
|
1237
|
+
*
|
|
1238
|
+
* Deterministic because the Spine file is (`A18` compares a second compile's
|
|
1239
|
+
* bytes), so two builds and two platforms write the same value wherever their
|
|
1240
|
+
* Spine files agree.
|
|
1241
|
+
*/
|
|
1242
|
+
export function spineFileSha256(skeletonText: string | Uint8Array): string {
|
|
1243
|
+
return createHash('sha256').update(skeletonText).digest('hex');
|
|
1244
|
+
}
|
|
1245
|
+
// --- #968 the Spine file the document was written beside: end ---
|