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
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Spine emitter — the compiled model (`src/model.ts`) written as Spine 4.3
|
|
3
|
+
* skeleton objects (issue #915, step 1b of #380; every structural record since
|
|
4
|
+
* #919, cut 1d; the animations since #921, cut 1e).
|
|
5
|
+
*
|
|
6
|
+
* The model holds values; this file owns the bytes. So every Spine 4.3
|
|
7
|
+
* spelling of a model field lives here and nowhere in the model: a bone's
|
|
8
|
+
* inherit mode is written under `inherit` (4.0/4.1 wrote `transform`, which 4.3
|
|
9
|
+
* loads silently as Normal — `A02`), and its skin-required flag under `skin`; a
|
|
10
|
+
* constraint's and an attachment's kind under `type` (a region's left out); a
|
|
11
|
+
* slot's setup attachment under `attachment`; the physics timeline that names
|
|
12
|
+
* no constraint under the empty name.
|
|
13
|
+
*
|
|
14
|
+
* 🔒 **Key insertion order is part of the byte contract.** `inEditorKeyOrder`
|
|
15
|
+
* (`src/keyorder.ts`) permutes only the keys its row lists; a key the row does
|
|
16
|
+
* not list — a bone's `shearX`, `shearY`, `skin` — keeps the position the
|
|
17
|
+
* constructor gave it, and a kind with no row (a linked mesh, a path
|
|
18
|
+
* attachment, a sequence, the path and slider constraints, an event) keeps the
|
|
19
|
+
* constructor's order whole. So each emitter here inserts keys in exactly the
|
|
20
|
+
* order `compile.ts` inserted them before the model existed, written down in
|
|
21
|
+
* its doc comment, and a reorder here is a byte change on every build that
|
|
22
|
+
* carries the key, which the byte-identity gate (`tools/emit_hashes.ts`) names.
|
|
23
|
+
*
|
|
24
|
+
* ✂️ **Omissions.** The parser defaults `PARSER_DEFAULTS` lists are dropped by
|
|
25
|
+
* `withoutParserDefaults` on the finished skeleton, once, as before. The
|
|
26
|
+
* omissions the constructors used to make INLINE are made here, because the
|
|
27
|
+
* model now holds the value: a slot's `null` setup attachment, a region's `x`,
|
|
28
|
+
* `y`, `rotation` at 0, a physics component at 0 and a parameter at its
|
|
29
|
+
* default (`PHYSICS_PARAMS`), and a linked mesh's own slot, `default` skin and
|
|
30
|
+
* `timelines: true`. Each restates what the parser reads in the key's absence.
|
|
31
|
+
* One omission is not here: a `path` equal to the name, which `attachmentPath`
|
|
32
|
+
* in `compile.ts` still decides (`src/model.ts`, the header's ⚠️).
|
|
33
|
+
*
|
|
34
|
+
* ⭐ **A weighted vertex's bone index is written here and nowhere else** (issue
|
|
35
|
+
* #917). The model binds by name; `emitVertices` writes Spine's run —
|
|
36
|
+
* `boneCount, (boneIndex, x, y, weight) × n` per vertex — with each index the
|
|
37
|
+
* bone's position in the model's bone array, which `emitBones` keeps in order.
|
|
38
|
+
*
|
|
39
|
+
* 🔸 **The editor's name comparator stays in `compile.ts`** and is handed to
|
|
40
|
+
* `emitSkins` (`EditorOrder`) and to `emitAnimations` (`AnimationOrder`): the
|
|
41
|
+
* selftest's scans of the fold read it in that file, and importing it from
|
|
42
|
+
* here would make the two modules import each other. The ORDER is applied
|
|
43
|
+
* here, and so is the refusal that comes with it — a set of animation names
|
|
44
|
+
* the editor could key two ways is refused inside `emitAnimations`, in the
|
|
45
|
+
* words `compile.ts` gives it; the comparator is passed in, not restated.
|
|
46
|
+
*
|
|
47
|
+
* 🧪 Every object this file returns is a fresh one. The parser-default and
|
|
48
|
+
* key-order passes run in place on the finished skeleton, and before issue
|
|
49
|
+
* #919 they reached the region and linked-mesh objects the skin tables held;
|
|
50
|
+
* no object of the model that those passes visit is handed to them — a
|
|
51
|
+
* timeline key included (issue #921): the passes delete a first key's
|
|
52
|
+
* `time: 0` and re-key an ik key's fields in place, and a key handed over by
|
|
53
|
+
* reference would come back out of the model that way.
|
|
54
|
+
*/
|
|
55
|
+
import { CompileError } from './errors.ts';
|
|
56
|
+
import { inEditorKeyOrder, PARSER_DEFAULTS, PHYSICS_PARAMS, UNSTATED_REFERENCE_SCALE, withoutParserDefaults } from './keyorder.ts';
|
|
57
|
+
import type {
|
|
58
|
+
CompiledAnimation,
|
|
59
|
+
CompiledModel,
|
|
60
|
+
ModelAttachmentTimelines,
|
|
61
|
+
ModelBone,
|
|
62
|
+
ModelBoundingBoxAttachment,
|
|
63
|
+
ModelClippingAttachment,
|
|
64
|
+
ModelConstraint,
|
|
65
|
+
ModelConstraintKind,
|
|
66
|
+
ModelEvent,
|
|
67
|
+
ModelKey,
|
|
68
|
+
ModelLinkedMeshAttachment,
|
|
69
|
+
ModelMeshAttachment,
|
|
70
|
+
ModelPathAttachment,
|
|
71
|
+
ModelRegionAttachment,
|
|
72
|
+
ModelSequence,
|
|
73
|
+
ModelSkin,
|
|
74
|
+
ModelSlot,
|
|
75
|
+
ModelTimelines,
|
|
76
|
+
ModelVertexAttachment,
|
|
77
|
+
ModelVertices,
|
|
78
|
+
SkinTable,
|
|
79
|
+
SkinTableEntry,
|
|
80
|
+
} from './model.ts';
|
|
81
|
+
import { EVERY_GLOBAL_PHYSICS } from './motion.ts';
|
|
82
|
+
import { RIG_SKIN_CONSTRAINT_KEYS, type RigSkinConstraintKey } from './rig.ts';
|
|
83
|
+
import type {
|
|
84
|
+
SpineAnimation,
|
|
85
|
+
SpineAttachment,
|
|
86
|
+
SpineBone,
|
|
87
|
+
SpineBoundingBoxAttachment,
|
|
88
|
+
SpineClippingAttachment,
|
|
89
|
+
SpineConstraint,
|
|
90
|
+
SpineEvent,
|
|
91
|
+
SpineLinkedMeshAttachment,
|
|
92
|
+
SpineMeshAttachment,
|
|
93
|
+
SpinePathAttachment,
|
|
94
|
+
SpineRegionAttachment,
|
|
95
|
+
SpineSequence,
|
|
96
|
+
SpineSkeletonJson,
|
|
97
|
+
SpineSkin,
|
|
98
|
+
SpineSlot,
|
|
99
|
+
SpineTimelineKey,
|
|
100
|
+
} from './types.ts';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* One `SpineBone` per model bone, in the model's order — which is the order a
|
|
104
|
+
* weighted vertex's bone index counts in, so it is never re-sorted.
|
|
105
|
+
*
|
|
106
|
+
* Keys, each only when the model bone carries it: `name, parent, length, x, y,
|
|
107
|
+
* rotation, scaleX, scaleY, shearX, shearY, inherit, skin, color, icon`.
|
|
108
|
+
*/
|
|
109
|
+
export function emitBones(bones: readonly ModelBone[]): SpineBone[] {
|
|
110
|
+
return bones.map((model) => {
|
|
111
|
+
const bone: SpineBone = { name: model.name };
|
|
112
|
+
if (model.parent !== undefined) bone.parent = model.parent;
|
|
113
|
+
if (model.length !== undefined) bone.length = model.length;
|
|
114
|
+
if (model.x !== undefined) bone.x = model.x;
|
|
115
|
+
if (model.y !== undefined) bone.y = model.y;
|
|
116
|
+
if (model.rotation !== undefined) bone.rotation = model.rotation;
|
|
117
|
+
if (model.scaleX !== undefined) bone.scaleX = model.scaleX;
|
|
118
|
+
if (model.scaleY !== undefined) bone.scaleY = model.scaleY;
|
|
119
|
+
if (model.shearX !== undefined) bone.shearX = model.shearX;
|
|
120
|
+
if (model.shearY !== undefined) bone.shearY = model.shearY;
|
|
121
|
+
if (model.inheritMode !== undefined) bone.inherit = model.inheritMode;
|
|
122
|
+
if (model.skinRequired !== undefined) bone.skin = model.skinRequired;
|
|
123
|
+
if (model.editor?.color !== undefined) bone.color = model.editor.color;
|
|
124
|
+
if (model.editor?.icon !== undefined) bone.icon = model.editor.icon;
|
|
125
|
+
return bone;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The bone index a weighted run writes for each name: its position in the model's
|
|
131
|
+
* bone array, the array `emitBones` writes in the same order. A name that is not
|
|
132
|
+
* a bone is refused by name — the builders resolve every binding before it
|
|
133
|
+
* reaches the model, so this is the emitter declining to write an index for
|
|
134
|
+
* something it cannot find rather than a check an author can reach.
|
|
135
|
+
*/
|
|
136
|
+
export function boneIndexOf(bones: readonly ModelBone[]): (bone: string) => number {
|
|
137
|
+
const index = new Map(bones.map((bone, i) => [bone.name, i] as const));
|
|
138
|
+
return (bone) => {
|
|
139
|
+
const at = index.get(bone);
|
|
140
|
+
if (at === undefined) throw new CompileError(`internal: a weighted vertex binds bone "${bone}", which is not in the model's bone list`);
|
|
141
|
+
return at;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A vertex attachment's `vertices` array in Spine 4.3's encoding.
|
|
147
|
+
*
|
|
148
|
+
* Unweighted: `xy` verbatim. Weighted: per vertex its binding count, then per
|
|
149
|
+
* binding `indexOf(bone), x, y, weight` — every number but the index copied from
|
|
150
|
+
* the model untouched, since the model already holds the float32 values the
|
|
151
|
+
* file carries. The reader tells the two apart by length alone (`readVertices`),
|
|
152
|
+
* which is why the model says `weighted` out loud and this is the one place it
|
|
153
|
+
* becomes a length.
|
|
154
|
+
*/
|
|
155
|
+
export function emitVertices(vertices: ModelVertices, indexOf: (bone: string) => number): number[] {
|
|
156
|
+
if (!vertices.weighted) return vertices.xy.slice();
|
|
157
|
+
const out: number[] = [];
|
|
158
|
+
for (const vertex of vertices.bindings) {
|
|
159
|
+
out.push(vertex.length);
|
|
160
|
+
for (const binding of vertex) out.push(indexOf(binding.bone), binding.x, binding.y, binding.weight);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A mesh, keys in the order every mesh builder in `compile.ts` inserted them
|
|
167
|
+
* before the model existed — `buildRigMesh`, `buildGeneratedMesh`,
|
|
168
|
+
* `buildGridAttachment`, `buildSegmentsAttachment`, `buildContourAttachment`
|
|
169
|
+
* and the manifest's `buildMesh` all wrote one order, with `withStatedName`
|
|
170
|
+
* putting `name` ahead of everything:
|
|
171
|
+
*
|
|
172
|
+
* `name, type, path, color, uvs, triangles, vertices, hull, edges, width, height, sequence`
|
|
173
|
+
*
|
|
174
|
+
* each optional key only when the record carries it. The mesh row of the key
|
|
175
|
+
* order lists `type` … `height`; `name`, `path`, `color` and `sequence` it does
|
|
176
|
+
* not list, so their positions are this constructor's and are bytes.
|
|
177
|
+
*/
|
|
178
|
+
export function emitMesh(mesh: ModelMeshAttachment, indexOf: (bone: string) => number): SpineMeshAttachment {
|
|
179
|
+
return {
|
|
180
|
+
...(mesh.name !== undefined ? { name: mesh.name } : {}),
|
|
181
|
+
type: 'mesh',
|
|
182
|
+
...(mesh.path !== undefined ? { path: mesh.path } : {}),
|
|
183
|
+
...(mesh.color !== undefined ? { color: mesh.color } : {}),
|
|
184
|
+
uvs: mesh.uvs,
|
|
185
|
+
triangles: mesh.triangles,
|
|
186
|
+
vertices: emitVertices(mesh.vertices, indexOf),
|
|
187
|
+
hull: mesh.hull,
|
|
188
|
+
edges: mesh.edges,
|
|
189
|
+
width: mesh.width,
|
|
190
|
+
height: mesh.height,
|
|
191
|
+
...(mesh.sequence !== undefined ? { sequence: emitSequenceBlock(mesh.sequence) } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A bounding box, keys in `buildRigBoundingBox`'s order after `withStatedName`:
|
|
197
|
+
* `name, type, vertexCount, vertices, color`. The row lists `type`,
|
|
198
|
+
* `vertexCount`, `vertices`; `name` and `color` hold the constructor's places.
|
|
199
|
+
*/
|
|
200
|
+
export function emitBoundingBox(box: ModelBoundingBoxAttachment, indexOf: (bone: string) => number): SpineBoundingBoxAttachment {
|
|
201
|
+
return {
|
|
202
|
+
...(box.name !== undefined ? { name: box.name } : {}),
|
|
203
|
+
type: 'boundingbox',
|
|
204
|
+
vertexCount: box.vertexCount,
|
|
205
|
+
vertices: emitVertices(box.vertices, indexOf),
|
|
206
|
+
...(box.editorColor !== undefined ? { color: box.editorColor } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A clipping polygon, keys in `buildRigClipping`'s order after `withStatedName`:
|
|
212
|
+
* `name, type, end, convex, inverse, vertexCount, vertices, color`. The row
|
|
213
|
+
* lists `type`, `end`, `vertexCount`, `vertices`, `color`; `name`, `convex` and
|
|
214
|
+
* `inverse` hold the constructor's places.
|
|
215
|
+
*/
|
|
216
|
+
export function emitClipping(clip: ModelClippingAttachment, indexOf: (bone: string) => number): SpineClippingAttachment {
|
|
217
|
+
return {
|
|
218
|
+
...(clip.name !== undefined ? { name: clip.name } : {}),
|
|
219
|
+
type: 'clipping',
|
|
220
|
+
...(clip.end !== undefined ? { end: clip.end } : {}),
|
|
221
|
+
...(clip.convex !== undefined ? { convex: clip.convex } : {}),
|
|
222
|
+
...(clip.inverse !== undefined ? { inverse: clip.inverse } : {}),
|
|
223
|
+
vertexCount: clip.vertexCount,
|
|
224
|
+
vertices: emitVertices(clip.vertices, indexOf),
|
|
225
|
+
...(clip.editorColor !== undefined ? { color: clip.editorColor } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A path, keys in `buildRigPath`'s order after `withStatedName`:
|
|
231
|
+
* `name, type, closed, constantSpeed, vertexCount, vertices, lengths, color`.
|
|
232
|
+
* The key order has no row for a path attachment, so every position here is
|
|
233
|
+
* this constructor's.
|
|
234
|
+
*/
|
|
235
|
+
export function emitPath(path: ModelPathAttachment, indexOf: (bone: string) => number): SpinePathAttachment {
|
|
236
|
+
return {
|
|
237
|
+
...(path.name !== undefined ? { name: path.name } : {}),
|
|
238
|
+
type: 'path',
|
|
239
|
+
...(path.closed !== undefined ? { closed: path.closed } : {}),
|
|
240
|
+
...(path.constantSpeed !== undefined ? { constantSpeed: path.constantSpeed } : {}),
|
|
241
|
+
vertexCount: path.vertexCount,
|
|
242
|
+
vertices: emitVertices(path.vertices, indexOf),
|
|
243
|
+
lengths: path.lengths,
|
|
244
|
+
...(path.editorColor !== undefined ? { color: path.editorColor } : {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** One model vertex attachment as its Spine 4.3 object. */
|
|
249
|
+
export function emitVertexAttachment(att: ModelVertexAttachment, indexOf: (bone: string) => number): SpineAttachment {
|
|
250
|
+
switch (att.kind) {
|
|
251
|
+
case 'mesh':
|
|
252
|
+
return emitMesh(att, indexOf);
|
|
253
|
+
case 'boundingbox':
|
|
254
|
+
return emitBoundingBox(att, indexOf);
|
|
255
|
+
case 'clipping':
|
|
256
|
+
return emitClipping(att, indexOf);
|
|
257
|
+
case 'path':
|
|
258
|
+
return emitPath(att, indexOf);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A `sequence` block, keys in `buildSequence`'s order in `compile.ts`:
|
|
264
|
+
* `count, start, digits, setup`, each optional key only when stated. The key
|
|
265
|
+
* order has no row for a sequence, so the order is this one's. A fresh object
|
|
266
|
+
* every time: the parser-default pass removes `start: 1` and `setup: 0` in
|
|
267
|
+
* place, and it must not reach into the model.
|
|
268
|
+
*/
|
|
269
|
+
export function emitSequenceBlock(seq: ModelSequence): SpineSequence {
|
|
270
|
+
const out: SpineSequence = { count: seq.count };
|
|
271
|
+
if (seq.start !== undefined) out.start = seq.start;
|
|
272
|
+
if (seq.digits !== undefined) out.digits = seq.digits;
|
|
273
|
+
if (seq.setup !== undefined) out.setup = seq.setup;
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A region, keys in the order `buildRigRegion` and `placeRegion` inserted them
|
|
279
|
+
* before the model existed (the two wrote one order; `withStatedName` put
|
|
280
|
+
* `name` ahead of everything):
|
|
281
|
+
*
|
|
282
|
+
* `name, width, height, path, x, y, rotation, scaleX, scaleY, color, sequence`
|
|
283
|
+
*
|
|
284
|
+
* The region row lists `x, y, scaleX, scaleY, rotation, width, height`; `name`,
|
|
285
|
+
* `path`, `color` and `sequence` hold this constructor's places.
|
|
286
|
+
*
|
|
287
|
+
* ✂️ `x`, `y` and `rotation` are left out at 0 — `placeRegion`'s inline omission,
|
|
288
|
+
* made here for every region. On a rig spec's region that is the parser-default
|
|
289
|
+
* pass's own decision one step early (the region row reads each at 0, and
|
|
290
|
+
* every number here is `f32`'d, which never leaves a `-0`), so no byte moves.
|
|
291
|
+
*/
|
|
292
|
+
export function emitRegion(region: ModelRegionAttachment): SpineRegionAttachment {
|
|
293
|
+
const out: SpineRegionAttachment = {
|
|
294
|
+
...(region.name !== undefined ? { name: region.name } : {}),
|
|
295
|
+
width: region.width,
|
|
296
|
+
height: region.height,
|
|
297
|
+
};
|
|
298
|
+
if (region.path !== undefined) out.path = region.path;
|
|
299
|
+
if (region.x !== undefined && region.x !== 0) out.x = region.x;
|
|
300
|
+
if (region.y !== undefined && region.y !== 0) out.y = region.y;
|
|
301
|
+
if (region.rotation !== undefined && region.rotation !== 0) out.rotation = region.rotation;
|
|
302
|
+
if (region.scaleX !== undefined) out.scaleX = region.scaleX;
|
|
303
|
+
if (region.scaleY !== undefined) out.scaleY = region.scaleY;
|
|
304
|
+
if (region.color !== undefined) out.color = region.color;
|
|
305
|
+
if (region.sequence !== undefined) out.sequence = emitSequenceBlock(region.sequence);
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* A linked mesh, keys in `buildRigLinkedMesh`'s order after `withStatedName`:
|
|
311
|
+
*
|
|
312
|
+
* `name, type, source, width, height, path, slot, skin, timelines, color, sequence`
|
|
313
|
+
*
|
|
314
|
+
* The key order has no row for a linked mesh, so every position is this one's.
|
|
315
|
+
*
|
|
316
|
+
* ✂️ The link is written only where it differs from the parser's fallback
|
|
317
|
+
* (`SkeletonJson` reads `skin` as the default skin, `slot` as the attachment's
|
|
318
|
+
* own, `timelines` as true): `slot` when it is not `ownSlot`, `skin` when it is
|
|
319
|
+
* not `default`, `timelines` only when false.
|
|
320
|
+
*/
|
|
321
|
+
export function emitLinkedMesh(link: ModelLinkedMeshAttachment, ownSlot: string): SpineLinkedMeshAttachment {
|
|
322
|
+
const out: SpineLinkedMeshAttachment = {
|
|
323
|
+
...(link.name !== undefined ? { name: link.name } : {}),
|
|
324
|
+
type: 'linkedmesh',
|
|
325
|
+
source: link.source,
|
|
326
|
+
width: link.width,
|
|
327
|
+
height: link.height,
|
|
328
|
+
};
|
|
329
|
+
if (link.path !== undefined) out.path = link.path;
|
|
330
|
+
if (link.slot !== ownSlot) out.slot = link.slot;
|
|
331
|
+
if (link.skin !== 'default') out.skin = link.skin;
|
|
332
|
+
if (!link.timelines) out.timelines = false;
|
|
333
|
+
if (link.color !== undefined) out.color = link.color;
|
|
334
|
+
if (link.sequence !== undefined) out.sequence = emitSequenceBlock(link.sequence);
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** One skin-table record as its Spine 4.3 object; `ownSlot` is the slot it is filed under. */
|
|
339
|
+
export function emitAttachment(att: SkinTableEntry, ownSlot: string, indexOf: (bone: string) => number): SpineAttachment {
|
|
340
|
+
switch (att.kind) {
|
|
341
|
+
case 'region':
|
|
342
|
+
return emitRegion(att);
|
|
343
|
+
case 'linkedmesh':
|
|
344
|
+
return emitLinkedMesh(att, ownSlot);
|
|
345
|
+
default:
|
|
346
|
+
return emitVertexAttachment(att, indexOf);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* One skin's slot -> placeholder table as the skeleton file carries it: every
|
|
352
|
+
* record emitted, slots and placeholders in the table's own order. The
|
|
353
|
+
* editor's slot-key order is applied by `emitSkins`.
|
|
354
|
+
*/
|
|
355
|
+
export function emitSkinAttachments(table: SkinTable, indexOf: (bone: string) => number): Record<string, Record<string, SpineAttachment>> {
|
|
356
|
+
const out: Record<string, Record<string, SpineAttachment>> = {};
|
|
357
|
+
for (const [slot, perSlot] of Object.entries(table)) {
|
|
358
|
+
const emitted: Record<string, SpineAttachment> = {};
|
|
359
|
+
for (const [placeholder, entry] of Object.entries(perSlot)) emitted[placeholder] = emitAttachment(entry, slot, indexOf);
|
|
360
|
+
out[slot] = emitted;
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The editor's orders for the two name-keyed collections a skin array carries:
|
|
367
|
+
* the skins themselves (`default` pinned first, the rest by the editor's
|
|
368
|
+
* comparator, refusing a pair it could key differently) and a skin's slot keys.
|
|
369
|
+
* `compile.ts`'s `editorSkinOrder` and `editorSlotKeyOrder` — see the header's
|
|
370
|
+
* 🔸 for why they are passed rather than defined here.
|
|
371
|
+
*/
|
|
372
|
+
export interface EditorOrder {
|
|
373
|
+
skins: <T extends { name: string }>(skins: readonly T[]) => T[];
|
|
374
|
+
slotKeys: <T>(attachments: Record<string, T>) => Record<string, T>;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The `skins` array, in the assembly's order before the model existed: each
|
|
379
|
+
* entry built in the model's (the spec's) order, then `order.skins` over the
|
|
380
|
+
* whole — `default` first, the rest in the editor's order. An entry's keys:
|
|
381
|
+
*
|
|
382
|
+
* `name, bones, ik, transform, path, physics, slider, attachments`
|
|
383
|
+
*
|
|
384
|
+
* — `readSkeletonData`'s own order; each member list only when non-empty, so a
|
|
385
|
+
* skin that activates nothing is the two-key entry it always was. `attachments`
|
|
386
|
+
* is `emitSkinAttachments` keyed by `order.slotKeys`.
|
|
387
|
+
*/
|
|
388
|
+
export function emitSkins(skins: readonly ModelSkin[], indexOf: (bone: string) => number, order: EditorOrder): SpineSkin[] {
|
|
389
|
+
return order.skins(
|
|
390
|
+
skins.map((skin): SpineSkin => {
|
|
391
|
+
const members: Partial<Record<'bones' | RigSkinConstraintKey, string[]>> = {};
|
|
392
|
+
if (skin.bones.length) members.bones = skin.bones;
|
|
393
|
+
for (const key of RIG_SKIN_CONSTRAINT_KEYS) if (skin.constraints[key].length) members[key] = skin.constraints[key];
|
|
394
|
+
return { name: skin.name, ...members, attachments: order.slotKeys(emitSkinAttachments(skin.attachments, indexOf)) };
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The `slots` array, in the model's order — the draw order, which a draw-order
|
|
401
|
+
* key's offsets count in, so it is never re-sorted. Keys in the slot
|
|
402
|
+
* constructor's order: `name, bone, attachment, color, dark, blend`, each only
|
|
403
|
+
* when the model carries it.
|
|
404
|
+
*
|
|
405
|
+
* ✂️ `attachment` is left out when the setup is `null`: `SkeletonJson` reads
|
|
406
|
+
* the key with a `null` default, so a slot with no `attachment` shows nothing,
|
|
407
|
+
* which is the shape the editor exports (34 of the 52 slots of `spineboy-pro`).
|
|
408
|
+
*/
|
|
409
|
+
export function emitSlots(slots: readonly ModelSlot[]): SpineSlot[] {
|
|
410
|
+
return slots.map((model) => {
|
|
411
|
+
const slot: SpineSlot = { name: model.name, bone: model.bone };
|
|
412
|
+
if (model.setup !== null) slot.attachment = model.setup;
|
|
413
|
+
if (model.color !== undefined) slot.color = model.color;
|
|
414
|
+
if (model.dark !== undefined) slot.dark = model.dark;
|
|
415
|
+
if (model.blend !== undefined) slot.blend = model.blend;
|
|
416
|
+
return slot;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The `referenceScale` a header stating none is read as, and a physics
|
|
422
|
+
* constraint's parameters with their parser defaults, in the physics table's
|
|
423
|
+
* order. Defined in `src/keyorder.ts` beside the parser's other defaults since
|
|
424
|
+
* issue #1026 — the compiler reads both, and the entry that compiles a model
|
|
425
|
+
* without the emitter reads nothing from this module — and re-exported here
|
|
426
|
+
* under the names they always had.
|
|
427
|
+
*/
|
|
428
|
+
export { PHYSICS_PARAMS, UNSTATED_REFERENCE_SCALE };
|
|
429
|
+
|
|
430
|
+
/** The five components a physics constraint drives; each reads 0 in its absence. */
|
|
431
|
+
const PHYSICS_COMPONENT_FIELDS = ['x', 'y', 'rotate', 'scaleX', 'shearX'] as const;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Each constraint's fields after `name, type`, in the order its builder
|
|
435
|
+
* inserted them before the model existed — read off `buildRigConstraint`'s
|
|
436
|
+
* branch per kind (its `copy` lists and assignments, in call order) and, for a
|
|
437
|
+
* physics constraint the motion spec's table declares, off that loop:
|
|
438
|
+
*
|
|
439
|
+
* - `ik`: `bones, target, scaleY, mix, softness, bendPositive, compress, stretch, skin`
|
|
440
|
+
* - `transform`: `bones, source, properties, localSource, localTarget, additive, clamp,
|
|
441
|
+
* rotation, x, y, scaleX, scaleY, shearY, mixRotate, mixX, mixY, mixScaleX, mixScaleY,
|
|
442
|
+
* mixShearY, skin`
|
|
443
|
+
* - `path`: `bones, slot, positionMode, spacingMode, rotateMode, rotation, position, spacing,
|
|
444
|
+
* mixRotate, mixX, mixY, skin`
|
|
445
|
+
* - `slider`: `animation, additive, loop, mix, bone, property, from, to, scale, max, local,
|
|
446
|
+
* time, skin` — `bone` … `local` and `time` are exclusive (the bone-less form), so one
|
|
447
|
+
* list holds both branches
|
|
448
|
+
* - `physics` (rig): `bone, scaleY, x, y, rotate, scaleX, shearX, limit, fps, inertia,
|
|
449
|
+
* strength, damping, mass, wind, gravity, mix, inertiaGlobal, strengthGlobal,
|
|
450
|
+
* dampingGlobal, massGlobal, windGlobal, gravityGlobal, mixGlobal, skin`
|
|
451
|
+
* - `physics` (motion table): `bone, x, y, rotate, scaleX, shearX`, then `PHYSICS_PARAMS`'
|
|
452
|
+
* order — `inertia` … `mix, fps, limit`
|
|
453
|
+
*
|
|
454
|
+
* The rows of the key-order table list `type, name` and a few fields of ik,
|
|
455
|
+
* transform and physics; path and slider have no row, so there every position
|
|
456
|
+
* here is a byte.
|
|
457
|
+
*/
|
|
458
|
+
const CONSTRAINT_FIELD_ORDER: Readonly<Record<ModelConstraintKind | 'physics table', readonly string[]>> = {
|
|
459
|
+
ik: ['bones', 'target', 'scaleY', 'mix', 'softness', 'bendPositive', 'compress', 'stretch', 'skin'],
|
|
460
|
+
transform: [
|
|
461
|
+
'bones', 'source', 'properties', 'localSource', 'localTarget', 'additive', 'clamp', 'rotation', 'x', 'y', 'scaleX',
|
|
462
|
+
'scaleY', 'shearY', 'mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY', 'skin',
|
|
463
|
+
],
|
|
464
|
+
path: [
|
|
465
|
+
'bones', 'slot', 'positionMode', 'spacingMode', 'rotateMode', 'rotation', 'position', 'spacing', 'mixRotate', 'mixX',
|
|
466
|
+
'mixY', 'skin',
|
|
467
|
+
],
|
|
468
|
+
slider: ['animation', 'additive', 'loop', 'mix', 'bone', 'property', 'from', 'to', 'scale', 'max', 'local', 'time', 'skin'],
|
|
469
|
+
physics: [
|
|
470
|
+
'bone', 'scaleY', 'x', 'y', 'rotate', 'scaleX', 'shearX', 'limit', 'fps', 'inertia', 'strength', 'damping', 'mass', 'wind',
|
|
471
|
+
'gravity', 'mix', 'inertiaGlobal', 'strengthGlobal', 'dampingGlobal', 'massGlobal', 'windGlobal', 'gravityGlobal',
|
|
472
|
+
'mixGlobal', 'skin',
|
|
473
|
+
],
|
|
474
|
+
'physics table': ['bone', ...PHYSICS_COMPONENT_FIELDS, ...PHYSICS_PARAMS.map(([param]) => param)],
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* One constraint as the `constraints[]` entry 4.3 reads: `name, type`, then
|
|
479
|
+
* the kind's fields in `CONSTRAINT_FIELD_ORDER`, each only when the model
|
|
480
|
+
* carries it. A field the list has no place for is refused by name rather than
|
|
481
|
+
* dropped — the model's fields are the builders', so this is the emitter
|
|
482
|
+
* declining to guess a position, not a check an author can reach.
|
|
483
|
+
*
|
|
484
|
+
* ✂️ On a physics constraint a component at 0 and a parameter at its
|
|
485
|
+
* `PHYSICS_PARAMS` default are left out — the table loop's inline omission,
|
|
486
|
+
* made here for both routes. On the rig's route it is the parser-default
|
|
487
|
+
* pass's own decision one step early (`PARSER_DEFAULTS['physics constraint']`
|
|
488
|
+
* holds the same values, and `f32` never leaves a `-0`), so no byte moves.
|
|
489
|
+
*/
|
|
490
|
+
export function emitConstraint(model: ModelConstraint): SpineConstraint {
|
|
491
|
+
const order = CONSTRAINT_FIELD_ORDER[model.kind === 'physics' && model.declaredIn === 'motion' ? 'physics table' : model.kind];
|
|
492
|
+
const out: SpineConstraint = { name: model.name, type: model.kind };
|
|
493
|
+
const stray = Object.keys(model).filter((key) => key !== 'kind' && key !== 'name' && key !== 'declaredIn' && !order.includes(key));
|
|
494
|
+
if (stray.length > 0) {
|
|
495
|
+
throw new CompileError(`internal: ${model.kind} constraint "${model.name}" carries ${stray.join(', ')}, which the emitter has no place for`);
|
|
496
|
+
}
|
|
497
|
+
for (const field of order) {
|
|
498
|
+
const value = model[field];
|
|
499
|
+
if (value === undefined) continue;
|
|
500
|
+
if (model.kind === 'physics') {
|
|
501
|
+
if ((PHYSICS_COMPONENT_FIELDS as readonly string[]).includes(field) && value === 0) continue;
|
|
502
|
+
if (PHYSICS_PARAMS.some(([param, dflt]) => param === field && value === dflt)) continue;
|
|
503
|
+
}
|
|
504
|
+
out[field] = value;
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** The `constraints[]` array, in the model's order: the rig's in declaration order, then the physics table's. */
|
|
510
|
+
export function emitConstraints(constraints: readonly ModelConstraint[]): SpineConstraint[] {
|
|
511
|
+
return constraints.map(emitConstraint);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The `events` map, in the model's (the rig's declared) order, each definition's
|
|
516
|
+
* keys in the order the assembly wrote them: `int, float, string, audio,
|
|
517
|
+
* volume, balance`, each only when declared. The key order has no row for an
|
|
518
|
+
* event, so every position is this one's. The caller writes the map only when
|
|
519
|
+
* it is non-empty.
|
|
520
|
+
*/
|
|
521
|
+
export function emitEvents(events: ReadonlyMap<string, ModelEvent>): Record<string, SpineEvent> {
|
|
522
|
+
const out: Record<string, SpineEvent> = {};
|
|
523
|
+
for (const [name, def] of events) {
|
|
524
|
+
const entry: SpineEvent = {};
|
|
525
|
+
if (def.int !== undefined) entry.int = def.int;
|
|
526
|
+
if (def.float !== undefined) entry.float = def.float;
|
|
527
|
+
if (def.string !== undefined) entry.string = def.string;
|
|
528
|
+
if (def.audio !== undefined) entry.audio = def.audio;
|
|
529
|
+
if (def.volume !== undefined) entry.volume = def.volume;
|
|
530
|
+
if (def.balance !== undefined) entry.balance = def.balance;
|
|
531
|
+
out[name] = entry;
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
// animations (issue #921, cut 1e)
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The order `animations` is keyed in, over the model's names — `compile.ts`'s
|
|
542
|
+
* `editorAnimationOrder`, which refuses by name every pair the editor could key
|
|
543
|
+
* two ways (`refuseNamesTheEditorCouldKeyDifferently`). Passed in, for the
|
|
544
|
+
* reason the header's 🔸 gives.
|
|
545
|
+
*/
|
|
546
|
+
export type AnimationOrder = (names: readonly string[]) => string[];
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* One key as the file carries it: a fresh object with the model key's fields,
|
|
550
|
+
* in the model key's order — the order its compiler inserted them, which is a
|
|
551
|
+
* byte wherever the key-order table has no row (the `scalex`, `inherit`, `rgb`,
|
|
552
|
+
* `sequence`, path, slider and several physics keys) or lists only some of the
|
|
553
|
+
* fields. Arrays inside (`curve`, `vertices`) are shared: neither pass reaches
|
|
554
|
+
* into an array of numbers.
|
|
555
|
+
*/
|
|
556
|
+
function emitKey(key: ModelKey): SpineTimelineKey {
|
|
557
|
+
return { ...key };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function emitKeys(keys: readonly ModelKey[]): SpineTimelineKey[] {
|
|
561
|
+
return keys.map(emitKey);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** One target's timelines, in the model's order. */
|
|
565
|
+
function emitTimelines(timelines: ModelTimelines): Record<string, SpineTimelineKey[]> {
|
|
566
|
+
const out: Record<string, SpineTimelineKey[]> = {};
|
|
567
|
+
for (const [name, keys] of timelines) out[name] = emitKeys(keys);
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** target -> timelines, in the model's order; `rename` spells a target the format names otherwise. */
|
|
572
|
+
function emitTargets(
|
|
573
|
+
targets: ReadonlyMap<string, ModelTimelines>,
|
|
574
|
+
rename: (target: string) => string = (target) => target,
|
|
575
|
+
): Record<string, Record<string, SpineTimelineKey[]>> {
|
|
576
|
+
const out: Record<string, Record<string, SpineTimelineKey[]>> = {};
|
|
577
|
+
for (const [target, timelines] of targets) out[rename(target)] = emitTimelines(timelines);
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* The three ik-key flags, and the value `SkeletonJson` reads for each on a key
|
|
583
|
+
* that omits it (`PARSER_DEFAULTS['ik key']`, which the selftest holds to the
|
|
584
|
+
* parser row by row).
|
|
585
|
+
*/
|
|
586
|
+
const IK_KEY_FLAGS: ReadonlyArray<readonly [string, unknown]> = (['bendPositive', 'compress', 'stretch'] as const).map(
|
|
587
|
+
(flag) => [flag, PARSER_DEFAULTS['ik key'][flag]] as const,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* An ik key. The model holds the flags IN EFFECT on every key (`ModelKey`); the
|
|
592
|
+
* file carries a flag only where it is not the parser's per-key default, which
|
|
593
|
+
* is where the restatement of issue #273 used to stop — the constraint's flag
|
|
594
|
+
* was carried onto a key only where it differed from that default. A key that
|
|
595
|
+
* STATED a flag at its default was written and then left out by the
|
|
596
|
+
* parser-default pass (its `ik key` row holds the same three values), so
|
|
597
|
+
* leaving it out here moves no byte. Every other field is copied in the key's
|
|
598
|
+
* own order.
|
|
599
|
+
*/
|
|
600
|
+
function emitIkKey(key: ModelKey): SpineTimelineKey {
|
|
601
|
+
const out: SpineTimelineKey = {};
|
|
602
|
+
for (const [field, value] of Object.entries(key)) {
|
|
603
|
+
if (IK_KEY_FLAGS.some(([flag, dflt]) => flag === field && value === dflt)) continue;
|
|
604
|
+
out[field] = value;
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** One draw-order move as the model holds it. */
|
|
610
|
+
interface DrawOrderMove {
|
|
611
|
+
slot: string;
|
|
612
|
+
offset: number;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function isDrawOrderMoves(value: unknown): value is DrawOrderMove[] {
|
|
616
|
+
return (
|
|
617
|
+
Array.isArray(value) &&
|
|
618
|
+
value.every(
|
|
619
|
+
(move: unknown) =>
|
|
620
|
+
typeof move === 'object' &&
|
|
621
|
+
move !== null &&
|
|
622
|
+
typeof (move as { slot?: unknown }).slot === 'string' &&
|
|
623
|
+
typeof (move as { offset?: unknown }).offset === 'number',
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* A draw-order key. Its `offsets` are written in SETUP order — each move's
|
|
630
|
+
* slot's index in the model's slot array, which is the emitted draw order:
|
|
631
|
+
* `readDrawOrder` walks the offsets with a forward-only cursor over the setup
|
|
632
|
+
* order, so an entry whose slot sits before the previous entry's never lets the
|
|
633
|
+
* cursor meet it and the loader runs away (`compileDrawOrder`'s first refusal
|
|
634
|
+
* note). The model holds the moves as stated; the sort is the format's
|
|
635
|
+
* requirement and so the emitter's. Each move is a fresh `slot, offset` object.
|
|
636
|
+
* A slot the model does not have is refused by name — the compiler resolved
|
|
637
|
+
* every move against the same slots, so this is the emitter declining to guess
|
|
638
|
+
* a position, not a check an author can reach.
|
|
639
|
+
*/
|
|
640
|
+
function emitDrawOrderKey(key: ModelKey, slotIndex: ReadonlyMap<string, number>): SpineTimelineKey {
|
|
641
|
+
const out = emitKey(key);
|
|
642
|
+
if (key.offsets === undefined) return out;
|
|
643
|
+
if (!isDrawOrderMoves(key.offsets)) {
|
|
644
|
+
throw new CompileError(`internal: a draw-order key at t=${key.time} carries offsets that are not slot/offset moves`);
|
|
645
|
+
}
|
|
646
|
+
const indexOf = (slot: string): number => {
|
|
647
|
+
const at = slotIndex.get(slot);
|
|
648
|
+
if (at === undefined) throw new CompileError(`internal: a draw-order key at t=${key.time} moves slot "${slot}", which is not in the model's slot list`);
|
|
649
|
+
return at;
|
|
650
|
+
};
|
|
651
|
+
out.offsets = key.offsets
|
|
652
|
+
.map((move) => ({ at: indexOf(move.slot), move: { slot: move.slot, offset: move.offset } }))
|
|
653
|
+
.sort((a, b) => a.at - b.at)
|
|
654
|
+
.map(({ move }) => move);
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** An attachment's timelines: `deform` then `sequence`, each only when the model holds it. */
|
|
659
|
+
function emitAttachmentTimelines(timelines: ModelAttachmentTimelines): Record<string, SpineTimelineKey[]> {
|
|
660
|
+
const out: Record<string, SpineTimelineKey[]> = {};
|
|
661
|
+
if (timelines.deform !== undefined) out.deform = emitKeys(timelines.deform);
|
|
662
|
+
if (timelines.sequence !== undefined) out.sequence = emitKeys(timelines.sequence);
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* One animation as Spine 4.3's `animations.<name>` object.
|
|
668
|
+
*
|
|
669
|
+
* The groups in `readAnimation`'s own reading order —
|
|
670
|
+
*
|
|
671
|
+
* `slots, bones, ik, transform, path, physics, slider, attachments, drawOrder, events`
|
|
672
|
+
*
|
|
673
|
+
* — each only when the model holds something for it, so an animation that keys
|
|
674
|
+
* one kind of thing is an object of one group, as it always was. (The
|
|
675
|
+
* `animation` row of the key-order table lists eight of the ten; `path` and
|
|
676
|
+
* `slider` hold the positions written here.)
|
|
677
|
+
*
|
|
678
|
+
* ✂️ Spellings and omissions that are Spine's: the physics timeline that names
|
|
679
|
+
* no constraint (the model's `EVERY_GLOBAL_PHYSICS` target) is written under the
|
|
680
|
+
* empty name, which is what `readAnimation` resolves as "every global
|
|
681
|
+
* constraint", in the position the model holds it; an ik key's flags at their
|
|
682
|
+
* per-key default are left out (`emitIkKey`); draw-order moves are sorted into
|
|
683
|
+
* setup order (`emitDrawOrderKey`). Every key is a fresh object.
|
|
684
|
+
*/
|
|
685
|
+
export function emitAnimation(animation: CompiledAnimation, slotIndex: ReadonlyMap<string, number>): SpineAnimation {
|
|
686
|
+
const out: SpineAnimation = {};
|
|
687
|
+
if (animation.slots.size) out.slots = emitTargets(animation.slots);
|
|
688
|
+
if (animation.bones.size) out.bones = emitTargets(animation.bones);
|
|
689
|
+
const { ik, transform, path, physics, slider } = animation.constraints;
|
|
690
|
+
if (ik.size) {
|
|
691
|
+
const byConstraint: Record<string, SpineTimelineKey[]> = {};
|
|
692
|
+
for (const [name, keys] of ik) byConstraint[name] = keys.map(emitIkKey);
|
|
693
|
+
out.ik = byConstraint;
|
|
694
|
+
}
|
|
695
|
+
if (transform.size) {
|
|
696
|
+
const byConstraint: Record<string, SpineTimelineKey[]> = {};
|
|
697
|
+
for (const [name, keys] of transform) byConstraint[name] = emitKeys(keys);
|
|
698
|
+
out.transform = byConstraint;
|
|
699
|
+
}
|
|
700
|
+
if (path.size) out.path = emitTargets(path);
|
|
701
|
+
if (physics.size) out.physics = emitTargets(physics, (target) => (target === EVERY_GLOBAL_PHYSICS ? '' : target));
|
|
702
|
+
if (slider.size) out.slider = emitTargets(slider);
|
|
703
|
+
if (animation.attachments.size) {
|
|
704
|
+
const bySkin: NonNullable<SpineAnimation['attachments']> = {};
|
|
705
|
+
for (const [skin, bySlot] of animation.attachments) {
|
|
706
|
+
const slots: Record<string, Record<string, Record<string, SpineTimelineKey[]>>> = {};
|
|
707
|
+
for (const [slot, byAttachment] of bySlot) {
|
|
708
|
+
const attachments: Record<string, Record<string, SpineTimelineKey[]>> = {};
|
|
709
|
+
for (const [attachment, timelines] of byAttachment) attachments[attachment] = emitAttachmentTimelines(timelines);
|
|
710
|
+
slots[slot] = attachments;
|
|
711
|
+
}
|
|
712
|
+
bySkin[skin] = slots;
|
|
713
|
+
}
|
|
714
|
+
out.attachments = bySkin;
|
|
715
|
+
}
|
|
716
|
+
if (animation.drawOrder.length) out.drawOrder = animation.drawOrder.map((key) => emitDrawOrderKey(key, slotIndex));
|
|
717
|
+
if (animation.events.length) out.events = emitKeys(animation.events);
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* The `animations` object: the model's names keyed in `order`'s order — the
|
|
723
|
+
* editor's, which raises the refusal for a pair it could key two ways before
|
|
724
|
+
* anything is written — each written by `emitAnimation`. `slots` is the model's
|
|
725
|
+
* slot array, the draw order a draw-order move's index counts in.
|
|
726
|
+
*/
|
|
727
|
+
export function emitAnimations(
|
|
728
|
+
animations: ReadonlyMap<string, CompiledAnimation>,
|
|
729
|
+
slots: readonly ModelSlot[],
|
|
730
|
+
order: AnimationOrder,
|
|
731
|
+
): Record<string, SpineAnimation> {
|
|
732
|
+
const names = order([...animations.keys()]);
|
|
733
|
+
const slotIndex = new Map(slots.map((slot, i) => [slot.name, i] as const));
|
|
734
|
+
const out: Record<string, SpineAnimation> = {};
|
|
735
|
+
for (const name of names) {
|
|
736
|
+
const animation = animations.get(name);
|
|
737
|
+
if (animation === undefined) throw new CompileError(`internal: the animation order named "${name}", which is not in the model`);
|
|
738
|
+
out[name] = emitAnimation(animation, slotIndex);
|
|
739
|
+
}
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// the skeleton: the emitter's one entry (issue #922, cut 1f)
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* What the emitter adds that the model does not hold: the skeleton header.
|
|
749
|
+
* Each field is the assembly's, passed by name — none is a value a posing core
|
|
750
|
+
* reads off the rig.
|
|
751
|
+
*
|
|
752
|
+
* - `spine` — the spine-core line the file is written for (`SPINE_VERSION`
|
|
753
|
+
* in `compile.ts`).
|
|
754
|
+
* - `fps`, `images`, `audio` — the rig spec's header bookkeeping as
|
|
755
|
+
* stated, `images` spelled relative to `--out` (`skeletonImagesPath`);
|
|
756
|
+
* each only when present.
|
|
757
|
+
*
|
|
758
|
+
* - `bounds` — the setup-pose bounding box, `x`, `y`, `width`, `height`,
|
|
759
|
+
* which is what the format says the header's four are (issue #907):
|
|
760
|
+
* computed by `compile` with rigc's core over the model
|
|
761
|
+
* (`headerBoundsOf`), each on the 1e-6 grid at float32 (`headerBoxNumber`), or `null` for no box — a rig that
|
|
762
|
+
* declares no stage (issue #578), a setup pose that draws nothing, a
|
|
763
|
+
* region with no atlas rectangle, or a setup pose the core leaves out
|
|
764
|
+
* (that function's header). Four fields or none.
|
|
765
|
+
*
|
|
766
|
+
* `referenceScale` is not here: wind and gravity act over it, so a posing
|
|
767
|
+
* core reads it, and the model holds it (`CompiledModel.referenceScale`,
|
|
768
|
+
* issue #958). The emitter writes the model's value into the header, where
|
|
769
|
+
* `withoutParserDefaults` drops it at the parser's 100.
|
|
770
|
+
*
|
|
771
|
+
* ⚠️ Nor is the stage (`CompiledModel.stage`, issue #1026) — and since issue
|
|
772
|
+
* #907 the header does not carry it at all. Until then the emitter copied the
|
|
773
|
+
* stage into the four box fields, so a reader that took the header for what
|
|
774
|
+
* the format says it is — the box around the figure at rest, for scaling and
|
|
775
|
+
* layout — got the crop the art was painted in. The stage stays the model's:
|
|
776
|
+
* the document states it, and everything that measures against it reads it
|
|
777
|
+
* there.
|
|
778
|
+
*/
|
|
779
|
+
export interface SkeletonHeader {
|
|
780
|
+
spine: string;
|
|
781
|
+
fps?: number;
|
|
782
|
+
images?: string;
|
|
783
|
+
audio?: string | null;
|
|
784
|
+
bounds: { x: number; y: number; width: number; height: number } | null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** The editor's orders the emitter applies, passed for the reason the header's 🔸 gives. */
|
|
788
|
+
export interface SkeletonOrder extends EditorOrder {
|
|
789
|
+
animations: AnimationOrder;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** The model fields a skeleton is written from. */
|
|
793
|
+
export type SkeletonSource = Pick<CompiledModel, 'referenceScale' | 'bones' | 'slots' | 'skins' | 'constraints' | 'events' | 'animations'>;
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* The Spine 4.3 skeleton of a compiled model: the emitter's one entry, and
|
|
797
|
+
* the one object `compile` assembles — `CompileResult.skeleton` is its value.
|
|
798
|
+
*
|
|
799
|
+
* Top-level keys in the order the assembly wrote them before the model
|
|
800
|
+
* existed, each section by its own emitter:
|
|
801
|
+
*
|
|
802
|
+
* `skeleton, bones, slots, skins, events, animations, constraints`
|
|
803
|
+
*
|
|
804
|
+
* — `events` only when the rig declares one (a conditional spread, so it lands
|
|
805
|
+
* between `skins` and `animations`, where the editor writes it), `constraints`
|
|
806
|
+
* only when the model holds one (assigned after, so it lands last until the
|
|
807
|
+
* key-order pass moves it). The header's keys: `spine, x, y, width, height,
|
|
808
|
+
* fps, referenceScale, images, audio`, the box's four only together.
|
|
809
|
+
*
|
|
810
|
+
* Then, on the finished object and once: `withoutParserDefaults` drops every
|
|
811
|
+
* key at the value the 4.3 parser reads in its absence, and `inEditorKeyOrder`
|
|
812
|
+
* puts every kind's keys in the editor's order (`src/keyorder.ts`). Neither
|
|
813
|
+
* adds, drops or re-values anything the two tables do not list, and neither
|
|
814
|
+
* throws, so a refusal raised here is raised by a section emitter, in the
|
|
815
|
+
* order above.
|
|
816
|
+
*
|
|
817
|
+
* The box (`header.bounds`, issue #907) is written as `compile` computed it,
|
|
818
|
+
* its four fields together or none. `referenceScale` is the model's, written
|
|
819
|
+
* always and dropped by the parser-default pass at 100 — so a rig stating none and a rig stating 100
|
|
820
|
+
* write the same bytes, as they did when the header carried the stated value.
|
|
821
|
+
*
|
|
822
|
+
* What the emitter adds that the model does not hold is `header`
|
|
823
|
+
* (`SkeletonHeader`); what it restates in Spine's words is every section
|
|
824
|
+
* emitter's own doc comment.
|
|
825
|
+
*/
|
|
826
|
+
export function emitSkeleton(model: SkeletonSource, header: SkeletonHeader, order: SkeletonOrder): SpineSkeletonJson {
|
|
827
|
+
const head: SpineSkeletonJson['skeleton'] = { spine: header.spine };
|
|
828
|
+
if (header.bounds !== null) {
|
|
829
|
+
head.x = header.bounds.x;
|
|
830
|
+
head.y = header.bounds.y;
|
|
831
|
+
head.width = header.bounds.width;
|
|
832
|
+
head.height = header.bounds.height;
|
|
833
|
+
}
|
|
834
|
+
if (header.fps !== undefined) head.fps = header.fps;
|
|
835
|
+
head.referenceScale = model.referenceScale;
|
|
836
|
+
if (header.images !== undefined) head.images = header.images;
|
|
837
|
+
if (header.audio !== undefined) head.audio = header.audio;
|
|
838
|
+
|
|
839
|
+
const events = emitEvents(model.events);
|
|
840
|
+
// A weighted vertex's bone index is its bone's position in the array
|
|
841
|
+
// `emitBones` writes; the skin tables bind by name and are encoded here.
|
|
842
|
+
const indexOf = boneIndexOf(model.bones);
|
|
843
|
+
const skeleton: SpineSkeletonJson = {
|
|
844
|
+
skeleton: head,
|
|
845
|
+
bones: emitBones(model.bones),
|
|
846
|
+
slots: emitSlots(model.slots),
|
|
847
|
+
skins: emitSkins(model.skins, indexOf, { skins: order.skins, slotKeys: order.slotKeys }),
|
|
848
|
+
...(Object.keys(events).length ? { events } : {}),
|
|
849
|
+
animations: emitAnimations(model.animations, model.slots, order.animations),
|
|
850
|
+
};
|
|
851
|
+
if (model.constraints.length) skeleton.constraints = emitConstraints(model.constraints);
|
|
852
|
+
inEditorKeyOrder(withoutParserDefaults(skeleton));
|
|
853
|
+
return skeleton;
|
|
854
|
+
}
|