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