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