Skip to main content

mmd_anim_format/vmd/
mod.rs

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