@flighthq/scene3d-formats 0.2.1-next.840.857425c
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/dist/awd2Inflate.d.ts +4 -0
- package/dist/awd2Inflate.d.ts.map +1 -0
- package/dist/awd2Inflate.js +249 -0
- package/dist/awd2Inflate.js.map +1 -0
- package/dist/awd2Parse.d.ts +7 -0
- package/dist/awd2Parse.d.ts.map +1 -0
- package/dist/awd2Parse.js +1533 -0
- package/dist/awd2Parse.js.map +1 -0
- package/dist/awd2Schema.d.ts +44 -0
- package/dist/awd2Schema.d.ts.map +1 -0
- package/dist/awd2Schema.js +67 -0
- package/dist/awd2Schema.js.map +1 -0
- package/dist/gltfParse.d.ts +9 -0
- package/dist/gltfParse.d.ts.map +1 -0
- package/dist/gltfParse.js +1456 -0
- package/dist/gltfParse.js.map +1 -0
- package/dist/gltfPunctualLights.d.ts +3 -0
- package/dist/gltfPunctualLights.d.ts.map +1 -0
- package/dist/gltfPunctualLights.js +103 -0
- package/dist/gltfPunctualLights.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/md2Parse.d.ts +5 -0
- package/dist/md2Parse.d.ts.map +1 -0
- package/dist/md2Parse.js +408 -0
- package/dist/md2Parse.js.map +1 -0
- package/dist/md2Schema.d.ts +12 -0
- package/dist/md2Schema.d.ts.map +1 -0
- package/dist/md2Schema.js +193 -0
- package/dist/md2Schema.js.map +1 -0
- package/dist/md5AnimParse.d.ts +3 -0
- package/dist/md5AnimParse.d.ts.map +1 -0
- package/dist/md5AnimParse.js +385 -0
- package/dist/md5AnimParse.js.map +1 -0
- package/dist/md5Parse.d.ts +6 -0
- package/dist/md5Parse.d.ts.map +1 -0
- package/dist/md5Parse.js +635 -0
- package/dist/md5Parse.js.map +1 -0
- package/dist/mtlParse.d.ts +3 -0
- package/dist/mtlParse.d.ts.map +1 -0
- package/dist/mtlParse.js +256 -0
- package/dist/mtlParse.js.map +1 -0
- package/dist/objParse.d.ts +5 -0
- package/dist/objParse.d.ts.map +1 -0
- package/dist/objParse.js +464 -0
- package/dist/objParse.js.map +1 -0
- package/dist/sceneSkeleton.d.ts +3 -0
- package/dist/sceneSkeleton.d.ts.map +1 -0
- package/dist/sceneSkeleton.js +34 -0
- package/dist/sceneSkeleton.js.map +1 -0
- package/dist/shared.d.ts +18 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +143 -0
- package/dist/shared.js.map +1 -0
- package/dist/threeDsParse.d.ts +4 -0
- package/dist/threeDsParse.d.ts.map +1 -0
- package/dist/threeDsParse.js +718 -0
- package/dist/threeDsParse.js.map +1 -0
- package/package.json +55 -0
- package/src/awd2Inflate.test.ts +118 -0
- package/src/awd2Parse.test.ts +2431 -0
- package/src/gltfParse.test.ts +2339 -0
- package/src/gltfPunctualLights.test.ts +184 -0
- package/src/gltfTreeShaking.test.ts +45 -0
- package/src/md2Parse.test.ts +1033 -0
- package/src/md5AnimParse.test.ts +673 -0
- package/src/md5Parse.test.ts +1094 -0
- package/src/mtlParse.test.ts +244 -0
- package/src/objParse.test.ts +690 -0
- package/src/sceneSkeleton.test.ts +35 -0
- package/src/shared.test.ts +210 -0
- package/src/threeDsParse.test.ts +1089 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { sampleAnimationTrack } from '@flighthq/animation';
|
|
2
|
+
import { createNode3D } from '@flighthq/scene3d';
|
|
3
|
+
import type { AnimationClip, ImportDiagnostic, Scene3DAnimationTarget, Node3D } from '@flighthq/types';
|
|
4
|
+
import {
|
|
5
|
+
ImportDiagnosticSeverity,
|
|
6
|
+
Scene3DAnimationPathRotation,
|
|
7
|
+
Scene3DAnimationPathTranslation,
|
|
8
|
+
} from '@flighthq/types';
|
|
9
|
+
|
|
10
|
+
import { parseMd5Anim } from './md5AnimParse';
|
|
11
|
+
|
|
12
|
+
// Minimal valid MD5 anim with one joint, one frame, no animated components (all from baseframe).
|
|
13
|
+
const SINGLE_JOINT_STATIC = [
|
|
14
|
+
'MD5Version 10',
|
|
15
|
+
'commandline ""',
|
|
16
|
+
'',
|
|
17
|
+
'numFrames 1',
|
|
18
|
+
'numJoints 1',
|
|
19
|
+
'frameRate 24',
|
|
20
|
+
'numAnimatedComponents 0',
|
|
21
|
+
'',
|
|
22
|
+
'hierarchy {',
|
|
23
|
+
' "root" -1 0 0',
|
|
24
|
+
'}',
|
|
25
|
+
'',
|
|
26
|
+
'bounds {',
|
|
27
|
+
' ( -1 -1 -1 ) ( 1 1 1 )',
|
|
28
|
+
'}',
|
|
29
|
+
'',
|
|
30
|
+
'baseframe {',
|
|
31
|
+
' ( 5 10 15 ) ( 0 0 0 )',
|
|
32
|
+
'}',
|
|
33
|
+
'',
|
|
34
|
+
'frame 0 {',
|
|
35
|
+
'}',
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
// Two joints, two frames, with animated tx/ty/tz on the first joint (flags=7, all position bits).
|
|
39
|
+
const TWO_JOINT_TWO_FRAME = [
|
|
40
|
+
'MD5Version 10',
|
|
41
|
+
'commandline ""',
|
|
42
|
+
'',
|
|
43
|
+
'numFrames 2',
|
|
44
|
+
'numJoints 2',
|
|
45
|
+
'frameRate 30',
|
|
46
|
+
'numAnimatedComponents 3',
|
|
47
|
+
'',
|
|
48
|
+
'hierarchy {',
|
|
49
|
+
' "root" -1 7 0',
|
|
50
|
+
' "child" 0 0 0',
|
|
51
|
+
'}',
|
|
52
|
+
'',
|
|
53
|
+
'bounds {',
|
|
54
|
+
' ( -1 -1 -1 ) ( 1 1 1 )',
|
|
55
|
+
' ( -1 -1 -1 ) ( 1 1 1 )',
|
|
56
|
+
'}',
|
|
57
|
+
'',
|
|
58
|
+
'baseframe {',
|
|
59
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
60
|
+
' ( 1 2 3 ) ( 0 0 0 )',
|
|
61
|
+
'}',
|
|
62
|
+
'',
|
|
63
|
+
'frame 0 {',
|
|
64
|
+
' 0 0 0',
|
|
65
|
+
'}',
|
|
66
|
+
'',
|
|
67
|
+
'frame 1 {',
|
|
68
|
+
' 10 20 30',
|
|
69
|
+
'}',
|
|
70
|
+
].join('\n');
|
|
71
|
+
|
|
72
|
+
// One joint with animated rotation (flags=56 = qx+qy+qz bits set).
|
|
73
|
+
const ANIMATED_ROTATION = [
|
|
74
|
+
'MD5Version 10',
|
|
75
|
+
'commandline ""',
|
|
76
|
+
'',
|
|
77
|
+
'numFrames 2',
|
|
78
|
+
'numJoints 1',
|
|
79
|
+
'frameRate 10',
|
|
80
|
+
'numAnimatedComponents 3',
|
|
81
|
+
'',
|
|
82
|
+
'hierarchy {',
|
|
83
|
+
' "root" -1 56 0',
|
|
84
|
+
'}',
|
|
85
|
+
'',
|
|
86
|
+
'bounds {',
|
|
87
|
+
' ( -1 -1 -1 ) ( 1 1 1 )',
|
|
88
|
+
' ( -1 -1 -1 ) ( 1 1 1 )',
|
|
89
|
+
'}',
|
|
90
|
+
'',
|
|
91
|
+
'baseframe {',
|
|
92
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
93
|
+
'}',
|
|
94
|
+
'',
|
|
95
|
+
'frame 0 {',
|
|
96
|
+
' 0 0 0',
|
|
97
|
+
'}',
|
|
98
|
+
'',
|
|
99
|
+
'frame 1 {',
|
|
100
|
+
' 0.5 0.5 0.5',
|
|
101
|
+
'}',
|
|
102
|
+
].join('\n');
|
|
103
|
+
|
|
104
|
+
function makeJoints(count: number): Node3D[] {
|
|
105
|
+
const nodes: Node3D[] = [];
|
|
106
|
+
for (let i = 0; i < count; i++) {
|
|
107
|
+
nodes.push(createNode3D(undefined, { name: `joint${i}` }));
|
|
108
|
+
}
|
|
109
|
+
return nodes;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findDiagnostic(diagnostics: readonly ImportDiagnostic[], kind: string): ImportDiagnostic | undefined {
|
|
113
|
+
return diagnostics.find((diagnostic) => diagnostic.kind === kind);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe('parseMd5Anim', () => {
|
|
117
|
+
it('parses a single static joint from baseframe values', () => {
|
|
118
|
+
const joints = makeJoints(1);
|
|
119
|
+
const clip = parseMd5Anim(SINGLE_JOINT_STATIC, joints);
|
|
120
|
+
expect(clip).not.toBeNull();
|
|
121
|
+
|
|
122
|
+
// One joint produces 2 channels (translation + rotation).
|
|
123
|
+
expect(clip!.channels).toHaveLength(2);
|
|
124
|
+
|
|
125
|
+
const translationChannel = clip!.channels[0];
|
|
126
|
+
const target = translationChannel.targetRef as Scene3DAnimationTarget;
|
|
127
|
+
expect(target.node).toBe(joints[0]);
|
|
128
|
+
expect(target.path).toBe(Scene3DAnimationPathTranslation);
|
|
129
|
+
|
|
130
|
+
// Baseframe (5, 10, 15) in Z-up → (5, 15, -10) in Y-up.
|
|
131
|
+
const out = [0, 0, 0];
|
|
132
|
+
sampleAnimationTrack(out, translationChannel.track, 0);
|
|
133
|
+
expect(out[0]).toBeCloseTo(5);
|
|
134
|
+
expect(out[1]).toBeCloseTo(15);
|
|
135
|
+
expect(out[2]).toBeCloseTo(-10);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('produces rotation channels targeting Scene3DAnimationPathRotation', () => {
|
|
139
|
+
const joints = makeJoints(1);
|
|
140
|
+
const clip = parseMd5Anim(SINGLE_JOINT_STATIC, joints)!;
|
|
141
|
+
|
|
142
|
+
const rotationChannel = clip.channels[1];
|
|
143
|
+
const target = rotationChannel.targetRef as Scene3DAnimationTarget;
|
|
144
|
+
expect(target.node).toBe(joints[0]);
|
|
145
|
+
expect(target.path).toBe(Scene3DAnimationPathRotation);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('animates translation across two frames', () => {
|
|
149
|
+
const joints = makeJoints(2);
|
|
150
|
+
const clip = parseMd5Anim(TWO_JOINT_TWO_FRAME, joints)!;
|
|
151
|
+
|
|
152
|
+
// First joint (root) has animated position, 4 channels total (2 per joint).
|
|
153
|
+
expect(clip.channels).toHaveLength(4);
|
|
154
|
+
|
|
155
|
+
const rootTranslation = clip.channels[0];
|
|
156
|
+
|
|
157
|
+
// Frame 0: (0, 0, 0) Z-up -> (0, 0, 0) Y-up.
|
|
158
|
+
const out = [0, 0, 0];
|
|
159
|
+
sampleAnimationTrack(out, rootTranslation.track, 0);
|
|
160
|
+
expect(out[0]).toBeCloseTo(0);
|
|
161
|
+
expect(out[1]).toBeCloseTo(0);
|
|
162
|
+
expect(out[2]).toBeCloseTo(0);
|
|
163
|
+
|
|
164
|
+
// Frame 1 at t=1/30: (10, 20, 30) Z-up → (10, 30, -20) Y-up.
|
|
165
|
+
sampleAnimationTrack(out, rootTranslation.track, 1 / 30);
|
|
166
|
+
expect(out[0]).toBeCloseTo(10);
|
|
167
|
+
expect(out[1]).toBeCloseTo(30);
|
|
168
|
+
expect(out[2]).toBeCloseTo(-20);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('binds channels to joints by name, not array position', () => {
|
|
172
|
+
// Nodes named to match the hierarchy ("root", "child") but passed in REVERSED order. Index binding
|
|
173
|
+
// would bind the root channel to the child node; name binding must resolve each to its named node —
|
|
174
|
+
// the fix for a caller that collects joints in a different order than MD5 (e.g. depth-first over a
|
|
175
|
+
// nested skeleton, which reorders branches like finger chains).
|
|
176
|
+
const child = createNode3D(undefined, { name: 'child' });
|
|
177
|
+
const root = createNode3D(undefined, { name: 'root' });
|
|
178
|
+
const clip = parseMd5Anim(TWO_JOINT_TWO_FRAME, [child, root])!;
|
|
179
|
+
|
|
180
|
+
// Channels are [root-translation, root-rotation, child-translation, child-rotation] (hierarchy order).
|
|
181
|
+
expect((clip.channels[0].targetRef as Scene3DAnimationTarget).node).toBe(root);
|
|
182
|
+
expect((clip.channels[2].targetRef as Scene3DAnimationTarget).node).toBe(child);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('uses baseframe values for unanimated joints', () => {
|
|
186
|
+
const joints = makeJoints(2);
|
|
187
|
+
const clip = parseMd5Anim(TWO_JOINT_TWO_FRAME, joints)!;
|
|
188
|
+
|
|
189
|
+
// Second joint (child) has flags=0, so all values come from baseframe (1, 2, 3).
|
|
190
|
+
// Z-up to Y-up: (1, 3, -2).
|
|
191
|
+
const childTranslation = clip.channels[2];
|
|
192
|
+
const out = [0, 0, 0];
|
|
193
|
+
|
|
194
|
+
sampleAnimationTrack(out, childTranslation.track, 0);
|
|
195
|
+
expect(out[0]).toBeCloseTo(1);
|
|
196
|
+
expect(out[1]).toBeCloseTo(3);
|
|
197
|
+
expect(out[2]).toBeCloseTo(-2);
|
|
198
|
+
|
|
199
|
+
// Same at frame 1 since no components are animated.
|
|
200
|
+
sampleAnimationTrack(out, childTranslation.track, 1 / 30);
|
|
201
|
+
expect(out[0]).toBeCloseTo(1);
|
|
202
|
+
expect(out[1]).toBeCloseTo(3);
|
|
203
|
+
expect(out[2]).toBeCloseTo(-2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('animates rotation and reconstructs quaternion W', () => {
|
|
207
|
+
const joints = makeJoints(1);
|
|
208
|
+
const clip = parseMd5Anim(ANIMATED_ROTATION, joints)!;
|
|
209
|
+
|
|
210
|
+
const rotationChannel = clip.channels[1];
|
|
211
|
+
const out = [0, 0, 0, 0];
|
|
212
|
+
|
|
213
|
+
// Frame 0: orientation (0, 0, 0), W = -sqrt(1 - 0) = -1.
|
|
214
|
+
// Z-up to Y-up: (qx, qy, qz, qw) → (qx, qz, -qy, qw) = (0, 0, 0, -1).
|
|
215
|
+
sampleAnimationTrack(out, rotationChannel.track, 0);
|
|
216
|
+
expect(out[0]).toBeCloseTo(0);
|
|
217
|
+
expect(out[1]).toBeCloseTo(0);
|
|
218
|
+
expect(out[2]).toBeCloseTo(0);
|
|
219
|
+
expect(out[3]).toBeCloseTo(-1);
|
|
220
|
+
|
|
221
|
+
// Frame 1: orientation (0.5, 0.5, 0.5), W = -sqrt(1 - 0.75) = -0.5.
|
|
222
|
+
// Z-up to Y-up: (qx, qy, qz, qw) → (qx, qz, -qy, qw) = (0.5, 0.5, -0.5, -0.5).
|
|
223
|
+
sampleAnimationTrack(out, rotationChannel.track, 1 / 10);
|
|
224
|
+
expect(out[0]).toBeCloseTo(0.5);
|
|
225
|
+
expect(out[1]).toBeCloseTo(0.5);
|
|
226
|
+
expect(out[2]).toBeCloseTo(-0.5);
|
|
227
|
+
expect(out[3]).toBeCloseTo(-0.5);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('computes clip duration from frameRate and numFrames', () => {
|
|
231
|
+
const joints = makeJoints(2);
|
|
232
|
+
const clip = parseMd5Anim(TWO_JOINT_TWO_FRAME, joints)!;
|
|
233
|
+
|
|
234
|
+
// 2 frames at 30 fps: last keyframe at t = 1/30.
|
|
235
|
+
expect(clip.duration).toBeCloseTo(1 / 30);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('returns null for empty input', () => {
|
|
239
|
+
const result = parseMd5Anim('', []);
|
|
240
|
+
expect(result).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns null for comment-only input', () => {
|
|
244
|
+
const result = parseMd5Anim('// just a comment\n', []);
|
|
245
|
+
expect(result).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('rejects and reports md5anim.joints-too-few when the joints array is too short', () => {
|
|
249
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
250
|
+
const result = parseMd5Anim(SINGLE_JOINT_STATIC, [], diagnostics);
|
|
251
|
+
expect(result).toBeNull();
|
|
252
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.joints-too-few');
|
|
253
|
+
expect(crumb).toBeDefined();
|
|
254
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Reject);
|
|
255
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
256
|
+
expect(crumb!.detail).toEqual({ animationJoints: 1, suppliedJoints: 0 });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('drops and reports md5anim.malformed-hierarchy for a bad hierarchy entry', () => {
|
|
260
|
+
const source = [
|
|
261
|
+
'MD5Version 10',
|
|
262
|
+
'numFrames 1',
|
|
263
|
+
'numJoints 1',
|
|
264
|
+
'frameRate 24',
|
|
265
|
+
'hierarchy {',
|
|
266
|
+
' bad hierarchy line',
|
|
267
|
+
'}',
|
|
268
|
+
'baseframe {',
|
|
269
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
270
|
+
'}',
|
|
271
|
+
'frame 0 {',
|
|
272
|
+
'}',
|
|
273
|
+
].join('\n');
|
|
274
|
+
|
|
275
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
276
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
277
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.malformed-hierarchy');
|
|
278
|
+
expect(crumb).toBeDefined();
|
|
279
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
280
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
281
|
+
expect(crumb!.detail?.count).toBe(1);
|
|
282
|
+
expect(crumb!.detail?.reason).toBe('missing-name-quotes');
|
|
283
|
+
expect(crumb!.detail?.firstLine).toBe(6);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('drops and reports md5anim.malformed-baseframe for a bad baseframe entry', () => {
|
|
287
|
+
const source = [
|
|
288
|
+
'MD5Version 10',
|
|
289
|
+
'numFrames 1',
|
|
290
|
+
'numJoints 1',
|
|
291
|
+
'frameRate 24',
|
|
292
|
+
'hierarchy {',
|
|
293
|
+
' "root" -1 0 0',
|
|
294
|
+
'}',
|
|
295
|
+
'baseframe {',
|
|
296
|
+
' ( abc def ) ( 0 0 0 )',
|
|
297
|
+
'}',
|
|
298
|
+
'frame 0 {',
|
|
299
|
+
'}',
|
|
300
|
+
].join('\n');
|
|
301
|
+
|
|
302
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
303
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
304
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.malformed-baseframe');
|
|
305
|
+
expect(crumb).toBeDefined();
|
|
306
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
307
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
308
|
+
expect(crumb!.detail?.count).toBe(1);
|
|
309
|
+
expect(crumb!.detail?.reason).toBe('not-enough-components');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('recovers and reports md5anim.unsupported-version', () => {
|
|
313
|
+
const source = [
|
|
314
|
+
'MD5Version 11',
|
|
315
|
+
'numFrames 1',
|
|
316
|
+
'numJoints 1',
|
|
317
|
+
'frameRate 24',
|
|
318
|
+
'hierarchy {',
|
|
319
|
+
' "root" -1 0 0',
|
|
320
|
+
'}',
|
|
321
|
+
'baseframe {',
|
|
322
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
323
|
+
'}',
|
|
324
|
+
'frame 0 {',
|
|
325
|
+
'}',
|
|
326
|
+
].join('\n');
|
|
327
|
+
|
|
328
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
329
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
330
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.unsupported-version');
|
|
331
|
+
expect(crumb).toBeDefined();
|
|
332
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
333
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
334
|
+
expect(crumb!.detail).toEqual({ version: 11 });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('handles mixed animated components (position and rotation bits)', () => {
|
|
338
|
+
// flags=63 = all 6 bits set (tx+ty+tz+qx+qy+qz).
|
|
339
|
+
const source = [
|
|
340
|
+
'MD5Version 10',
|
|
341
|
+
'commandline ""',
|
|
342
|
+
'numFrames 1',
|
|
343
|
+
'numJoints 1',
|
|
344
|
+
'frameRate 24',
|
|
345
|
+
'numAnimatedComponents 6',
|
|
346
|
+
'hierarchy {',
|
|
347
|
+
' "root" -1 63 0',
|
|
348
|
+
'}',
|
|
349
|
+
'bounds {',
|
|
350
|
+
' ( -1 -1 -1 ) ( 1 1 1 )',
|
|
351
|
+
'}',
|
|
352
|
+
'baseframe {',
|
|
353
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
354
|
+
'}',
|
|
355
|
+
'frame 0 {',
|
|
356
|
+
' 100 200 300 0 0 0',
|
|
357
|
+
'}',
|
|
358
|
+
].join('\n');
|
|
359
|
+
|
|
360
|
+
const joints = makeJoints(1);
|
|
361
|
+
const clip = parseMd5Anim(source, joints)!;
|
|
362
|
+
expect(clip).not.toBeNull();
|
|
363
|
+
|
|
364
|
+
// Translation: (100, 200, 300) Z-up → (100, 300, -200) Y-up.
|
|
365
|
+
const out = [0, 0, 0];
|
|
366
|
+
sampleAnimationTrack(out, clip.channels[0].track, 0);
|
|
367
|
+
expect(out[0]).toBeCloseTo(100);
|
|
368
|
+
expect(out[1]).toBeCloseTo(300);
|
|
369
|
+
expect(out[2]).toBeCloseTo(-200);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('skips comment lines inside blocks', () => {
|
|
373
|
+
const source = [
|
|
374
|
+
'MD5Version 10',
|
|
375
|
+
'numFrames 1',
|
|
376
|
+
'numJoints 1',
|
|
377
|
+
'frameRate 24',
|
|
378
|
+
'hierarchy {',
|
|
379
|
+
' // This is a comment',
|
|
380
|
+
' "root" -1 0 0',
|
|
381
|
+
'}',
|
|
382
|
+
'baseframe {',
|
|
383
|
+
' // Another comment',
|
|
384
|
+
' ( 5 10 15 ) ( 0 0 0 )',
|
|
385
|
+
'}',
|
|
386
|
+
'frame 0 {',
|
|
387
|
+
' // Frame comment',
|
|
388
|
+
'}',
|
|
389
|
+
].join('\n');
|
|
390
|
+
|
|
391
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
392
|
+
const joints = makeJoints(1);
|
|
393
|
+
const clip = parseMd5Anim(source, joints, diagnostics);
|
|
394
|
+
expect(clip).not.toBeNull();
|
|
395
|
+
expect(diagnostics).toHaveLength(0);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('interpolates translation values between frames', () => {
|
|
399
|
+
const joints = makeJoints(2);
|
|
400
|
+
const clip = parseMd5Anim(TWO_JOINT_TWO_FRAME, joints)!;
|
|
401
|
+
|
|
402
|
+
const rootTranslation = clip.channels[0];
|
|
403
|
+
const out = [0, 0, 0];
|
|
404
|
+
|
|
405
|
+
// Midpoint: t = 0.5 * (1/30).
|
|
406
|
+
const midTime = 0.5 / 30;
|
|
407
|
+
sampleAnimationTrack(out, rootTranslation.track, midTime);
|
|
408
|
+
// Halfway between (0,0,0) and (10,30,-20) Y-up = (5, 15, -10).
|
|
409
|
+
expect(out[0]).toBeCloseTo(5);
|
|
410
|
+
expect(out[1]).toBeCloseTo(15);
|
|
411
|
+
expect(out[2]).toBeCloseTo(-10);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('recovers and reports md5anim.non-numeric-numframes', () => {
|
|
415
|
+
const source = [
|
|
416
|
+
'MD5Version 10',
|
|
417
|
+
'numFrames xx',
|
|
418
|
+
'numJoints 1',
|
|
419
|
+
'frameRate 24',
|
|
420
|
+
'hierarchy {',
|
|
421
|
+
' "root" -1 0 0',
|
|
422
|
+
'}',
|
|
423
|
+
'baseframe {',
|
|
424
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
425
|
+
'}',
|
|
426
|
+
'frame 0 {',
|
|
427
|
+
'}',
|
|
428
|
+
].join('\n');
|
|
429
|
+
|
|
430
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
431
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
432
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.non-numeric-numframes');
|
|
433
|
+
expect(crumb).toBeDefined();
|
|
434
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
435
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('recovers and reports md5anim.non-numeric-numjoints', () => {
|
|
439
|
+
const source = [
|
|
440
|
+
'MD5Version 10',
|
|
441
|
+
'numFrames 1',
|
|
442
|
+
'numJoints zz',
|
|
443
|
+
'frameRate 24',
|
|
444
|
+
'hierarchy {',
|
|
445
|
+
' "root" -1 0 0',
|
|
446
|
+
'}',
|
|
447
|
+
'baseframe {',
|
|
448
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
449
|
+
'}',
|
|
450
|
+
'frame 0 {',
|
|
451
|
+
'}',
|
|
452
|
+
].join('\n');
|
|
453
|
+
|
|
454
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
455
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
456
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.non-numeric-numjoints');
|
|
457
|
+
expect(crumb).toBeDefined();
|
|
458
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
459
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('recovers and reports md5anim.invalid-framerate', () => {
|
|
463
|
+
const source = [
|
|
464
|
+
'MD5Version 10',
|
|
465
|
+
'numFrames 1',
|
|
466
|
+
'numJoints 1',
|
|
467
|
+
'frameRate 0',
|
|
468
|
+
'hierarchy {',
|
|
469
|
+
' "root" -1 0 0',
|
|
470
|
+
'}',
|
|
471
|
+
'baseframe {',
|
|
472
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
473
|
+
'}',
|
|
474
|
+
'frame 0 {',
|
|
475
|
+
'}',
|
|
476
|
+
].join('\n');
|
|
477
|
+
|
|
478
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
479
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
480
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.invalid-framerate');
|
|
481
|
+
expect(crumb).toBeDefined();
|
|
482
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
483
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('rejects and reports md5anim.no-data when no hierarchy or frame data is present', () => {
|
|
487
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
488
|
+
const result = parseMd5Anim('MD5Version 10\nnumFrames 0\nnumJoints 0\n', makeJoints(1), diagnostics);
|
|
489
|
+
expect(result).toBeNull();
|
|
490
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.no-data');
|
|
491
|
+
expect(crumb).toBeDefined();
|
|
492
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Reject);
|
|
493
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('recovers and reports md5anim.joint-count-mismatch with declared/found detail', () => {
|
|
497
|
+
const source = [
|
|
498
|
+
'MD5Version 10',
|
|
499
|
+
'numFrames 1',
|
|
500
|
+
'numJoints 5',
|
|
501
|
+
'frameRate 24',
|
|
502
|
+
'hierarchy {',
|
|
503
|
+
' "root" -1 0 0',
|
|
504
|
+
'}',
|
|
505
|
+
'baseframe {',
|
|
506
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
507
|
+
'}',
|
|
508
|
+
'frame 0 {',
|
|
509
|
+
'}',
|
|
510
|
+
].join('\n');
|
|
511
|
+
|
|
512
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
513
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
514
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.joint-count-mismatch');
|
|
515
|
+
expect(crumb).toBeDefined();
|
|
516
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
517
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
518
|
+
expect(crumb!.detail).toEqual({ declared: 5, found: 1 });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('recovers and reports md5anim.frame-count-mismatch with declared/found detail', () => {
|
|
522
|
+
const source = [
|
|
523
|
+
'MD5Version 10',
|
|
524
|
+
'numFrames 9',
|
|
525
|
+
'numJoints 1',
|
|
526
|
+
'frameRate 24',
|
|
527
|
+
'hierarchy {',
|
|
528
|
+
' "root" -1 0 0',
|
|
529
|
+
'}',
|
|
530
|
+
'baseframe {',
|
|
531
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
532
|
+
'}',
|
|
533
|
+
'frame 0 {',
|
|
534
|
+
'}',
|
|
535
|
+
].join('\n');
|
|
536
|
+
|
|
537
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
538
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
539
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.frame-count-mismatch');
|
|
540
|
+
expect(crumb).toBeDefined();
|
|
541
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
542
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
543
|
+
expect(crumb!.detail).toEqual({ declared: 9, found: 1 });
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('recovers and reports md5anim.hierarchy-block-unclosed', () => {
|
|
547
|
+
const source = [
|
|
548
|
+
'MD5Version 10',
|
|
549
|
+
'numFrames 1',
|
|
550
|
+
'numJoints 1',
|
|
551
|
+
'frameRate 24',
|
|
552
|
+
'hierarchy {',
|
|
553
|
+
' "root" -1 0 0',
|
|
554
|
+
].join('\n');
|
|
555
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
556
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
557
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.hierarchy-block-unclosed');
|
|
558
|
+
expect(crumb).toBeDefined();
|
|
559
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
560
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('recovers and reports md5anim.baseframe-block-unclosed', () => {
|
|
564
|
+
const source = [
|
|
565
|
+
'MD5Version 10',
|
|
566
|
+
'numFrames 1',
|
|
567
|
+
'numJoints 1',
|
|
568
|
+
'frameRate 24',
|
|
569
|
+
'hierarchy {',
|
|
570
|
+
' "root" -1 0 0',
|
|
571
|
+
'}',
|
|
572
|
+
'baseframe {',
|
|
573
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
574
|
+
].join('\n');
|
|
575
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
576
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
577
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.baseframe-block-unclosed');
|
|
578
|
+
expect(crumb).toBeDefined();
|
|
579
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
580
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('drops and reports md5anim.non-numeric-frame-value with firstToken detail', () => {
|
|
584
|
+
const source = [
|
|
585
|
+
'MD5Version 10',
|
|
586
|
+
'numFrames 1',
|
|
587
|
+
'numJoints 1',
|
|
588
|
+
'frameRate 24',
|
|
589
|
+
'hierarchy {',
|
|
590
|
+
' "root" -1 63 0',
|
|
591
|
+
'}',
|
|
592
|
+
'baseframe {',
|
|
593
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
594
|
+
'}',
|
|
595
|
+
'frame 0 {',
|
|
596
|
+
' 1 2 nope 4 5 6',
|
|
597
|
+
'}',
|
|
598
|
+
].join('\n');
|
|
599
|
+
|
|
600
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
601
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
602
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.non-numeric-frame-value');
|
|
603
|
+
expect(crumb).toBeDefined();
|
|
604
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
605
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
606
|
+
expect(crumb!.detail?.count).toBe(1);
|
|
607
|
+
expect(crumb!.detail?.firstToken).toBe('nope');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('recovers and reports md5anim.frame-block-unclosed', () => {
|
|
611
|
+
const source = [
|
|
612
|
+
'MD5Version 10',
|
|
613
|
+
'numFrames 1',
|
|
614
|
+
'numJoints 1',
|
|
615
|
+
'frameRate 24',
|
|
616
|
+
'hierarchy {',
|
|
617
|
+
' "root" -1 0 0',
|
|
618
|
+
'}',
|
|
619
|
+
'baseframe {',
|
|
620
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
621
|
+
'}',
|
|
622
|
+
'frame 0 {',
|
|
623
|
+
' 0 0 0',
|
|
624
|
+
].join('\n');
|
|
625
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
626
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
627
|
+
const crumb = findDiagnostic(diagnostics, 'md5anim.frame-block-unclosed');
|
|
628
|
+
expect(crumb).toBeDefined();
|
|
629
|
+
expect(crumb!.severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
630
|
+
expect(crumb!.origin).toBe('parseMd5Anim');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('aggregates repeated malformed-hierarchy drops into one crumb with a count', () => {
|
|
634
|
+
const source = [
|
|
635
|
+
'MD5Version 10',
|
|
636
|
+
'numFrames 1',
|
|
637
|
+
'numJoints 1',
|
|
638
|
+
'frameRate 24',
|
|
639
|
+
'hierarchy {',
|
|
640
|
+
' bad one',
|
|
641
|
+
' bad two',
|
|
642
|
+
' bad three',
|
|
643
|
+
' "root" -1 0 0',
|
|
644
|
+
'}',
|
|
645
|
+
'baseframe {',
|
|
646
|
+
' ( 0 0 0 ) ( 0 0 0 )',
|
|
647
|
+
'}',
|
|
648
|
+
'frame 0 {',
|
|
649
|
+
'}',
|
|
650
|
+
].join('\n');
|
|
651
|
+
|
|
652
|
+
const diagnostics: ImportDiagnostic[] = [];
|
|
653
|
+
parseMd5Anim(source, makeJoints(1), diagnostics);
|
|
654
|
+
const matching = diagnostics.filter((d) => d.kind === 'md5anim.malformed-hierarchy');
|
|
655
|
+
expect(matching).toHaveLength(1);
|
|
656
|
+
expect(matching[0].detail?.count).toBe(3);
|
|
657
|
+
expect(matching[0].detail?.firstLine).toBe(6);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('emits no diagnostics when no collector array is supplied', () => {
|
|
661
|
+
const source = [
|
|
662
|
+
'MD5Version 11',
|
|
663
|
+
'numFrames 9',
|
|
664
|
+
'numJoints 5',
|
|
665
|
+
'frameRate 0',
|
|
666
|
+
'hierarchy {',
|
|
667
|
+
' bad line',
|
|
668
|
+
' "root" -1 0 0',
|
|
669
|
+
].join('\n');
|
|
670
|
+
// Exercising every crumb path without a sink must not throw and must be side-effect-free.
|
|
671
|
+
expect(() => parseMd5Anim(source, makeJoints(1))).not.toThrow();
|
|
672
|
+
});
|
|
673
|
+
});
|