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