Skip to main content

mmd_anim_format/vmd/
mod.rs

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