Skip to main content

mmd_anim_format/vmd/
mod.rs

1use glam::{Quat, Vec3A};
2use serde::{Deserialize, Serialize};
3
4use mmd_anim_runtime::{
5    AnimationClip, BoneAnimationBinding, BoneIndex, InterpolationScalar, InterpolationVector3,
6    MorphAnimationBinding, MorphIndex, MorphKeyframe, MorphTrack, MovableBoneKeyframe,
7    MovableBoneTrack, PropertyAnimationBinding, PropertyKeyframe,
8};
9
10use crate::binary::{
11    ByteReader, write_f32_le as write_f32, write_fixed_bytes, write_u32_le as write_u32,
12};
13use crate::error::ImportError;
14use crate::normalize::normalize_vmd_name;
15use crate::sjis::{decode_sjis_fixed_trimmed, encode_sjis};
16
17mod reduced;
18pub use reduced::{
19    VmdExportMorphKind, VmdExportName, VmdPoseExport, VmdPoseExportBindings, VmdPoseExportError,
20    VmdPoseExportReport, export_reduced_pose_to_vmd,
21};
22
23type Reader<'a> = ByteReader<'a>;
24
25const VMD_MAGIC: [u8; 30] = *b"Vocaloid Motion Data 0002\0\0\0\0\0";
26const VMD_MAGIC_PREFIX: &[u8] = b"Vocaloid Motion Data 0002\0";
27
28impl<'a> ByteReader<'a> {
29    fn read_optional_u32_le(&mut self) -> Result<Option<u32>, ImportError> {
30        if self.remaining() == 0 {
31            Ok(None)
32        } else {
33            self.read_u32_le().map(Some)
34        }
35    }
36
37    fn read_record_count(&mut self, record_size: usize) -> Result<usize, ImportError> {
38        let count = self.read_u32_le()? as usize;
39        self.require_record_bytes(count, record_size)?;
40        Ok(count)
41    }
42
43    fn skip_optional_ignored_records(&mut self, record_size: usize) -> Result<bool, ImportError> {
44        let Some(count) = self.read_optional_u32_le()? else {
45            return Ok(false);
46        };
47        let Some(bytes) = (count as usize).checked_mul(record_size) else {
48            self.pos = self.data.len();
49            return Ok(false);
50        };
51        if bytes > self.remaining() {
52            self.pos = self.data.len();
53            return Ok(false);
54        }
55        self.read_slice(bytes)?;
56        Ok(true)
57    }
58
59    fn read_optional_record_count(
60        &mut self,
61        record_size: usize,
62    ) -> Result<Option<usize>, ImportError> {
63        let Some(count) = self.read_optional_u32_le()? else {
64            return Ok(None);
65        };
66        let count = count as usize;
67        let Some(bytes) = count.checked_mul(record_size) else {
68            self.pos = self.data.len();
69            return Ok(None);
70        };
71        if bytes > self.remaining() {
72            self.pos = self.data.len();
73            return Ok(None);
74        }
75        Ok(Some(count))
76    }
77
78    fn read_quat(&mut self) -> Result<Quat, ImportError> {
79        Ok(Quat::from_xyzw(
80            self.read_f32_le()?,
81            self.read_f32_le()?,
82            self.read_f32_le()?,
83            self.read_f32_le()?,
84        ))
85    }
86
87    fn read_shifts_jis_name(&mut self) -> Result<Vec<u8>, ImportError> {
88        let raw = self.read_slice(15)?;
89        let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
90        Ok(raw[..end].to_vec())
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct VmdHeader {
96    pub model_name_bytes: [u8; 20],
97}
98
99#[derive(Debug, Clone)]
100pub enum VmdBoneImportMode {
101    ByName(Vec<u8>),
102    ByIndex(u32),
103}
104
105#[derive(Debug, Clone)]
106pub struct VmdBoneKeyframeRaw {
107    pub bone_mode: VmdBoneImportMode,
108    pub frame: u32,
109    pub position: Vec3A,
110    pub rotation: Quat,
111    pub interpolation: [u8; 64],
112    pub bone_name_normalized: Vec<u8>,
113}
114
115#[derive(Debug, Clone)]
116pub struct VmdIkEntry {
117    pub name_bytes: Vec<u8>,
118    pub enabled: u8,
119    pub name_normalized: Vec<u8>,
120}
121
122#[derive(Debug, Clone)]
123pub struct VmdPropertyIkFrame {
124    pub frame: u32,
125    pub show: u8,
126    pub entries: Vec<VmdIkEntry>,
127}
128
129#[derive(Debug, Clone)]
130pub struct VmdImportResult {
131    pub bone_keyframes: Vec<VmdBoneKeyframeRaw>,
132    pub morph_keyframes: Vec<(Vec<u8>, u32, f32)>,
133    pub property_keyframes: Vec<PropertyKeyframe>,
134    pub property_ik_frames: Vec<VmdPropertyIkFrame>,
135}
136
137pub fn read_header(data: &[u8]) -> Result<(VmdHeader, usize), ImportError> {
138    let mut r = Reader::new(data);
139
140    let magic = r.read_slice(30)?;
141    if magic != VMD_MAGIC && !magic.starts_with(VMD_MAGIC_PREFIX) {
142        return Err(ImportError::InvalidVmdMagic);
143    }
144
145    let model_name_bytes: [u8; 20] = r
146        .read_slice(20)?
147        .try_into()
148        .map_err(|_| ImportError::InvalidVmdModelName)?;
149
150    Ok((VmdHeader { model_name_bytes }, r.pos))
151}
152
153pub fn import_vmd_motion(data: &[u8]) -> Result<VmdImportResult, ImportError> {
154    let (_header, pos) = read_header(data)?;
155    let mut r = Reader { data, pos };
156
157    let bone_count = r.read_record_count(111)?;
158    let mut bone_keyframes = Vec::with_capacity(bone_count);
159    for _ in 0..bone_count {
160        let bone_name = r.read_shifts_jis_name()?;
161        let frame = r.read_u32_le()?;
162        let position = r.read_vec3()?;
163        let rotation = r.read_quat()?;
164        let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
165
166        let bone_name_normalized = normalize_vmd_name(&bone_name);
167
168        bone_keyframes.push(VmdBoneKeyframeRaw {
169            bone_mode: VmdBoneImportMode::ByName(bone_name),
170            frame,
171            position,
172            rotation,
173            interpolation,
174            bone_name_normalized,
175        });
176    }
177
178    let Some(morph_count) = r.read_optional_u32_le()? else {
179        return Ok(VmdImportResult {
180            bone_keyframes,
181            morph_keyframes: Vec::new(),
182            property_keyframes: Vec::new(),
183            property_ik_frames: Vec::new(),
184        });
185    };
186    let morph_count = morph_count as usize;
187    r.require_record_bytes(morph_count, 23)?;
188    let mut morph_keyframes = Vec::with_capacity(morph_count);
189    for _ in 0..morph_count {
190        let morph_name = r.read_shifts_jis_name()?;
191        let frame = r.read_u32_le()?;
192        let weight = r.read_f32_le()?;
193        morph_keyframes.push((morph_name, frame, weight));
194    }
195
196    if !r.skip_optional_ignored_records(61)? {
197        return Ok(VmdImportResult {
198            bone_keyframes,
199            morph_keyframes,
200            property_keyframes: Vec::new(),
201            property_ik_frames: Vec::new(),
202        });
203    }
204
205    if !r.skip_optional_ignored_records(28)? {
206        return Ok(VmdImportResult {
207            bone_keyframes,
208            morph_keyframes,
209            property_keyframes: Vec::new(),
210            property_ik_frames: Vec::new(),
211        });
212    }
213
214    if !r.skip_optional_ignored_records(9)? {
215        return Ok(VmdImportResult {
216            bone_keyframes,
217            morph_keyframes,
218            property_keyframes: Vec::new(),
219            property_ik_frames: Vec::new(),
220        });
221    }
222
223    let Some(show_ik_count) = r.read_optional_u32_le()? else {
224        return Ok(VmdImportResult {
225            bone_keyframes,
226            morph_keyframes,
227            property_keyframes: Vec::new(),
228            property_ik_frames: Vec::new(),
229        });
230    };
231    let show_ik_count = show_ik_count as usize;
232    r.require_record_bytes(show_ik_count, 9)?;
233    let mut property_keyframes = Vec::with_capacity(show_ik_count);
234    let mut property_ik_frames = Vec::with_capacity(show_ik_count);
235    for _ in 0..show_ik_count {
236        let frame = r.read_u32_le()?;
237        let show = r.read_u8()?;
238        let ik_count = r.read_u32_le()? as usize;
239        r.require_record_bytes(ik_count, 21)?;
240        let mut ik_enabled = Vec::with_capacity(ik_count);
241        let mut ik_entries = Vec::with_capacity(ik_count);
242        for _ in 0..ik_count {
243            let ik_name_bytes = r.read_slice(20)?.to_vec();
244            let enabled = r.read_u8()?;
245            let end = ik_name_bytes
246                .iter()
247                .position(|&b| b == 0)
248                .unwrap_or(ik_name_bytes.len());
249            let name_normalized = normalize_vmd_name(&ik_name_bytes[..end]);
250            ik_enabled.push(enabled);
251            ik_entries.push(VmdIkEntry {
252                name_bytes: ik_name_bytes,
253                enabled,
254                name_normalized,
255            });
256        }
257        property_keyframes.push(PropertyKeyframe::new(
258            frame,
259            ik_enabled.iter().map(|&b| b != 0).collect(),
260        ));
261        property_ik_frames.push(VmdPropertyIkFrame {
262            frame,
263            show,
264            entries: ik_entries,
265        });
266    }
267
268    Ok(VmdImportResult {
269        bone_keyframes,
270        morph_keyframes,
271        property_keyframes,
272        property_ik_frames,
273    })
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct VmdParsedAnimation {
279    #[serde(default = "default_vmd_kind", skip_deserializing)]
280    pub kind: &'static str,
281    pub metadata: VmdParsedMetadata,
282    pub bone_frames: Vec<VmdParsedBoneFrame>,
283    pub morph_frames: Vec<VmdParsedMorphFrame>,
284    pub camera_frames: Vec<VmdParsedCameraFrame>,
285    pub light_frames: Vec<VmdParsedLightFrame>,
286    pub self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
287    pub property_frames: Vec<VmdParsedPropertyFrame>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct VmdParsedMetadata {
293    #[serde(default = "default_vmd_format", skip_deserializing)]
294    pub format: &'static str,
295    pub model_name: String,
296    #[serde(default, skip_serializing_if = "Vec::is_empty")]
297    pub model_name_bytes: Vec<u8>,
298    pub counts: VmdParsedCounts,
299    pub max_frame: u32,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct VmdParsedCounts {
304    pub bones: usize,
305    pub morphs: usize,
306    pub cameras: usize,
307    pub lights: usize,
308    #[serde(rename = "selfShadows")]
309    pub self_shadows: usize,
310    pub properties: usize,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct VmdParsedBoneFrame {
316    pub bone_name: String,
317    #[serde(default, skip_serializing_if = "Vec::is_empty")]
318    pub bone_name_bytes: Vec<u8>,
319    pub frame: u32,
320    pub translation: [f32; 3],
321    pub rotation: [f32; 4],
322    pub interpolation: Vec<u8>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327pub struct VmdParsedMorphFrame {
328    pub morph_name: String,
329    #[serde(default, skip_serializing_if = "Vec::is_empty")]
330    pub morph_name_bytes: Vec<u8>,
331    pub frame: u32,
332    pub weight: f32,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct VmdParsedCameraFrame {
338    pub frame: u32,
339    pub distance: f32,
340    pub position: [f32; 3],
341    pub rotation: [f32; 3],
342    pub interpolation: [u8; 24],
343    pub fov: u32,
344    pub perspective: bool,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct VmdCameraState {
350    pub distance: f32,
351    pub position: [f32; 3],
352    pub rotation: [f32; 3],
353    pub fov: f32,
354    pub perspective: bool,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct VmdParsedLightFrame {
360    pub frame: u32,
361    pub color: [f32; 3],
362    pub direction: [f32; 3],
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct VmdLightState {
368    pub color: [f32; 3],
369    pub direction: [f32; 3],
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374pub struct VmdParsedSelfShadowFrame {
375    pub frame: u32,
376    pub mode: u8,
377    pub distance: f32,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct VmdSelfShadowState {
383    pub mode: u8,
384    pub distance: f32,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct VmdParsedPropertyFrame {
390    pub frame: u32,
391    pub visible: bool,
392    pub ik_states: Vec<VmdParsedIkState>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct VmdParsedIkState {
398    pub bone_name: String,
399    #[serde(default, skip_serializing_if = "Vec::is_empty")]
400    pub bone_name_bytes: Vec<u8>,
401    pub enabled: bool,
402}
403
404pub fn parse_vmd_animation(data: &[u8]) -> Result<VmdParsedAnimation, ImportError> {
405    let (_header, pos) = read_header(data)?;
406    let mut r = Reader { data, pos };
407    let model_name = decode_sjis_fixed(&_header.model_name_bytes);
408    let model_name_bytes = trim_fixed_bytes(&_header.model_name_bytes).to_vec();
409    let mut max_frame = 0u32;
410
411    let bone_count = r.read_record_count(111)?;
412    let mut bone_frames = Vec::with_capacity(bone_count);
413    for _ in 0..bone_count {
414        let bone_name_bytes = r.read_slice(15)?;
415        let bone_name = decode_sjis_fixed(bone_name_bytes);
416        let frame = r.read_u32_le()?;
417        max_frame = max_frame.max(frame);
418        let position = r.read_vec3()?;
419        let rotation = r.read_quat()?;
420        let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
421        bone_frames.push(VmdParsedBoneFrame {
422            bone_name,
423            bone_name_bytes: trim_fixed_bytes(bone_name_bytes).to_vec(),
424            frame,
425            translation: [position.x, position.y, position.z],
426            rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
427            interpolation: interpolation.to_vec(),
428        });
429    }
430
431    let Some(morph_count) = r.read_optional_u32_le()? else {
432        return Ok(vmd_parsed_animation(
433            model_name,
434            model_name_bytes,
435            max_frame,
436            VmdParsedSections {
437                bone_frames,
438                morph_frames: Vec::new(),
439                camera_frames: Vec::new(),
440                light_frames: Vec::new(),
441                self_shadow_frames: Vec::new(),
442                property_frames: Vec::new(),
443            },
444        ));
445    };
446    let morph_count = morph_count as usize;
447    r.require_record_bytes(morph_count, 23)?;
448    let mut morph_frames = Vec::with_capacity(morph_count);
449    for _ in 0..morph_count {
450        let morph_name_bytes = r.read_slice(15)?;
451        let morph_name = decode_sjis_fixed(morph_name_bytes);
452        let frame = r.read_u32_le()?;
453        max_frame = max_frame.max(frame);
454        morph_frames.push(VmdParsedMorphFrame {
455            morph_name,
456            morph_name_bytes: trim_fixed_bytes(morph_name_bytes).to_vec(),
457            frame,
458            weight: r.read_f32_le()?,
459        });
460    }
461
462    let camera_frames = read_parsed_camera_frames(&mut r, &mut max_frame)?;
463    let light_frames = read_parsed_light_frames(&mut r, &mut max_frame)?;
464    let self_shadow_frames = read_parsed_self_shadow_frames(&mut r, &mut max_frame)?;
465    let property_frames = read_parsed_property_frames(&mut r, &mut max_frame)?;
466
467    Ok(vmd_parsed_animation(
468        model_name,
469        model_name_bytes,
470        max_frame,
471        VmdParsedSections {
472            bone_frames,
473            morph_frames,
474            camera_frames,
475            light_frames,
476            self_shadow_frames,
477            property_frames,
478        },
479    ))
480}
481
482fn default_vmd_kind() -> &'static str {
483    "vmd"
484}
485
486fn default_vmd_format() -> &'static str {
487    "vmd"
488}
489
490pub fn export_vmd_animation(animation: &VmdParsedAnimation) -> Vec<u8> {
491    let mut out = Vec::new();
492    out.extend_from_slice(&VMD_MAGIC);
493    write_fixed_name_bytes(
494        &mut out,
495        &animation.metadata.model_name,
496        &animation.metadata.model_name_bytes,
497        20,
498    );
499
500    write_u32(&mut out, animation.bone_frames.len() as u32);
501    for frame in &animation.bone_frames {
502        write_fixed_name_bytes(&mut out, &frame.bone_name, &frame.bone_name_bytes, 15);
503        write_u32(&mut out, frame.frame);
504        write_f32(&mut out, frame.translation[0]);
505        write_f32(&mut out, frame.translation[1]);
506        write_f32(&mut out, frame.translation[2]);
507        write_f32(&mut out, frame.rotation[0]);
508        write_f32(&mut out, frame.rotation[1]);
509        write_f32(&mut out, frame.rotation[2]);
510        write_f32(&mut out, frame.rotation[3]);
511        write_fixed_bytes(&mut out, &frame.interpolation, 64);
512    }
513
514    write_u32(&mut out, animation.morph_frames.len() as u32);
515    for frame in &animation.morph_frames {
516        write_fixed_name_bytes(&mut out, &frame.morph_name, &frame.morph_name_bytes, 15);
517        write_u32(&mut out, frame.frame);
518        write_f32(&mut out, frame.weight);
519    }
520
521    write_u32(&mut out, animation.camera_frames.len() as u32);
522    for frame in &animation.camera_frames {
523        write_u32(&mut out, frame.frame);
524        write_f32(&mut out, frame.distance);
525        write_f32(&mut out, frame.position[0]);
526        write_f32(&mut out, frame.position[1]);
527        write_f32(&mut out, frame.position[2]);
528        write_f32(&mut out, frame.rotation[0]);
529        write_f32(&mut out, frame.rotation[1]);
530        write_f32(&mut out, frame.rotation[2]);
531        out.extend_from_slice(&frame.interpolation);
532        write_u32(&mut out, frame.fov);
533        out.push(if frame.perspective { 0 } else { 1 });
534    }
535
536    write_u32(&mut out, animation.light_frames.len() as u32);
537    for frame in &animation.light_frames {
538        write_u32(&mut out, frame.frame);
539        write_f32(&mut out, frame.color[0]);
540        write_f32(&mut out, frame.color[1]);
541        write_f32(&mut out, frame.color[2]);
542        write_f32(&mut out, frame.direction[0]);
543        write_f32(&mut out, frame.direction[1]);
544        write_f32(&mut out, frame.direction[2]);
545    }
546
547    write_u32(&mut out, animation.self_shadow_frames.len() as u32);
548    for frame in &animation.self_shadow_frames {
549        write_u32(&mut out, frame.frame);
550        out.push(frame.mode);
551        write_f32(&mut out, frame.distance);
552    }
553
554    write_u32(&mut out, animation.property_frames.len() as u32);
555    for frame in &animation.property_frames {
556        write_u32(&mut out, frame.frame);
557        out.push(u8::from(frame.visible));
558        write_u32(&mut out, frame.ik_states.len() as u32);
559        for state in &frame.ik_states {
560            write_fixed_name_bytes(&mut out, &state.bone_name, &state.bone_name_bytes, 20);
561            out.push(u8::from(state.enabled));
562        }
563    }
564
565    out
566}
567
568struct VmdParsedSections {
569    bone_frames: Vec<VmdParsedBoneFrame>,
570    morph_frames: Vec<VmdParsedMorphFrame>,
571    camera_frames: Vec<VmdParsedCameraFrame>,
572    light_frames: Vec<VmdParsedLightFrame>,
573    self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
574    property_frames: Vec<VmdParsedPropertyFrame>,
575}
576
577fn vmd_parsed_animation(
578    model_name: String,
579    model_name_bytes: Vec<u8>,
580    max_frame: u32,
581    sections: VmdParsedSections,
582) -> VmdParsedAnimation {
583    VmdParsedAnimation {
584        kind: "vmd",
585        metadata: VmdParsedMetadata {
586            format: "vmd",
587            model_name,
588            model_name_bytes,
589            counts: VmdParsedCounts {
590                bones: sections.bone_frames.len(),
591                morphs: sections.morph_frames.len(),
592                cameras: sections.camera_frames.len(),
593                lights: sections.light_frames.len(),
594                self_shadows: sections.self_shadow_frames.len(),
595                properties: sections.property_frames.len(),
596            },
597            max_frame,
598        },
599        bone_frames: sections.bone_frames,
600        morph_frames: sections.morph_frames,
601        camera_frames: sections.camera_frames,
602        light_frames: sections.light_frames,
603        self_shadow_frames: sections.self_shadow_frames,
604        property_frames: sections.property_frames,
605    }
606}
607
608fn read_parsed_camera_frames(
609    r: &mut Reader<'_>,
610    max_frame: &mut u32,
611) -> Result<Vec<VmdParsedCameraFrame>, ImportError> {
612    let Some(count) = r.read_optional_record_count(61)? else {
613        return Ok(Vec::new());
614    };
615    let mut frames = Vec::with_capacity(count);
616    for _ in 0..count {
617        let frame = r.read_u32_le()?;
618        *max_frame = (*max_frame).max(frame);
619        frames.push(VmdParsedCameraFrame {
620            frame,
621            distance: r.read_f32_le()?,
622            position: {
623                let p = r.read_vec3()?;
624                [p.x, p.y, p.z]
625            },
626            rotation: {
627                let p = r.read_vec3()?;
628                [p.x, p.y, p.z]
629            },
630            interpolation: r.read_slice(24)?.try_into().unwrap(),
631            fov: r.read_u32_le()?,
632            perspective: r.read_u8()? == 0,
633        });
634    }
635    Ok(frames)
636}
637
638fn read_parsed_light_frames(
639    r: &mut Reader<'_>,
640    max_frame: &mut u32,
641) -> Result<Vec<VmdParsedLightFrame>, ImportError> {
642    let Some(count) = r.read_optional_record_count(28)? else {
643        return Ok(Vec::new());
644    };
645    let mut frames = Vec::with_capacity(count);
646    for _ in 0..count {
647        let frame = r.read_u32_le()?;
648        *max_frame = (*max_frame).max(frame);
649        let color = r.read_vec3()?;
650        let direction = r.read_vec3()?;
651        frames.push(VmdParsedLightFrame {
652            frame,
653            color: [color.x, color.y, color.z],
654            direction: [direction.x, direction.y, direction.z],
655        });
656    }
657    Ok(frames)
658}
659
660fn read_parsed_self_shadow_frames(
661    r: &mut Reader<'_>,
662    max_frame: &mut u32,
663) -> Result<Vec<VmdParsedSelfShadowFrame>, ImportError> {
664    let Some(count) = r.read_optional_record_count(9)? else {
665        return Ok(Vec::new());
666    };
667    let mut frames = Vec::with_capacity(count);
668    for _ in 0..count {
669        let frame = r.read_u32_le()?;
670        *max_frame = (*max_frame).max(frame);
671        frames.push(VmdParsedSelfShadowFrame {
672            frame,
673            mode: r.read_u8()?,
674            distance: r.read_f32_le()?,
675        });
676    }
677    Ok(frames)
678}
679
680fn read_parsed_property_frames(
681    r: &mut Reader<'_>,
682    max_frame: &mut u32,
683) -> Result<Vec<VmdParsedPropertyFrame>, ImportError> {
684    let Some(count) = r.read_optional_u32_le()? else {
685        return Ok(Vec::new());
686    };
687    let count = count as usize;
688    r.require_record_bytes(count, 9)?;
689    let mut frames = Vec::with_capacity(count);
690    for _ in 0..count {
691        let frame = r.read_u32_le()?;
692        *max_frame = (*max_frame).max(frame);
693        let visible = r.read_u8()? != 0;
694        let ik_count = r.read_u32_le()? as usize;
695        r.require_record_bytes(ik_count, 21)?;
696        let mut ik_states = Vec::with_capacity(ik_count);
697        for _ in 0..ik_count {
698            let bone_name_bytes = r.read_slice(20)?;
699            ik_states.push(VmdParsedIkState {
700                bone_name: decode_sjis_fixed(bone_name_bytes),
701                bone_name_bytes: trim_fixed_bytes(bone_name_bytes).to_vec(),
702                enabled: r.read_u8()? != 0,
703            });
704        }
705        frames.push(VmdParsedPropertyFrame {
706            frame,
707            visible,
708            ik_states,
709        });
710    }
711    Ok(frames)
712}
713
714pub fn sample_vmd_camera_frames(
715    frames: &[VmdParsedCameraFrame],
716    frame: f32,
717) -> Option<VmdCameraState> {
718    if frames.is_empty() {
719        return None;
720    }
721
722    let mut sorted: Vec<&VmdParsedCameraFrame> = frames.iter().collect();
723    sorted.sort_by_key(|keyframe| keyframe.frame);
724
725    let mut index = 0usize;
726    while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
727        index += 1;
728    }
729
730    let previous = sorted[index];
731    let next = sorted.get(index + 1).copied().unwrap_or(previous);
732    let t = interpolation_ratio(previous.frame, next.frame, frame);
733    let interpolation = decode_camera_interpolation(&next.interpolation);
734
735    let distance_t = interpolation.distance.evaluate(t);
736    let position_x_t = interpolation.position.x.evaluate(t);
737    let position_y_t = interpolation.position.y.evaluate(t);
738    let position_z_t = interpolation.position.z.evaluate(t);
739    let rotation_t = interpolation.rotation.evaluate(t);
740    let fov_t = interpolation.fov.evaluate(t);
741
742    Some(VmdCameraState {
743        distance: lerp(previous.distance, next.distance, distance_t),
744        position: [
745            lerp(previous.position[0], next.position[0], position_x_t),
746            lerp(previous.position[1], next.position[1], position_y_t),
747            lerp(previous.position[2], next.position[2], position_z_t),
748        ],
749        rotation: [
750            lerp(previous.rotation[0], next.rotation[0], rotation_t),
751            lerp(previous.rotation[1], next.rotation[1], rotation_t),
752            lerp(previous.rotation[2], next.rotation[2], rotation_t),
753        ],
754        fov: lerp(previous.fov as f32, next.fov as f32, fov_t),
755        perspective: if t < 1.0 {
756            previous.perspective
757        } else {
758            next.perspective
759        },
760    })
761}
762
763pub fn sample_vmd_light_frames(
764    frames: &[VmdParsedLightFrame],
765    frame: f32,
766) -> Option<VmdLightState> {
767    if frames.is_empty() {
768        return None;
769    }
770
771    let mut sorted: Vec<&VmdParsedLightFrame> = frames.iter().collect();
772    sorted.sort_by_key(|keyframe| keyframe.frame);
773
774    let mut index = 0usize;
775    while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
776        index += 1;
777    }
778
779    let previous = sorted[index];
780    let next = sorted.get(index + 1).copied().unwrap_or(previous);
781    let t = interpolation_ratio(previous.frame, next.frame, frame);
782
783    Some(VmdLightState {
784        color: [
785            lerp(previous.color[0], next.color[0], t),
786            lerp(previous.color[1], next.color[1], t),
787            lerp(previous.color[2], next.color[2], t),
788        ],
789        direction: [
790            lerp(previous.direction[0], next.direction[0], t),
791            lerp(previous.direction[1], next.direction[1], t),
792            lerp(previous.direction[2], next.direction[2], t),
793        ],
794    })
795}
796
797pub fn sample_vmd_self_shadow_frames(
798    frames: &[VmdParsedSelfShadowFrame],
799    frame: f32,
800) -> Option<VmdSelfShadowState> {
801    if frames.is_empty() {
802        return None;
803    }
804
805    let mut sorted: Vec<&VmdParsedSelfShadowFrame> = frames.iter().collect();
806    sorted.sort_by_key(|keyframe| keyframe.frame);
807
808    let mut index = 0usize;
809    while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
810        index += 1;
811    }
812
813    let previous = sorted[index];
814    let next = sorted.get(index + 1).copied().unwrap_or(previous);
815    let t = interpolation_ratio(previous.frame, next.frame, frame);
816
817    Some(VmdSelfShadowState {
818        mode: if t < 1.0 { previous.mode } else { next.mode },
819        distance: lerp(previous.distance, next.distance, t),
820    })
821}
822
823struct CameraInterpolation {
824    position: InterpolationVector3,
825    rotation: InterpolationScalar,
826    distance: InterpolationScalar,
827    fov: InterpolationScalar,
828}
829
830fn decode_camera_interpolation(interpolation: &[u8; 24]) -> CameraInterpolation {
831    CameraInterpolation {
832        position: InterpolationVector3 {
833            x: decode_camera_interpolation_scalar(interpolation, 0),
834            y: decode_camera_interpolation_scalar(interpolation, 1),
835            z: decode_camera_interpolation_scalar(interpolation, 2),
836        },
837        rotation: decode_camera_interpolation_scalar(interpolation, 3),
838        distance: decode_camera_interpolation_scalar(interpolation, 4),
839        fov: decode_camera_interpolation_scalar(interpolation, 5),
840    }
841}
842
843fn decode_camera_interpolation_scalar(
844    interpolation: &[u8; 24],
845    channel: usize,
846) -> InterpolationScalar {
847    let offset = channel * 4;
848    decode_interpolation_scalar([
849        interpolation[offset],
850        interpolation[offset + 1],
851        interpolation[offset + 2],
852        interpolation[offset + 3],
853    ])
854}
855
856fn interpolation_ratio(previous_frame: u32, next_frame: u32, frame: f32) -> f32 {
857    if next_frame <= previous_frame {
858        return 0.0;
859    }
860    let span = next_frame - previous_frame;
861    if span <= 1 {
862        return if frame >= next_frame as f32 { 1.0 } else { 0.0 };
863    }
864    ((frame - previous_frame as f32) / span as f32).clamp(0.0, 1.0)
865}
866
867fn lerp(a: f32, b: f32, t: f32) -> f32 {
868    a + (b - a) * t
869}
870
871fn decode_sjis_fixed(bytes: &[u8]) -> String {
872    decode_sjis_fixed_trimmed(bytes)
873}
874
875fn trim_fixed_bytes(bytes: &[u8]) -> &[u8] {
876    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
877    &bytes[..end]
878}
879
880fn write_fixed_name_bytes(out: &mut Vec<u8>, value: &str, raw_bytes: &[u8], len: usize) {
881    if raw_bytes.is_empty() {
882        let encoded = encode_sjis(value);
883        write_fixed_bytes(out, &encoded, len);
884    } else {
885        write_fixed_bytes(out, raw_bytes, len);
886    }
887}
888
889fn decode_interpolation_scalar(data: [u8; 4]) -> InterpolationScalar {
890    InterpolationScalar {
891        x1: data[0].min(127),
892        y1: data[1].min(127),
893        x2: data[2].min(127),
894        y2: data[3].min(127),
895    }
896}
897
898fn decode_bone_interpolation(
899    interpolation: &[u8; 64],
900) -> (InterpolationVector3, InterpolationScalar) {
901    let position = InterpolationVector3 {
902        x: decode_interpolation_scalar([
903            interpolation[0],
904            interpolation[4],
905            interpolation[8],
906            interpolation[12],
907        ]),
908        y: decode_interpolation_scalar([
909            interpolation[1],
910            interpolation[5],
911            interpolation[9],
912            interpolation[13],
913        ]),
914        z: decode_interpolation_scalar([
915            interpolation[2],
916            interpolation[6],
917            interpolation[10],
918            interpolation[14],
919        ]),
920    };
921    let rotation = decode_interpolation_scalar([
922        interpolation[3],
923        interpolation[7],
924        interpolation[11],
925        interpolation[15],
926    ]);
927    (position, rotation)
928}
929
930pub fn build_clip_from_import(
931    result: VmdImportResult,
932    bone_name_to_index: &dyn Fn(&[u8]) -> Option<BoneIndex>,
933    morph_name_to_index: &dyn Fn(&[u8]) -> Option<MorphIndex>,
934) -> AnimationClip {
935    let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
936        std::collections::BTreeMap::new();
937
938    for kf in result.bone_keyframes {
939        let bone_index = match &kf.bone_mode {
940            VmdBoneImportMode::ByName(_name) => {
941                match bone_name_to_index(&kf.bone_name_normalized) {
942                    Some(idx) => idx,
943                    None => continue,
944                }
945            }
946            VmdBoneImportMode::ByIndex(idx) => BoneIndex(*idx),
947        };
948
949        let (pos_interp, rot_interp) = decode_bone_interpolation(&kf.interpolation);
950
951        bone_tracks_map
952            .entry(bone_index.0)
953            .or_default()
954            .push(MovableBoneKeyframe {
955                frame: kf.frame,
956                position: kf.position,
957                rotation: kf.rotation,
958                position_interpolation: pos_interp,
959                rotation_interpolation: rot_interp,
960            });
961    }
962
963    let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
964        .into_iter()
965        .map(|(bone_idx, kfs)| BoneAnimationBinding {
966            bone: BoneIndex(bone_idx),
967            track: MovableBoneTrack::from_keyframes(kfs),
968        })
969        .collect();
970
971    let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
972        std::collections::BTreeMap::new();
973
974    for (morph_name, frame, weight) in result.morph_keyframes {
975        let morph_name_normalized = normalize_vmd_name(&morph_name);
976        let morph_index = match morph_name_to_index(&morph_name_normalized) {
977            Some(idx) => idx,
978            None => continue,
979        };
980        morph_tracks_map
981            .entry(morph_index.0)
982            .or_default()
983            .push(MorphKeyframe::new(frame, weight));
984    }
985
986    let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
987        .into_iter()
988        .map(|(morph_idx, kfs)| MorphAnimationBinding {
989            morph: MorphIndex(morph_idx),
990            track: MorphTrack::from_keyframes(kfs),
991        })
992        .collect();
993
994    let property_track = if result.property_keyframes.is_empty() {
995        None
996    } else {
997        Some(PropertyAnimationBinding::from_keyframes(
998            result.property_keyframes,
999        ))
1000    };
1001
1002    AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1003}
1004
1005pub fn build_property_binding_with_ik_resolver(
1006    ik_frames: &[VmdPropertyIkFrame],
1007    ik_name_to_solver_index: &dyn Fn(&[u8]) -> Option<usize>,
1008    solver_count: usize,
1009) -> Option<PropertyAnimationBinding> {
1010    if solver_count == 0 || ik_frames.is_empty() {
1011        return None;
1012    }
1013
1014    let keyframes: Vec<PropertyKeyframe> = ik_frames
1015        .iter()
1016        .map(|frame| {
1017            let mut ik_enabled = vec![1u8; solver_count];
1018            for entry in &frame.entries {
1019                match ik_name_to_solver_index(&entry.name_normalized) {
1020                    Some(idx) if idx < solver_count => {
1021                        ik_enabled[idx] = entry.enabled;
1022                    }
1023                    _ => {}
1024                }
1025            }
1026            PropertyKeyframe {
1027                frame: frame.frame,
1028                ik_enabled: ik_enabled.into_boxed_slice(),
1029            }
1030        })
1031        .collect();
1032
1033    Some(PropertyAnimationBinding::from_keyframes(keyframes))
1034}
1035
1036/// Options for controlling VMD clip construction behavior.
1037#[derive(Debug, Clone, Copy)]
1038pub struct VmdClipBuildOptions {
1039    /// When `true` (the default), the property IK enable/disable data from the
1040    /// VMD is baked into the clip. When `false`, the clip omits the property
1041    /// track entirely, so all IK solvers remain at their runtime default
1042    /// (enabled) state — useful when comparing against toolchain outputs that
1043    /// do not preserve property IK.
1044    pub honor_property_ik: bool,
1045}
1046
1047impl Default for VmdClipBuildOptions {
1048    fn default() -> Self {
1049        Self {
1050            honor_property_ik: true,
1051        }
1052    }
1053}
1054
1055pub fn build_pair_clip(
1056    result: &VmdImportResult,
1057    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1058    morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1059    ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1060    solver_count: usize,
1061) -> AnimationClip {
1062    build_pair_clip_with_options(
1063        result,
1064        bone_name_to_index,
1065        morph_name_to_index,
1066        ik_solver_bone_name_to_index,
1067        solver_count,
1068        VmdClipBuildOptions::default(),
1069    )
1070}
1071
1072pub fn build_pair_clip_with_options(
1073    result: &VmdImportResult,
1074    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1075    morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1076    ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1077    solver_count: usize,
1078    options: VmdClipBuildOptions,
1079) -> AnimationClip {
1080    let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
1081        std::collections::BTreeMap::new();
1082
1083    for kf in &result.bone_keyframes {
1084        let bone_index = match bone_name_to_index.get(&kf.bone_name_normalized) {
1085            Some(idx) => *idx,
1086            None => continue,
1087        };
1088
1089        let (pos_interp, rot_interp) = decode_bone_interpolation(&kf.interpolation);
1090
1091        bone_tracks_map
1092            .entry(bone_index.0)
1093            .or_default()
1094            .push(MovableBoneKeyframe {
1095                frame: kf.frame,
1096                position: kf.position,
1097                rotation: kf.rotation,
1098                position_interpolation: pos_interp,
1099                rotation_interpolation: rot_interp,
1100            });
1101    }
1102
1103    let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
1104        .into_iter()
1105        .map(|(bone_idx, kfs)| BoneAnimationBinding {
1106            bone: BoneIndex(bone_idx),
1107            track: MovableBoneTrack::from_keyframes(kfs),
1108        })
1109        .collect();
1110
1111    let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
1112        std::collections::BTreeMap::new();
1113
1114    for (morph_name, frame, weight) in &result.morph_keyframes {
1115        let morph_name_normalized = normalize_vmd_name(morph_name);
1116        let morph_index = match morph_name_to_index.get(&morph_name_normalized) {
1117            Some(idx) => *idx,
1118            None => continue,
1119        };
1120        morph_tracks_map
1121            .entry(morph_index.0)
1122            .or_default()
1123            .push(MorphKeyframe::new(*frame, *weight));
1124    }
1125
1126    let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
1127        .into_iter()
1128        .map(|(morph_idx, kfs)| MorphAnimationBinding {
1129            morph: MorphIndex(morph_idx),
1130            track: MorphTrack::from_keyframes(kfs),
1131        })
1132        .collect();
1133
1134    let property_track = if options.honor_property_ik {
1135        build_property_binding_with_ik_resolver(
1136            &result.property_ik_frames,
1137            &|name| ik_solver_bone_name_to_index.get(name).copied(),
1138            solver_count,
1139        )
1140    } else {
1141        None
1142    };
1143
1144    AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149    use super::*;
1150    #[allow(unused_imports)]
1151    use serde_json;
1152
1153    fn build_vmd_header_bytes() -> Vec<u8> {
1154        let mut buf = Vec::new();
1155        buf.extend_from_slice(&VMD_MAGIC);
1156        buf.extend_from_slice(&[0u8; 20]);
1157        buf
1158    }
1159
1160    #[test]
1161    fn rejects_impossible_vmd_bone_count_before_allocation() {
1162        let mut buf = build_vmd_header_bytes();
1163        buf.extend_from_slice(&u32::MAX.to_le_bytes());
1164
1165        assert!(matches!(
1166            import_vmd_motion(&buf),
1167            Err(ImportError::UnexpectedEof(_))
1168        ));
1169        assert!(matches!(
1170            parse_vmd_animation(&buf),
1171            Err(ImportError::UnexpectedEof(_))
1172        ));
1173    }
1174
1175    #[test]
1176    fn rejects_impossible_vmd_property_ik_count_before_allocation() {
1177        let mut buf = build_vmd_header_bytes();
1178        buf.extend_from_slice(&0u32.to_le_bytes()); // bones
1179        buf.extend_from_slice(&0u32.to_le_bytes()); // morphs
1180        buf.extend_from_slice(&0u32.to_le_bytes()); // cameras
1181        buf.extend_from_slice(&0u32.to_le_bytes()); // lights
1182        buf.extend_from_slice(&0u32.to_le_bytes()); // self shadows
1183        buf.extend_from_slice(&1u32.to_le_bytes()); // property frames
1184        buf.extend_from_slice(&0u32.to_le_bytes()); // frame
1185        buf.push(1); // visible
1186        buf.extend_from_slice(&u32::MAX.to_le_bytes()); // IK entries
1187
1188        assert!(matches!(
1189            import_vmd_motion(&buf),
1190            Err(ImportError::UnexpectedEof(_))
1191        ));
1192        assert!(matches!(
1193            parse_vmd_animation(&buf),
1194            Err(ImportError::UnexpectedEof(_))
1195        ));
1196    }
1197
1198    #[test]
1199    fn parses_vmd_header() {
1200        let header_bytes = build_vmd_header_bytes();
1201        let (header, pos) = read_header(&header_bytes).unwrap();
1202        assert_eq!(header.model_name_bytes, [0u8; 20]);
1203        assert!(pos > 0);
1204    }
1205
1206    #[test]
1207    fn rejects_bad_vmd_magic() {
1208        let mut buf = build_vmd_header_bytes();
1209        buf[0] = 0xFF;
1210        assert_eq!(read_header(&buf).unwrap_err(), ImportError::InvalidVmdMagic);
1211    }
1212
1213    #[test]
1214    fn accepts_vmd_magic_with_nonzero_padding_bytes() {
1215        let mut buf = build_vmd_header_bytes();
1216        buf[26..30].copy_from_slice(b"JKLM");
1217
1218        let (_header, pos) = read_header(&buf).unwrap();
1219        assert_eq!(pos, 50);
1220    }
1221
1222    #[test]
1223    fn parses_minimal_vmd_motion() {
1224        let mut buf = build_vmd_header_bytes();
1225
1226        buf.extend_from_slice(&0u32.to_le_bytes());
1227        buf.extend_from_slice(&0u32.to_le_bytes());
1228        buf.extend_from_slice(&0u32.to_le_bytes());
1229        buf.extend_from_slice(&0u32.to_le_bytes());
1230        buf.extend_from_slice(&0u32.to_le_bytes());
1231        buf.extend_from_slice(&0u32.to_le_bytes());
1232
1233        let result = import_vmd_motion(&buf).unwrap();
1234        assert!(result.bone_keyframes.is_empty());
1235        assert!(result.morph_keyframes.is_empty());
1236        assert!(result.property_keyframes.is_empty());
1237    }
1238
1239    #[test]
1240    fn accepts_vmd_without_optional_tail_sections() {
1241        let mut buf = build_vmd_header_bytes();
1242        buf.extend_from_slice(&0u32.to_le_bytes());
1243
1244        let result = import_vmd_motion(&buf).unwrap();
1245        assert!(result.bone_keyframes.is_empty());
1246        assert!(result.morph_keyframes.is_empty());
1247        assert!(result.property_keyframes.is_empty());
1248        assert!(result.property_ik_frames.is_empty());
1249    }
1250
1251    #[test]
1252    fn accepts_vmd_ending_after_shadow_section() {
1253        let mut buf = build_vmd_header_bytes();
1254        buf.extend_from_slice(&0u32.to_le_bytes());
1255        buf.extend_from_slice(&0u32.to_le_bytes());
1256        buf.extend_from_slice(&0u32.to_le_bytes());
1257        buf.extend_from_slice(&0u32.to_le_bytes());
1258        buf.extend_from_slice(&0u32.to_le_bytes());
1259
1260        let result = import_vmd_motion(&buf).unwrap();
1261        assert!(result.bone_keyframes.is_empty());
1262        assert!(result.morph_keyframes.is_empty());
1263        assert!(result.property_keyframes.is_empty());
1264        assert!(result.property_ik_frames.is_empty());
1265    }
1266
1267    #[test]
1268    fn rejects_partial_optional_count() {
1269        let mut buf = build_vmd_header_bytes();
1270        buf.extend_from_slice(&0u32.to_le_bytes());
1271        buf.extend_from_slice(&[1, 2, 3]);
1272
1273        assert_eq!(
1274            import_vmd_motion(&buf).unwrap_err(),
1275            ImportError::UnexpectedEof(1)
1276        );
1277    }
1278
1279    #[test]
1280    fn ignores_truncated_unused_tail_sections() {
1281        let mut buf = build_vmd_header_bytes();
1282        buf.extend_from_slice(&0u32.to_le_bytes());
1283        buf.extend_from_slice(&0u32.to_le_bytes());
1284        buf.extend_from_slice(&0u32.to_le_bytes());
1285        buf.extend_from_slice(&0u32.to_le_bytes());
1286        buf.extend_from_slice(&10u32.to_le_bytes());
1287        buf.extend_from_slice(&[0u8; 8]);
1288
1289        let result = import_vmd_motion(&buf).unwrap();
1290        assert!(result.bone_keyframes.is_empty());
1291        assert!(result.morph_keyframes.is_empty());
1292        assert!(result.property_keyframes.is_empty());
1293        assert!(result.property_ik_frames.is_empty());
1294    }
1295
1296    #[test]
1297    fn parsed_animation_ignores_implausible_optional_tail() {
1298        let mut buf = build_vmd_header_bytes();
1299        buf.extend_from_slice(&0u32.to_le_bytes()); // bones
1300        buf.extend_from_slice(&0u32.to_le_bytes()); // morphs
1301        buf.extend_from_slice(&0u32.to_le_bytes()); // camera count
1302
1303        // Some real-world VMD files contain a 61-byte camera-shaped tail after
1304        // a zero camera count. Treat it as an optional malformed tail instead
1305        // of failing the parser/exporter gate.
1306        buf.extend_from_slice(&300u32.to_le_bytes());
1307        buf.extend_from_slice(&[0u8; 57]);
1308
1309        let parsed = parse_vmd_animation(&buf).unwrap();
1310        assert!(parsed.camera_frames.is_empty());
1311        assert!(parsed.light_frames.is_empty());
1312        assert!(parsed.self_shadow_frames.is_empty());
1313        assert!(parsed.property_frames.is_empty());
1314    }
1315
1316    #[test]
1317    fn parses_single_bone_keyframe() {
1318        let mut buf = build_vmd_header_bytes();
1319
1320        buf.extend_from_slice(&1u32.to_le_bytes());
1321        let mut bone_name = [0u8; 15];
1322        bone_name[..4].copy_from_slice(b"Bone");
1323        buf.extend_from_slice(&bone_name);
1324        buf.extend_from_slice(&10u32.to_le_bytes());
1325        buf.extend_from_slice(&1.0f32.to_le_bytes());
1326        buf.extend_from_slice(&2.0f32.to_le_bytes());
1327        buf.extend_from_slice(&3.0f32.to_le_bytes());
1328        buf.extend_from_slice(&0.0f32.to_le_bytes());
1329        buf.extend_from_slice(&0.0f32.to_le_bytes());
1330        buf.extend_from_slice(&0.0f32.to_le_bytes());
1331        buf.extend_from_slice(&1.0f32.to_le_bytes());
1332        buf.extend_from_slice(&[20u8; 64]);
1333
1334        buf.extend_from_slice(&0u32.to_le_bytes());
1335        buf.extend_from_slice(&0u32.to_le_bytes());
1336        buf.extend_from_slice(&0u32.to_le_bytes());
1337        buf.extend_from_slice(&0u32.to_le_bytes());
1338        buf.extend_from_slice(&0u32.to_le_bytes());
1339
1340        let result = import_vmd_motion(&buf).unwrap();
1341        assert_eq!(result.bone_keyframes.len(), 1);
1342        assert_eq!(result.bone_keyframes[0].frame, 10);
1343        assert!((result.bone_keyframes[0].position.x - 1.0).abs() < 0.001);
1344        assert!((result.bone_keyframes[0].position.y - 2.0).abs() < 0.001);
1345        assert!((result.bone_keyframes[0].position.z - 3.0).abs() < 0.001);
1346        assert_eq!(&result.bone_keyframes[0].bone_name_normalized[..], b"Bone");
1347    }
1348
1349    #[test]
1350    fn exports_parsed_vmd_animation_for_roundtrip() {
1351        let mut buf = build_vmd_header_bytes();
1352
1353        buf.extend_from_slice(&1u32.to_le_bytes());
1354        let mut bone_name = [0u8; 15];
1355        bone_name[..4].copy_from_slice(b"Bone");
1356        buf.extend_from_slice(&bone_name);
1357        buf.extend_from_slice(&10u32.to_le_bytes());
1358        buf.extend_from_slice(&1.0f32.to_le_bytes());
1359        buf.extend_from_slice(&2.0f32.to_le_bytes());
1360        buf.extend_from_slice(&3.0f32.to_le_bytes());
1361        buf.extend_from_slice(&0.1f32.to_le_bytes());
1362        buf.extend_from_slice(&0.2f32.to_le_bytes());
1363        buf.extend_from_slice(&0.3f32.to_le_bytes());
1364        buf.extend_from_slice(&0.4f32.to_le_bytes());
1365        buf.extend_from_slice(&[20u8; 64]);
1366
1367        buf.extend_from_slice(&1u32.to_le_bytes());
1368        let mut morph_name = [0u8; 15];
1369        morph_name[..5].copy_from_slice(b"Smile");
1370        buf.extend_from_slice(&morph_name);
1371        buf.extend_from_slice(&11u32.to_le_bytes());
1372        buf.extend_from_slice(&0.75f32.to_le_bytes());
1373
1374        buf.extend_from_slice(&1u32.to_le_bytes());
1375        buf.extend_from_slice(&12u32.to_le_bytes());
1376        buf.extend_from_slice(&30.0f32.to_le_bytes());
1377        buf.extend_from_slice(&1.0f32.to_le_bytes());
1378        buf.extend_from_slice(&2.0f32.to_le_bytes());
1379        buf.extend_from_slice(&3.0f32.to_le_bytes());
1380        buf.extend_from_slice(&0.1f32.to_le_bytes());
1381        buf.extend_from_slice(&0.2f32.to_le_bytes());
1382        buf.extend_from_slice(&0.3f32.to_le_bytes());
1383        buf.extend_from_slice(&[30u8; 24]);
1384        buf.extend_from_slice(&45u32.to_le_bytes());
1385        buf.push(0);
1386
1387        buf.extend_from_slice(&1u32.to_le_bytes());
1388        buf.extend_from_slice(&13u32.to_le_bytes());
1389        buf.extend_from_slice(&1.0f32.to_le_bytes());
1390        buf.extend_from_slice(&0.5f32.to_le_bytes());
1391        buf.extend_from_slice(&0.25f32.to_le_bytes());
1392        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
1393        buf.extend_from_slice(&(-0.5f32).to_le_bytes());
1394        buf.extend_from_slice(&(-0.25f32).to_le_bytes());
1395
1396        buf.extend_from_slice(&1u32.to_le_bytes());
1397        buf.extend_from_slice(&14u32.to_le_bytes());
1398        buf.push(2);
1399        buf.extend_from_slice(&0.6f32.to_le_bytes());
1400
1401        buf.extend_from_slice(&1u32.to_le_bytes());
1402        buf.extend_from_slice(&15u32.to_le_bytes());
1403        buf.push(1);
1404        buf.extend_from_slice(&1u32.to_le_bytes());
1405        let mut ik_name = [0u8; 20];
1406        ik_name[..2].copy_from_slice(b"IK");
1407        buf.extend_from_slice(&ik_name);
1408        buf.push(1);
1409
1410        let parsed = parse_vmd_animation(&buf).unwrap();
1411        let exported = export_vmd_animation(&parsed);
1412        let reparsed = parse_vmd_animation(&exported).unwrap();
1413
1414        assert_vmd_roundtrip_eq(&parsed, &reparsed);
1415    }
1416
1417    fn assert_vmd_roundtrip_eq(left: &VmdParsedAnimation, right: &VmdParsedAnimation) {
1418        assert_eq!(left.metadata.model_name, right.metadata.model_name);
1419        assert_eq!(left.metadata.max_frame, right.metadata.max_frame);
1420        assert_eq!(left.bone_frames.len(), right.bone_frames.len());
1421        assert_eq!(left.morph_frames.len(), right.morph_frames.len());
1422        assert_eq!(left.camera_frames.len(), right.camera_frames.len());
1423        assert_eq!(left.light_frames.len(), right.light_frames.len());
1424        assert_eq!(
1425            left.self_shadow_frames.len(),
1426            right.self_shadow_frames.len()
1427        );
1428        assert_eq!(left.property_frames.len(), right.property_frames.len());
1429        assert_eq!(
1430            left.bone_frames[0].bone_name,
1431            right.bone_frames[0].bone_name
1432        );
1433        assert_eq!(left.bone_frames[0].frame, right.bone_frames[0].frame);
1434        assert_eq!(
1435            left.bone_frames[0].translation,
1436            right.bone_frames[0].translation
1437        );
1438        assert_eq!(left.bone_frames[0].rotation, right.bone_frames[0].rotation);
1439        assert_eq!(
1440            left.bone_frames[0].interpolation,
1441            right.bone_frames[0].interpolation
1442        );
1443        assert_eq!(
1444            left.morph_frames[0].morph_name,
1445            right.morph_frames[0].morph_name
1446        );
1447        assert_eq!(left.morph_frames[0].frame, right.morph_frames[0].frame);
1448        assert_eq!(left.morph_frames[0].weight, right.morph_frames[0].weight);
1449        assert_eq!(left.camera_frames[0].frame, right.camera_frames[0].frame);
1450        assert_eq!(
1451            left.camera_frames[0].position,
1452            right.camera_frames[0].position
1453        );
1454        assert_eq!(
1455            left.camera_frames[0].rotation,
1456            right.camera_frames[0].rotation
1457        );
1458        assert_eq!(
1459            left.camera_frames[0].interpolation,
1460            right.camera_frames[0].interpolation
1461        );
1462        assert_eq!(
1463            left.camera_frames[0].perspective,
1464            right.camera_frames[0].perspective
1465        );
1466        assert_eq!(left.light_frames[0].color, right.light_frames[0].color);
1467        assert_eq!(
1468            left.light_frames[0].direction,
1469            right.light_frames[0].direction
1470        );
1471        assert_eq!(
1472            left.self_shadow_frames[0].mode,
1473            right.self_shadow_frames[0].mode
1474        );
1475        assert_eq!(
1476            left.self_shadow_frames[0].distance,
1477            right.self_shadow_frames[0].distance
1478        );
1479        assert_eq!(
1480            left.property_frames[0].visible,
1481            right.property_frames[0].visible
1482        );
1483        assert_eq!(
1484            left.property_frames[0].ik_states[0].bone_name,
1485            right.property_frames[0].ik_states[0].bone_name
1486        );
1487        assert_eq!(
1488            left.property_frames[0].ik_states[0].enabled,
1489            right.property_frames[0].ik_states[0].enabled
1490        );
1491    }
1492
1493    #[test]
1494    fn decodes_raw_vmd_bone_interpolation_as_strided_curves() {
1495        let mut interpolation = [0u8; 64];
1496        for (index, value) in interpolation.iter_mut().enumerate().take(16) {
1497            *value = index as u8;
1498        }
1499
1500        let (position, rotation) = decode_bone_interpolation(&interpolation);
1501
1502        assert_eq!(
1503            position.x,
1504            InterpolationScalar {
1505                x1: 0,
1506                y1: 4,
1507                x2: 8,
1508                y2: 12
1509            }
1510        );
1511        assert_eq!(
1512            position.y,
1513            InterpolationScalar {
1514                x1: 1,
1515                y1: 5,
1516                x2: 9,
1517                y2: 13
1518            }
1519        );
1520        assert_eq!(
1521            position.z,
1522            InterpolationScalar {
1523                x1: 2,
1524                y1: 6,
1525                x2: 10,
1526                y2: 14
1527            }
1528        );
1529        assert_eq!(
1530            rotation,
1531            InterpolationScalar {
1532                x1: 3,
1533                y1: 7,
1534                x2: 11,
1535                y2: 15
1536            }
1537        );
1538    }
1539
1540    #[test]
1541    fn decodes_raw_vmd_camera_interpolation_as_contiguous_curves() {
1542        let mut interpolation = [0u8; 24];
1543        for (index, value) in interpolation.iter_mut().enumerate() {
1544            *value = index as u8;
1545        }
1546
1547        let decoded = decode_camera_interpolation(&interpolation);
1548
1549        assert_eq!(
1550            decoded.position.x,
1551            InterpolationScalar {
1552                x1: 0,
1553                y1: 1,
1554                x2: 2,
1555                y2: 3
1556            }
1557        );
1558        assert_eq!(
1559            decoded.position.y,
1560            InterpolationScalar {
1561                x1: 4,
1562                y1: 5,
1563                x2: 6,
1564                y2: 7
1565            }
1566        );
1567        assert_eq!(
1568            decoded.position.z,
1569            InterpolationScalar {
1570                x1: 8,
1571                y1: 9,
1572                x2: 10,
1573                y2: 11
1574            }
1575        );
1576        assert_eq!(
1577            decoded.rotation,
1578            InterpolationScalar {
1579                x1: 12,
1580                y1: 13,
1581                x2: 14,
1582                y2: 15
1583            }
1584        );
1585        assert_eq!(
1586            decoded.distance,
1587            InterpolationScalar {
1588                x1: 16,
1589                y1: 17,
1590                x2: 18,
1591                y2: 19
1592            }
1593        );
1594        assert_eq!(
1595            decoded.fov,
1596            InterpolationScalar {
1597                x1: 20,
1598                y1: 21,
1599                x2: 22,
1600                y2: 23
1601            }
1602        );
1603    }
1604
1605    #[test]
1606    fn skips_camera_light_shadow_and_reads_ik_property_names() {
1607        let mut buf = build_vmd_header_bytes();
1608
1609        buf.extend_from_slice(&0u32.to_le_bytes());
1610        buf.extend_from_slice(&0u32.to_le_bytes());
1611        buf.extend_from_slice(&1u32.to_le_bytes());
1612        buf.extend_from_slice(&[0u8; 61]);
1613        buf.extend_from_slice(&1u32.to_le_bytes());
1614        buf.extend_from_slice(&[0u8; 28]);
1615        buf.extend_from_slice(&1u32.to_le_bytes());
1616        buf.extend_from_slice(&0u32.to_le_bytes());
1617        buf.push(1);
1618        buf.extend_from_slice(&0.5f32.to_le_bytes());
1619        buf.extend_from_slice(&1u32.to_le_bytes());
1620        buf.extend_from_slice(&24u32.to_le_bytes());
1621        buf.push(1);
1622        buf.extend_from_slice(&1u32.to_le_bytes());
1623        let mut ik_name = [0u8; 20];
1624        ik_name[..6].copy_from_slice(b"LeftIK");
1625        buf.extend_from_slice(&ik_name);
1626        buf.push(0);
1627
1628        let result = import_vmd_motion(&buf).unwrap();
1629        assert_eq!(result.property_keyframes.len(), 1);
1630        assert_eq!(result.property_keyframes[0].frame, 24);
1631        assert_eq!(&*result.property_keyframes[0].ik_enabled, &[0]);
1632
1633        assert_eq!(result.property_ik_frames.len(), 1);
1634        assert_eq!(result.property_ik_frames[0].frame, 24);
1635        assert_eq!(result.property_ik_frames[0].show, 1);
1636        assert_eq!(result.property_ik_frames[0].entries.len(), 1);
1637        let name_end = result.property_ik_frames[0].entries[0]
1638            .name_bytes
1639            .iter()
1640            .position(|&b| b == 0)
1641            .unwrap_or(20);
1642        assert_eq!(
1643            &result.property_ik_frames[0].entries[0].name_bytes[..name_end],
1644            b"LeftIK"
1645        );
1646        assert_eq!(result.property_ik_frames[0].entries[0].enabled, 0);
1647        assert_eq!(
1648            &result.property_ik_frames[0].entries[0].name_normalized[..],
1649            b"LeftIK"
1650        );
1651    }
1652
1653    #[test]
1654    fn builds_clip_from_bone_track() {
1655        let kfs = vec![
1656            VmdBoneKeyframeRaw {
1657                bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
1658                frame: 0,
1659                position: Vec3A::ZERO,
1660                rotation: Quat::IDENTITY,
1661                interpolation: [
1662                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
1663                    107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
1664                    107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1665                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1666                ],
1667                bone_name_normalized: b"BoneA".to_vec(),
1668            },
1669            VmdBoneKeyframeRaw {
1670                bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
1671                frame: 30,
1672                position: Vec3A::new(10.0, 0.0, 0.0),
1673                rotation: Quat::IDENTITY,
1674                interpolation: [
1675                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
1676                    107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
1677                    107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1678                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
1679                ],
1680                bone_name_normalized: b"BoneA".to_vec(),
1681            },
1682        ];
1683
1684        fn lookup(name: &[u8]) -> Option<BoneIndex> {
1685            if name == b"BoneA" {
1686                Some(BoneIndex(0))
1687            } else {
1688                None
1689            }
1690        }
1691        fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
1692            None
1693        }
1694
1695        let result = VmdImportResult {
1696            bone_keyframes: kfs,
1697            morph_keyframes: Vec::new(),
1698            property_keyframes: Vec::new(),
1699            property_ik_frames: Vec::new(),
1700        };
1701
1702        let _clip = build_clip_from_import(result, &lookup, &morph_lookup);
1703    }
1704
1705    #[test]
1706    fn build_clip_from_import_resolves_morph_by_normalized_name() {
1707        let sjis_morph = vec![0x83, 0x65, 0x83, 0x58, 0x83, 0x67];
1708        let result = VmdImportResult {
1709            bone_keyframes: Vec::new(),
1710            morph_keyframes: vec![(sjis_morph, 0, 1.0)],
1711            property_keyframes: Vec::new(),
1712            property_ik_frames: Vec::new(),
1713        };
1714
1715        fn morph_lookup(name: &[u8]) -> Option<MorphIndex> {
1716            if name == b"\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88" {
1717                Some(MorphIndex(0))
1718            } else {
1719                None
1720            }
1721        }
1722        fn bone_lookup(_name: &[u8]) -> Option<BoneIndex> {
1723            None
1724        }
1725
1726        let clip = build_clip_from_import(result, &bone_lookup, &morph_lookup);
1727        assert_eq!(clip.morph_track_count(), 1);
1728    }
1729
1730    fn ik_name_bytes(name: &str) -> [u8; 20] {
1731        let mut buf = [0u8; 20];
1732        let name_bytes = name.as_bytes();
1733        let len = name_bytes.len().min(20);
1734        buf[..len].copy_from_slice(&name_bytes[..len]);
1735        buf
1736    }
1737
1738    #[test]
1739    fn parses_property_ik_entry_names() {
1740        let mut buf = build_vmd_header_bytes();
1741
1742        buf.extend_from_slice(&0u32.to_le_bytes());
1743        buf.extend_from_slice(&0u32.to_le_bytes());
1744        buf.extend_from_slice(&0u32.to_le_bytes());
1745        buf.extend_from_slice(&0u32.to_le_bytes());
1746        buf.extend_from_slice(&0u32.to_le_bytes());
1747        buf.extend_from_slice(&1u32.to_le_bytes());
1748        buf.extend_from_slice(&10u32.to_le_bytes());
1749        buf.push(0);
1750        buf.extend_from_slice(&2u32.to_le_bytes());
1751        buf.extend_from_slice(&ik_name_bytes("LeftLegIK"));
1752        buf.push(1);
1753        buf.extend_from_slice(&ik_name_bytes("RightLegIK"));
1754        buf.push(0);
1755
1756        let result = import_vmd_motion(&buf).unwrap();
1757        assert_eq!(result.property_ik_frames.len(), 1);
1758        assert_eq!(result.property_ik_frames[0].frame, 10);
1759        assert_eq!(result.property_ik_frames[0].entries.len(), 2);
1760
1761        let name1: Vec<u8> = result.property_ik_frames[0].entries[0]
1762            .name_bytes
1763            .iter()
1764            .copied()
1765            .take_while(|&b| b != 0)
1766            .collect();
1767        assert_eq!(&name1[..], b"LeftLegIK");
1768        assert_eq!(result.property_ik_frames[0].entries[0].enabled, 1);
1769        assert_eq!(
1770            &result.property_ik_frames[0].entries[0].name_normalized[..],
1771            b"LeftLegIK"
1772        );
1773
1774        let name2: Vec<u8> = result.property_ik_frames[0].entries[1]
1775            .name_bytes
1776            .iter()
1777            .copied()
1778            .take_while(|&b| b != 0)
1779            .collect();
1780        assert_eq!(&name2[..], b"RightLegIK");
1781        assert_eq!(result.property_ik_frames[0].entries[1].enabled, 0);
1782    }
1783
1784    #[test]
1785    fn reorders_ik_enabled_to_solver_order() {
1786        let frames = vec![VmdPropertyIkFrame {
1787            frame: 0,
1788            show: 0,
1789            entries: vec![
1790                VmdIkEntry {
1791                    name_bytes: {
1792                        let mut buf = [0u8; 20];
1793                        buf[..9].copy_from_slice(b"RightLegI");
1794                        buf.to_vec()
1795                    },
1796                    enabled: 1,
1797                    name_normalized: b"RightLegI".to_vec(),
1798                },
1799                VmdIkEntry {
1800                    name_bytes: {
1801                        let mut buf = [0u8; 20];
1802                        buf[..8].copy_from_slice(b"LeftLegI");
1803                        buf.to_vec()
1804                    },
1805                    enabled: 0,
1806                    name_normalized: b"LeftLegI".to_vec(),
1807                },
1808            ],
1809        }];
1810
1811        fn ik_resolver(name: &[u8]) -> Option<usize> {
1812            match name {
1813                b"LeftLegI" => Some(0),
1814                b"RightLegI" => Some(1),
1815                _ => None,
1816            }
1817        }
1818
1819        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
1820        let sample = binding.sample(0.0).unwrap();
1821        assert_eq!(sample, &[0, 1]);
1822    }
1823
1824    #[test]
1825    fn unknown_and_unmentioned_ik_names_keep_default_enabled_state() {
1826        let frames = vec![VmdPropertyIkFrame {
1827            frame: 5,
1828            show: 0,
1829            entries: vec![
1830                VmdIkEntry {
1831                    name_bytes: {
1832                        let mut buf = [0u8; 20];
1833                        buf[..7].copy_from_slice(b"KnownIK");
1834                        buf.to_vec()
1835                    },
1836                    enabled: 1,
1837                    name_normalized: b"KnownIK".to_vec(),
1838                },
1839                VmdIkEntry {
1840                    name_bytes: {
1841                        let mut buf = [0u8; 20];
1842                        buf[..9].copy_from_slice(b"UnknownIK");
1843                        buf.to_vec()
1844                    },
1845                    enabled: 1,
1846                    name_normalized: b"UnknownIK".to_vec(),
1847                },
1848            ],
1849        }];
1850
1851        fn ik_resolver(name: &[u8]) -> Option<usize> {
1852            if name == b"KnownIK" { Some(0) } else { None }
1853        }
1854
1855        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
1856        let sample = binding.sample(5.0).unwrap();
1857        assert_eq!(sample, &[1, 1]);
1858    }
1859
1860    #[test]
1861    fn empty_ik_frames_returns_none() {
1862        fn ik_resolver(_name: &[u8]) -> Option<usize> {
1863            None
1864        }
1865        let binding = build_property_binding_with_ik_resolver(&[], &ik_resolver, 2);
1866        assert!(binding.is_none());
1867    }
1868
1869    #[test]
1870    fn zero_solver_count_returns_none() {
1871        let frames = vec![VmdPropertyIkFrame {
1872            frame: 0,
1873            show: 0,
1874            entries: vec![VmdIkEntry {
1875                name_bytes: ik_name_bytes("IK").to_vec(),
1876                enabled: 1,
1877                name_normalized: b"IK".to_vec(),
1878            }],
1879        }];
1880        fn ik_resolver(_name: &[u8]) -> Option<usize> {
1881            Some(0)
1882        }
1883        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 0);
1884        assert!(binding.is_none());
1885    }
1886
1887    #[test]
1888    fn build_property_binding_resolves_ik_by_normalized_name() {
1889        let frames = vec![VmdPropertyIkFrame {
1890            frame: 0,
1891            show: 0,
1892            entries: vec![VmdIkEntry {
1893                name_bytes: vec![0x8D, 0xB6, 0x91, 0xAB],
1894                enabled: 1,
1895                name_normalized: vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3],
1896            }],
1897        }];
1898
1899        fn ik_resolver(name: &[u8]) -> Option<usize> {
1900            if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
1901                Some(0)
1902            } else {
1903                None
1904            }
1905        }
1906
1907        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 1).unwrap();
1908        let sample = binding.sample(0.0).unwrap();
1909        assert_eq!(sample, &[1]);
1910    }
1911
1912    #[test]
1913    fn build_clip_from_import_resolves_bone_by_normalized_name() {
1914        let sjis_name: Vec<u8> = vec![0x8D, 0xB6, 0x91, 0xAB];
1915        let utf8_name: Vec<u8> = vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
1916
1917        let kf = VmdBoneKeyframeRaw {
1918            bone_mode: VmdBoneImportMode::ByName(sjis_name),
1919            frame: 0,
1920            position: Vec3A::ZERO,
1921            rotation: Quat::IDENTITY,
1922            interpolation: [20u8; 64],
1923            bone_name_normalized: utf8_name.clone(),
1924        };
1925
1926        fn lookup(name: &[u8]) -> Option<BoneIndex> {
1927            if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
1928                Some(BoneIndex(0))
1929            } else {
1930                None
1931            }
1932        }
1933        fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
1934            None
1935        }
1936
1937        let result = VmdImportResult {
1938            bone_keyframes: vec![kf],
1939            morph_keyframes: Vec::new(),
1940            property_keyframes: Vec::new(),
1941            property_ik_frames: Vec::new(),
1942        };
1943
1944        let clip = build_clip_from_import(result, &lookup, &morph_lookup);
1945        assert_eq!(
1946            clip.bone_track_count(),
1947            1,
1948            "bone should be resolved via normalized UTF-8 name, not raw Shift-JIS bytes"
1949        );
1950    }
1951
1952    #[test]
1953    fn japanese_vmd_name_matches_pmx_utf8_name_via_normalization() {
1954        let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
1955        let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
1956
1957        let mut pmx_buf = Vec::new();
1958        pmx_buf.extend_from_slice(b"PMX ");
1959        pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
1960        pmx_buf.push(8);
1961        pmx_buf.push(1);
1962        pmx_buf.push(0);
1963        pmx_buf.push(4);
1964        pmx_buf.push(1);
1965        pmx_buf.push(1);
1966        pmx_buf.push(2);
1967        pmx_buf.push(1);
1968        pmx_buf.push(1);
1969        for _ in 0..4 {
1970            pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1971        }
1972        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1973        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1974        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1975        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1976        pmx_buf.extend_from_slice(&1i32.to_le_bytes());
1977        pmx_buf.extend_from_slice(&(utf8_name.len() as i32).to_le_bytes());
1978        pmx_buf.extend_from_slice(utf8_name);
1979        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1980        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1981        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1982        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1983        pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
1984        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1985        pmx_buf.extend_from_slice(&0u16.to_le_bytes());
1986        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1987        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1988        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
1989        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
1990
1991        let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
1992
1993        let mut vmd_buf = build_vmd_header_bytes();
1994        vmd_buf.extend_from_slice(&1u32.to_le_bytes());
1995        let mut bone_name = [0u8; 15];
1996        bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
1997        vmd_buf.extend_from_slice(&bone_name);
1998        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
1999        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2000        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2001        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2002        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2003        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2004        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2005        vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
2006        vmd_buf.extend_from_slice(&[20u8; 64]);
2007        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2008        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2009        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2010        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2011        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2012
2013        let vmd = import_vmd_motion(&vmd_buf).unwrap();
2014
2015        let normalized = normalize_vmd_name(sjis_name);
2016        assert_eq!(&normalized[..], utf8_name);
2017
2018        assert!(
2019            pmx.bone_name_to_index.contains_key(&normalized),
2020            "PMX should have normalized bone name key"
2021        );
2022
2023        let clip = build_pair_clip(
2024            &vmd,
2025            &pmx.bone_name_to_index,
2026            &pmx.morph_name_to_index,
2027            &pmx.ik_solver_bone_name_to_index,
2028            pmx.model.ik_count(),
2029        );
2030
2031        assert_eq!(clip.bone_track_count(), 1);
2032        assert_eq!(clip.morph_track_count(), 0);
2033        assert!(!clip.has_property_track());
2034    }
2035
2036    #[test]
2037    fn japanese_vmd_name_matches_pmx_utf16le_name_via_decoded_key() {
2038        let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
2039        let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
2040        let utf16le_name: Vec<u8> = "\u{5DE6}\u{8DB3}"
2041            .encode_utf16()
2042            .flat_map(u16::to_le_bytes)
2043            .collect();
2044
2045        let mut pmx_buf = Vec::new();
2046        pmx_buf.extend_from_slice(b"PMX ");
2047        pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
2048        pmx_buf.push(8);
2049        pmx_buf.push(0);
2050        pmx_buf.push(0);
2051        pmx_buf.push(4);
2052        pmx_buf.push(1);
2053        pmx_buf.push(1);
2054        pmx_buf.push(2);
2055        pmx_buf.push(1);
2056        pmx_buf.push(1);
2057        for _ in 0..4 {
2058            pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2059        }
2060        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2061        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2062        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2063        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2064        pmx_buf.extend_from_slice(&1i32.to_le_bytes());
2065        pmx_buf.extend_from_slice(&(utf16le_name.len() as i32).to_le_bytes());
2066        pmx_buf.extend_from_slice(&utf16le_name);
2067        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2068        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2069        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2070        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2071        pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
2072        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2073        pmx_buf.extend_from_slice(&0u16.to_le_bytes());
2074        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2075        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2076        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
2077        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
2078
2079        let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
2080
2081        let mut vmd_buf = build_vmd_header_bytes();
2082        vmd_buf.extend_from_slice(&1u32.to_le_bytes());
2083        let mut bone_name = [0u8; 15];
2084        bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
2085        vmd_buf.extend_from_slice(&bone_name);
2086        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
2087        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2088        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2089        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2090        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2091        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2092        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
2093        vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
2094        vmd_buf.extend_from_slice(&[20u8; 64]);
2095
2096        let vmd = import_vmd_motion(&vmd_buf).unwrap();
2097        let normalized = normalize_vmd_name(sjis_name);
2098        assert_eq!(&normalized[..], utf8_name);
2099        assert!(
2100            pmx.bone_name_to_index.contains_key(&utf16le_name),
2101            "PMX should retain the raw UTF-16LE name key"
2102        );
2103        assert!(
2104            pmx.bone_name_to_index.contains_key(&normalized),
2105            "PMX should also index UTF-16LE names by decoded UTF-8 bytes"
2106        );
2107
2108        let clip = build_pair_clip(
2109            &vmd,
2110            &pmx.bone_name_to_index,
2111            &pmx.morph_name_to_index,
2112            &pmx.ik_solver_bone_name_to_index,
2113            pmx.model.ik_count(),
2114        );
2115
2116        assert_eq!(clip.bone_track_count(), 1);
2117    }
2118
2119    #[test]
2120    fn build_pair_clip_default_includes_property_ik() {
2121        // VMD with one property IK frame that disables "LeftLegIK" at frame 0.
2122        let result = VmdImportResult {
2123            bone_keyframes: Vec::new(),
2124            morph_keyframes: Vec::new(),
2125            property_keyframes: Vec::new(),
2126            property_ik_frames: vec![VmdPropertyIkFrame {
2127                frame: 0,
2128                show: 0,
2129                entries: vec![VmdIkEntry {
2130                    name_bytes: ik_name_bytes("LeftLegIK").to_vec(),
2131                    enabled: 0,
2132                    name_normalized: b"LeftLegIK".to_vec(),
2133                }],
2134            }],
2135        };
2136
2137        let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
2138            std::collections::HashMap::new();
2139        let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
2140            std::collections::HashMap::new();
2141        let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
2142            std::collections::HashMap::new();
2143        ik_solver_bone_name_to_index.insert(b"LeftLegIK".to_vec(), 0);
2144
2145        let clip = build_pair_clip(
2146            &result,
2147            &bone_name_to_index,
2148            &morph_name_to_index,
2149            &ik_solver_bone_name_to_index,
2150            2, // 2 solvers total
2151        );
2152
2153        assert!(
2154            clip.has_property_track(),
2155            "default build_pair_clip should include property IK track"
2156        );
2157    }
2158
2159    #[test]
2160    fn build_pair_clip_with_options_omits_property_ik() {
2161        // Same VMD data, but with honor_property_ik: false => no property track.
2162        let result = VmdImportResult {
2163            bone_keyframes: Vec::new(),
2164            morph_keyframes: Vec::new(),
2165            property_keyframes: Vec::new(),
2166            property_ik_frames: vec![VmdPropertyIkFrame {
2167                frame: 5,
2168                show: 0,
2169                entries: vec![VmdIkEntry {
2170                    name_bytes: ik_name_bytes("RightLegIK").to_vec(),
2171                    enabled: 0,
2172                    name_normalized: b"RightLegIK".to_vec(),
2173                }],
2174            }],
2175        };
2176
2177        let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
2178            std::collections::HashMap::new();
2179        let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
2180            std::collections::HashMap::new();
2181        let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
2182            std::collections::HashMap::new();
2183        ik_solver_bone_name_to_index.insert(b"RightLegIK".to_vec(), 1);
2184
2185        let clip = build_pair_clip_with_options(
2186            &result,
2187            &bone_name_to_index,
2188            &morph_name_to_index,
2189            &ik_solver_bone_name_to_index,
2190            2,
2191            VmdClipBuildOptions {
2192                honor_property_ik: false,
2193            },
2194        );
2195
2196        assert!(
2197            !clip.has_property_track(),
2198            "build_pair_clip_with_options(honor_property_ik: false) should omit property IK track"
2199        );
2200    }
2201
2202    // ---------------------------------------------------------------------------
2203    // Synthetic roundtrip fixtures (CI gate for P0 Exporter Roundtrip Gate)
2204    // ---------------------------------------------------------------------------
2205
2206    /// Build a minimal VMD binary from plain components so tests carry no external files.
2207    type SyntheticBoneFrame<'a> = (&'a str, u32, [f32; 3], [f32; 4], [u8; 64]);
2208    type SyntheticMorphFrame<'a> = (&'a str, u32, f32);
2209    type SyntheticPropertyFrame<'a> = (&'a [(&'a str, bool)], u32, bool);
2210
2211    fn assert_near(actual: f32, expected: f32) {
2212        let delta = (actual - expected).abs();
2213        assert!(
2214            delta < 1.0e-4,
2215            "actual={actual:?} expected={expected:?} delta={delta:?}"
2216        );
2217    }
2218
2219    fn assert_vec3_near(actual: [f32; 3], expected: [f32; 3]) {
2220        for (actual, expected) in actual.into_iter().zip(expected) {
2221            assert_near(actual, expected);
2222        }
2223    }
2224
2225    fn simple_camera_vmd_fixture() -> &'static [u8] {
2226        include_bytes!("../../fixtures/vmd/simple_camera.vmd")
2227    }
2228
2229    fn make_vmd_bytes(
2230        model_name_ascii: &str,
2231        bone_frames: &[SyntheticBoneFrame<'_>],
2232        morph_frames: &[SyntheticMorphFrame<'_>],
2233        property_frames: &[SyntheticPropertyFrame<'_>],
2234    ) -> Vec<u8> {
2235        let mut v = Vec::new();
2236        v.extend_from_slice(&VMD_MAGIC);
2237
2238        let mut name_buf = [0u8; 20];
2239        let nb = model_name_ascii.as_bytes();
2240        name_buf[..nb.len().min(20)].copy_from_slice(&nb[..nb.len().min(20)]);
2241        v.extend_from_slice(&name_buf);
2242
2243        v.extend_from_slice(&(bone_frames.len() as u32).to_le_bytes());
2244        for (name, frame, trans, rot, interp) in bone_frames {
2245            let mut nb = [0u8; 15];
2246            let b = name.as_bytes();
2247            nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
2248            v.extend_from_slice(&nb);
2249            v.extend_from_slice(&frame.to_le_bytes());
2250            for &x in trans {
2251                v.extend_from_slice(&x.to_le_bytes());
2252            }
2253            for &x in rot {
2254                v.extend_from_slice(&x.to_le_bytes());
2255            }
2256            v.extend_from_slice(interp);
2257        }
2258
2259        v.extend_from_slice(&(morph_frames.len() as u32).to_le_bytes());
2260        for (name, frame, weight) in morph_frames {
2261            let mut nb = [0u8; 15];
2262            let b = name.as_bytes();
2263            nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
2264            v.extend_from_slice(&nb);
2265            v.extend_from_slice(&frame.to_le_bytes());
2266            v.extend_from_slice(&weight.to_le_bytes());
2267        }
2268
2269        // camera / light / self-shadow: 0 frames each
2270        v.extend_from_slice(&0u32.to_le_bytes());
2271        v.extend_from_slice(&0u32.to_le_bytes());
2272        v.extend_from_slice(&0u32.to_le_bytes());
2273
2274        v.extend_from_slice(&(property_frames.len() as u32).to_le_bytes());
2275        for (ik_states, frame, visible) in property_frames {
2276            v.extend_from_slice(&frame.to_le_bytes());
2277            v.push(u8::from(*visible));
2278            v.extend_from_slice(&(ik_states.len() as u32).to_le_bytes());
2279            for (ik_name, enabled) in *ik_states {
2280                let mut nb = [0u8; 20];
2281                let b = ik_name.as_bytes();
2282                nb[..b.len().min(20)].copy_from_slice(&b[..b.len().min(20)]);
2283                v.extend_from_slice(&nb);
2284                v.push(u8::from(*enabled));
2285            }
2286        }
2287        v
2288    }
2289
2290    fn json_keys(value: &serde_json::Value) -> Vec<String> {
2291        let mut keys = value
2292            .as_object()
2293            .unwrap()
2294            .keys()
2295            .map(ToOwned::to_owned)
2296            .collect::<Vec<_>>();
2297        keys.sort();
2298        keys
2299    }
2300
2301    #[test]
2302    fn vmd_animation_json_top_level_schema_is_stable() {
2303        let vmd = make_vmd_bytes("miku", &[], &[], &[]);
2304        let parsed = parse_vmd_animation(&vmd).unwrap();
2305        let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
2306
2307        assert_eq!(
2308            keys,
2309            vec![
2310                "boneFrames",
2311                "cameraFrames",
2312                "kind",
2313                "lightFrames",
2314                "metadata",
2315                "morphFrames",
2316                "propertyFrames",
2317                "selfShadowFrames",
2318            ]
2319        );
2320    }
2321
2322    #[test]
2323    fn parses_simple_camera_vmd_fixture() {
2324        let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
2325
2326        assert_eq!(parsed.metadata.model_name, "camera_fixture");
2327        assert_eq!(parsed.metadata.counts.bones, 0);
2328        assert_eq!(parsed.metadata.counts.morphs, 0);
2329        assert_eq!(parsed.metadata.counts.cameras, 2);
2330        assert_eq!(parsed.metadata.counts.lights, 0);
2331        assert_eq!(parsed.metadata.counts.self_shadows, 0);
2332        assert_eq!(parsed.metadata.counts.properties, 0);
2333        assert_eq!(parsed.metadata.max_frame, 45);
2334        assert_eq!(parsed.camera_frames.len(), 2);
2335
2336        let first = &parsed.camera_frames[0];
2337        assert_eq!(first.frame, 0);
2338        assert_eq!(first.distance, -30.5);
2339        assert_eq!(first.position, [1.0, 2.0, 3.0]);
2340        assert_eq!(first.rotation, [0.1, -0.2, 0.3]);
2341        assert_eq!(
2342            first.interpolation,
2343            [
2344                20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
2345                41, 42, 43
2346            ]
2347        );
2348        assert_eq!(first.fov, 35);
2349        assert!(first.perspective);
2350
2351        let second = &parsed.camera_frames[1];
2352        assert_eq!(second.frame, 45);
2353        assert_eq!(second.distance, -50.0);
2354        assert_eq!(second.position, [-1.5, 10.0, 0.25]);
2355        assert_eq!(second.rotation, [-0.3, 0.0, 1.2]);
2356        assert_eq!(
2357            second.interpolation,
2358            [
2359                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 127, 127, 127, 127, 127, 127, 127, 127,
2360                127, 127, 127
2361            ]
2362        );
2363        assert_eq!(second.fov, 60);
2364        assert!(!second.perspective);
2365    }
2366
2367    #[test]
2368    fn roundtrip_simple_camera_vmd_fixture_parse_export_parse() {
2369        let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
2370        let exported = export_vmd_animation(&parsed);
2371        let reparsed = parse_vmd_animation(&exported).unwrap();
2372
2373        assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
2374        assert_eq!(reparsed.metadata.counts.cameras, 2);
2375        assert_eq!(reparsed.metadata.max_frame, 45);
2376        assert_eq!(reparsed.camera_frames.len(), parsed.camera_frames.len());
2377        for (left, right) in parsed.camera_frames.iter().zip(&reparsed.camera_frames) {
2378            assert_eq!(left.frame, right.frame);
2379            assert_eq!(left.distance, right.distance);
2380            assert_eq!(left.position, right.position);
2381            assert_eq!(left.rotation, right.rotation);
2382            assert_eq!(left.interpolation, right.interpolation);
2383            assert_eq!(left.fov, right.fov);
2384            assert_eq!(left.perspective, right.perspective);
2385        }
2386    }
2387
2388    #[test]
2389    fn samples_simple_camera_vmd_fixture_with_channel_interpolation() {
2390        let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
2391        let camera = sample_vmd_camera_frames(&parsed.camera_frames, 22.5).unwrap();
2392
2393        assert_near(camera.distance, -40.25);
2394        assert_vec3_near(camera.position, [-0.25, 6.0, 1.625]);
2395        assert_vec3_near(camera.rotation, [-0.1, -0.1, 0.75]);
2396        assert_near(camera.fov, 47.5);
2397        assert!(camera.perspective);
2398
2399        let last = sample_vmd_camera_frames(&parsed.camera_frames, 45.0).unwrap();
2400        assert!(!last.perspective);
2401    }
2402
2403    #[test]
2404    fn samples_vmd_light_frames_linearly() {
2405        let frames = vec![
2406            VmdParsedLightFrame {
2407                frame: 30,
2408                color: [1.0, 0.5, 0.0],
2409                direction: [0.0, -1.0, 0.0],
2410            },
2411            VmdParsedLightFrame {
2412                frame: 10,
2413                color: [0.0, 0.0, 1.0],
2414                direction: [1.0, 0.0, 0.0],
2415            },
2416        ];
2417
2418        let light = sample_vmd_light_frames(&frames, 20.0).unwrap();
2419
2420        assert_vec3_near(light.color, [0.5, 0.25, 0.5]);
2421        assert_vec3_near(light.direction, [0.5, -0.5, 0.0]);
2422    }
2423
2424    #[test]
2425    fn samples_vmd_self_shadow_frames_linearly_with_stepped_mode() {
2426        let frames = vec![
2427            VmdParsedSelfShadowFrame {
2428                frame: 10,
2429                mode: 1,
2430                distance: 20.0,
2431            },
2432            VmdParsedSelfShadowFrame {
2433                frame: 30,
2434                mode: 2,
2435                distance: 60.0,
2436            },
2437        ];
2438
2439        let middle = sample_vmd_self_shadow_frames(&frames, 20.0).unwrap();
2440        assert_eq!(middle.mode, 1);
2441        assert_near(middle.distance, 40.0);
2442
2443        let last = sample_vmd_self_shadow_frames(&frames, 30.0).unwrap();
2444        assert_eq!(last.mode, 2);
2445        assert_near(last.distance, 60.0);
2446    }
2447
2448    #[test]
2449    fn roundtrip_bone_frame_parse_export_parse() {
2450        let interp = [20u8; 64];
2451        let vmd = make_vmd_bytes(
2452            "miku",
2453            &[("arm", 30, [1.0, 0.5, -0.5], [0.0, 0.0, 0.0, 1.0], interp)],
2454            &[],
2455            &[],
2456        );
2457        let parsed = parse_vmd_animation(&vmd).unwrap();
2458        assert_eq!(parsed.bone_frames.len(), 1);
2459        assert_eq!(parsed.bone_frames[0].bone_name, "arm");
2460        assert_eq!(parsed.bone_frames[0].frame, 30);
2461        assert_eq!(parsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
2462        assert_eq!(parsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
2463        assert_eq!(parsed.bone_frames[0].interpolation, vec![20u8; 64]);
2464
2465        let exported = export_vmd_animation(&parsed);
2466        let reparsed = parse_vmd_animation(&exported).unwrap();
2467        assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
2468        assert_eq!(reparsed.metadata.max_frame, 30);
2469        assert_eq!(reparsed.bone_frames.len(), 1);
2470        assert_eq!(reparsed.bone_frames[0].bone_name, "arm");
2471        assert_eq!(reparsed.bone_frames[0].frame, 30);
2472        assert_eq!(reparsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
2473        assert_eq!(reparsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
2474        assert_eq!(reparsed.bone_frames[0].interpolation, vec![20u8; 64]);
2475    }
2476
2477    #[test]
2478    fn roundtrip_morph_frame_parse_export_parse() {
2479        let vmd = make_vmd_bytes("miku", &[], &[("blink", 15, 0.75)], &[]);
2480        let parsed = parse_vmd_animation(&vmd).unwrap();
2481        let exported = export_vmd_animation(&parsed);
2482        let reparsed = parse_vmd_animation(&exported).unwrap();
2483        assert_eq!(reparsed.morph_frames.len(), 1);
2484        assert_eq!(reparsed.morph_frames[0].morph_name, "blink");
2485        assert_eq!(reparsed.morph_frames[0].frame, 15);
2486        assert!((reparsed.morph_frames[0].weight - 0.75f32).abs() < 1e-6);
2487    }
2488
2489    #[test]
2490    fn roundtrip_property_ik_frames_parse_export_parse() {
2491        let vmd = make_vmd_bytes(
2492            "camera",
2493            &[],
2494            &[],
2495            &[(&[("leftIK", true), ("rightIK", false)], 20, true)],
2496        );
2497        let parsed = parse_vmd_animation(&vmd).unwrap();
2498        let exported = export_vmd_animation(&parsed);
2499        let reparsed = parse_vmd_animation(&exported).unwrap();
2500        assert_eq!(reparsed.property_frames.len(), 1);
2501        assert_eq!(reparsed.property_frames[0].frame, 20);
2502        assert!(reparsed.property_frames[0].visible);
2503        assert_eq!(reparsed.property_frames[0].ik_states.len(), 2);
2504        assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "leftIK");
2505        assert!(reparsed.property_frames[0].ik_states[0].enabled);
2506        assert_eq!(
2507            reparsed.property_frames[0].ik_states[1].bone_name,
2508            "rightIK"
2509        );
2510        assert!(!reparsed.property_frames[0].ik_states[1].enabled);
2511    }
2512
2513    #[test]
2514    fn roundtrip_json_dto_bone_and_morph() {
2515        let interp = [20u8; 64];
2516        let vmd = make_vmd_bytes(
2517            "miku",
2518            &[("spine", 60, [0.0, 1.0, 0.0], [0.1, 0.2, 0.3, 0.9], interp)],
2519            &[("mouth", 30, 0.8)],
2520            &[],
2521        );
2522        let parsed = parse_vmd_animation(&vmd).unwrap();
2523        let json = serde_json::to_string(&parsed).unwrap();
2524        let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
2525        let exported = export_vmd_animation(&from_json);
2526        let reparsed = parse_vmd_animation(&exported).unwrap();
2527
2528        assert_eq!(
2529            reparsed.bone_frames[0].bone_name,
2530            parsed.bone_frames[0].bone_name
2531        );
2532        assert_eq!(reparsed.bone_frames[0].frame, parsed.bone_frames[0].frame);
2533        assert_eq!(
2534            reparsed.bone_frames[0].translation,
2535            parsed.bone_frames[0].translation
2536        );
2537        assert_eq!(
2538            reparsed.bone_frames[0].rotation,
2539            parsed.bone_frames[0].rotation
2540        );
2541        assert_eq!(
2542            reparsed.morph_frames[0].morph_name,
2543            parsed.morph_frames[0].morph_name
2544        );
2545        assert_eq!(reparsed.morph_frames[0].frame, parsed.morph_frames[0].frame);
2546        assert!((reparsed.morph_frames[0].weight - parsed.morph_frames[0].weight).abs() < 1e-6);
2547    }
2548
2549    fn expected_sjis_name_bytes(value: &str, len: usize) -> Vec<u8> {
2550        let (encoded, _, _) = encoding_rs::SHIFT_JIS.encode(value);
2551        encoded.as_ref()[..encoded.len().min(len)].to_vec()
2552    }
2553
2554    #[test]
2555    fn export_json_dto_encodes_shift_jis_when_raw_name_bytes_are_missing() {
2556        let animation = vmd_parsed_animation(
2557            "初音ミク".to_owned(),
2558            Vec::new(),
2559            30,
2560            VmdParsedSections {
2561                bone_frames: vec![VmdParsedBoneFrame {
2562                    bone_name: "左足".to_owned(),
2563                    bone_name_bytes: Vec::new(),
2564                    frame: 10,
2565                    translation: [1.0, 2.0, 3.0],
2566                    rotation: [0.0, 0.0, 0.0, 1.0],
2567                    interpolation: vec![20; 64],
2568                }],
2569                morph_frames: vec![VmdParsedMorphFrame {
2570                    morph_name: "笑い".to_owned(),
2571                    morph_name_bytes: Vec::new(),
2572                    frame: 20,
2573                    weight: 0.75,
2574                }],
2575                camera_frames: Vec::new(),
2576                light_frames: Vec::new(),
2577                self_shadow_frames: Vec::new(),
2578                property_frames: vec![VmdParsedPropertyFrame {
2579                    frame: 30,
2580                    visible: true,
2581                    ik_states: vec![VmdParsedIkState {
2582                        bone_name: "右足IK".to_owned(),
2583                        bone_name_bytes: Vec::new(),
2584                        enabled: false,
2585                    }],
2586                }],
2587            },
2588        );
2589        let json = serde_json::to_string(&animation).unwrap();
2590        assert!(!json.contains("modelNameBytes"));
2591        assert!(!json.contains("boneNameBytes"));
2592        assert!(!json.contains("morphNameBytes"));
2593
2594        let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
2595        let exported = export_vmd_animation(&from_json);
2596        let reparsed = parse_vmd_animation(&exported).unwrap();
2597
2598        assert_eq!(reparsed.metadata.model_name, "初音ミク");
2599        assert!(!reparsed.metadata.model_name_bytes.is_empty());
2600        assert_eq!(
2601            reparsed.metadata.model_name_bytes,
2602            expected_sjis_name_bytes("初音ミク", 20)
2603        );
2604        assert_eq!(reparsed.bone_frames[0].bone_name, "左足");
2605        assert!(!reparsed.bone_frames[0].bone_name_bytes.is_empty());
2606        assert_eq!(
2607            reparsed.bone_frames[0].bone_name_bytes,
2608            expected_sjis_name_bytes("左足", 15)
2609        );
2610        assert_eq!(reparsed.morph_frames[0].morph_name, "笑い");
2611        assert!(!reparsed.morph_frames[0].morph_name_bytes.is_empty());
2612        assert_eq!(
2613            reparsed.morph_frames[0].morph_name_bytes,
2614            expected_sjis_name_bytes("笑い", 15)
2615        );
2616        assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "右足IK");
2617        assert!(
2618            !reparsed.property_frames[0].ik_states[0]
2619                .bone_name_bytes
2620                .is_empty()
2621        );
2622        assert_eq!(
2623            reparsed.property_frames[0].ik_states[0].bone_name_bytes,
2624            expected_sjis_name_bytes("右足IK", 20)
2625        );
2626        assert!(!reparsed.property_frames[0].ik_states[0].enabled);
2627    }
2628
2629    #[test]
2630    fn export_json_dto_uses_encoding_rs_replacement_for_non_shift_jis_names() {
2631        let animation = vmd_parsed_animation(
2632            "miku".to_owned(),
2633            Vec::new(),
2634            1,
2635            VmdParsedSections {
2636                bone_frames: vec![VmdParsedBoneFrame {
2637                    bone_name: "左足🧪".to_owned(),
2638                    bone_name_bytes: Vec::new(),
2639                    frame: 1,
2640                    translation: [0.0, 0.0, 0.0],
2641                    rotation: [0.0, 0.0, 0.0, 1.0],
2642                    interpolation: vec![20; 64],
2643                }],
2644                morph_frames: Vec::new(),
2645                camera_frames: Vec::new(),
2646                light_frames: Vec::new(),
2647                self_shadow_frames: Vec::new(),
2648                property_frames: Vec::new(),
2649            },
2650        );
2651        let json = serde_json::to_string(&animation).unwrap();
2652        let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
2653        let exported = export_vmd_animation(&from_json);
2654        let reparsed = parse_vmd_animation(&exported).unwrap();
2655
2656        assert_eq!(
2657            reparsed.bone_frames[0].bone_name_bytes,
2658            expected_sjis_name_bytes("左足🧪", 15)
2659        );
2660        assert_eq!(
2661            reparsed.bone_frames[0].bone_name,
2662            encoding_rs::SHIFT_JIS
2663                .decode(&expected_sjis_name_bytes("左足🧪", 15))
2664                .0
2665                .trim()
2666        );
2667    }
2668}