@flighthq/skeleton2d-formats 0.3.0-edge.1458.0c01b63

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.
@@ -0,0 +1,874 @@
1
+ import { createAnimationChannel, createAnimationClip, createAnimationTrack } from '@flighthq/animation/contract';
2
+ import { easeCubicBezier } from '@flighthq/easing/contract';
3
+ import { reportImportDiagnostic } from '@flighthq/importdiagnostics/contract';
4
+ import { createSkeleton2D } from '@flighthq/skeleton2d/contract';
5
+ import { AnimationInterpolationLinear, AnimationInterpolationStep, ImportDiagnosticSeverity, Skeleton2DSlotAnimationPath, MeshAttachment2DKind, RegionAttachment2DKind, Skeleton2DAnimationPath, TransformMode2D, } from '@flighthq/types/contract';
6
+ import { createSpineBinaryReader, isSpineBinaryReaderOverrun, readSpineBinaryBoolean, readSpineBinaryByte, readSpineBinaryFloat, readSpineBinaryInt, readSpineBinaryString, readSpineBinaryUnsignedShort, readSpineBinaryVarint, skipSpineBinaryBytes, } from './spineBinaryReader';
7
+ // Parses Spine's `.skel` BINARY skeleton into the same `Skeleton2DImport` `parseSpineSkeleton` produces from
8
+ // `.json` — the binary sibling of that parser, mirroring how `parseGlb` sits beside `parseGltf`. Tolerant and
9
+ // best-effort on the same terms: `null` is reserved for the "this is not a file we can read" failure
10
+ // (unreadable header, unsupported version), and a readable file with unmodeled pieces yields best-effort
11
+ // data plus `ImportDiagnostic` crumbs. Wire decoding lives in `spineBinaryReader`; this file owns only the
12
+ // RECORD LAYOUT — which field follows which.
13
+ //
14
+ // The binary is stream-positional in a way JSON is not: records have no keys and no lengths, so a reader
15
+ // cannot skip a section it does not model — it can only CONSUME it or stop. That is why constraint records,
16
+ // slot colour timelines, deform timelines, and draw-order/event timelines are all walked field-for-field
17
+ // even though Flight models none of them: each stands between something this importer does want and the
18
+ // next thing after it. The whole file is consumed, and what is not modeled is Skip-crumbed rather than
19
+ // skipped over.
20
+ //
21
+ // VERSION GATE. The layout below is Spine 4.x's and was verified byte-for-byte against a real 4.1.17 export
22
+ // (see the package status). Spine changed record layouts across major versions, so a file outside 4.x is
23
+ // REJECTED with its version in the crumb instead of being decoded by a layout that does not describe it —
24
+ // a wrong layout does not fail loudly, it silently yields plausible garbage.
25
+ export function parseSpineSkeletonBinary(bytes, diagnostics) {
26
+ const reader = createSpineBinaryReader(bytes);
27
+ // The 8-byte hash identifies the export; it carries no skeleton data, so it is stepped over rather than read.
28
+ skipSpineBinaryBytes(reader, SPINE_BINARY_HASH_BYTES);
29
+ const version = readSpineBinaryString(reader);
30
+ if (isSpineBinaryReaderOverrun(reader) || version === null) {
31
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'spine.binary-header-unreadable', 'parseSpineSkeletonBinary', { bytes: bytes.byteLength });
32
+ return null;
33
+ }
34
+ if (!isSupportedSpineBinaryVersion(version)) {
35
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'spine.binary-version-unsupported', 'parseSpineSkeletonBinary', { version });
36
+ return null;
37
+ }
38
+ // Skeleton bounds (x, y, width, height) describe the authoring canvas, which Skeleton2D does not model.
39
+ skipSpineBinaryBytes(reader, SPINE_BINARY_BOUNDS_BYTES);
40
+ // "Nonessential" data is what Spine writes only for editor round-tripping: the authoring frame rate, the
41
+ // images/audio folder paths, and a per-bone editor color. Its PRESENCE changes the record layout below,
42
+ // so the flag has to be carried down even though none of the values are modeled.
43
+ const nonessential = readSpineBinaryBoolean(reader);
44
+ if (nonessential) {
45
+ skipSpineBinaryBytes(reader, SPINE_BINARY_FPS_BYTES);
46
+ readSpineBinaryString(reader); // images path
47
+ readSpineBinaryString(reader); // audio path
48
+ }
49
+ const strings = readSpineBinaryStringTable(reader);
50
+ const bones = parseSpineBinaryBones(reader, nonessential);
51
+ const { attachmentNames, slots } = parseSpineBinarySlots(reader, strings, diagnostics);
52
+ skipSpineBinaryConstraints(reader, diagnostics);
53
+ const skins = parseSpineBinarySkins(reader, strings, nonessential, diagnostics);
54
+ // A slot names its setup attachment BEFORE the skin that defines it has been read, so resolution waits
55
+ // until here — the file orders slots first, but the name only means something once the skins exist.
56
+ const setup = skins.find((skin) => skin.name === SPINE_BINARY_DEFAULT_SKIN_NAME);
57
+ if (setup !== undefined) {
58
+ for (const entry of setup.attachments) {
59
+ if (entry.slotIndex < slots.length && attachmentNames[entry.slotIndex] === entry.name) {
60
+ slots[entry.slotIndex].attachment = entry.attachment;
61
+ }
62
+ }
63
+ }
64
+ skipSpineBinaryEvents(reader, diagnostics);
65
+ const animations = parseSpineBinaryAnimations(reader, strings, setup, diagnostics);
66
+ if (isSpineBinaryReaderOverrun(reader)) {
67
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Recover, 'spine.binary-truncated', 'parseSpineSkeletonBinary', { bones: bones.length, slots: slots.length });
68
+ }
69
+ else {
70
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, 'spine.binary-tail-unparsed', 'parseSpineSkeletonBinary', { bytes: bytes.byteLength - reader.offset });
71
+ }
72
+ const skeleton = createSkeleton2D(bones, slots);
73
+ if (skins.length > 0)
74
+ skeleton.skins = skins;
75
+ return { animations, skeleton };
76
+ }
77
+ // The event DEFINITIONS a file declares (name plus default int/float/string/audio payload). Flight's
78
+ // Skeleton2DImport carries no event vocabulary, so these are consumed and Skip-crumbed — but consumed they
79
+ // must be, since the animation section follows them in a stream with no keys or lengths.
80
+ function skipSpineBinaryEvents(reader, diagnostics) {
81
+ const count = readSpineBinaryVarint(reader);
82
+ for (let i = 0; i < count && !isSpineBinaryReaderOverrun(reader); i++) {
83
+ readSpineBinaryVarint(reader); // name reference
84
+ readSpineBinaryVarint(reader); // int value
85
+ skipSpineBinaryBytes(reader, 4); // float value
86
+ readSpineBinaryString(reader); // string value
87
+ // An audio path is what gates the trailing volume/balance pair, so its presence changes the record width.
88
+ if (readSpineBinaryString(reader) !== null)
89
+ skipSpineBinaryBytes(reader, 8);
90
+ }
91
+ reportSpineBinaryCrumb(diagnostics, count, 'spine.event-unsupported', 'events');
92
+ }
93
+ // Builds one AnimationClip per animation from its BONE timelines, mirroring what `parseSpineSkeleton` does
94
+ // for `.json` — relative deltas over `Skeleton2DAnimationTarget`, composed onto the setup pose by
95
+ // `applyAnimationClipToSkeleton2D`.
96
+ //
97
+ // Every other timeline family (slot attachment/colour, IK, transform, path, deform, draw order, event) is
98
+ // unmodeled, yet each is still walked field-for-field: the animation record is positional, so reaching the
99
+ // NEXT animation requires consuming this one completely. An animation opens with its total timeline count,
100
+ // which this importer does not need but must read.
101
+ function parseSpineBinaryAnimations(reader, strings, setup, diagnostics) {
102
+ const animations = [];
103
+ const count = readSpineBinaryVarint(reader);
104
+ const unmodeled = new Map();
105
+ for (let i = 0; i < count && !isSpineBinaryReaderOverrun(reader); i++) {
106
+ const name = readSpineBinaryString(reader);
107
+ readSpineBinaryVarint(reader); // total timeline count across all families
108
+ const channels = [];
109
+ parseSpineBinarySlotTimelines(reader, channels, strings, setup, unmodeled, diagnostics);
110
+ parseSpineBinaryBoneTimelines(reader, channels, diagnostics);
111
+ skipSpineBinaryConstraintTimelines(reader, unmodeled);
112
+ skipSpineBinaryDeformTimelines(reader, unmodeled);
113
+ skipSpineBinaryDrawOrderTimelines(reader, unmodeled);
114
+ skipSpineBinaryEventTimelines(reader, unmodeled);
115
+ animations.push({ clip: createAnimationClip(channels), name: name ?? '' });
116
+ }
117
+ for (const [kind, tally] of unmodeled) {
118
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, `spine.${kind}-timeline-unsupported`, 'parseSpineSkeletonBinary', { timelines: tally });
119
+ }
120
+ return animations;
121
+ }
122
+ // The bone timelines of one animation. Spine splits each transform group into a combined form and per-axis
123
+ // forms (`translate` vs `translateX`/`translateY`); a per-axis timeline becomes a two-component channel
124
+ // whose OTHER axis holds the identity delta — 0 for translate/shear, 1 for the scale multiplier — so it
125
+ // composes onto the setup pose as "this axis moves, the other does not".
126
+ function parseSpineBinaryBoneTimelines(reader, channels, diagnostics) {
127
+ const bones = readSpineBinaryVarint(reader);
128
+ for (let i = 0; i < bones && !isSpineBinaryReaderOverrun(reader); i++) {
129
+ const boneIndex = readSpineBinaryVarint(reader);
130
+ const timelines = readSpineBinaryVarint(reader);
131
+ for (let j = 0; j < timelines && !isSpineBinaryReaderOverrun(reader); j++) {
132
+ const ordinal = readSpineBinaryByte(reader);
133
+ const frameCount = readSpineBinaryVarint(reader);
134
+ readSpineBinaryVarint(reader); // bezier count — a capacity hint, not needed to read the frames
135
+ const kind = ordinal < SPINE_BINARY_BONE_TIMELINES.length ? SPINE_BINARY_BONE_TIMELINES[ordinal] : null;
136
+ if (kind === null) {
137
+ // The payload width of an unknown timeline is unknowable, so the stream cannot continue past it.
138
+ skipSpineBinaryBytes(reader, reader.view.byteLength + 1);
139
+ return;
140
+ }
141
+ const timeline = readSpineBinaryValueTimeline(reader, frameCount, kind.values);
142
+ channels.push(buildSpineBinaryBoneChannel(timeline, kind, boneIndex, diagnostics));
143
+ }
144
+ }
145
+ }
146
+ // Reads a curve timeline: a leading keyframe, then per gap another keyframe preceded by a curve tag. The
147
+ // tag is per-SEGMENT, and a bezier tag carries four floats per animated value.
148
+ function readSpineBinaryValueTimeline(reader, frameCount, values) {
149
+ const times = [];
150
+ const flat = [];
151
+ const curves = [];
152
+ if (frameCount <= 0)
153
+ return { curves, times, values: flat };
154
+ times.push(readSpineBinaryFloat(reader));
155
+ for (let v = 0; v < values; v++)
156
+ flat.push(readSpineBinaryFloat(reader));
157
+ for (let frame = 0; frame + 1 < frameCount && !isSpineBinaryReaderOverrun(reader); frame++) {
158
+ times.push(readSpineBinaryFloat(reader));
159
+ for (let v = 0; v < values; v++)
160
+ flat.push(readSpineBinaryFloat(reader));
161
+ const tag = readSpineBinaryByte(reader);
162
+ if (tag === SPINE_BINARY_CURVE_BEZIER) {
163
+ const points = [];
164
+ for (let v = 0; v < values * 4; v++)
165
+ points.push(readSpineBinaryFloat(reader));
166
+ curves.push(points);
167
+ }
168
+ else {
169
+ curves.push(null); // linear, or stepped — which Flight expresses per track, not per segment
170
+ }
171
+ }
172
+ return { curves, times, values: flat };
173
+ }
174
+ // Turns one decoded bone timeline into an AnimationChannel. A per-axis timeline is widened to the path's
175
+ // full component count by filling the untouched axis with its identity delta, and any bezier segments
176
+ // become per-interval easings under the same absolute-units rebase the `.json` parser uses.
177
+ function buildSpineBinaryBoneChannel(timeline, kind, boneIndex, diagnostics) {
178
+ const frames = timeline.times.length;
179
+ const components = kind.components;
180
+ const values = new Array(frames * components);
181
+ for (let f = 0; f < frames; f++) {
182
+ for (let c = 0; c < components; c++)
183
+ values[f * components + c] = kind.identity;
184
+ if (kind.axis < 0) {
185
+ for (let c = 0; c < kind.values; c++)
186
+ values[f * components + c] = timeline.values[f * kind.values + c];
187
+ }
188
+ else {
189
+ values[f * components + kind.axis] = timeline.values[f];
190
+ }
191
+ }
192
+ const track = createAnimationTrack({
193
+ components,
194
+ interpolation: AnimationInterpolationLinear,
195
+ segmentEasings: buildSpineBinarySegmentEasings(timeline, kind.values, diagnostics),
196
+ times: timeline.times,
197
+ values,
198
+ });
199
+ return createAnimationChannel(track, { boneIndex, path: kind.path });
200
+ }
201
+ // Rebases each bezier segment's absolute control points onto its own segment, exactly as the `.json` parser
202
+ // does — Spine stores them in time/value units and four numbers per animated value. The first MEANINGFUL
203
+ // value (one that actually changes across the segment) supplies the easing; a divergent sibling is crumbed
204
+ // rather than silently dropped, and x is clamped so the curve stays invertible.
205
+ function buildSpineBinarySegmentEasings(timeline, values, diagnostics) {
206
+ const easings = [];
207
+ let curved = false;
208
+ let divergent = 0;
209
+ for (let i = 0; i < timeline.curves.length; i++) {
210
+ const points = timeline.curves[i];
211
+ const span = timeline.times[i + 1] - timeline.times[i];
212
+ if (points === null || span <= 0) {
213
+ easings.push(null);
214
+ continue;
215
+ }
216
+ // Same rule as the `.json` path: the component with the LARGEST value change supplies the easing,
217
+ // because the rebase divides by that change and a near-constant component is a near-zero denominator.
218
+ let winner = -1;
219
+ let widest = 0;
220
+ for (let v = 0; v < values && (v + 1) * 4 <= points.length; v++) {
221
+ const rise = Math.abs(timeline.values[(i + 1) * values + v] - timeline.values[i * values + v]);
222
+ if (rise > widest) {
223
+ widest = rise;
224
+ winner = v;
225
+ }
226
+ }
227
+ // Winner first, then compare — a single pass would measure earlier components against zeros.
228
+ const rebase = (v) => {
229
+ const from = timeline.values[i * values + v];
230
+ const rise = timeline.values[(i + 1) * values + v] - from;
231
+ if (rise === 0)
232
+ return null;
233
+ return [
234
+ (points[v * 4] - timeline.times[i]) / span,
235
+ (points[v * 4 + 1] - from) / rise,
236
+ (points[v * 4 + 2] - timeline.times[i]) / span,
237
+ (points[v * 4 + 3] - from) / rise,
238
+ ];
239
+ };
240
+ const won = winner < 0 ? null : rebase(winner);
241
+ if (won !== null) {
242
+ for (let v = 0; v < values && (v + 1) * 4 <= points.length; v++) {
243
+ if (v === winner)
244
+ continue;
245
+ const other = rebase(v);
246
+ if (other === null)
247
+ continue;
248
+ for (let k = 0; k < 4; k++) {
249
+ if (Math.abs(other[k] - won[k]) > SPINE_BINARY_CURVE_EPSILON)
250
+ divergent++;
251
+ }
252
+ }
253
+ }
254
+ const chosen = won !== null;
255
+ const x1 = won === null ? 0 : won[0];
256
+ const y1 = won === null ? 0 : won[1];
257
+ const x2 = won === null ? 0 : won[2];
258
+ const y2 = won === null ? 0 : won[3];
259
+ if (!chosen) {
260
+ easings.push(null);
261
+ continue;
262
+ }
263
+ curved = true;
264
+ easings.push(easeCubicBezier(clampSpineBinaryUnit(x1), y1, clampSpineBinaryUnit(x2), y2));
265
+ }
266
+ if (divergent > 0) {
267
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, 'spine.per-component-curve-easing-unsupported', 'parseSpineSkeletonBinary', { segments: divergent });
268
+ }
269
+ return curved ? easings : null;
270
+ }
271
+ // Slot timelines. `attachment` becomes a Step index channel plus a lookup table; `rgba` becomes a
272
+ // four-component 0..1 colour channel. The remaining colour variants (rgb, alpha, and the two dark forms)
273
+ // are consumed and Skip-crumbed — `Slot2D` carries one packed colour and no dark colour.
274
+ //
275
+ // Colour components are stored as single BYTES here while the bezier control points around them are floats
276
+ // already in 0..1 (Spine divides by 255 before recording a curve), so the bytes are normalized on read and
277
+ // the curve rebase then matches the `.json` path exactly.
278
+ function parseSpineBinarySlotTimelines(reader, channels, strings, setup, unmodeled, diagnostics) {
279
+ const slots = readSpineBinaryVarint(reader);
280
+ for (let i = 0; i < slots && !isSpineBinaryReaderOverrun(reader); i++) {
281
+ const slotIndex = readSpineBinaryVarint(reader);
282
+ const timelines = readSpineBinaryVarint(reader);
283
+ for (let j = 0; j < timelines && !isSpineBinaryReaderOverrun(reader); j++) {
284
+ const type = readSpineBinaryByte(reader);
285
+ const frameCount = readSpineBinaryVarint(reader);
286
+ if (type === SPINE_BINARY_SLOT_ATTACHMENT) {
287
+ addSpineBinaryAttachmentChannel(reader, channels, strings, setup, slotIndex, frameCount);
288
+ continue;
289
+ }
290
+ readSpineBinaryVarint(reader); // bezier count
291
+ const count = SPINE_BINARY_SLOT_COLOR_CHANNELS[type] ?? 1;
292
+ if (type !== SPINE_BINARY_SLOT_RGBA) {
293
+ tally(unmodeled, 'slot-color');
294
+ skipSpineBinaryCurveFrames(reader, frameCount, count, count);
295
+ continue;
296
+ }
297
+ const timeline = readSpineBinaryColorTimeline(reader, frameCount, count);
298
+ const track = createAnimationTrack({
299
+ components: count,
300
+ interpolation: AnimationInterpolationLinear,
301
+ segmentEasings: buildSpineBinarySegmentEasings(timeline, count, diagnostics),
302
+ times: timeline.times,
303
+ values: timeline.values,
304
+ });
305
+ channels.push(createAnimationChannel(track, { path: Skeleton2DSlotAnimationPath.Color, slotIndex }));
306
+ }
307
+ }
308
+ }
309
+ // An attachment-swap timeline: per frame a time and a string-table reference, `null` meaning hide. Names
310
+ // resolve against the setup skin ONCE into a deduplicated table, and the track carries only the index.
311
+ function addSpineBinaryAttachmentChannel(reader, channels, strings, setup, slotIndex, frameCount) {
312
+ const attachments = [];
313
+ const indexByName = new Map();
314
+ const times = [];
315
+ const values = [];
316
+ for (let f = 0; f < frameCount && !isSpineBinaryReaderOverrun(reader); f++) {
317
+ times.push(readSpineBinaryFloat(reader));
318
+ const name = readSpineBinaryStringReference(reader, strings);
319
+ if (name === null) {
320
+ values.push(SPINE_BINARY_NO_ATTACHMENT_INDEX);
321
+ continue;
322
+ }
323
+ let index = indexByName.get(name);
324
+ if (index === undefined) {
325
+ const found = setup?.attachments.find((entry) => entry.slotIndex === slotIndex && entry.name === name);
326
+ index = found === undefined ? SPINE_BINARY_NO_ATTACHMENT_INDEX : attachments.push(found.attachment) - 1;
327
+ indexByName.set(name, index);
328
+ }
329
+ values.push(index);
330
+ }
331
+ if (times.length === 0)
332
+ return;
333
+ const track = createAnimationTrack({ components: 1, interpolation: AnimationInterpolationStep, times, values });
334
+ channels.push(createAnimationChannel(track, { attachments, path: Skeleton2DSlotAnimationPath.Attachment, slotIndex }));
335
+ }
336
+ // A colour curve timeline: a time float then one byte per channel, with a per-segment curve tag. Bytes are
337
+ // normalized to 0..1 so they share the track space (and therefore the curve rebase) with the `.json` path.
338
+ function readSpineBinaryColorTimeline(reader, frameCount, channelCount) {
339
+ const times = [];
340
+ const values = [];
341
+ const curves = [];
342
+ if (frameCount <= 0)
343
+ return { curves, times, values };
344
+ times.push(readSpineBinaryFloat(reader));
345
+ for (let c = 0; c < channelCount; c++)
346
+ values.push(readSpineBinaryByte(reader) / 255);
347
+ for (let f = 0; f + 1 < frameCount && !isSpineBinaryReaderOverrun(reader); f++) {
348
+ times.push(readSpineBinaryFloat(reader));
349
+ for (let c = 0; c < channelCount; c++)
350
+ values.push(readSpineBinaryByte(reader) / 255);
351
+ const tag = readSpineBinaryByte(reader);
352
+ if (tag === SPINE_BINARY_CURVE_BEZIER) {
353
+ const points = [];
354
+ for (let v = 0; v < channelCount * 4; v++)
355
+ points.push(readSpineBinaryFloat(reader));
356
+ curves.push(points);
357
+ }
358
+ else {
359
+ curves.push(null);
360
+ }
361
+ }
362
+ return { curves, times, values };
363
+ }
364
+ // IK, transform, and path constraint timelines.
365
+ function skipSpineBinaryConstraintTimelines(reader, unmodeled) {
366
+ const ik = readSpineBinaryVarint(reader);
367
+ for (let i = 0; i < ik && !isSpineBinaryReaderOverrun(reader); i++) {
368
+ tally(unmodeled, 'ik');
369
+ readSpineBinaryVarint(reader); // constraint index
370
+ const frameCount = readSpineBinaryVarint(reader);
371
+ readSpineBinaryVarint(reader); // bezier count
372
+ skipSpineBinaryBytes(reader, 12); // time, mix, softness
373
+ for (let f = 0; f < frameCount && !isSpineBinaryReaderOverrun(reader); f++) {
374
+ skipSpineBinaryBytes(reader, 3); // bend direction, compress, stretch
375
+ if (f === frameCount - 1)
376
+ break;
377
+ skipSpineBinaryBytes(reader, 12);
378
+ skipSpineBinaryCurveTag(reader, 2);
379
+ }
380
+ }
381
+ const transform = readSpineBinaryVarint(reader);
382
+ for (let i = 0; i < transform && !isSpineBinaryReaderOverrun(reader); i++) {
383
+ tally(unmodeled, 'transform');
384
+ readSpineBinaryVarint(reader);
385
+ const frameCount = readSpineBinaryVarint(reader);
386
+ readSpineBinaryVarint(reader);
387
+ skipSpineBinaryCurveFrames(reader, frameCount, 24, 6);
388
+ }
389
+ const path = readSpineBinaryVarint(reader);
390
+ for (let i = 0; i < path && !isSpineBinaryReaderOverrun(reader); i++) {
391
+ readSpineBinaryVarint(reader);
392
+ const timelines = readSpineBinaryVarint(reader);
393
+ for (let j = 0; j < timelines && !isSpineBinaryReaderOverrun(reader); j++) {
394
+ tally(unmodeled, 'path');
395
+ const type = readSpineBinaryByte(reader);
396
+ const frameCount = readSpineBinaryVarint(reader);
397
+ readSpineBinaryVarint(reader);
398
+ const values = type === SPINE_BINARY_PATH_MIX ? 3 : 1;
399
+ skipSpineBinaryCurveFrames(reader, frameCount, values * 4, values);
400
+ }
401
+ }
402
+ }
403
+ // Deform (mesh vertex offset) and attachment-sequence timelines, nested skin → slot → attachment.
404
+ function skipSpineBinaryDeformTimelines(reader, unmodeled) {
405
+ const skins = readSpineBinaryVarint(reader);
406
+ for (let i = 0; i < skins && !isSpineBinaryReaderOverrun(reader); i++) {
407
+ readSpineBinaryVarint(reader); // skin index
408
+ const slots = readSpineBinaryVarint(reader);
409
+ for (let j = 0; j < slots && !isSpineBinaryReaderOverrun(reader); j++) {
410
+ readSpineBinaryVarint(reader); // slot index
411
+ const attachments = readSpineBinaryVarint(reader);
412
+ for (let k = 0; k < attachments && !isSpineBinaryReaderOverrun(reader); k++) {
413
+ readSpineBinaryVarint(reader); // attachment name reference
414
+ const type = readSpineBinaryByte(reader);
415
+ const frameCount = readSpineBinaryVarint(reader);
416
+ if (type === SPINE_BINARY_ATTACHMENT_SEQUENCE) {
417
+ tally(unmodeled, 'attachment-sequence');
418
+ skipSpineBinaryBytes(reader, frameCount * 12); // time, packed mode+index, delay
419
+ continue;
420
+ }
421
+ tally(unmodeled, 'deform');
422
+ readSpineBinaryVarint(reader); // bezier count
423
+ skipSpineBinaryBytes(reader, 4); // first time
424
+ for (let f = 0; f < frameCount && !isSpineBinaryReaderOverrun(reader); f++) {
425
+ // A run length of zero means "the attachment's own vertices", carrying no payload at all.
426
+ const run = readSpineBinaryVarint(reader);
427
+ if (run !== 0) {
428
+ readSpineBinaryVarint(reader); // start offset into the vertex array
429
+ skipSpineBinaryBytes(reader, run * 4);
430
+ }
431
+ if (f === frameCount - 1)
432
+ break;
433
+ skipSpineBinaryBytes(reader, 4); // next time
434
+ skipSpineBinaryCurveTag(reader, 1);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ }
440
+ // The draw-order timeline: per frame, a time and a list of slot-index/offset pairs.
441
+ function skipSpineBinaryDrawOrderTimelines(reader, unmodeled) {
442
+ const frames = readSpineBinaryVarint(reader);
443
+ if (frames > 0)
444
+ tally(unmodeled, 'draworder');
445
+ for (let i = 0; i < frames && !isSpineBinaryReaderOverrun(reader); i++) {
446
+ skipSpineBinaryBytes(reader, 4); // time
447
+ const offsets = readSpineBinaryVarint(reader);
448
+ for (let j = 0; j < offsets && !isSpineBinaryReaderOverrun(reader); j++) {
449
+ readSpineBinaryVarint(reader); // slot index
450
+ readSpineBinaryVarint(reader); // draw-order offset
451
+ }
452
+ }
453
+ }
454
+ // The event timeline: per frame, a time, the event it fires, and any values overriding the definition.
455
+ function skipSpineBinaryEventTimelines(reader, unmodeled) {
456
+ const frames = readSpineBinaryVarint(reader);
457
+ if (frames > 0)
458
+ tally(unmodeled, 'event');
459
+ for (let i = 0; i < frames && !isSpineBinaryReaderOverrun(reader); i++) {
460
+ skipSpineBinaryBytes(reader, 4); // time
461
+ readSpineBinaryVarint(reader); // event index
462
+ readSpineBinaryVarint(reader); // int value
463
+ skipSpineBinaryBytes(reader, 4); // float value
464
+ // A flag says whether this frame overrides the definition's string; only then is one written.
465
+ if (readSpineBinaryBoolean(reader))
466
+ readSpineBinaryString(reader);
467
+ }
468
+ }
469
+ // Walks a curve timeline whose values are consumed rather than kept. `payloadBytes` is the per-keyframe
470
+ // value payload IN BYTES, excluding the 4-byte time — it is not a value count, because the two families
471
+ // differ in width: a constraint timeline stores floats, while a slot COLOUR timeline stores one byte per
472
+ // channel. `curveValues` is how many bezier curves a tagged segment carries, which tracks the value count
473
+ // either way (a colour's curves are still floats).
474
+ function skipSpineBinaryCurveFrames(reader, frameCount, payloadBytes, curveValues) {
475
+ if (frameCount <= 0)
476
+ return;
477
+ skipSpineBinaryBytes(reader, 4 + payloadBytes);
478
+ for (let f = 0; f + 1 < frameCount && !isSpineBinaryReaderOverrun(reader); f++) {
479
+ skipSpineBinaryBytes(reader, 4 + payloadBytes);
480
+ skipSpineBinaryCurveTag(reader, curveValues);
481
+ }
482
+ }
483
+ // One per-segment curve tag, plus the four floats per value a bezier tag carries.
484
+ function skipSpineBinaryCurveTag(reader, curveValues) {
485
+ if (readSpineBinaryByte(reader) === SPINE_BINARY_CURVE_BEZIER) {
486
+ skipSpineBinaryBytes(reader, curveValues * 16);
487
+ }
488
+ }
489
+ function clampSpineBinaryUnit(value) {
490
+ return value < 0 ? 0 : value > 1 ? 1 : value;
491
+ }
492
+ function tally(counts, kind) {
493
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
494
+ }
495
+ // Whether this importer's record layout describes `version`. Only the 4.x line is claimed: it is what the
496
+ // layout was verified against. Anything else (3.8 and earlier, or a future major) is rejected rather than
497
+ // guessed, because a mismatched layout desynchronizes the stream and yields plausible-looking garbage.
498
+ function isSupportedSpineBinaryVersion(version) {
499
+ return version.startsWith('4.');
500
+ }
501
+ // Spine's bone records, in file order — the order weighted-mesh influences and slot bone references index
502
+ // into, and the order that guarantees a parent precedes its children (bone 0 is the root and writes no
503
+ // parent index at all).
504
+ function parseSpineBinaryBones(reader, nonessential) {
505
+ const count = readSpineBinaryVarint(reader);
506
+ const bones = [];
507
+ for (let i = 0; i < count; i++) {
508
+ if (isSpineBinaryReaderOverrun(reader))
509
+ break;
510
+ const name = readSpineBinaryString(reader);
511
+ const parentIndex = i === 0 ? -1 : readSpineBinaryVarint(reader);
512
+ const rotation = readSpineBinaryFloat(reader);
513
+ const x = readSpineBinaryFloat(reader);
514
+ const y = readSpineBinaryFloat(reader);
515
+ const scaleX = readSpineBinaryFloat(reader);
516
+ const scaleY = readSpineBinaryFloat(reader);
517
+ const shearX = readSpineBinaryFloat(reader);
518
+ const shearY = readSpineBinaryFloat(reader);
519
+ const length = readSpineBinaryFloat(reader);
520
+ const transformMode = spineBinaryTransformMode(readSpineBinaryVarint(reader));
521
+ readSpineBinaryBoolean(reader); // skinRequired — a skin-set feature, not modeled
522
+ if (nonessential)
523
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES); // editor bone color
524
+ bones.push({ length, name, parentIndex, rotation, scaleX, scaleY, shearX, shearY, transformMode, x, y });
525
+ }
526
+ return bones;
527
+ }
528
+ // Spine's slot records, in draw order. `color`/`darkColor` are rgba8888 ints, matching `Slot2D.color`'s
529
+ // packed convention directly; a dark color of -1 means "none". The setup attachment is a STRING-TABLE
530
+ // REFERENCE naming an attachment inside a skin, which the file has not written yet — so the NAME is returned
531
+ // alongside the slots and the caller resolves it once the skin is read.
532
+ function parseSpineBinarySlots(reader, strings, diagnostics) {
533
+ const count = readSpineBinaryVarint(reader);
534
+ const attachmentNames = [];
535
+ const slots = [];
536
+ let darkColors = 0;
537
+ for (let i = 0; i < count; i++) {
538
+ if (isSpineBinaryReaderOverrun(reader))
539
+ break;
540
+ const name = readSpineBinaryString(reader);
541
+ const boneIndex = readSpineBinaryVarint(reader);
542
+ const color = readSpineBinaryInt(reader) >>> 0;
543
+ if (readSpineBinaryInt(reader) !== SPINE_BINARY_NO_DARK_COLOR)
544
+ darkColors++;
545
+ attachmentNames.push(readSpineBinaryStringReference(reader, strings));
546
+ readSpineBinaryVarint(reader); // blend mode — Slot2D carries no per-slot blend today
547
+ slots.push({ attachment: null, boneIndex, color, name });
548
+ }
549
+ if (darkColors > 0) {
550
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, 'spine.slot-dark-color-unsupported', 'parseSpineSkeletonBinary', { slots: darkColors });
551
+ }
552
+ return { attachmentNames, slots };
553
+ }
554
+ // The IK, transform, and path constraint sections. Flight models no constraint solvers (a skeleton2d P2
555
+ // concern), but the stream is positional — these records carry no keys and no lengths — so they must be
556
+ // CONSUMED field-for-field to reach the skins that follow. Reading them is not optional the way ignoring a
557
+ // JSON key is; a single miscounted field desynchronizes every later section.
558
+ function skipSpineBinaryConstraints(reader, diagnostics) {
559
+ const ik = readSpineBinaryVarint(reader);
560
+ for (let i = 0; i < ik && !isSpineBinaryReaderOverrun(reader); i++) {
561
+ skipSpineBinaryConstraintHead(reader);
562
+ readSpineBinaryVarint(reader); // target bone
563
+ skipSpineBinaryBytes(reader, 8); // mix, softness
564
+ skipSpineBinaryBytes(reader, 4); // bendDirection byte + compress/stretch/uniform booleans
565
+ }
566
+ const transform = readSpineBinaryVarint(reader);
567
+ for (let i = 0; i < transform && !isSpineBinaryReaderOverrun(reader); i++) {
568
+ skipSpineBinaryConstraintHead(reader);
569
+ readSpineBinaryVarint(reader); // target bone
570
+ skipSpineBinaryBytes(reader, 2); // local, relative
571
+ skipSpineBinaryBytes(reader, 48); // six offsets + six mix weights
572
+ }
573
+ const path = readSpineBinaryVarint(reader);
574
+ for (let i = 0; i < path && !isSpineBinaryReaderOverrun(reader); i++) {
575
+ skipSpineBinaryConstraintHead(reader);
576
+ readSpineBinaryVarint(reader); // target slot
577
+ readSpineBinaryVarint(reader); // position mode
578
+ readSpineBinaryVarint(reader); // spacing mode
579
+ readSpineBinaryVarint(reader); // rotate mode
580
+ skipSpineBinaryBytes(reader, 24); // offsetRotation, position, spacing, mixRotate, mixX, mixY
581
+ }
582
+ reportSpineBinaryCrumb(diagnostics, ik, 'spine.ik-constraint-unsupported', 'constraints');
583
+ reportSpineBinaryCrumb(diagnostics, transform, 'spine.transform-constraint-unsupported', 'constraints');
584
+ reportSpineBinaryCrumb(diagnostics, path, 'spine.path-constraint-unsupported', 'constraints');
585
+ }
586
+ // The head every constraint record shares: name, ordering index, skin-required flag, then its bone list.
587
+ function skipSpineBinaryConstraintHead(reader) {
588
+ readSpineBinaryString(reader);
589
+ readSpineBinaryVarint(reader); // order
590
+ readSpineBinaryBoolean(reader); // skinRequired
591
+ const bones = readSpineBinaryVarint(reader);
592
+ for (let i = 0; i < bones && !isSpineBinaryReaderOverrun(reader); i++)
593
+ readSpineBinaryVarint(reader);
594
+ }
595
+ // The rig's wardrobe. The DEFAULT skin is written first in an abbreviated form — just its slot count, with
596
+ // no name and no bone/constraint lists — and the named alternates follow, each carrying a name plus the
597
+ // bone and constraint indices it requires. Both forms share the same slot → attachment body.
598
+ //
599
+ // Region and mesh attachments are modeled; bounding-box, path, point, clipping, and linked-mesh entries are
600
+ // recognized — and still fully consumed, since skipping their bytes is not possible — then Skip-crumbed.
601
+ function parseSpineBinarySkins(reader, strings, nonessential, diagnostics) {
602
+ const skins = [];
603
+ const unmodeled = new Map();
604
+ const defaultSlots = readSpineBinaryVarint(reader);
605
+ if (defaultSlots > 0) {
606
+ skins.push({
607
+ attachments: readSpineBinarySkinBody(reader, strings, defaultSlots, nonessential, unmodeled),
608
+ name: SPINE_BINARY_DEFAULT_SKIN_NAME,
609
+ });
610
+ }
611
+ const alternates = readSpineBinaryVarint(reader);
612
+ for (let i = 0; i < alternates && !isSpineBinaryReaderOverrun(reader); i++) {
613
+ const name = readSpineBinaryStringReference(reader, strings);
614
+ // A named skin declares the bones and the IK / transform / path constraints it requires, as four index
615
+ // lists, before its slots. Flight applies a skin as a slot write, so these are consumed for position only.
616
+ for (let list = 0; list < SPINE_BINARY_SKIN_REQUIREMENT_LISTS; list++) {
617
+ const required = readSpineBinaryVarint(reader);
618
+ for (let j = 0; j < required && !isSpineBinaryReaderOverrun(reader); j++)
619
+ readSpineBinaryVarint(reader);
620
+ }
621
+ const slotCount = readSpineBinaryVarint(reader);
622
+ skins.push({
623
+ attachments: readSpineBinarySkinBody(reader, strings, slotCount, nonessential, unmodeled),
624
+ name: name ?? '',
625
+ });
626
+ }
627
+ for (const [type, count] of unmodeled) {
628
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, `spine.${type}-attachment-unsupported`, 'parseSpineSkeletonBinary', { attachments: count });
629
+ }
630
+ return skins;
631
+ }
632
+ // One skin's slot → attachment body, shared by the default and named forms. Entries carry an explicit slot
633
+ // index and are NOT written in slot order.
634
+ function readSpineBinarySkinBody(reader, strings, slotCount, nonessential, unmodeled) {
635
+ const attachments = [];
636
+ for (let i = 0; i < slotCount && !isSpineBinaryReaderOverrun(reader); i++) {
637
+ const slotIndex = readSpineBinaryVarint(reader);
638
+ const entries = readSpineBinaryVarint(reader);
639
+ for (let j = 0; j < entries && !isSpineBinaryReaderOverrun(reader); j++) {
640
+ const key = readSpineBinaryStringReference(reader, strings);
641
+ const attachment = readSpineBinaryAttachment(reader, strings, key, nonessential, unmodeled);
642
+ if (attachment !== null && key !== null)
643
+ attachments.push({ attachment, name: key, slotIndex });
644
+ }
645
+ }
646
+ return attachments;
647
+ }
648
+ // One attachment record. Its own name overrides the skin key when present (a slot can show the same image
649
+ // under a different key). The type is an ORDINAL into Spine's attachment-type enum, so the order of
650
+ // SPINE_BINARY_ATTACHMENT_TYPES is load-bearing.
651
+ function readSpineBinaryAttachment(reader, strings, key, nonessential, unmodeled) {
652
+ const name = readSpineBinaryStringReference(reader, strings) ?? key;
653
+ const ordinal = readSpineBinaryByte(reader);
654
+ const type = ordinal < SPINE_BINARY_ATTACHMENT_TYPES.length ? SPINE_BINARY_ATTACHMENT_TYPES[ordinal] : null;
655
+ if (type === 'region')
656
+ return readSpineBinaryRegionAttachment(reader, strings, name);
657
+ if (type === 'mesh')
658
+ return readSpineBinaryMeshAttachment(reader, strings, name, nonessential);
659
+ const label = type ?? 'unknown';
660
+ unmodeled.set(label, (unmodeled.get(label) ?? 0) + 1);
661
+ if (type === 'boundingbox') {
662
+ skipSpineBinaryVertices(reader, readSpineBinaryVarint(reader));
663
+ if (nonessential)
664
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES);
665
+ }
666
+ else if (type === 'clipping') {
667
+ readSpineBinaryVarint(reader); // end slot
668
+ skipSpineBinaryVertices(reader, readSpineBinaryVarint(reader));
669
+ if (nonessential)
670
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES);
671
+ }
672
+ else if (type === 'point') {
673
+ skipSpineBinaryBytes(reader, 12); // rotation, x, y
674
+ if (nonessential)
675
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES);
676
+ }
677
+ else if (type === 'linkedmesh') {
678
+ readSpineBinaryVarint(reader); // path
679
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES);
680
+ readSpineBinaryVarint(reader); // skin name
681
+ readSpineBinaryVarint(reader); // parent mesh
682
+ readSpineBinaryBoolean(reader); // inherit timelines
683
+ skipSpineBinarySequence(reader);
684
+ if (nonessential)
685
+ skipSpineBinaryBytes(reader, 8); // width, height
686
+ }
687
+ else if (type === 'path') {
688
+ skipSpineBinaryBytes(reader, 2); // closed, constantSpeed
689
+ const vertexCount = readSpineBinaryVarint(reader);
690
+ skipSpineBinaryVertices(reader, vertexCount);
691
+ skipSpineBinaryBytes(reader, Math.floor(vertexCount / 3) * 4); // per-curve lengths
692
+ if (nonessential)
693
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES);
694
+ }
695
+ // An unknown ordinal cannot be stepped over — its payload width is unknown — so the stream is abandoned
696
+ // by marking overrun rather than guessing and emitting garbage for everything after it.
697
+ if (type === null)
698
+ skipSpineBinaryBytes(reader, reader.view.byteLength + 1);
699
+ return null;
700
+ }
701
+ // A mesh attachment. `uvs` and `triangles` map straight across; the vertex stream is either rigid positions
702
+ // (local to the slot's bone) or a weighted `Skin2D` whose influences are already in Flight's
703
+ // `[boneIndex, x, y, weight]` layout, so no re-packing is needed.
704
+ function readSpineBinaryMeshAttachment(reader, strings, name, nonessential) {
705
+ readSpineBinaryVarint(reader); // atlas region path — resolved at atlas-binding time
706
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES);
707
+ const vertexCount = readSpineBinaryVarint(reader);
708
+ const uvs = new Float32Array(vertexCount * 2);
709
+ for (let i = 0; i < uvs.length; i++)
710
+ uvs[i] = readSpineBinaryFloat(reader);
711
+ const triangleCount = readSpineBinaryVarint(reader);
712
+ const triangles = new Uint16Array(triangleCount);
713
+ for (let i = 0; i < triangleCount; i++)
714
+ triangles[i] = readSpineBinaryUnsignedShort(reader);
715
+ const geometry = readSpineBinaryVertices(reader, vertexCount);
716
+ readSpineBinaryVarint(reader); // hull length — a rendering hint Flight does not model
717
+ skipSpineBinarySequence(reader);
718
+ if (nonessential) {
719
+ const edges = readSpineBinaryVarint(reader);
720
+ skipSpineBinaryBytes(reader, edges * 2 + 8); // editor edge list, then width and height
721
+ }
722
+ return {
723
+ kind: MeshAttachment2DKind,
724
+ name,
725
+ skin: geometry.skin,
726
+ triangles,
727
+ uvs,
728
+ vertexCount,
729
+ vertices: geometry.vertices,
730
+ };
731
+ }
732
+ // A region attachment. Width/height are the source region's size in the atlas; `path` names the atlas region
733
+ // and is resolved when the `.atlas` sidecar binds, which is `@flighthq/spritesheet-formats`' domain.
734
+ function readSpineBinaryRegionAttachment(reader, strings, name) {
735
+ readSpineBinaryVarint(reader); // atlas region path
736
+ const rotation = readSpineBinaryFloat(reader);
737
+ const x = readSpineBinaryFloat(reader);
738
+ const y = readSpineBinaryFloat(reader);
739
+ const scaleX = readSpineBinaryFloat(reader);
740
+ const scaleY = readSpineBinaryFloat(reader);
741
+ const width = readSpineBinaryFloat(reader);
742
+ const height = readSpineBinaryFloat(reader);
743
+ skipSpineBinaryBytes(reader, SPINE_BINARY_COLOR_BYTES);
744
+ skipSpineBinarySequence(reader);
745
+ return { height, kind: RegionAttachment2DKind, name, rotation, scaleX, scaleY, width, x, y };
746
+ }
747
+ // A vertex stream: a leading flag picks rigid positions (2 floats per vertex, in the slot bone's space) or
748
+ // weighted influences (per vertex, a count then that many bone/x/y/weight quads).
749
+ function readSpineBinaryVertices(reader, vertexCount) {
750
+ if (!readSpineBinaryBoolean(reader)) {
751
+ const vertices = new Float32Array(vertexCount * 2);
752
+ for (let i = 0; i < vertices.length; i++)
753
+ vertices[i] = readSpineBinaryFloat(reader);
754
+ return { skin: null, vertices };
755
+ }
756
+ const influenceCounts = new Uint16Array(vertexCount);
757
+ const influences = [];
758
+ for (let v = 0; v < vertexCount && !isSpineBinaryReaderOverrun(reader); v++) {
759
+ const count = readSpineBinaryVarint(reader);
760
+ influenceCounts[v] = count;
761
+ for (let i = 0; i < count; i++) {
762
+ influences.push(readSpineBinaryVarint(reader), readSpineBinaryFloat(reader), readSpineBinaryFloat(reader), readSpineBinaryFloat(reader));
763
+ }
764
+ }
765
+ return { skin: { influenceCounts, influences: Float32Array.from(influences) }, vertices: null };
766
+ }
767
+ // Consumes a vertex stream whose geometry is not kept (an unmodeled attachment type still occupies bytes).
768
+ function skipSpineBinaryVertices(reader, vertexCount) {
769
+ readSpineBinaryVertices(reader, vertexCount);
770
+ }
771
+ // Spine 4.1 added an optional `sequence` block to image-backed attachments, describing a numbered frame set.
772
+ // Flight does not model it, but its PRESENCE FLAG is always written, so it must be consumed — this single
773
+ // byte is what desynchronizes every later record if it is missed.
774
+ function skipSpineBinarySequence(reader) {
775
+ if (!readSpineBinaryBoolean(reader))
776
+ return;
777
+ readSpineBinaryVarint(reader); // frame count
778
+ readSpineBinaryVarint(reader); // start index
779
+ readSpineBinaryVarint(reader); // digit count
780
+ readSpineBinaryVarint(reader); // setup index
781
+ }
782
+ // Resolves a 1-based string-table index (0 meaning "no string") into its pooled string.
783
+ function readSpineBinaryStringReference(reader, strings) {
784
+ const index = readSpineBinaryVarint(reader);
785
+ return index > 0 && index <= strings.length ? strings[index - 1] : null;
786
+ }
787
+ // Reports one aggregated Skip crumb for a recognized-but-unmodeled section, keyed by its element count.
788
+ function reportSpineBinaryCrumb(diagnostics, count, kind, unit) {
789
+ if (count > 0) {
790
+ reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, kind, 'parseSpineSkeletonBinary', {
791
+ [unit]: count,
792
+ });
793
+ }
794
+ }
795
+ // The file-wide string pool that later sections reference by 1-based index (0 = no string). Reading it is
796
+ // what makes those references resolvable, so it is consumed even though this landing resolves none yet.
797
+ function readSpineBinaryStringTable(reader) {
798
+ const count = readSpineBinaryVarint(reader);
799
+ const strings = [];
800
+ for (let i = 0; i < count && !isSpineBinaryReaderOverrun(reader); i++)
801
+ strings.push(readSpineBinaryString(reader));
802
+ return strings;
803
+ }
804
+ // Spine writes the bone transform mode as an ORDINAL into its own enum, so the mapping is positional rather
805
+ // than by name (the `.json` sibling reads the same modes as strings). An out-of-range ordinal — a file from a
806
+ // version with more modes — falls back to Normal rather than producing an undefined inherit rule.
807
+ function spineBinaryTransformMode(ordinal) {
808
+ return ordinal >= 0 && ordinal < SPINE_BINARY_TRANSFORM_MODES.length
809
+ ? SPINE_BINARY_TRANSFORM_MODES[ordinal]
810
+ : TransformMode2D.Normal;
811
+ }
812
+ // Fixed-width header fields the importer steps over: the 8-byte export hash, the four floats of the
813
+ // authoring bounds, the nonessential frame rate, and a packed rgba8888 color.
814
+ const SPINE_BINARY_BOUNDS_BYTES = 16;
815
+ const SPINE_BINARY_COLOR_BYTES = 4;
816
+ const SPINE_BINARY_FPS_BYTES = 4;
817
+ const SPINE_BINARY_HASH_BYTES = 8;
818
+ // Spine's attachment types in its own enum ORDER — the file writes an ordinal into this list, so the order
819
+ // is load-bearing and must not be alphabetized.
820
+ const SPINE_BINARY_ATTACHMENT_TYPES = [
821
+ 'region',
822
+ 'boundingbox',
823
+ 'mesh',
824
+ 'linkedmesh',
825
+ 'path',
826
+ 'point',
827
+ 'clipping',
828
+ ];
829
+ // Spine's bone timeline ORDINALS, in its own enum order — the file writes an index into this table, so the
830
+ // order is load-bearing and must not be alphabetized. `values` is how many numbers a keyframe carries;
831
+ // `axis` is which component a per-axis form drives (-1 for the combined form); `identity` is the delta that
832
+ // leaves the untouched axis at its setup value.
833
+ const SPINE_BINARY_BONE_TIMELINES = [
834
+ { axis: -1, components: 1, identity: 0, path: Skeleton2DAnimationPath.Rotation, values: 1 },
835
+ { axis: -1, components: 2, identity: 0, path: Skeleton2DAnimationPath.Translation, values: 2 },
836
+ { axis: 0, components: 2, identity: 0, path: Skeleton2DAnimationPath.Translation, values: 1 },
837
+ { axis: 1, components: 2, identity: 0, path: Skeleton2DAnimationPath.Translation, values: 1 },
838
+ { axis: -1, components: 2, identity: 1, path: Skeleton2DAnimationPath.Scale, values: 2 },
839
+ { axis: 0, components: 2, identity: 1, path: Skeleton2DAnimationPath.Scale, values: 1 },
840
+ { axis: 1, components: 2, identity: 1, path: Skeleton2DAnimationPath.Scale, values: 1 },
841
+ { axis: -1, components: 2, identity: 0, path: Skeleton2DAnimationPath.Shear, values: 2 },
842
+ { axis: 0, components: 2, identity: 0, path: Skeleton2DAnimationPath.Shear, values: 1 },
843
+ { axis: 1, components: 2, identity: 0, path: Skeleton2DAnimationPath.Shear, values: 1 },
844
+ ];
845
+ // A slot colour timeline's channel count, indexed by its timeline ordinal (1 = RGBA, 2 = RGB, 3 = RGBA with
846
+ // a dark colour, 4 = RGB with a dark colour, 5 = alpha only). Ordinal 0 is the attachment-swap timeline.
847
+ const SPINE_BINARY_SLOT_COLOR_CHANNELS = [0, 4, 3, 7, 6, 1];
848
+ // Per-segment curve tags. Linear (0) and stepped (1) carry no payload; bezier carries four floats per value.
849
+ const SPINE_BINARY_CURVE_BEZIER = 2;
850
+ const SPINE_BINARY_SLOT_ATTACHMENT = 0;
851
+ const SPINE_BINARY_SLOT_RGBA = 1;
852
+ // The index an attachment channel uses for "show nothing": Spine's null name, or a name the setup skin lacks.
853
+ const SPINE_BINARY_NO_ATTACHMENT_INDEX = -1;
854
+ const SPINE_BINARY_ATTACHMENT_SEQUENCE = 1;
855
+ const SPINE_BINARY_PATH_MIX = 2;
856
+ // Normalized control points closer than this are the same curve shape; see the `.json` parser for why the
857
+ // comparison must happen after rebasing rather than on the raw numbers.
858
+ const SPINE_BINARY_CURVE_EPSILON = 1e-6;
859
+ // Spine writes -1 into a slot's dark color to mean "this slot has none".
860
+ const SPINE_BINARY_NO_DARK_COLOR = -1;
861
+ // Spine writes the base skin first, unnamed; this is the name it is filed under in the wardrobe.
862
+ const SPINE_BINARY_DEFAULT_SKIN_NAME = 'default';
863
+ // A named skin lists what it requires as four index lists: bones, then IK, transform, and path constraints.
864
+ const SPINE_BINARY_SKIN_REQUIREMENT_LISTS = 4;
865
+ // The bone transform modes in Spine's own enum ORDER — the ordinal written in the file indexes this array,
866
+ // so the order is load-bearing and must not be alphabetized.
867
+ const SPINE_BINARY_TRANSFORM_MODES = [
868
+ TransformMode2D.Normal,
869
+ TransformMode2D.OnlyTranslation,
870
+ TransformMode2D.NoRotationOrReflection,
871
+ TransformMode2D.NoScale,
872
+ TransformMode2D.NoScaleOrReflection,
873
+ ];
874
+ //# sourceMappingURL=spineBinaryParse.js.map