Skip to main content

mmd_anim_format/vmd/
mod.rs

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