Skip to main content

mmd_anim_format/vmd/
mod.rs

1use glam::{Quat, Vec3A};
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4
5use mmd_anim_runtime::{
6    AnimationClip, BoneAnimationBinding, BoneIndex, InterpolationScalar, InterpolationVector3,
7    ModelArena, MorphAnimationBinding, MorphIndex, MorphKeyframe, MorphTrack, MovableBoneKeyframe,
8    MovableBoneTrack, PropertyAnimationBinding, PropertyKeyframe,
9};
10
11use crate::binary::{
12    ByteReader, write_f32_le as write_f32, write_fixed_bytes, write_u32_le as write_u32,
13};
14use crate::error::ImportError;
15use crate::normalize::normalize_vmd_name;
16use crate::sjis::{decode_sjis_fixed_trimmed, encode_sjis};
17use thiserror::Error;
18
19#[cfg(test)]
20use mmd_anim_runtime::BoneInit;
21
22mod reduced;
23pub use reduced::{
24    VmdExportMorphKind, VmdExportName, VmdPoseExport, VmdPoseExportBindings, VmdPoseExportError,
25    VmdPoseExportReport, export_reduced_pose_to_vmd,
26};
27
28type Reader<'a> = ByteReader<'a>;
29
30const VMD_MAGIC: [u8; 30] = *b"Vocaloid Motion Data 0002\0\0\0\0\0";
31const VMD_MAGIC_PREFIX: &[u8] = b"Vocaloid Motion Data 0002\0";
32
33impl<'a> ByteReader<'a> {
34    fn read_optional_u32_le(&mut self) -> Result<Option<u32>, ImportError> {
35        if self.remaining() == 0 {
36            Ok(None)
37        } else {
38            self.read_u32_le().map(Some)
39        }
40    }
41
42    fn read_record_count(&mut self, record_size: usize) -> Result<usize, ImportError> {
43        let count = self.read_u32_le()? as usize;
44        self.require_record_bytes(count, record_size)?;
45        Ok(count)
46    }
47
48    fn read_optional_record_count(
49        &mut self,
50        record_size: usize,
51    ) -> Result<Option<usize>, ImportError> {
52        let Some(count) = self.read_optional_u32_le()? else {
53            return Ok(None);
54        };
55        let count = count as usize;
56        let Some(bytes) = count.checked_mul(record_size) else {
57            self.pos = self.data.len();
58            return Ok(None);
59        };
60        if bytes > self.remaining() {
61            self.pos = self.data.len();
62            return Ok(None);
63        }
64        Ok(Some(count))
65    }
66}
67
68#[derive(Debug, Clone)]
69pub struct VmdHeader {
70    pub model_name_bytes: [u8; 20],
71}
72
73#[derive(Debug, Clone)]
74pub enum VmdBoneImportMode {
75    ByName(Vec<u8>),
76    ByIndex(u32),
77}
78
79#[derive(Debug, Clone)]
80pub struct VmdBoneKeyframeRaw {
81    pub bone_mode: VmdBoneImportMode,
82    pub frame: u32,
83    pub position: Vec3A,
84    pub rotation: Quat,
85    pub interpolation: [u8; 64],
86    pub bone_name_normalized: Vec<u8>,
87}
88
89#[derive(Debug, Clone)]
90pub struct VmdIkEntry {
91    pub name_bytes: Vec<u8>,
92    pub enabled: u8,
93    pub name_normalized: Vec<u8>,
94}
95
96#[derive(Debug, Clone)]
97pub struct VmdPropertyIkFrame {
98    pub frame: u32,
99    pub show: u8,
100    pub entries: Vec<VmdIkEntry>,
101}
102
103#[derive(Debug, Clone)]
104pub struct VmdImportResult {
105    pub bone_keyframes: Vec<VmdBoneKeyframeRaw>,
106    pub morph_keyframes: Vec<(Vec<u8>, u32, f32)>,
107    pub property_keyframes: Vec<PropertyKeyframe>,
108    pub property_ik_frames: Vec<VmdPropertyIkFrame>,
109}
110
111/// Owns all VMD channels produced by one parser pass.
112///
113/// The parsed animation is the typed scene/raw-track view used by camera,
114/// light, self-shadow, and property consumers. The import result retains the
115/// runtime-facing bone/morph/property representation used by model-aware clip
116/// construction. Both views are owned by the context so callers can create a
117/// clip and copy scene channels without reparsing the input bytes.
118#[derive(Debug, Clone)]
119pub struct VmdSharedContext {
120    raw: VmdImportResult,
121    parsed: VmdParsedAnimation,
122    summary: VmdSharedContextSummary,
123}
124
125/// Counts for one VMD source channel in a shared-context summary.
126///
127/// Bone and morph track counts are the number of distinct raw target names.
128/// Camera, light, self-shadow, and property channels have one track when they
129/// contain at least one key. `key_count` always counts source keyframe records.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct VmdSharedContextTrackSummary {
132    pub track_count: usize,
133    pub key_count: usize,
134}
135
136/// Fixed-width summary retained by a parsed shared VMD context.
137///
138/// `target_model_name_bytes` is the original 20-byte VMD header field. It is
139/// Shift-JIS/CP932 data with the same first-NUL trimming convention as the
140/// existing parsed model-name value. Counts remain `usize` inside the format
141/// layer; the native ABI exposes them as checked `uint32_t` values.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct VmdSharedContextSummary {
144    pub target_model_name_bytes: [u8; 20],
145    pub max_frame: u32,
146    pub bones: VmdSharedContextTrackSummary,
147    pub morphs: VmdSharedContextTrackSummary,
148    pub cameras: VmdSharedContextTrackSummary,
149    pub lights: VmdSharedContextTrackSummary,
150    pub self_shadows: VmdSharedContextTrackSummary,
151    pub properties: VmdSharedContextTrackSummary,
152    pub property_ik_entry_count: usize,
153}
154
155/// Errors returned by the non-materializing VMD summary scanner.
156///
157/// The summary-only validation surface is intentionally separate from
158/// `ImportError` so adding summary diagnostics does not make the existing
159/// public import error enum's exhaustive matches non-exhaustive.
160#[derive(Debug, Error, PartialEq)]
161pub enum VmdSummaryError {
162    #[error("VMD summary parse failed: {0}")]
163    Parse(ImportError),
164    #[error("invalid VMD summary data: {0}")]
165    Invalid(&'static str),
166}
167
168impl VmdSharedContextSummary {
169    fn from_parsed(target_model_name_bytes: [u8; 20], parsed: &VmdParsedAnimation) -> Self {
170        Self {
171            target_model_name_bytes,
172            max_frame: parsed.metadata.max_frame,
173            bones: VmdSharedContextTrackSummary {
174                track_count: distinct_name_count(
175                    parsed
176                        .bone_frames
177                        .iter()
178                        .map(|frame| frame.bone_name_bytes.as_slice()),
179                ),
180                key_count: parsed.bone_frames.len(),
181            },
182            morphs: VmdSharedContextTrackSummary {
183                track_count: distinct_name_count(
184                    parsed
185                        .morph_frames
186                        .iter()
187                        .map(|frame| frame.morph_name_bytes.as_slice()),
188                ),
189                key_count: parsed.morph_frames.len(),
190            },
191            cameras: singleton_track_summary(parsed.camera_frames.len()),
192            lights: singleton_track_summary(parsed.light_frames.len()),
193            self_shadows: singleton_track_summary(parsed.self_shadow_frames.len()),
194            properties: singleton_track_summary(parsed.property_frames.len()),
195            property_ik_entry_count: parsed
196                .property_frames
197                .iter()
198                .map(|frame| frame.ik_states.len())
199                .sum(),
200        }
201    }
202}
203
204fn distinct_name_count<'a>(names: impl Iterator<Item = &'a [u8]>) -> usize {
205    names
206        .map(|name| name.to_vec())
207        .collect::<HashSet<_>>()
208        .len()
209}
210
211fn singleton_track_summary(key_count: usize) -> VmdSharedContextTrackSummary {
212    VmdSharedContextTrackSummary {
213        track_count: usize::from(key_count != 0),
214        key_count,
215    }
216}
217
218impl VmdSharedContext {
219    pub fn import_result(&self) -> &VmdImportResult {
220        &self.raw
221    }
222
223    pub fn parsed_animation(&self) -> &VmdParsedAnimation {
224        &self.parsed
225    }
226
227    pub fn summary(&self) -> &VmdSharedContextSummary {
228        &self.summary
229    }
230}
231
232/// Parses VMD bytes once and retains every supported channel for shared
233/// model-clip and scene-track consumers.
234pub fn parse_vmd_shared_context(data: &[u8]) -> Result<VmdSharedContext, ImportError> {
235    let scan = parse_vmd_scan(data, VmdScanOptions::BOTH)?;
236    let VmdScanResult {
237        raw,
238        parsed,
239        summary: _,
240        target_model_name_bytes,
241    } = scan;
242    let raw = raw.expect("combined VMD scan must produce raw output");
243    let parsed = parsed.expect("combined VMD scan must produce parsed output");
244    Ok(VmdSharedContext {
245        summary: VmdSharedContextSummary::from_parsed(target_model_name_bytes, &parsed),
246        raw,
247        parsed,
248    })
249}
250
251/// Reads only the fixed-width VMD summary without materializing raw or parsed
252/// keyframe tracks.
253///
254/// Bone and morph distinct-track counts borrow their fixed-width names from
255/// `data` for the duration of the scan. All other channels retain only their
256/// key and IK-entry counts. Optional camera/light/self-shadow tails preserve
257/// the compatibility parser's absent-tail behavior when a complete count is
258/// present but its records are unavailable; a partial four-byte count remains
259/// a structural parse error.
260pub fn parse_vmd_summary(data: &[u8]) -> Result<VmdSharedContextSummary, VmdSummaryError> {
261    let scan = parse_vmd_scan(data, VmdScanOptions::SUMMARY).map_err(|error| match error {
262        ImportError::UnsupportedFormat {
263            format: "VMD",
264            detail,
265        } => VmdSummaryError::Invalid(detail),
266        error => VmdSummaryError::Parse(error),
267    })?;
268    Ok(scan
269        .summary
270        .expect("summary-only VMD scan must produce summary output"))
271}
272
273pub fn read_header(data: &[u8]) -> Result<(VmdHeader, usize), ImportError> {
274    let mut r = Reader::new(data);
275
276    let magic = r.read_slice(30)?;
277    if magic != VMD_MAGIC && !magic.starts_with(VMD_MAGIC_PREFIX) {
278        return Err(ImportError::InvalidVmdMagic);
279    }
280
281    let model_name_bytes: [u8; 20] = r
282        .read_slice(20)?
283        .try_into()
284        .map_err(|_| ImportError::InvalidVmdModelName)?;
285
286    Ok((VmdHeader { model_name_bytes }, r.pos))
287}
288
289pub fn import_vmd_motion(data: &[u8]) -> Result<VmdImportResult, ImportError> {
290    Ok(parse_vmd_scan(data, VmdScanOptions::RAW)?
291        .raw
292        .expect("raw-only VMD scan must produce raw output"))
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct VmdParsedAnimation {
298    #[serde(default = "default_vmd_kind", skip_deserializing)]
299    pub kind: &'static str,
300    pub metadata: VmdParsedMetadata,
301    pub bone_frames: Vec<VmdParsedBoneFrame>,
302    pub morph_frames: Vec<VmdParsedMorphFrame>,
303    pub camera_frames: Vec<VmdParsedCameraFrame>,
304    pub light_frames: Vec<VmdParsedLightFrame>,
305    pub self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
306    pub property_frames: Vec<VmdParsedPropertyFrame>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct VmdParsedMetadata {
312    #[serde(default = "default_vmd_format", skip_deserializing)]
313    pub format: &'static str,
314    pub model_name: String,
315    #[serde(default, skip_serializing_if = "Vec::is_empty")]
316    pub model_name_bytes: Vec<u8>,
317    pub counts: VmdParsedCounts,
318    pub max_frame: u32,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct VmdParsedCounts {
323    pub bones: usize,
324    pub morphs: usize,
325    pub cameras: usize,
326    pub lights: usize,
327    #[serde(rename = "selfShadows")]
328    pub self_shadows: usize,
329    pub properties: usize,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase")]
334pub struct VmdParsedBoneFrame {
335    pub bone_name: String,
336    #[serde(default, skip_serializing_if = "Vec::is_empty")]
337    pub bone_name_bytes: Vec<u8>,
338    pub frame: u32,
339    pub translation: [f32; 3],
340    pub rotation: [f32; 4],
341    pub interpolation: Vec<u8>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct VmdParsedMorphFrame {
347    pub morph_name: String,
348    #[serde(default, skip_serializing_if = "Vec::is_empty")]
349    pub morph_name_bytes: Vec<u8>,
350    pub frame: u32,
351    pub weight: f32,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct VmdParsedCameraFrame {
357    pub frame: u32,
358    pub distance: f32,
359    pub position: [f32; 3],
360    pub rotation: [f32; 3],
361    pub interpolation: [u8; 24],
362    pub fov: u32,
363    pub perspective: bool,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct VmdCameraState {
369    pub distance: f32,
370    pub position: [f32; 3],
371    pub rotation: [f32; 3],
372    pub fov: f32,
373    pub perspective: bool,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct VmdParsedLightFrame {
379    pub frame: u32,
380    pub color: [f32; 3],
381    pub direction: [f32; 3],
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct VmdLightState {
387    pub color: [f32; 3],
388    pub direction: [f32; 3],
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct VmdParsedSelfShadowFrame {
394    pub frame: u32,
395    pub mode: u8,
396    pub distance: f32,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct VmdSelfShadowState {
402    pub mode: u8,
403    pub distance: f32,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
407#[serde(rename_all = "camelCase")]
408pub struct VmdParsedPropertyFrame {
409    pub frame: u32,
410    pub visible: bool,
411    pub ik_states: Vec<VmdParsedIkState>,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct VmdParsedIkState {
417    pub bone_name: String,
418    #[serde(default, skip_serializing_if = "Vec::is_empty")]
419    pub bone_name_bytes: Vec<u8>,
420    pub enabled: bool,
421}
422
423#[derive(Clone, Copy)]
424struct VmdScanOptions {
425    build_raw: bool,
426    build_parsed: bool,
427    build_summary: bool,
428}
429
430impl VmdScanOptions {
431    const RAW: Self = Self {
432        build_raw: true,
433        build_parsed: false,
434        build_summary: false,
435    };
436    const PARSED: Self = Self {
437        build_raw: false,
438        build_parsed: true,
439        build_summary: false,
440    };
441    const BOTH: Self = Self {
442        build_raw: true,
443        build_parsed: true,
444        build_summary: false,
445    };
446    const SUMMARY: Self = Self {
447        build_raw: false,
448        build_parsed: false,
449        build_summary: true,
450    };
451}
452
453struct VmdScanResult {
454    raw: Option<VmdImportResult>,
455    parsed: Option<VmdParsedAnimation>,
456    summary: Option<VmdSharedContextSummary>,
457    target_model_name_bytes: [u8; 20],
458}
459
460struct VmdPropertyFrameReadResult {
461    parsed_frames: Option<Vec<VmdParsedPropertyFrame>>,
462    raw_keyframes: Option<Vec<PropertyKeyframe>>,
463    raw_ik_frames: Option<Vec<VmdPropertyIkFrame>>,
464}
465
466struct VmdSummaryAccumulator {
467    bone_names: HashSet<Vec<u8>>,
468    morph_names: HashSet<Vec<u8>>,
469    bone_key_count: usize,
470    morph_key_count: usize,
471    camera_key_count: usize,
472    light_key_count: usize,
473    self_shadow_key_count: usize,
474    property_key_count: usize,
475    property_ik_entry_count: usize,
476}
477
478impl VmdSummaryAccumulator {
479    fn new() -> Self {
480        Self {
481            bone_names: HashSet::new(),
482            morph_names: HashSet::new(),
483            bone_key_count: 0,
484            morph_key_count: 0,
485            camera_key_count: 0,
486            light_key_count: 0,
487            self_shadow_key_count: 0,
488            property_key_count: 0,
489            property_ik_entry_count: 0,
490        }
491    }
492
493    fn record_bone(&mut self, name: &[u8]) -> Result<(), ImportError> {
494        validate_summary_name(name, "empty bone name")?;
495        self.bone_names.insert(name.to_vec());
496        self.bone_key_count = self
497            .bone_key_count
498            .checked_add(1)
499            .ok_or(ImportError::SectionOverflow)?;
500        Ok(())
501    }
502
503    fn record_morph(&mut self, name: &[u8]) -> Result<(), ImportError> {
504        validate_summary_name(name, "empty morph name")?;
505        self.morph_names.insert(name.to_vec());
506        self.morph_key_count = self
507            .morph_key_count
508            .checked_add(1)
509            .ok_or(ImportError::SectionOverflow)?;
510        Ok(())
511    }
512
513    fn record_camera(&mut self) -> Result<(), ImportError> {
514        self.camera_key_count = self
515            .camera_key_count
516            .checked_add(1)
517            .ok_or(ImportError::SectionOverflow)?;
518        Ok(())
519    }
520
521    fn record_light(&mut self) -> Result<(), ImportError> {
522        self.light_key_count = self
523            .light_key_count
524            .checked_add(1)
525            .ok_or(ImportError::SectionOverflow)?;
526        Ok(())
527    }
528
529    fn record_self_shadow(&mut self) -> Result<(), ImportError> {
530        self.self_shadow_key_count = self
531            .self_shadow_key_count
532            .checked_add(1)
533            .ok_or(ImportError::SectionOverflow)?;
534        Ok(())
535    }
536
537    fn record_property(&mut self, ik_count: usize) -> Result<(), ImportError> {
538        self.property_key_count = self
539            .property_key_count
540            .checked_add(1)
541            .ok_or(ImportError::SectionOverflow)?;
542        self.property_ik_entry_count = self
543            .property_ik_entry_count
544            .checked_add(ik_count)
545            .ok_or(ImportError::SectionOverflow)?;
546        Ok(())
547    }
548
549    fn finish(self, target_model_name_bytes: [u8; 20], max_frame: u32) -> VmdSharedContextSummary {
550        VmdSharedContextSummary {
551            target_model_name_bytes,
552            max_frame,
553            bones: VmdSharedContextTrackSummary {
554                track_count: self.bone_names.len(),
555                key_count: self.bone_key_count,
556            },
557            morphs: VmdSharedContextTrackSummary {
558                track_count: self.morph_names.len(),
559                key_count: self.morph_key_count,
560            },
561            cameras: singleton_track_summary(self.camera_key_count),
562            lights: singleton_track_summary(self.light_key_count),
563            self_shadows: singleton_track_summary(self.self_shadow_key_count),
564            properties: singleton_track_summary(self.property_key_count),
565            property_ik_entry_count: self.property_ik_entry_count,
566        }
567    }
568}
569
570fn summary_invalid_error(detail: &'static str) -> ImportError {
571    ImportError::UnsupportedFormat {
572        format: "VMD",
573        detail,
574    }
575}
576
577fn validate_summary_name(name: &[u8], detail: &'static str) -> Result<(), ImportError> {
578    if vmd_fixed_name_is_blank(name) {
579        Err(summary_invalid_error(detail))
580    } else {
581        Ok(())
582    }
583}
584
585fn read_vmd_f32(
586    r: &mut Reader<'_>,
587    validate: bool,
588    field: &'static str,
589) -> Result<f32, ImportError> {
590    let value = r.read_f32_le()?;
591    if validate && !value.is_finite() {
592        return Err(summary_invalid_error(field));
593    }
594    Ok(value)
595}
596
597fn read_vmd_vec3(
598    r: &mut Reader<'_>,
599    validate: bool,
600    field: &'static str,
601) -> Result<Vec3A, ImportError> {
602    Ok(Vec3A::new(
603        read_vmd_f32(r, validate, field)?,
604        read_vmd_f32(r, validate, field)?,
605        read_vmd_f32(r, validate, field)?,
606    ))
607}
608
609fn read_vmd_quat(
610    r: &mut Reader<'_>,
611    validate: bool,
612    field: &'static str,
613) -> Result<Quat, ImportError> {
614    Ok(Quat::from_xyzw(
615        read_vmd_f32(r, validate, field)?,
616        read_vmd_f32(r, validate, field)?,
617        read_vmd_f32(r, validate, field)?,
618        read_vmd_f32(r, validate, field)?,
619    ))
620}
621
622fn vmd_fixed_name_is_blank(bytes: &[u8]) -> bool {
623    decode_sjis_fixed(bytes).trim().is_empty()
624}
625
626fn parse_vmd_scan(data: &[u8], options: VmdScanOptions) -> Result<VmdScanResult, ImportError> {
627    let (header, pos) = read_header(data)?;
628    let mut r = Reader { data, pos };
629    let model_name = options
630        .build_parsed
631        .then(|| decode_sjis_fixed(&header.model_name_bytes));
632    let model_name_bytes = options
633        .build_parsed
634        .then(|| trim_fixed_bytes(&header.model_name_bytes).to_vec());
635    let mut max_frame = 0u32;
636    let mut summary = options.build_summary.then(VmdSummaryAccumulator::new);
637
638    let bone_count = r.read_record_count(111)?;
639    let mut raw = options.build_raw.then(|| VmdImportResult {
640        bone_keyframes: Vec::with_capacity(bone_count),
641        morph_keyframes: Vec::new(),
642        property_keyframes: Vec::new(),
643        property_ik_frames: Vec::new(),
644    });
645    let mut bone_frames = options.build_parsed.then(|| Vec::with_capacity(bone_count));
646    for _ in 0..bone_count {
647        let bone_name_field = r.read_slice(15)?;
648        if let Some(summary) = summary.as_mut() {
649            summary.record_bone(trim_fixed_bytes(bone_name_field))?;
650        }
651        let raw_bone_name = options
652            .build_raw
653            .then(|| trim_fixed_bytes(bone_name_field).to_vec());
654        let raw_bone_name_normalized = raw_bone_name
655            .as_ref()
656            .map(|name_bytes| normalize_vmd_name(name_bytes));
657        let parsed_bone_name = options
658            .build_parsed
659            .then(|| decode_sjis_fixed(bone_name_field));
660        let parsed_bone_name_bytes = options
661            .build_parsed
662            .then(|| trim_fixed_bytes(bone_name_field).to_vec());
663        let frame = r.read_u32_le()?;
664        if options.build_parsed || options.build_summary {
665            max_frame = max_frame.max(frame);
666        }
667        let position = read_vmd_vec3(&mut r, options.build_summary, "bone translation")?;
668        let rotation = read_vmd_quat(&mut r, options.build_summary, "bone rotation")?;
669        let interpolation: [u8; 64] = r.read_slice(64)?.try_into().unwrap();
670        if let Some(raw) = raw.as_mut() {
671            raw.bone_keyframes.push(VmdBoneKeyframeRaw {
672                bone_mode: VmdBoneImportMode::ByName(
673                    raw_bone_name.expect("raw VMD scan must build bone names"),
674                ),
675                frame,
676                position,
677                rotation,
678                interpolation,
679                bone_name_normalized: raw_bone_name_normalized
680                    .expect("raw VMD scan must normalize bone names"),
681            });
682        }
683        if let Some(bone_frames) = bone_frames.as_mut() {
684            bone_frames.push(VmdParsedBoneFrame {
685                bone_name: parsed_bone_name.expect("parsed VMD scan must decode bone names"),
686                bone_name_bytes: parsed_bone_name_bytes
687                    .expect("parsed VMD scan must retain bone name bytes"),
688                frame,
689                translation: [position.x, position.y, position.z],
690                rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
691                interpolation: interpolation.to_vec(),
692            });
693        }
694    }
695
696    let Some(morph_count) = r.read_optional_u32_le()? else {
697        return Ok(vmd_scan_result(
698            model_name,
699            model_name_bytes,
700            header.model_name_bytes,
701            max_frame,
702            raw,
703            bone_frames.map(|bone_frames| VmdParsedSections {
704                bone_frames,
705                morph_frames: Vec::new(),
706                camera_frames: Vec::new(),
707                light_frames: Vec::new(),
708                self_shadow_frames: Vec::new(),
709                property_frames: Vec::new(),
710            }),
711            summary.map(|summary| summary.finish(header.model_name_bytes, max_frame)),
712        ));
713    };
714    let morph_count = morph_count as usize;
715    r.require_record_bytes(morph_count, 23)?;
716    if let Some(raw) = raw.as_mut() {
717        raw.morph_keyframes = Vec::with_capacity(morph_count);
718    }
719    let mut morph_frames = options
720        .build_parsed
721        .then(|| Vec::with_capacity(morph_count));
722    for _ in 0..morph_count {
723        let morph_name_field = r.read_slice(15)?;
724        if let Some(summary) = summary.as_mut() {
725            summary.record_morph(trim_fixed_bytes(morph_name_field))?;
726        }
727        let raw_morph_name = options
728            .build_raw
729            .then(|| trim_fixed_bytes(morph_name_field).to_vec());
730        let parsed_morph_name = options
731            .build_parsed
732            .then(|| decode_sjis_fixed(morph_name_field));
733        let parsed_morph_name_bytes = options
734            .build_parsed
735            .then(|| trim_fixed_bytes(morph_name_field).to_vec());
736        let frame = r.read_u32_le()?;
737        if options.build_parsed || options.build_summary {
738            max_frame = max_frame.max(frame);
739        }
740        let weight = read_vmd_f32(&mut r, options.build_summary, "morph weight")?;
741        if let Some(raw) = raw.as_mut() {
742            raw.morph_keyframes.push((
743                raw_morph_name.expect("raw VMD scan must build morph names"),
744                frame,
745                weight,
746            ));
747        }
748        if let Some(morph_frames) = morph_frames.as_mut() {
749            morph_frames.push(VmdParsedMorphFrame {
750                morph_name: parsed_morph_name.expect("parsed VMD scan must decode morph names"),
751                morph_name_bytes: parsed_morph_name_bytes
752                    .expect("parsed VMD scan must retain morph name bytes"),
753                frame,
754                weight,
755            });
756        }
757    }
758
759    let camera_frames = if options.build_parsed || options.build_summary {
760        read_camera_frames(&mut r, &mut max_frame, options.build_parsed, &mut summary)?
761    } else {
762        skip_optional_records(&mut r, 61)?;
763        None
764    };
765    let light_frames = if options.build_parsed || options.build_summary {
766        read_light_frames(&mut r, &mut max_frame, options.build_parsed, &mut summary)?
767    } else {
768        skip_optional_records(&mut r, 28)?;
769        None
770    };
771    let self_shadow_frames = if options.build_parsed || options.build_summary {
772        read_self_shadow_frames(&mut r, &mut max_frame, options.build_parsed, &mut summary)?
773    } else {
774        skip_optional_records(&mut r, 9)?;
775        None
776    };
777    let property_frames = read_property_frames(&mut r, &mut max_frame, options, &mut summary)?;
778
779    if let Some(raw) = raw.as_mut() {
780        raw.property_keyframes = property_frames
781            .raw_keyframes
782            .expect("raw VMD scan must build property keyframes");
783        raw.property_ik_frames = property_frames
784            .raw_ik_frames
785            .expect("raw VMD scan must build property IK frames");
786    }
787    let parsed = bone_frames.map(|bone_frames| VmdParsedSections {
788        bone_frames,
789        morph_frames: morph_frames.expect("parsed VMD scan must build morph frames"),
790        camera_frames: camera_frames.expect("parsed VMD scan must build camera frames"),
791        light_frames: light_frames.expect("parsed VMD scan must build light frames"),
792        self_shadow_frames: self_shadow_frames
793            .expect("parsed VMD scan must build self-shadow frames"),
794        property_frames: property_frames
795            .parsed_frames
796            .expect("parsed VMD scan must build property frames"),
797    });
798
799    Ok(vmd_scan_result(
800        model_name,
801        model_name_bytes,
802        header.model_name_bytes,
803        max_frame,
804        raw,
805        parsed,
806        summary.map(|summary| summary.finish(header.model_name_bytes, max_frame)),
807    ))
808}
809
810pub fn parse_vmd_animation(data: &[u8]) -> Result<VmdParsedAnimation, ImportError> {
811    Ok(parse_vmd_scan(data, VmdScanOptions::PARSED)?
812        .parsed
813        .expect("parsed-only VMD scan must produce parsed output"))
814}
815
816fn default_vmd_kind() -> &'static str {
817    "vmd"
818}
819
820fn default_vmd_format() -> &'static str {
821    "vmd"
822}
823
824pub fn export_vmd_animation(animation: &VmdParsedAnimation) -> Vec<u8> {
825    let mut out = Vec::new();
826    out.extend_from_slice(&VMD_MAGIC);
827    write_fixed_name_bytes(
828        &mut out,
829        &animation.metadata.model_name,
830        &animation.metadata.model_name_bytes,
831        20,
832    );
833
834    write_u32(&mut out, animation.bone_frames.len() as u32);
835    for frame in &animation.bone_frames {
836        write_fixed_name_bytes(&mut out, &frame.bone_name, &frame.bone_name_bytes, 15);
837        write_u32(&mut out, frame.frame);
838        write_f32(&mut out, frame.translation[0]);
839        write_f32(&mut out, frame.translation[1]);
840        write_f32(&mut out, frame.translation[2]);
841        write_f32(&mut out, frame.rotation[0]);
842        write_f32(&mut out, frame.rotation[1]);
843        write_f32(&mut out, frame.rotation[2]);
844        write_f32(&mut out, frame.rotation[3]);
845        write_fixed_bytes(&mut out, &frame.interpolation, 64);
846    }
847
848    write_u32(&mut out, animation.morph_frames.len() as u32);
849    for frame in &animation.morph_frames {
850        write_fixed_name_bytes(&mut out, &frame.morph_name, &frame.morph_name_bytes, 15);
851        write_u32(&mut out, frame.frame);
852        write_f32(&mut out, frame.weight);
853    }
854
855    write_u32(&mut out, animation.camera_frames.len() as u32);
856    for frame in &animation.camera_frames {
857        write_u32(&mut out, frame.frame);
858        write_f32(&mut out, frame.distance);
859        write_f32(&mut out, frame.position[0]);
860        write_f32(&mut out, frame.position[1]);
861        write_f32(&mut out, frame.position[2]);
862        write_f32(&mut out, frame.rotation[0]);
863        write_f32(&mut out, frame.rotation[1]);
864        write_f32(&mut out, frame.rotation[2]);
865        out.extend_from_slice(&frame.interpolation);
866        write_u32(&mut out, frame.fov);
867        out.push(if frame.perspective { 0 } else { 1 });
868    }
869
870    write_u32(&mut out, animation.light_frames.len() as u32);
871    for frame in &animation.light_frames {
872        write_u32(&mut out, frame.frame);
873        write_f32(&mut out, frame.color[0]);
874        write_f32(&mut out, frame.color[1]);
875        write_f32(&mut out, frame.color[2]);
876        write_f32(&mut out, frame.direction[0]);
877        write_f32(&mut out, frame.direction[1]);
878        write_f32(&mut out, frame.direction[2]);
879    }
880
881    write_u32(&mut out, animation.self_shadow_frames.len() as u32);
882    for frame in &animation.self_shadow_frames {
883        write_u32(&mut out, frame.frame);
884        out.push(frame.mode);
885        write_f32(&mut out, frame.distance);
886    }
887
888    write_u32(&mut out, animation.property_frames.len() as u32);
889    for frame in &animation.property_frames {
890        write_u32(&mut out, frame.frame);
891        out.push(u8::from(frame.visible));
892        write_u32(&mut out, frame.ik_states.len() as u32);
893        for state in &frame.ik_states {
894            write_fixed_name_bytes(&mut out, &state.bone_name, &state.bone_name_bytes, 20);
895            out.push(u8::from(state.enabled));
896        }
897    }
898
899    out
900}
901
902struct VmdParsedSections {
903    bone_frames: Vec<VmdParsedBoneFrame>,
904    morph_frames: Vec<VmdParsedMorphFrame>,
905    camera_frames: Vec<VmdParsedCameraFrame>,
906    light_frames: Vec<VmdParsedLightFrame>,
907    self_shadow_frames: Vec<VmdParsedSelfShadowFrame>,
908    property_frames: Vec<VmdParsedPropertyFrame>,
909}
910
911fn vmd_parsed_animation(
912    model_name: String,
913    model_name_bytes: Vec<u8>,
914    max_frame: u32,
915    sections: VmdParsedSections,
916) -> VmdParsedAnimation {
917    VmdParsedAnimation {
918        kind: "vmd",
919        metadata: VmdParsedMetadata {
920            format: "vmd",
921            model_name,
922            model_name_bytes,
923            counts: VmdParsedCounts {
924                bones: sections.bone_frames.len(),
925                morphs: sections.morph_frames.len(),
926                cameras: sections.camera_frames.len(),
927                lights: sections.light_frames.len(),
928                self_shadows: sections.self_shadow_frames.len(),
929                properties: sections.property_frames.len(),
930            },
931            max_frame,
932        },
933        bone_frames: sections.bone_frames,
934        morph_frames: sections.morph_frames,
935        camera_frames: sections.camera_frames,
936        light_frames: sections.light_frames,
937        self_shadow_frames: sections.self_shadow_frames,
938        property_frames: sections.property_frames,
939    }
940}
941
942fn vmd_scan_result(
943    model_name: Option<String>,
944    model_name_bytes: Option<Vec<u8>>,
945    target_model_name_bytes: [u8; 20],
946    max_frame: u32,
947    raw: Option<VmdImportResult>,
948    sections: Option<VmdParsedSections>,
949    summary: Option<VmdSharedContextSummary>,
950) -> VmdScanResult {
951    VmdScanResult {
952        raw,
953        target_model_name_bytes,
954        summary,
955        parsed: sections.map(|sections| {
956            vmd_parsed_animation(
957                model_name.expect("parsed VMD scan must decode model name"),
958                model_name_bytes.expect("parsed VMD scan must retain model name bytes"),
959                max_frame,
960                sections,
961            )
962        }),
963    }
964}
965
966fn skip_optional_records(r: &mut Reader<'_>, record_size: usize) -> Result<(), ImportError> {
967    if let Some(count) = r.read_optional_record_count(record_size)? {
968        r.skip(count * record_size)?;
969    }
970    Ok(())
971}
972
973fn read_camera_frames(
974    r: &mut Reader<'_>,
975    max_frame: &mut u32,
976    build_parsed: bool,
977    summary: &mut Option<VmdSummaryAccumulator>,
978) -> Result<Option<Vec<VmdParsedCameraFrame>>, ImportError> {
979    let Some(count) = r.read_optional_record_count(61)? else {
980        return Ok(build_parsed.then(Vec::new));
981    };
982    let mut frames = build_parsed.then(|| Vec::with_capacity(count));
983    for _ in 0..count {
984        let frame = r.read_u32_le()?;
985        *max_frame = (*max_frame).max(frame);
986        let distance = read_vmd_f32(r, summary.is_some(), "camera distance")?;
987        let position = read_vmd_vec3(r, summary.is_some(), "camera position")?;
988        let rotation = read_vmd_vec3(r, summary.is_some(), "camera rotation")?;
989        let interpolation: [u8; 24] = r.read_slice(24)?.try_into().unwrap();
990        let fov = r.read_u32_le()?;
991        let perspective = r.read_u8()? == 0;
992        if let Some(summary) = summary.as_mut() {
993            summary.record_camera()?;
994        }
995        if let Some(frames) = frames.as_mut() {
996            frames.push(VmdParsedCameraFrame {
997                frame,
998                distance,
999                position: [position.x, position.y, position.z],
1000                rotation: [rotation.x, rotation.y, rotation.z],
1001                interpolation,
1002                fov,
1003                perspective,
1004            });
1005        }
1006    }
1007    Ok(frames)
1008}
1009
1010fn read_light_frames(
1011    r: &mut Reader<'_>,
1012    max_frame: &mut u32,
1013    build_parsed: bool,
1014    summary: &mut Option<VmdSummaryAccumulator>,
1015) -> Result<Option<Vec<VmdParsedLightFrame>>, ImportError> {
1016    let Some(count) = r.read_optional_record_count(28)? else {
1017        return Ok(build_parsed.then(Vec::new));
1018    };
1019    let mut frames = build_parsed.then(|| Vec::with_capacity(count));
1020    for _ in 0..count {
1021        let frame = r.read_u32_le()?;
1022        *max_frame = (*max_frame).max(frame);
1023        let color = read_vmd_vec3(r, summary.is_some(), "light color")?;
1024        let direction = read_vmd_vec3(r, summary.is_some(), "light direction")?;
1025        if let Some(summary) = summary.as_mut() {
1026            summary.record_light()?;
1027        }
1028        if let Some(frames) = frames.as_mut() {
1029            frames.push(VmdParsedLightFrame {
1030                frame,
1031                color: [color.x, color.y, color.z],
1032                direction: [direction.x, direction.y, direction.z],
1033            });
1034        }
1035    }
1036    Ok(frames)
1037}
1038
1039fn read_self_shadow_frames(
1040    r: &mut Reader<'_>,
1041    max_frame: &mut u32,
1042    build_parsed: bool,
1043    summary: &mut Option<VmdSummaryAccumulator>,
1044) -> Result<Option<Vec<VmdParsedSelfShadowFrame>>, ImportError> {
1045    let Some(count) = r.read_optional_record_count(9)? else {
1046        return Ok(build_parsed.then(Vec::new));
1047    };
1048    let mut frames = build_parsed.then(|| Vec::with_capacity(count));
1049    for _ in 0..count {
1050        let frame = r.read_u32_le()?;
1051        *max_frame = (*max_frame).max(frame);
1052        let mode = r.read_u8()?;
1053        let distance = read_vmd_f32(r, summary.is_some(), "self-shadow distance")?;
1054        if let Some(summary) = summary.as_mut() {
1055            summary.record_self_shadow()?;
1056        }
1057        if let Some(frames) = frames.as_mut() {
1058            frames.push(VmdParsedSelfShadowFrame {
1059                frame,
1060                mode,
1061                distance,
1062            });
1063        }
1064    }
1065    Ok(frames)
1066}
1067
1068fn read_property_frames(
1069    r: &mut Reader<'_>,
1070    max_frame: &mut u32,
1071    options: VmdScanOptions,
1072    summary: &mut Option<VmdSummaryAccumulator>,
1073) -> Result<VmdPropertyFrameReadResult, ImportError> {
1074    let Some(count) = r.read_optional_u32_le()? else {
1075        return Ok(VmdPropertyFrameReadResult {
1076            parsed_frames: options.build_parsed.then(Vec::new),
1077            raw_keyframes: options.build_raw.then(Vec::new),
1078            raw_ik_frames: options.build_raw.then(Vec::new),
1079        });
1080    };
1081    let count = count as usize;
1082    r.require_record_bytes(count, 9)?;
1083    let mut parsed_frames = options.build_parsed.then(|| Vec::with_capacity(count));
1084    let mut raw_keyframes = options.build_raw.then(|| Vec::with_capacity(count));
1085    let mut raw_ik_frames = options.build_raw.then(|| Vec::with_capacity(count));
1086    for _ in 0..count {
1087        let frame = r.read_u32_le()?;
1088        if options.build_parsed || options.build_summary {
1089            *max_frame = (*max_frame).max(frame);
1090        }
1091        let show = r.read_u8()?;
1092        let ik_count = r.read_u32_le()? as usize;
1093        r.require_record_bytes(ik_count, 21)?;
1094        if let Some(summary) = summary.as_mut() {
1095            summary.record_property(ik_count)?;
1096        }
1097        let mut raw_ik_enabled = options.build_raw.then(|| Vec::with_capacity(ik_count));
1098        let mut raw_ik_entries = options.build_raw.then(|| Vec::with_capacity(ik_count));
1099        let mut ik_states = options.build_parsed.then(|| Vec::with_capacity(ik_count));
1100        for _ in 0..ik_count {
1101            let name_field = r.read_slice(20)?;
1102            if options.build_summary {
1103                validate_summary_name(trim_fixed_bytes(name_field), "empty property IK name")?;
1104            }
1105            let raw_name_bytes = options.build_raw.then(|| name_field.to_vec());
1106            let raw_name_normalized = raw_name_bytes
1107                .as_ref()
1108                .map(|name_bytes| normalize_vmd_name(trim_fixed_bytes(name_bytes)));
1109            let parsed_name = options.build_parsed.then(|| decode_sjis_fixed(name_field));
1110            let parsed_name_bytes = options
1111                .build_parsed
1112                .then(|| trim_fixed_bytes(name_field).to_vec());
1113            let enabled = r.read_u8()?;
1114            if let Some(raw_ik_enabled) = raw_ik_enabled.as_mut() {
1115                raw_ik_enabled.push(enabled);
1116            }
1117            if let Some(raw_ik_entries) = raw_ik_entries.as_mut() {
1118                raw_ik_entries.push(VmdIkEntry {
1119                    name_bytes: raw_name_bytes
1120                        .expect("raw VMD scan must retain property name bytes"),
1121                    enabled,
1122                    name_normalized: raw_name_normalized
1123                        .expect("raw VMD scan must normalize property names"),
1124                });
1125            }
1126            if let Some(ik_states) = ik_states.as_mut() {
1127                ik_states.push(VmdParsedIkState {
1128                    bone_name: parsed_name.expect("parsed VMD scan must decode property names"),
1129                    bone_name_bytes: parsed_name_bytes
1130                        .expect("parsed VMD scan must retain property name bytes"),
1131                    enabled: enabled != 0,
1132                });
1133            }
1134        }
1135        if let Some(parsed_frames) = parsed_frames.as_mut() {
1136            parsed_frames.push(VmdParsedPropertyFrame {
1137                frame,
1138                visible: show != 0,
1139                ik_states: ik_states.expect("parsed VMD scan must build IK states"),
1140            });
1141        }
1142        if let Some(raw_keyframes) = raw_keyframes.as_mut() {
1143            raw_keyframes.push(PropertyKeyframe::new(
1144                frame,
1145                raw_ik_enabled
1146                    .expect("raw VMD scan must build IK enabled values")
1147                    .into_iter()
1148                    .map(|value| value != 0)
1149                    .collect(),
1150            ));
1151        }
1152        if let Some(raw_ik_frames) = raw_ik_frames.as_mut() {
1153            raw_ik_frames.push(VmdPropertyIkFrame {
1154                frame,
1155                show,
1156                entries: raw_ik_entries.expect("raw VMD scan must build property IK entries"),
1157            });
1158        }
1159    }
1160    Ok(VmdPropertyFrameReadResult {
1161        parsed_frames,
1162        raw_keyframes,
1163        raw_ik_frames,
1164    })
1165}
1166
1167pub fn sample_vmd_camera_frames(
1168    frames: &[VmdParsedCameraFrame],
1169    frame: f32,
1170) -> Option<VmdCameraState> {
1171    if frames.is_empty() {
1172        return None;
1173    }
1174
1175    let mut sorted: Vec<&VmdParsedCameraFrame> = frames.iter().collect();
1176    sorted.sort_by_key(|keyframe| keyframe.frame);
1177
1178    let mut index = 0usize;
1179    while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
1180        index += 1;
1181    }
1182
1183    let previous = sorted[index];
1184    let next = sorted.get(index + 1).copied().unwrap_or(previous);
1185    let t = interpolation_ratio(previous.frame, next.frame, frame);
1186    let interpolation = decode_camera_interpolation(&next.interpolation);
1187
1188    let distance_t = interpolation.distance.evaluate(t);
1189    let position_x_t = interpolation.position.x.evaluate(t);
1190    let position_y_t = interpolation.position.y.evaluate(t);
1191    let position_z_t = interpolation.position.z.evaluate(t);
1192    let rotation_t = interpolation.rotation.evaluate(t);
1193    let fov_t = interpolation.fov.evaluate(t);
1194
1195    Some(VmdCameraState {
1196        distance: lerp(previous.distance, next.distance, distance_t),
1197        position: [
1198            lerp(previous.position[0], next.position[0], position_x_t),
1199            lerp(previous.position[1], next.position[1], position_y_t),
1200            lerp(previous.position[2], next.position[2], position_z_t),
1201        ],
1202        rotation: [
1203            lerp(previous.rotation[0], next.rotation[0], rotation_t),
1204            lerp(previous.rotation[1], next.rotation[1], rotation_t),
1205            lerp(previous.rotation[2], next.rotation[2], rotation_t),
1206        ],
1207        fov: lerp(previous.fov as f32, next.fov as f32, fov_t),
1208        perspective: if t < 1.0 {
1209            previous.perspective
1210        } else {
1211            next.perspective
1212        },
1213    })
1214}
1215
1216pub fn sample_vmd_light_frames(
1217    frames: &[VmdParsedLightFrame],
1218    frame: f32,
1219) -> Option<VmdLightState> {
1220    if frames.is_empty() {
1221        return None;
1222    }
1223
1224    let mut sorted: Vec<&VmdParsedLightFrame> = frames.iter().collect();
1225    sorted.sort_by_key(|keyframe| keyframe.frame);
1226
1227    let mut index = 0usize;
1228    while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
1229        index += 1;
1230    }
1231
1232    let previous = sorted[index];
1233    let next = sorted.get(index + 1).copied().unwrap_or(previous);
1234    let t = interpolation_ratio(previous.frame, next.frame, frame);
1235
1236    Some(VmdLightState {
1237        color: [
1238            lerp(previous.color[0], next.color[0], t),
1239            lerp(previous.color[1], next.color[1], t),
1240            lerp(previous.color[2], next.color[2], t),
1241        ],
1242        direction: [
1243            lerp(previous.direction[0], next.direction[0], t),
1244            lerp(previous.direction[1], next.direction[1], t),
1245            lerp(previous.direction[2], next.direction[2], t),
1246        ],
1247    })
1248}
1249
1250pub fn sample_vmd_self_shadow_frames(
1251    frames: &[VmdParsedSelfShadowFrame],
1252    frame: f32,
1253) -> Option<VmdSelfShadowState> {
1254    if frames.is_empty() {
1255        return None;
1256    }
1257
1258    let mut sorted: Vec<&VmdParsedSelfShadowFrame> = frames.iter().collect();
1259    sorted.sort_by_key(|keyframe| keyframe.frame);
1260
1261    let mut index = 0usize;
1262    while index + 1 < sorted.len() && sorted[index + 1].frame as f32 <= frame {
1263        index += 1;
1264    }
1265
1266    let previous = sorted[index];
1267    let next = sorted.get(index + 1).copied().unwrap_or(previous);
1268    let t = interpolation_ratio(previous.frame, next.frame, frame);
1269
1270    Some(VmdSelfShadowState {
1271        mode: if t < 1.0 { previous.mode } else { next.mode },
1272        distance: lerp(previous.distance, next.distance, t),
1273    })
1274}
1275
1276struct CameraInterpolation {
1277    position: InterpolationVector3,
1278    rotation: InterpolationScalar,
1279    distance: InterpolationScalar,
1280    fov: InterpolationScalar,
1281}
1282
1283fn decode_camera_interpolation(interpolation: &[u8; 24]) -> CameraInterpolation {
1284    CameraInterpolation {
1285        position: InterpolationVector3 {
1286            x: decode_camera_interpolation_scalar(interpolation, 0),
1287            y: decode_camera_interpolation_scalar(interpolation, 1),
1288            z: decode_camera_interpolation_scalar(interpolation, 2),
1289        },
1290        rotation: decode_camera_interpolation_scalar(interpolation, 3),
1291        distance: decode_camera_interpolation_scalar(interpolation, 4),
1292        fov: decode_camera_interpolation_scalar(interpolation, 5),
1293    }
1294}
1295
1296fn decode_camera_interpolation_scalar(
1297    interpolation: &[u8; 24],
1298    channel: usize,
1299) -> InterpolationScalar {
1300    let offset = channel * 4;
1301    decode_interpolation_scalar([
1302        interpolation[offset],
1303        interpolation[offset + 1],
1304        interpolation[offset + 2],
1305        interpolation[offset + 3],
1306    ])
1307}
1308
1309fn interpolation_ratio(previous_frame: u32, next_frame: u32, frame: f32) -> f32 {
1310    if next_frame <= previous_frame {
1311        return 0.0;
1312    }
1313    let span = next_frame - previous_frame;
1314    if span <= 1 {
1315        return if frame >= next_frame as f32 { 1.0 } else { 0.0 };
1316    }
1317    ((frame - previous_frame as f32) / span as f32).clamp(0.0, 1.0)
1318}
1319
1320fn lerp(a: f32, b: f32, t: f32) -> f32 {
1321    a + (b - a) * t
1322}
1323
1324fn decode_sjis_fixed(bytes: &[u8]) -> String {
1325    decode_sjis_fixed_trimmed(bytes)
1326}
1327
1328fn trim_fixed_bytes(bytes: &[u8]) -> &[u8] {
1329    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1330    &bytes[..end]
1331}
1332
1333fn write_fixed_name_bytes(out: &mut Vec<u8>, value: &str, raw_bytes: &[u8], len: usize) {
1334    if raw_bytes.is_empty() {
1335        let encoded = encode_sjis(value);
1336        write_fixed_bytes(out, &encoded, len);
1337    } else {
1338        write_fixed_bytes(out, raw_bytes, len);
1339    }
1340}
1341
1342fn decode_interpolation_scalar(data: [u8; 4]) -> InterpolationScalar {
1343    InterpolationScalar {
1344        x1: data[0].min(127),
1345        y1: data[1].min(127),
1346        x2: data[2].min(127),
1347        y2: data[3].min(127),
1348    }
1349}
1350
1351fn decode_bone_interpolation(
1352    interpolation: &[u8; 64],
1353) -> (InterpolationVector3, InterpolationScalar) {
1354    let position = InterpolationVector3 {
1355        x: decode_interpolation_scalar([
1356            interpolation[0],
1357            interpolation[4],
1358            interpolation[8],
1359            interpolation[12],
1360        ]),
1361        y: decode_interpolation_scalar([
1362            interpolation[1],
1363            interpolation[5],
1364            interpolation[9],
1365            interpolation[13],
1366        ]),
1367        z: decode_interpolation_scalar([
1368            interpolation[2],
1369            interpolation[6],
1370            interpolation[10],
1371            interpolation[14],
1372        ]),
1373    };
1374    let rotation = decode_interpolation_scalar([
1375        interpolation[3],
1376        interpolation[7],
1377        interpolation[11],
1378        interpolation[15],
1379    ]);
1380    (position, rotation)
1381}
1382
1383/// Decodes the interpolation block as MMD's PMX/VMD registration path uses it.
1384/// The raw parser intentionally retains nanoem's first-16-byte strided layout;
1385/// this alternate layout is used only by the model-aware paired clip builder.
1386fn decode_mmd_registered_bone_interpolation(
1387    interpolation: &[u8; 64],
1388) -> (InterpolationVector3, InterpolationScalar) {
1389    let position = InterpolationVector3 {
1390        x: decode_interpolation_scalar([
1391            interpolation[0],
1392            interpolation[4],
1393            interpolation[8],
1394            interpolation[12],
1395        ]),
1396        y: decode_interpolation_scalar([
1397            interpolation[16],
1398            interpolation[20],
1399            interpolation[24],
1400            interpolation[28],
1401        ]),
1402        z: decode_interpolation_scalar([
1403            interpolation[32],
1404            interpolation[36],
1405            interpolation[40],
1406            interpolation[44],
1407        ]),
1408    };
1409    let rotation = decode_interpolation_scalar([
1410        interpolation[48],
1411        interpolation[52],
1412        interpolation[56],
1413        interpolation[60],
1414    ]);
1415    (position, rotation)
1416}
1417
1418pub fn build_clip_from_import(
1419    result: VmdImportResult,
1420    bone_name_to_index: &dyn Fn(&[u8]) -> Option<BoneIndex>,
1421    morph_name_to_index: &dyn Fn(&[u8]) -> Option<MorphIndex>,
1422) -> AnimationClip {
1423    let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
1424        std::collections::BTreeMap::new();
1425
1426    for kf in result.bone_keyframes {
1427        let bone_index = match &kf.bone_mode {
1428            VmdBoneImportMode::ByName(_name) => {
1429                match bone_name_to_index(&kf.bone_name_normalized) {
1430                    Some(idx) => idx,
1431                    None => continue,
1432                }
1433            }
1434            VmdBoneImportMode::ByIndex(idx) => BoneIndex(*idx),
1435        };
1436
1437        let (pos_interp, rot_interp) = decode_bone_interpolation(&kf.interpolation);
1438
1439        bone_tracks_map
1440            .entry(bone_index.0)
1441            .or_default()
1442            .push(MovableBoneKeyframe {
1443                frame: kf.frame,
1444                position: kf.position,
1445                rotation: kf.rotation,
1446                position_interpolation: pos_interp,
1447                rotation_interpolation: rot_interp,
1448            });
1449    }
1450
1451    let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
1452        .into_iter()
1453        .map(|(bone_idx, kfs)| BoneAnimationBinding {
1454            bone: BoneIndex(bone_idx),
1455            track: MovableBoneTrack::from_keyframes(kfs),
1456        })
1457        .collect();
1458
1459    let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
1460        std::collections::BTreeMap::new();
1461
1462    for (morph_name, frame, weight) in result.morph_keyframes {
1463        let morph_name_normalized = normalize_vmd_name(&morph_name);
1464        let morph_index = match morph_name_to_index(&morph_name_normalized) {
1465            Some(idx) => idx,
1466            None => continue,
1467        };
1468        morph_tracks_map
1469            .entry(morph_index.0)
1470            .or_default()
1471            .push(MorphKeyframe::new(frame, weight));
1472    }
1473
1474    let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
1475        .into_iter()
1476        .map(|(morph_idx, kfs)| MorphAnimationBinding {
1477            morph: MorphIndex(morph_idx),
1478            track: MorphTrack::from_keyframes(kfs),
1479        })
1480        .collect();
1481
1482    let property_track = if result.property_keyframes.is_empty() {
1483        None
1484    } else {
1485        Some(PropertyAnimationBinding::from_keyframes(
1486            result.property_keyframes,
1487        ))
1488    };
1489
1490    AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1491}
1492
1493pub fn build_property_binding_with_ik_resolver(
1494    ik_frames: &[VmdPropertyIkFrame],
1495    ik_name_to_solver_index: &dyn Fn(&[u8]) -> Option<usize>,
1496    solver_count: usize,
1497) -> Option<PropertyAnimationBinding> {
1498    if solver_count == 0 || ik_frames.is_empty() {
1499        return None;
1500    }
1501
1502    let keyframes: Vec<PropertyKeyframe> = ik_frames
1503        .iter()
1504        .map(|frame| {
1505            let mut ik_enabled = vec![1u8; solver_count];
1506            for entry in &frame.entries {
1507                match ik_name_to_solver_index(&entry.name_normalized) {
1508                    Some(idx) if idx < solver_count => {
1509                        ik_enabled[idx] = entry.enabled;
1510                    }
1511                    _ => {}
1512                }
1513            }
1514            PropertyKeyframe {
1515                frame: frame.frame,
1516                ik_enabled: ik_enabled.into_boxed_slice(),
1517            }
1518        })
1519        .collect();
1520
1521    Some(PropertyAnimationBinding::from_keyframes(keyframes))
1522}
1523
1524/// Options for controlling VMD clip construction behavior.
1525#[derive(Debug, Clone, Copy)]
1526pub struct VmdClipBuildOptions {
1527    /// When `true` (the default), the property IK enable/disable data from the
1528    /// VMD is baked into the clip. When `false`, the clip omits the property
1529    /// track entirely, so all IK solvers remain at their runtime default
1530    /// (enabled) state — useful when comparing against toolchain outputs that
1531    /// do not preserve property IK.
1532    pub honor_property_ik: bool,
1533}
1534
1535/// Errors returned while constructing an MMD-registered VMD clip.
1536#[derive(Debug, Error, Clone, PartialEq, Eq)]
1537pub enum VmdClipBuildError {
1538    /// A model-aware bone map contains a bone index that cannot be looked up
1539    /// in the supplied [`ModelArena`].
1540    #[error(
1541        "VMD bone map contains bone index {bone_index:?} outside model bone count {bone_count}"
1542    )]
1543    BoneIndexOutOfRange {
1544        bone_index: BoneIndex,
1545        bone_count: usize,
1546    },
1547}
1548
1549impl Default for VmdClipBuildOptions {
1550    fn default() -> Self {
1551        Self {
1552            honor_property_ik: true,
1553        }
1554    }
1555}
1556
1557pub fn build_pair_clip(
1558    result: &VmdImportResult,
1559    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1560    morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1561    ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1562    solver_count: usize,
1563) -> AnimationClip {
1564    build_pair_clip_with_options(
1565        result,
1566        bone_name_to_index,
1567        morph_name_to_index,
1568        ik_solver_bone_name_to_index,
1569        solver_count,
1570        VmdClipBuildOptions::default(),
1571    )
1572}
1573
1574/// Builds a VMD clip using the paired PMX model's MMD registration semantics.
1575///
1576/// The default VMD builder intentionally preserves the raw imported rotations.
1577/// This opt-in paired path applies MMD's fixed-axis registration rule to
1578/// mapped bone keys and decodes their interpolation using MMD's registered
1579/// block layout; positions, morphs, and property IK remain identical to
1580/// [`build_pair_clip_with_options`].
1581pub fn build_mmd_registered_pair_clip(
1582    model: &ModelArena,
1583    result: &VmdImportResult,
1584    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1585    morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1586    ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1587    solver_count: usize,
1588) -> Result<AnimationClip, VmdClipBuildError> {
1589    build_mmd_registered_pair_clip_with_options(
1590        model,
1591        result,
1592        bone_name_to_index,
1593        morph_name_to_index,
1594        ik_solver_bone_name_to_index,
1595        solver_count,
1596        VmdClipBuildOptions::default(),
1597    )
1598}
1599
1600pub fn build_pair_clip_with_options(
1601    result: &VmdImportResult,
1602    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1603    morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1604    ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1605    solver_count: usize,
1606    options: VmdClipBuildOptions,
1607) -> AnimationClip {
1608    build_pair_clip_with_registration_policy(
1609        result,
1610        bone_name_to_index,
1611        morph_name_to_index,
1612        ik_solver_bone_name_to_index,
1613        solver_count,
1614        options,
1615        VmdRegistrationPolicy::Raw,
1616    )
1617}
1618
1619/// Builds a VMD clip using the paired PMX model's MMD registration semantics and
1620/// explicit clip construction options.
1621pub fn build_mmd_registered_pair_clip_with_options(
1622    model: &ModelArena,
1623    result: &VmdImportResult,
1624    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1625    morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1626    ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1627    solver_count: usize,
1628    options: VmdClipBuildOptions,
1629) -> Result<AnimationClip, VmdClipBuildError> {
1630    validate_bone_map(model, bone_name_to_index)?;
1631    Ok(build_pair_clip_with_registration_policy(
1632        result,
1633        bone_name_to_index,
1634        morph_name_to_index,
1635        ik_solver_bone_name_to_index,
1636        solver_count,
1637        options,
1638        VmdRegistrationPolicy::MmdRegistered(model),
1639    ))
1640}
1641
1642#[derive(Clone, Copy)]
1643enum VmdRegistrationPolicy<'a> {
1644    Raw,
1645    MmdRegistered(&'a ModelArena),
1646}
1647
1648fn validate_bone_map(
1649    model: &ModelArena,
1650    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1651) -> Result<(), VmdClipBuildError> {
1652    let bone_count = model.bone_count();
1653    if let Some(&bone_index) = bone_name_to_index
1654        .values()
1655        .find(|bone_index| bone_index.as_usize() >= bone_count)
1656    {
1657        return Err(VmdClipBuildError::BoneIndexOutOfRange {
1658            bone_index,
1659            bone_count,
1660        });
1661    }
1662    Ok(())
1663}
1664
1665fn build_pair_clip_with_registration_policy(
1666    result: &VmdImportResult,
1667    bone_name_to_index: &std::collections::HashMap<Vec<u8>, BoneIndex>,
1668    morph_name_to_index: &std::collections::HashMap<Vec<u8>, MorphIndex>,
1669    ik_solver_bone_name_to_index: &std::collections::HashMap<Vec<u8>, usize>,
1670    solver_count: usize,
1671    options: VmdClipBuildOptions,
1672    registration_policy: VmdRegistrationPolicy<'_>,
1673) -> AnimationClip {
1674    let mut bone_tracks_map: std::collections::BTreeMap<u32, Vec<MovableBoneKeyframe>> =
1675        std::collections::BTreeMap::new();
1676
1677    for kf in &result.bone_keyframes {
1678        let bone_index = match bone_name_to_index.get(&kf.bone_name_normalized) {
1679            Some(idx) => *idx,
1680            None => continue,
1681        };
1682
1683        bone_tracks_map
1684            .entry(bone_index.0)
1685            .or_default()
1686            .push(build_mapped_bone_keyframe(
1687                kf,
1688                bone_index,
1689                registration_policy,
1690            ));
1691    }
1692
1693    let bone_tracks: Vec<BoneAnimationBinding> = bone_tracks_map
1694        .into_iter()
1695        .map(|(bone_idx, kfs)| BoneAnimationBinding {
1696            bone: BoneIndex(bone_idx),
1697            track: MovableBoneTrack::from_keyframes(kfs),
1698        })
1699        .collect();
1700
1701    let mut morph_tracks_map: std::collections::BTreeMap<u32, Vec<MorphKeyframe>> =
1702        std::collections::BTreeMap::new();
1703
1704    for (morph_name, frame, weight) in &result.morph_keyframes {
1705        let morph_name_normalized = normalize_vmd_name(morph_name);
1706        let morph_index = match morph_name_to_index.get(&morph_name_normalized) {
1707            Some(idx) => *idx,
1708            None => continue,
1709        };
1710        morph_tracks_map
1711            .entry(morph_index.0)
1712            .or_default()
1713            .push(MorphKeyframe::new(*frame, *weight));
1714    }
1715
1716    let morph_tracks: Vec<MorphAnimationBinding> = morph_tracks_map
1717        .into_iter()
1718        .map(|(morph_idx, kfs)| MorphAnimationBinding {
1719            morph: MorphIndex(morph_idx),
1720            track: MorphTrack::from_keyframes(kfs),
1721        })
1722        .collect();
1723
1724    let property_track = if options.honor_property_ik {
1725        build_property_binding_with_ik_resolver(
1726            &result.property_ik_frames,
1727            &|name| ik_solver_bone_name_to_index.get(name).copied(),
1728            solver_count,
1729        )
1730    } else {
1731        None
1732    };
1733
1734    AnimationClip::new_full(bone_tracks, morph_tracks, property_track)
1735}
1736
1737fn build_mapped_bone_keyframe(
1738    kf: &VmdBoneKeyframeRaw,
1739    bone_index: BoneIndex,
1740    registration_policy: VmdRegistrationPolicy<'_>,
1741) -> MovableBoneKeyframe {
1742    let (position_interpolation, rotation_interpolation) = match registration_policy {
1743        VmdRegistrationPolicy::Raw => decode_bone_interpolation(&kf.interpolation),
1744        VmdRegistrationPolicy::MmdRegistered(_) => {
1745            decode_mmd_registered_bone_interpolation(&kf.interpolation)
1746        }
1747    };
1748    let rotation = match registration_policy {
1749        VmdRegistrationPolicy::Raw => kf.rotation,
1750        VmdRegistrationPolicy::MmdRegistered(model) => {
1751            model.fixed_axis(bone_index).map_or(kf.rotation, |axis| {
1752                project_rotation_to_fixed_axis(kf.rotation, axis)
1753            })
1754        }
1755    };
1756    MovableBoneKeyframe {
1757        frame: kf.frame,
1758        position: kf.position,
1759        rotation,
1760        position_interpolation,
1761        rotation_interpolation,
1762    }
1763}
1764
1765fn project_rotation_to_fixed_axis(rotation: Quat, axis: Vec3A) -> Quat {
1766    let axis = axis.normalize();
1767    let vector = Vec3A::new(rotation.x, rotation.y, rotation.z);
1768    let sign = if vector.dot(axis) < 0.0 { -1.0 } else { 1.0 };
1769    let projected = axis * vector.length() * sign;
1770    Quat::from_xyzw(projected.x, projected.y, projected.z, rotation.w)
1771}
1772
1773#[cfg(test)]
1774mod tests {
1775    use super::*;
1776    #[allow(unused_imports)]
1777    use serde_json;
1778
1779    fn build_vmd_header_bytes() -> Vec<u8> {
1780        let mut buf = Vec::new();
1781        buf.extend_from_slice(&VMD_MAGIC);
1782        buf.extend_from_slice(&[0u8; 20]);
1783        buf
1784    }
1785
1786    #[test]
1787    fn rejects_impossible_vmd_bone_count_before_allocation() {
1788        let mut buf = build_vmd_header_bytes();
1789        buf.extend_from_slice(&u32::MAX.to_le_bytes());
1790
1791        assert!(matches!(
1792            import_vmd_motion(&buf),
1793            Err(ImportError::UnexpectedEof(_))
1794        ));
1795        assert!(matches!(
1796            parse_vmd_animation(&buf),
1797            Err(ImportError::UnexpectedEof(_))
1798        ));
1799    }
1800
1801    #[test]
1802    fn rejects_impossible_vmd_property_ik_count_before_allocation() {
1803        let mut buf = build_vmd_header_bytes();
1804        buf.extend_from_slice(&0u32.to_le_bytes()); // bones
1805        buf.extend_from_slice(&0u32.to_le_bytes()); // morphs
1806        buf.extend_from_slice(&0u32.to_le_bytes()); // cameras
1807        buf.extend_from_slice(&0u32.to_le_bytes()); // lights
1808        buf.extend_from_slice(&0u32.to_le_bytes()); // self shadows
1809        buf.extend_from_slice(&1u32.to_le_bytes()); // property frames
1810        buf.extend_from_slice(&0u32.to_le_bytes()); // frame
1811        buf.push(1); // visible
1812        buf.extend_from_slice(&u32::MAX.to_le_bytes()); // IK entries
1813
1814        assert!(matches!(
1815            import_vmd_motion(&buf),
1816            Err(ImportError::UnexpectedEof(_))
1817        ));
1818        assert!(matches!(
1819            parse_vmd_animation(&buf),
1820            Err(ImportError::UnexpectedEof(_))
1821        ));
1822    }
1823
1824    #[test]
1825    fn parses_vmd_header() {
1826        let header_bytes = build_vmd_header_bytes();
1827        let (header, pos) = read_header(&header_bytes).unwrap();
1828        assert_eq!(header.model_name_bytes, [0u8; 20]);
1829        assert!(pos > 0);
1830    }
1831
1832    #[test]
1833    fn rejects_bad_vmd_magic() {
1834        let mut buf = build_vmd_header_bytes();
1835        buf[0] = 0xFF;
1836        assert_eq!(read_header(&buf).unwrap_err(), ImportError::InvalidVmdMagic);
1837    }
1838
1839    #[test]
1840    fn accepts_vmd_magic_with_nonzero_padding_bytes() {
1841        let mut buf = build_vmd_header_bytes();
1842        buf[26..30].copy_from_slice(b"JKLM");
1843
1844        let (_header, pos) = read_header(&buf).unwrap();
1845        assert_eq!(pos, 50);
1846    }
1847
1848    #[test]
1849    fn parses_minimal_vmd_motion() {
1850        let mut buf = build_vmd_header_bytes();
1851
1852        buf.extend_from_slice(&0u32.to_le_bytes());
1853        buf.extend_from_slice(&0u32.to_le_bytes());
1854        buf.extend_from_slice(&0u32.to_le_bytes());
1855        buf.extend_from_slice(&0u32.to_le_bytes());
1856        buf.extend_from_slice(&0u32.to_le_bytes());
1857        buf.extend_from_slice(&0u32.to_le_bytes());
1858
1859        let result = import_vmd_motion(&buf).unwrap();
1860        assert!(result.bone_keyframes.is_empty());
1861        assert!(result.morph_keyframes.is_empty());
1862        assert!(result.property_keyframes.is_empty());
1863    }
1864
1865    #[test]
1866    fn accepts_vmd_without_optional_tail_sections() {
1867        let mut buf = build_vmd_header_bytes();
1868        buf.extend_from_slice(&0u32.to_le_bytes());
1869
1870        let result = import_vmd_motion(&buf).unwrap();
1871        assert!(result.bone_keyframes.is_empty());
1872        assert!(result.morph_keyframes.is_empty());
1873        assert!(result.property_keyframes.is_empty());
1874        assert!(result.property_ik_frames.is_empty());
1875    }
1876
1877    #[test]
1878    fn accepts_vmd_ending_after_shadow_section() {
1879        let mut buf = build_vmd_header_bytes();
1880        buf.extend_from_slice(&0u32.to_le_bytes());
1881        buf.extend_from_slice(&0u32.to_le_bytes());
1882        buf.extend_from_slice(&0u32.to_le_bytes());
1883        buf.extend_from_slice(&0u32.to_le_bytes());
1884        buf.extend_from_slice(&0u32.to_le_bytes());
1885
1886        let result = import_vmd_motion(&buf).unwrap();
1887        assert!(result.bone_keyframes.is_empty());
1888        assert!(result.morph_keyframes.is_empty());
1889        assert!(result.property_keyframes.is_empty());
1890        assert!(result.property_ik_frames.is_empty());
1891    }
1892
1893    #[test]
1894    fn rejects_partial_optional_count() {
1895        let mut buf = build_vmd_header_bytes();
1896        buf.extend_from_slice(&0u32.to_le_bytes());
1897        buf.extend_from_slice(&[1, 2, 3]);
1898
1899        assert_eq!(
1900            import_vmd_motion(&buf).unwrap_err(),
1901            ImportError::UnexpectedEof(1)
1902        );
1903    }
1904
1905    #[test]
1906    fn ignores_truncated_unused_tail_sections() {
1907        let mut buf = build_vmd_header_bytes();
1908        buf.extend_from_slice(&0u32.to_le_bytes());
1909        buf.extend_from_slice(&0u32.to_le_bytes());
1910        buf.extend_from_slice(&0u32.to_le_bytes());
1911        buf.extend_from_slice(&0u32.to_le_bytes());
1912        buf.extend_from_slice(&10u32.to_le_bytes());
1913        buf.extend_from_slice(&[0u8; 8]);
1914
1915        let result = import_vmd_motion(&buf).unwrap();
1916        assert!(result.bone_keyframes.is_empty());
1917        assert!(result.morph_keyframes.is_empty());
1918        assert!(result.property_keyframes.is_empty());
1919        assert!(result.property_ik_frames.is_empty());
1920    }
1921
1922    #[test]
1923    fn parsed_animation_ignores_implausible_optional_tail() {
1924        let mut buf = build_vmd_header_bytes();
1925        buf.extend_from_slice(&0u32.to_le_bytes()); // bones
1926        buf.extend_from_slice(&0u32.to_le_bytes()); // morphs
1927        buf.extend_from_slice(&0u32.to_le_bytes()); // camera count
1928
1929        // Some real-world VMD files contain a 61-byte camera-shaped tail after
1930        // a zero camera count. Treat it as an optional malformed tail instead
1931        // of failing the parser/exporter gate.
1932        buf.extend_from_slice(&300u32.to_le_bytes());
1933        buf.extend_from_slice(&[0u8; 57]);
1934
1935        let parsed = parse_vmd_animation(&buf).unwrap();
1936        assert!(parsed.camera_frames.is_empty());
1937        assert!(parsed.light_frames.is_empty());
1938        assert!(parsed.self_shadow_frames.is_empty());
1939        assert!(parsed.property_frames.is_empty());
1940    }
1941
1942    #[test]
1943    fn parses_single_bone_keyframe() {
1944        let mut buf = build_vmd_header_bytes();
1945
1946        buf.extend_from_slice(&1u32.to_le_bytes());
1947        let mut bone_name = [0u8; 15];
1948        bone_name[..4].copy_from_slice(b"Bone");
1949        buf.extend_from_slice(&bone_name);
1950        buf.extend_from_slice(&10u32.to_le_bytes());
1951        buf.extend_from_slice(&1.0f32.to_le_bytes());
1952        buf.extend_from_slice(&2.0f32.to_le_bytes());
1953        buf.extend_from_slice(&3.0f32.to_le_bytes());
1954        buf.extend_from_slice(&0.0f32.to_le_bytes());
1955        buf.extend_from_slice(&0.0f32.to_le_bytes());
1956        buf.extend_from_slice(&0.0f32.to_le_bytes());
1957        buf.extend_from_slice(&1.0f32.to_le_bytes());
1958        buf.extend_from_slice(&[20u8; 64]);
1959
1960        buf.extend_from_slice(&0u32.to_le_bytes());
1961        buf.extend_from_slice(&0u32.to_le_bytes());
1962        buf.extend_from_slice(&0u32.to_le_bytes());
1963        buf.extend_from_slice(&0u32.to_le_bytes());
1964        buf.extend_from_slice(&0u32.to_le_bytes());
1965
1966        let result = import_vmd_motion(&buf).unwrap();
1967        assert_eq!(result.bone_keyframes.len(), 1);
1968        assert_eq!(result.bone_keyframes[0].frame, 10);
1969        assert!((result.bone_keyframes[0].position.x - 1.0).abs() < 0.001);
1970        assert!((result.bone_keyframes[0].position.y - 2.0).abs() < 0.001);
1971        assert!((result.bone_keyframes[0].position.z - 3.0).abs() < 0.001);
1972        assert_eq!(&result.bone_keyframes[0].bone_name_normalized[..], b"Bone");
1973    }
1974
1975    #[test]
1976    fn exports_parsed_vmd_animation_for_roundtrip() {
1977        let mut buf = build_vmd_header_bytes();
1978
1979        buf.extend_from_slice(&1u32.to_le_bytes());
1980        let mut bone_name = [0u8; 15];
1981        bone_name[..4].copy_from_slice(b"Bone");
1982        buf.extend_from_slice(&bone_name);
1983        buf.extend_from_slice(&10u32.to_le_bytes());
1984        buf.extend_from_slice(&1.0f32.to_le_bytes());
1985        buf.extend_from_slice(&2.0f32.to_le_bytes());
1986        buf.extend_from_slice(&3.0f32.to_le_bytes());
1987        buf.extend_from_slice(&0.1f32.to_le_bytes());
1988        buf.extend_from_slice(&0.2f32.to_le_bytes());
1989        buf.extend_from_slice(&0.3f32.to_le_bytes());
1990        buf.extend_from_slice(&0.4f32.to_le_bytes());
1991        buf.extend_from_slice(&[20u8; 64]);
1992
1993        buf.extend_from_slice(&1u32.to_le_bytes());
1994        let mut morph_name = [0u8; 15];
1995        morph_name[..5].copy_from_slice(b"Smile");
1996        buf.extend_from_slice(&morph_name);
1997        buf.extend_from_slice(&11u32.to_le_bytes());
1998        buf.extend_from_slice(&0.75f32.to_le_bytes());
1999
2000        buf.extend_from_slice(&1u32.to_le_bytes());
2001        buf.extend_from_slice(&12u32.to_le_bytes());
2002        buf.extend_from_slice(&30.0f32.to_le_bytes());
2003        buf.extend_from_slice(&1.0f32.to_le_bytes());
2004        buf.extend_from_slice(&2.0f32.to_le_bytes());
2005        buf.extend_from_slice(&3.0f32.to_le_bytes());
2006        buf.extend_from_slice(&0.1f32.to_le_bytes());
2007        buf.extend_from_slice(&0.2f32.to_le_bytes());
2008        buf.extend_from_slice(&0.3f32.to_le_bytes());
2009        buf.extend_from_slice(&[30u8; 24]);
2010        buf.extend_from_slice(&45u32.to_le_bytes());
2011        buf.push(0);
2012
2013        buf.extend_from_slice(&1u32.to_le_bytes());
2014        buf.extend_from_slice(&13u32.to_le_bytes());
2015        buf.extend_from_slice(&1.0f32.to_le_bytes());
2016        buf.extend_from_slice(&0.5f32.to_le_bytes());
2017        buf.extend_from_slice(&0.25f32.to_le_bytes());
2018        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
2019        buf.extend_from_slice(&(-0.5f32).to_le_bytes());
2020        buf.extend_from_slice(&(-0.25f32).to_le_bytes());
2021
2022        buf.extend_from_slice(&1u32.to_le_bytes());
2023        buf.extend_from_slice(&14u32.to_le_bytes());
2024        buf.push(2);
2025        buf.extend_from_slice(&0.6f32.to_le_bytes());
2026
2027        buf.extend_from_slice(&1u32.to_le_bytes());
2028        buf.extend_from_slice(&15u32.to_le_bytes());
2029        buf.push(1);
2030        buf.extend_from_slice(&1u32.to_le_bytes());
2031        let mut ik_name = [0u8; 20];
2032        ik_name[..2].copy_from_slice(b"IK");
2033        buf.extend_from_slice(&ik_name);
2034        buf.push(1);
2035
2036        let parsed = parse_vmd_animation(&buf).unwrap();
2037        let exported = export_vmd_animation(&parsed);
2038        let reparsed = parse_vmd_animation(&exported).unwrap();
2039
2040        assert_vmd_roundtrip_eq(&parsed, &reparsed);
2041    }
2042
2043    fn assert_vmd_roundtrip_eq(left: &VmdParsedAnimation, right: &VmdParsedAnimation) {
2044        assert_eq!(left.metadata.model_name, right.metadata.model_name);
2045        assert_eq!(left.metadata.max_frame, right.metadata.max_frame);
2046        assert_eq!(left.bone_frames.len(), right.bone_frames.len());
2047        assert_eq!(left.morph_frames.len(), right.morph_frames.len());
2048        assert_eq!(left.camera_frames.len(), right.camera_frames.len());
2049        assert_eq!(left.light_frames.len(), right.light_frames.len());
2050        assert_eq!(
2051            left.self_shadow_frames.len(),
2052            right.self_shadow_frames.len()
2053        );
2054        assert_eq!(left.property_frames.len(), right.property_frames.len());
2055        assert_eq!(
2056            left.bone_frames[0].bone_name,
2057            right.bone_frames[0].bone_name
2058        );
2059        assert_eq!(left.bone_frames[0].frame, right.bone_frames[0].frame);
2060        assert_eq!(
2061            left.bone_frames[0].translation,
2062            right.bone_frames[0].translation
2063        );
2064        assert_eq!(left.bone_frames[0].rotation, right.bone_frames[0].rotation);
2065        assert_eq!(
2066            left.bone_frames[0].interpolation,
2067            right.bone_frames[0].interpolation
2068        );
2069        assert_eq!(
2070            left.morph_frames[0].morph_name,
2071            right.morph_frames[0].morph_name
2072        );
2073        assert_eq!(left.morph_frames[0].frame, right.morph_frames[0].frame);
2074        assert_eq!(left.morph_frames[0].weight, right.morph_frames[0].weight);
2075        assert_eq!(left.camera_frames[0].frame, right.camera_frames[0].frame);
2076        assert_eq!(
2077            left.camera_frames[0].position,
2078            right.camera_frames[0].position
2079        );
2080        assert_eq!(
2081            left.camera_frames[0].rotation,
2082            right.camera_frames[0].rotation
2083        );
2084        assert_eq!(
2085            left.camera_frames[0].interpolation,
2086            right.camera_frames[0].interpolation
2087        );
2088        assert_eq!(
2089            left.camera_frames[0].perspective,
2090            right.camera_frames[0].perspective
2091        );
2092        assert_eq!(left.light_frames[0].color, right.light_frames[0].color);
2093        assert_eq!(
2094            left.light_frames[0].direction,
2095            right.light_frames[0].direction
2096        );
2097        assert_eq!(
2098            left.self_shadow_frames[0].mode,
2099            right.self_shadow_frames[0].mode
2100        );
2101        assert_eq!(
2102            left.self_shadow_frames[0].distance,
2103            right.self_shadow_frames[0].distance
2104        );
2105        assert_eq!(
2106            left.property_frames[0].visible,
2107            right.property_frames[0].visible
2108        );
2109        assert_eq!(
2110            left.property_frames[0].ik_states[0].bone_name,
2111            right.property_frames[0].ik_states[0].bone_name
2112        );
2113        assert_eq!(
2114            left.property_frames[0].ik_states[0].enabled,
2115            right.property_frames[0].ik_states[0].enabled
2116        );
2117    }
2118
2119    #[test]
2120    fn decodes_raw_vmd_bone_interpolation_as_strided_curves() {
2121        let mut interpolation = [0u8; 64];
2122        for (index, value) in interpolation.iter_mut().enumerate() {
2123            *value = index as u8;
2124        }
2125
2126        let (position, rotation) = decode_bone_interpolation(&interpolation);
2127
2128        assert_eq!(
2129            position.x,
2130            InterpolationScalar {
2131                x1: 0,
2132                y1: 4,
2133                x2: 8,
2134                y2: 12
2135            }
2136        );
2137        assert_eq!(
2138            position.y,
2139            InterpolationScalar {
2140                x1: 1,
2141                y1: 5,
2142                x2: 9,
2143                y2: 13
2144            }
2145        );
2146        assert_eq!(
2147            position.z,
2148            InterpolationScalar {
2149                x1: 2,
2150                y1: 6,
2151                x2: 10,
2152                y2: 14
2153            }
2154        );
2155        assert_eq!(
2156            rotation,
2157            InterpolationScalar {
2158                x1: 3,
2159                y1: 7,
2160                x2: 11,
2161                y2: 15
2162            }
2163        );
2164    }
2165
2166    #[test]
2167    fn decodes_mmd_registered_bone_interpolation_as_block_curves() {
2168        let mut interpolation = [0u8; 64];
2169        for (index, value) in interpolation.iter_mut().enumerate() {
2170            *value = index as u8;
2171        }
2172
2173        let (position, rotation) = decode_mmd_registered_bone_interpolation(&interpolation);
2174
2175        assert_eq!(
2176            position.x,
2177            InterpolationScalar {
2178                x1: 0,
2179                y1: 4,
2180                x2: 8,
2181                y2: 12
2182            }
2183        );
2184        assert_eq!(
2185            position.y,
2186            InterpolationScalar {
2187                x1: 16,
2188                y1: 20,
2189                x2: 24,
2190                y2: 28
2191            }
2192        );
2193        assert_eq!(
2194            position.z,
2195            InterpolationScalar {
2196                x1: 32,
2197                y1: 36,
2198                x2: 40,
2199                y2: 44
2200            }
2201        );
2202        assert_eq!(
2203            rotation,
2204            InterpolationScalar {
2205                x1: 48,
2206                y1: 52,
2207                x2: 56,
2208                y2: 60
2209            }
2210        );
2211    }
2212
2213    #[test]
2214    fn decodes_raw_vmd_camera_interpolation_as_contiguous_curves() {
2215        let mut interpolation = [0u8; 24];
2216        for (index, value) in interpolation.iter_mut().enumerate() {
2217            *value = index as u8;
2218        }
2219
2220        let decoded = decode_camera_interpolation(&interpolation);
2221
2222        assert_eq!(
2223            decoded.position.x,
2224            InterpolationScalar {
2225                x1: 0,
2226                y1: 1,
2227                x2: 2,
2228                y2: 3
2229            }
2230        );
2231        assert_eq!(
2232            decoded.position.y,
2233            InterpolationScalar {
2234                x1: 4,
2235                y1: 5,
2236                x2: 6,
2237                y2: 7
2238            }
2239        );
2240        assert_eq!(
2241            decoded.position.z,
2242            InterpolationScalar {
2243                x1: 8,
2244                y1: 9,
2245                x2: 10,
2246                y2: 11
2247            }
2248        );
2249        assert_eq!(
2250            decoded.rotation,
2251            InterpolationScalar {
2252                x1: 12,
2253                y1: 13,
2254                x2: 14,
2255                y2: 15
2256            }
2257        );
2258        assert_eq!(
2259            decoded.distance,
2260            InterpolationScalar {
2261                x1: 16,
2262                y1: 17,
2263                x2: 18,
2264                y2: 19
2265            }
2266        );
2267        assert_eq!(
2268            decoded.fov,
2269            InterpolationScalar {
2270                x1: 20,
2271                y1: 21,
2272                x2: 22,
2273                y2: 23
2274            }
2275        );
2276    }
2277
2278    #[test]
2279    fn skips_camera_light_shadow_and_reads_ik_property_names() {
2280        let mut buf = build_vmd_header_bytes();
2281
2282        buf.extend_from_slice(&0u32.to_le_bytes());
2283        buf.extend_from_slice(&0u32.to_le_bytes());
2284        buf.extend_from_slice(&1u32.to_le_bytes());
2285        buf.extend_from_slice(&[0u8; 61]);
2286        buf.extend_from_slice(&1u32.to_le_bytes());
2287        buf.extend_from_slice(&[0u8; 28]);
2288        buf.extend_from_slice(&1u32.to_le_bytes());
2289        buf.extend_from_slice(&0u32.to_le_bytes());
2290        buf.push(1);
2291        buf.extend_from_slice(&0.5f32.to_le_bytes());
2292        buf.extend_from_slice(&1u32.to_le_bytes());
2293        buf.extend_from_slice(&24u32.to_le_bytes());
2294        buf.push(1);
2295        buf.extend_from_slice(&1u32.to_le_bytes());
2296        let mut ik_name = [0u8; 20];
2297        ik_name[..6].copy_from_slice(b"LeftIK");
2298        buf.extend_from_slice(&ik_name);
2299        buf.push(0);
2300
2301        let result = import_vmd_motion(&buf).unwrap();
2302        assert_eq!(result.property_keyframes.len(), 1);
2303        assert_eq!(result.property_keyframes[0].frame, 24);
2304        assert_eq!(&*result.property_keyframes[0].ik_enabled, &[0]);
2305
2306        assert_eq!(result.property_ik_frames.len(), 1);
2307        assert_eq!(result.property_ik_frames[0].frame, 24);
2308        assert_eq!(result.property_ik_frames[0].show, 1);
2309        assert_eq!(result.property_ik_frames[0].entries.len(), 1);
2310        let name_end = result.property_ik_frames[0].entries[0]
2311            .name_bytes
2312            .iter()
2313            .position(|&b| b == 0)
2314            .unwrap_or(20);
2315        assert_eq!(
2316            &result.property_ik_frames[0].entries[0].name_bytes[..name_end],
2317            b"LeftIK"
2318        );
2319        assert_eq!(result.property_ik_frames[0].entries[0].enabled, 0);
2320        assert_eq!(
2321            &result.property_ik_frames[0].entries[0].name_normalized[..],
2322            b"LeftIK"
2323        );
2324    }
2325
2326    #[test]
2327    fn builds_clip_from_bone_track() {
2328        let kfs = vec![
2329            VmdBoneKeyframeRaw {
2330                bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
2331                frame: 0,
2332                position: Vec3A::ZERO,
2333                rotation: Quat::IDENTITY,
2334                interpolation: [
2335                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
2336                    107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
2337                    107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2338                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2339                ],
2340                bone_name_normalized: b"BoneA".to_vec(),
2341            },
2342            VmdBoneKeyframeRaw {
2343                bone_mode: VmdBoneImportMode::ByName(b"BoneA".to_vec()),
2344                frame: 30,
2345                position: Vec3A::new(10.0, 0.0, 0.0),
2346                rotation: Quat::IDENTITY,
2347                interpolation: [
2348                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20,
2349                    107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107,
2350                    107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2351                    20, 20, 107, 107, 20, 20, 107, 107, 20, 20, 107, 107,
2352                ],
2353                bone_name_normalized: b"BoneA".to_vec(),
2354            },
2355        ];
2356
2357        fn lookup(name: &[u8]) -> Option<BoneIndex> {
2358            if name == b"BoneA" {
2359                Some(BoneIndex(0))
2360            } else {
2361                None
2362            }
2363        }
2364        fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
2365            None
2366        }
2367
2368        let result = VmdImportResult {
2369            bone_keyframes: kfs,
2370            morph_keyframes: Vec::new(),
2371            property_keyframes: Vec::new(),
2372            property_ik_frames: Vec::new(),
2373        };
2374
2375        let _clip = build_clip_from_import(result, &lookup, &morph_lookup);
2376    }
2377
2378    #[test]
2379    fn build_clip_from_import_resolves_morph_by_normalized_name() {
2380        let sjis_morph = vec![0x83, 0x65, 0x83, 0x58, 0x83, 0x67];
2381        let result = VmdImportResult {
2382            bone_keyframes: Vec::new(),
2383            morph_keyframes: vec![(sjis_morph, 0, 1.0)],
2384            property_keyframes: Vec::new(),
2385            property_ik_frames: Vec::new(),
2386        };
2387
2388        fn morph_lookup(name: &[u8]) -> Option<MorphIndex> {
2389            if name == b"\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88" {
2390                Some(MorphIndex(0))
2391            } else {
2392                None
2393            }
2394        }
2395        fn bone_lookup(_name: &[u8]) -> Option<BoneIndex> {
2396            None
2397        }
2398
2399        let clip = build_clip_from_import(result, &bone_lookup, &morph_lookup);
2400        assert_eq!(clip.morph_track_count(), 1);
2401    }
2402
2403    fn ik_name_bytes(name: &str) -> [u8; 20] {
2404        let mut buf = [0u8; 20];
2405        let name_bytes = name.as_bytes();
2406        let len = name_bytes.len().min(20);
2407        buf[..len].copy_from_slice(&name_bytes[..len]);
2408        buf
2409    }
2410
2411    #[test]
2412    fn parses_property_ik_entry_names() {
2413        let mut buf = build_vmd_header_bytes();
2414
2415        buf.extend_from_slice(&0u32.to_le_bytes());
2416        buf.extend_from_slice(&0u32.to_le_bytes());
2417        buf.extend_from_slice(&0u32.to_le_bytes());
2418        buf.extend_from_slice(&0u32.to_le_bytes());
2419        buf.extend_from_slice(&0u32.to_le_bytes());
2420        buf.extend_from_slice(&1u32.to_le_bytes());
2421        buf.extend_from_slice(&10u32.to_le_bytes());
2422        buf.push(0);
2423        buf.extend_from_slice(&2u32.to_le_bytes());
2424        buf.extend_from_slice(&ik_name_bytes("LeftLegIK"));
2425        buf.push(1);
2426        buf.extend_from_slice(&ik_name_bytes("RightLegIK"));
2427        buf.push(0);
2428
2429        let result = import_vmd_motion(&buf).unwrap();
2430        assert_eq!(result.property_ik_frames.len(), 1);
2431        assert_eq!(result.property_ik_frames[0].frame, 10);
2432        assert_eq!(result.property_ik_frames[0].entries.len(), 2);
2433
2434        let name1: Vec<u8> = result.property_ik_frames[0].entries[0]
2435            .name_bytes
2436            .iter()
2437            .copied()
2438            .take_while(|&b| b != 0)
2439            .collect();
2440        assert_eq!(&name1[..], b"LeftLegIK");
2441        assert_eq!(result.property_ik_frames[0].entries[0].enabled, 1);
2442        assert_eq!(
2443            &result.property_ik_frames[0].entries[0].name_normalized[..],
2444            b"LeftLegIK"
2445        );
2446
2447        let name2: Vec<u8> = result.property_ik_frames[0].entries[1]
2448            .name_bytes
2449            .iter()
2450            .copied()
2451            .take_while(|&b| b != 0)
2452            .collect();
2453        assert_eq!(&name2[..], b"RightLegIK");
2454        assert_eq!(result.property_ik_frames[0].entries[1].enabled, 0);
2455    }
2456
2457    #[test]
2458    fn shared_context_preserves_raw_property_bytes_and_values() {
2459        let mut buf = build_vmd_header_bytes();
2460        buf.extend_from_slice(&0u32.to_le_bytes()); // bones
2461        buf.extend_from_slice(&0u32.to_le_bytes()); // morphs
2462        buf.extend_from_slice(&0u32.to_le_bytes()); // cameras
2463        buf.extend_from_slice(&0u32.to_le_bytes()); // lights
2464        buf.extend_from_slice(&0u32.to_le_bytes()); // self shadows
2465        buf.extend_from_slice(&1u32.to_le_bytes()); // property frames
2466        buf.extend_from_slice(&42u32.to_le_bytes());
2467        buf.push(0x7f); // preserve a nonstandard show byte
2468        buf.extend_from_slice(&1u32.to_le_bytes()); // IK entries
2469        let mut name_bytes = [0u8; 20];
2470        name_bytes[..2].copy_from_slice(b"IK");
2471        name_bytes[2] = 0;
2472        name_bytes[3] = 0xa5; // preserve bytes after the terminating NUL
2473        name_bytes[19] = 0x5a;
2474        buf.extend_from_slice(&name_bytes);
2475        buf.push(0x7f); // preserve a nonstandard enabled byte
2476
2477        let shared = parse_vmd_shared_context(&buf).unwrap();
2478        let raw = shared.import_result();
2479        assert_eq!(raw.property_keyframes[0].frame, 42);
2480        assert_eq!(raw.property_keyframes[0].ik_enabled.as_ref(), &[1]);
2481        assert_eq!(raw.property_ik_frames[0].show, 0x7f);
2482        assert_eq!(
2483            raw.property_ik_frames[0].entries[0].name_bytes,
2484            name_bytes.to_vec()
2485        );
2486        assert_eq!(raw.property_ik_frames[0].entries[0].enabled, 0x7f);
2487        assert_eq!(
2488            raw.property_ik_frames[0].entries[0].name_normalized,
2489            normalize_vmd_name(b"IK")
2490        );
2491
2492        let parsed = shared.parsed_animation();
2493        assert!(parsed.property_frames[0].visible);
2494        assert_eq!(parsed.property_frames[0].ik_states[0].bone_name, "IK");
2495        assert!(parsed.property_frames[0].ik_states[0].enabled);
2496    }
2497
2498    #[test]
2499    fn shared_context_preserves_raw_bone_and_morph_bytes_order_and_interpolation() {
2500        let mut buf = build_vmd_header_bytes();
2501        buf.extend_from_slice(&2u32.to_le_bytes()); // bones
2502
2503        let mut first_bone_name = [0u8; 15];
2504        first_bone_name[..3].copy_from_slice(&[0x82, 0xa0, b'1']);
2505        first_bone_name[4] = 0xa5; // bytes after NUL stay outside the raw name
2506        buf.extend_from_slice(&first_bone_name);
2507        buf.extend_from_slice(&11u32.to_le_bytes());
2508        buf.extend_from_slice(&1.0f32.to_le_bytes());
2509        buf.extend_from_slice(&2.0f32.to_le_bytes());
2510        buf.extend_from_slice(&3.0f32.to_le_bytes());
2511        buf.extend_from_slice(&0.0f32.to_le_bytes());
2512        buf.extend_from_slice(&0.1f32.to_le_bytes());
2513        buf.extend_from_slice(&0.2f32.to_le_bytes());
2514        buf.extend_from_slice(&1.0f32.to_le_bytes());
2515        let first_interpolation = (0u8..64).collect::<Vec<_>>();
2516        buf.extend_from_slice(&first_interpolation);
2517
2518        let mut second_bone_name = [0u8; 15];
2519        second_bone_name[..4].copy_from_slice(b"bone");
2520        buf.extend_from_slice(&second_bone_name);
2521        buf.extend_from_slice(&3u32.to_le_bytes());
2522        buf.extend_from_slice(&4.0f32.to_le_bytes());
2523        buf.extend_from_slice(&5.0f32.to_le_bytes());
2524        buf.extend_from_slice(&6.0f32.to_le_bytes());
2525        buf.extend_from_slice(&0.3f32.to_le_bytes());
2526        buf.extend_from_slice(&0.4f32.to_le_bytes());
2527        buf.extend_from_slice(&0.5f32.to_le_bytes());
2528        buf.extend_from_slice(&0.6f32.to_le_bytes());
2529        let second_interpolation = (64u8..=127).collect::<Vec<_>>();
2530        buf.extend_from_slice(&second_interpolation);
2531
2532        buf.extend_from_slice(&2u32.to_le_bytes()); // morphs
2533        let mut first_morph_name = [0u8; 15];
2534        first_morph_name[..2].copy_from_slice(&[0x83, 0x65]);
2535        buf.extend_from_slice(&first_morph_name);
2536        buf.extend_from_slice(&20u32.to_le_bytes());
2537        buf.extend_from_slice(&0.25f32.to_le_bytes());
2538
2539        let mut second_morph_name = [0u8; 15];
2540        second_morph_name[..5].copy_from_slice(b"morph");
2541        buf.extend_from_slice(&second_morph_name);
2542        buf.extend_from_slice(&7u32.to_le_bytes());
2543        buf.extend_from_slice(&0.75f32.to_le_bytes());
2544
2545        // Empty camera, light, self-shadow, and property sections.
2546        buf.extend_from_slice(&[0u8; 16]);
2547
2548        let context = parse_vmd_shared_context(&buf).unwrap();
2549        let raw = context.import_result();
2550        assert_eq!(raw.bone_keyframes.len(), 2);
2551        assert_eq!(raw.morph_keyframes.len(), 2);
2552        match &raw.bone_keyframes[0].bone_mode {
2553            VmdBoneImportMode::ByName(name) => {
2554                assert_eq!(name, &vec![0x82, 0xa0, b'1']);
2555            }
2556            VmdBoneImportMode::ByIndex(_) => panic!("raw VMD key must retain its name"),
2557        }
2558        assert_eq!(raw.bone_keyframes[0].frame, 11);
2559        assert_eq!(
2560            raw.bone_keyframes[0].interpolation,
2561            [
2562                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
2563                23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
2564                44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
2565            ]
2566        );
2567        match &raw.bone_keyframes[1].bone_mode {
2568            VmdBoneImportMode::ByName(name) => assert_eq!(name, &b"bone".to_vec()),
2569            VmdBoneImportMode::ByIndex(_) => panic!("raw VMD key must retain its name"),
2570        }
2571        assert_eq!(raw.bone_keyframes[1].frame, 3);
2572        assert_eq!(
2573            raw.bone_keyframes[1].interpolation,
2574            [
2575                64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
2576                85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103,
2577                104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
2578                120, 121, 122, 123, 124, 125, 126, 127,
2579            ]
2580        );
2581        assert_eq!(raw.morph_keyframes[0], (vec![0x83, 0x65], 20, 0.25f32));
2582        assert_eq!(raw.morph_keyframes[1], (b"morph".to_vec(), 7, 0.75f32));
2583    }
2584
2585    #[test]
2586    fn shared_context_summary_reports_target_and_channel_counts() {
2587        let animation = VmdParsedAnimation {
2588            kind: "vmd",
2589            metadata: VmdParsedMetadata {
2590                format: "vmd",
2591                model_name: "target".to_owned(),
2592                model_name_bytes: Vec::new(),
2593                counts: VmdParsedCounts {
2594                    bones: 3,
2595                    morphs: 2,
2596                    cameras: 1,
2597                    lights: 1,
2598                    self_shadows: 1,
2599                    properties: 1,
2600                },
2601                max_frame: 0,
2602            },
2603            bone_frames: vec![
2604                VmdParsedBoneFrame {
2605                    bone_name: "bone".to_owned(),
2606                    bone_name_bytes: Vec::new(),
2607                    frame: 5,
2608                    translation: [0.0; 3],
2609                    rotation: [0.0, 0.0, 0.0, 1.0],
2610                    interpolation: vec![0; 64],
2611                },
2612                VmdParsedBoneFrame {
2613                    bone_name: "bone".to_owned(),
2614                    bone_name_bytes: Vec::new(),
2615                    frame: 10,
2616                    translation: [0.0; 3],
2617                    rotation: [0.0, 0.0, 0.0, 1.0],
2618                    interpolation: vec![0; 64],
2619                },
2620                VmdParsedBoneFrame {
2621                    bone_name: "other".to_owned(),
2622                    bone_name_bytes: Vec::new(),
2623                    frame: 15,
2624                    translation: [0.0; 3],
2625                    rotation: [0.0, 0.0, 0.0, 1.0],
2626                    interpolation: vec![0; 64],
2627                },
2628            ],
2629            morph_frames: vec![
2630                VmdParsedMorphFrame {
2631                    morph_name: "smile".to_owned(),
2632                    morph_name_bytes: Vec::new(),
2633                    frame: 20,
2634                    weight: 0.5,
2635                },
2636                VmdParsedMorphFrame {
2637                    morph_name: "smile".to_owned(),
2638                    morph_name_bytes: Vec::new(),
2639                    frame: 25,
2640                    weight: 1.0,
2641                },
2642            ],
2643            camera_frames: vec![VmdParsedCameraFrame {
2644                frame: 30,
2645                distance: -40.0,
2646                position: [0.0; 3],
2647                rotation: [0.0; 3],
2648                interpolation: [0; 24],
2649                fov: 45,
2650                perspective: true,
2651            }],
2652            light_frames: vec![VmdParsedLightFrame {
2653                frame: 35,
2654                color: [1.0; 3],
2655                direction: [0.0; 3],
2656            }],
2657            self_shadow_frames: vec![VmdParsedSelfShadowFrame {
2658                frame: 40,
2659                mode: 1,
2660                distance: 10.0,
2661            }],
2662            property_frames: vec![VmdParsedPropertyFrame {
2663                frame: 42,
2664                visible: true,
2665                ik_states: vec![
2666                    VmdParsedIkState {
2667                        bone_name: "ik_a".to_owned(),
2668                        bone_name_bytes: Vec::new(),
2669                        enabled: true,
2670                    },
2671                    VmdParsedIkState {
2672                        bone_name: "ik_b".to_owned(),
2673                        bone_name_bytes: Vec::new(),
2674                        enabled: false,
2675                    },
2676                ],
2677            }],
2678        };
2679
2680        let bytes = export_vmd_animation(&animation);
2681        let shared = parse_vmd_shared_context(&bytes).unwrap();
2682        let summary_only = parse_vmd_summary(&bytes).unwrap();
2683        assert_eq!(summary_only, *shared.summary());
2684        let summary = shared.summary();
2685        let mut expected_name = [0u8; 20];
2686        expected_name[..6].copy_from_slice(b"target");
2687
2688        assert_eq!(summary.target_model_name_bytes, expected_name);
2689        assert_eq!(summary.max_frame, 42);
2690        assert_eq!(
2691            summary.bones,
2692            VmdSharedContextTrackSummary {
2693                track_count: 2,
2694                key_count: 3,
2695            }
2696        );
2697        assert_eq!(
2698            summary.morphs,
2699            VmdSharedContextTrackSummary {
2700                track_count: 1,
2701                key_count: 2,
2702            }
2703        );
2704        assert_eq!(
2705            summary.cameras,
2706            VmdSharedContextTrackSummary {
2707                track_count: 1,
2708                key_count: 1,
2709            }
2710        );
2711        assert_eq!(summary.lights.key_count, 1);
2712        assert_eq!(summary.self_shadows.key_count, 1);
2713        assert_eq!(summary.properties.key_count, 1);
2714        assert_eq!(summary.property_ik_entry_count, 2);
2715    }
2716
2717    fn summary_validation_vmd_bytes() -> Vec<u8> {
2718        let mut buf = build_vmd_header_bytes();
2719        buf.extend_from_slice(&1u32.to_le_bytes()); // one bone
2720        let mut bone_name = [0u8; 15];
2721        bone_name[..4].copy_from_slice(b"bone");
2722        buf.extend_from_slice(&bone_name);
2723        buf.extend_from_slice(&1u32.to_le_bytes());
2724        buf.extend_from_slice(&[0u8; 12]); // translation
2725        buf.extend_from_slice(&[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); // rotation
2726        buf.extend_from_slice(&[0u8; 64]); // interpolation
2727
2728        buf.extend_from_slice(&1u32.to_le_bytes()); // one morph
2729        let mut morph_name = [0u8; 15];
2730        morph_name[..5].copy_from_slice(b"morph");
2731        buf.extend_from_slice(&morph_name);
2732        buf.extend_from_slice(&2u32.to_le_bytes());
2733        buf.extend_from_slice(&0.0f32.to_le_bytes());
2734
2735        buf.extend_from_slice(&1u32.to_le_bytes()); // one camera
2736        buf.extend_from_slice(&3u32.to_le_bytes());
2737        buf.extend_from_slice(&0.0f32.to_le_bytes()); // distance
2738        buf.extend_from_slice(&[0u8; 12]); // position
2739        buf.extend_from_slice(&[0u8; 12]); // rotation
2740        buf.extend_from_slice(&[0u8; 24]); // interpolation
2741        buf.extend_from_slice(&45u32.to_le_bytes());
2742        buf.push(0);
2743
2744        buf.extend_from_slice(&1u32.to_le_bytes()); // one light
2745        buf.extend_from_slice(&4u32.to_le_bytes());
2746        buf.extend_from_slice(&[0u8; 12]); // color
2747        buf.extend_from_slice(&[0u8; 12]); // direction
2748
2749        buf.extend_from_slice(&1u32.to_le_bytes()); // one self-shadow
2750        buf.extend_from_slice(&5u32.to_le_bytes());
2751        buf.push(1);
2752        buf.extend_from_slice(&0.0f32.to_le_bytes());
2753
2754        buf.extend_from_slice(&1u32.to_le_bytes()); // one property
2755        buf.extend_from_slice(&6u32.to_le_bytes());
2756        buf.push(1);
2757        buf.extend_from_slice(&1u32.to_le_bytes()); // one IK entry
2758        let mut ik_name = [0u8; 20];
2759        ik_name[..2].copy_from_slice(b"ik");
2760        buf.extend_from_slice(&ik_name);
2761        buf.push(1);
2762        buf
2763    }
2764
2765    #[test]
2766    fn summary_scanner_rejects_empty_names_and_non_finite_values() {
2767        let mut empty_bone = summary_validation_vmd_bytes();
2768        empty_bone[54..69].fill(0);
2769        assert_eq!(
2770            parse_vmd_summary(&empty_bone),
2771            Err(VmdSummaryError::Invalid("empty bone name"))
2772        );
2773
2774        let mut empty_morph = summary_validation_vmd_bytes();
2775        empty_morph[169..184].fill(0);
2776        assert_eq!(
2777            parse_vmd_summary(&empty_morph),
2778            Err(VmdSummaryError::Invalid("empty morph name"))
2779        );
2780
2781        let non_finite_offsets = [
2782            (73, "bone translation"),
2783            (85, "bone rotation"),
2784            (188, "morph weight"),
2785            (200, "camera distance"),
2786            (204, "camera position"),
2787            (216, "camera rotation"),
2788            (265, "light color"),
2789            (277, "light direction"),
2790            (298, "self-shadow distance"),
2791        ];
2792        for (offset, field) in non_finite_offsets {
2793            let mut malformed = summary_validation_vmd_bytes();
2794            malformed[offset..offset + 4].copy_from_slice(&f32::NAN.to_le_bytes());
2795            assert_eq!(
2796                parse_vmd_summary(&malformed),
2797                Err(VmdSummaryError::Invalid(field)),
2798                "summary scanner must reject non-finite {field}"
2799            );
2800        }
2801
2802        let mut empty_property_ik = summary_validation_vmd_bytes();
2803        empty_property_ik[315..335].fill(0);
2804        assert_eq!(
2805            parse_vmd_summary(&empty_property_ik),
2806            Err(VmdSummaryError::Invalid("empty property IK name"))
2807        );
2808
2809        let mut whitespace_property_ik = summary_validation_vmd_bytes();
2810        whitespace_property_ik[315..335].fill(0x20);
2811        assert_eq!(
2812            parse_vmd_summary(&whitespace_property_ik),
2813            Err(VmdSummaryError::Invalid("empty property IK name"))
2814        );
2815    }
2816
2817    #[test]
2818    fn summary_scanner_preserves_optional_tail_absence_semantics() {
2819        let bytes = summary_validation_vmd_bytes();
2820        let context = parse_vmd_shared_context(&bytes).unwrap();
2821
2822        // The camera count starts after the 50-byte header, bone record, and
2823        // morph record. A complete count with insufficient records is the
2824        // same absent-tail condition used by the existing parser.
2825        let mut truncated_camera_tail = bytes.clone();
2826        truncated_camera_tail[192..196].copy_from_slice(&10u32.to_le_bytes());
2827        let summary = parse_vmd_summary(&truncated_camera_tail).unwrap();
2828        let parsed_context = parse_vmd_shared_context(&truncated_camera_tail).unwrap();
2829        assert_eq!(summary, *parsed_context.summary());
2830        assert_eq!(summary.bones, context.summary().bones);
2831        assert_eq!(summary.morphs, context.summary().morphs);
2832        let empty_track_summary = VmdSharedContextTrackSummary {
2833            track_count: 0,
2834            key_count: 0,
2835        };
2836        assert_eq!(summary.cameras, empty_track_summary);
2837        assert_eq!(summary.lights, empty_track_summary);
2838        assert_eq!(summary.properties, empty_track_summary);
2839
2840        let mut partial_camera_count = build_vmd_header_bytes();
2841        partial_camera_count.extend_from_slice(&0u32.to_le_bytes()); // bones
2842        partial_camera_count.extend_from_slice(&0u32.to_le_bytes()); // morphs
2843        partial_camera_count.extend_from_slice(&[1, 2, 3]);
2844        assert_eq!(
2845            parse_vmd_summary(&partial_camera_count),
2846            Err(VmdSummaryError::Parse(ImportError::UnexpectedEof(1)))
2847        );
2848    }
2849
2850    #[test]
2851    fn reorders_ik_enabled_to_solver_order() {
2852        let frames = vec![VmdPropertyIkFrame {
2853            frame: 0,
2854            show: 0,
2855            entries: vec![
2856                VmdIkEntry {
2857                    name_bytes: {
2858                        let mut buf = [0u8; 20];
2859                        buf[..9].copy_from_slice(b"RightLegI");
2860                        buf.to_vec()
2861                    },
2862                    enabled: 1,
2863                    name_normalized: b"RightLegI".to_vec(),
2864                },
2865                VmdIkEntry {
2866                    name_bytes: {
2867                        let mut buf = [0u8; 20];
2868                        buf[..8].copy_from_slice(b"LeftLegI");
2869                        buf.to_vec()
2870                    },
2871                    enabled: 0,
2872                    name_normalized: b"LeftLegI".to_vec(),
2873                },
2874            ],
2875        }];
2876
2877        fn ik_resolver(name: &[u8]) -> Option<usize> {
2878            match name {
2879                b"LeftLegI" => Some(0),
2880                b"RightLegI" => Some(1),
2881                _ => None,
2882            }
2883        }
2884
2885        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
2886        let sample = binding.sample(0.0).unwrap();
2887        assert_eq!(sample, &[0, 1]);
2888    }
2889
2890    #[test]
2891    fn unknown_and_unmentioned_ik_names_keep_default_enabled_state() {
2892        let frames = vec![VmdPropertyIkFrame {
2893            frame: 5,
2894            show: 0,
2895            entries: vec![
2896                VmdIkEntry {
2897                    name_bytes: {
2898                        let mut buf = [0u8; 20];
2899                        buf[..7].copy_from_slice(b"KnownIK");
2900                        buf.to_vec()
2901                    },
2902                    enabled: 1,
2903                    name_normalized: b"KnownIK".to_vec(),
2904                },
2905                VmdIkEntry {
2906                    name_bytes: {
2907                        let mut buf = [0u8; 20];
2908                        buf[..9].copy_from_slice(b"UnknownIK");
2909                        buf.to_vec()
2910                    },
2911                    enabled: 1,
2912                    name_normalized: b"UnknownIK".to_vec(),
2913                },
2914            ],
2915        }];
2916
2917        fn ik_resolver(name: &[u8]) -> Option<usize> {
2918            if name == b"KnownIK" { Some(0) } else { None }
2919        }
2920
2921        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 2).unwrap();
2922        let sample = binding.sample(5.0).unwrap();
2923        assert_eq!(sample, &[1, 1]);
2924    }
2925
2926    #[test]
2927    fn empty_ik_frames_returns_none() {
2928        fn ik_resolver(_name: &[u8]) -> Option<usize> {
2929            None
2930        }
2931        let binding = build_property_binding_with_ik_resolver(&[], &ik_resolver, 2);
2932        assert!(binding.is_none());
2933    }
2934
2935    #[test]
2936    fn zero_solver_count_returns_none() {
2937        let frames = vec![VmdPropertyIkFrame {
2938            frame: 0,
2939            show: 0,
2940            entries: vec![VmdIkEntry {
2941                name_bytes: ik_name_bytes("IK").to_vec(),
2942                enabled: 1,
2943                name_normalized: b"IK".to_vec(),
2944            }],
2945        }];
2946        fn ik_resolver(_name: &[u8]) -> Option<usize> {
2947            Some(0)
2948        }
2949        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 0);
2950        assert!(binding.is_none());
2951    }
2952
2953    #[test]
2954    fn build_property_binding_resolves_ik_by_normalized_name() {
2955        let frames = vec![VmdPropertyIkFrame {
2956            frame: 0,
2957            show: 0,
2958            entries: vec![VmdIkEntry {
2959                name_bytes: vec![0x8D, 0xB6, 0x91, 0xAB],
2960                enabled: 1,
2961                name_normalized: vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3],
2962            }],
2963        }];
2964
2965        fn ik_resolver(name: &[u8]) -> Option<usize> {
2966            if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
2967                Some(0)
2968            } else {
2969                None
2970            }
2971        }
2972
2973        let binding = build_property_binding_with_ik_resolver(&frames, &ik_resolver, 1).unwrap();
2974        let sample = binding.sample(0.0).unwrap();
2975        assert_eq!(sample, &[1]);
2976    }
2977
2978    #[test]
2979    fn build_clip_from_import_resolves_bone_by_normalized_name() {
2980        let sjis_name: Vec<u8> = vec![0x8D, 0xB6, 0x91, 0xAB];
2981        let utf8_name: Vec<u8> = vec![0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
2982
2983        let kf = VmdBoneKeyframeRaw {
2984            bone_mode: VmdBoneImportMode::ByName(sjis_name),
2985            frame: 0,
2986            position: Vec3A::ZERO,
2987            rotation: Quat::IDENTITY,
2988            interpolation: [20u8; 64],
2989            bone_name_normalized: utf8_name.clone(),
2990        };
2991
2992        fn lookup(name: &[u8]) -> Option<BoneIndex> {
2993            if name == b"\xE5\xB7\xA6\xE8\xB6\xB3" {
2994                Some(BoneIndex(0))
2995            } else {
2996                None
2997            }
2998        }
2999        fn morph_lookup(_name: &[u8]) -> Option<MorphIndex> {
3000            None
3001        }
3002
3003        let result = VmdImportResult {
3004            bone_keyframes: vec![kf],
3005            morph_keyframes: Vec::new(),
3006            property_keyframes: Vec::new(),
3007            property_ik_frames: Vec::new(),
3008        };
3009
3010        let clip = build_clip_from_import(result, &lookup, &morph_lookup);
3011        assert_eq!(
3012            clip.bone_track_count(),
3013            1,
3014            "bone should be resolved via normalized UTF-8 name, not raw Shift-JIS bytes"
3015        );
3016    }
3017
3018    #[test]
3019    fn japanese_vmd_name_matches_pmx_utf8_name_via_normalization() {
3020        let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
3021        let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
3022
3023        let mut pmx_buf = Vec::new();
3024        pmx_buf.extend_from_slice(b"PMX ");
3025        pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
3026        pmx_buf.push(8);
3027        pmx_buf.push(1);
3028        pmx_buf.push(0);
3029        pmx_buf.push(4);
3030        pmx_buf.push(1);
3031        pmx_buf.push(1);
3032        pmx_buf.push(2);
3033        pmx_buf.push(1);
3034        pmx_buf.push(1);
3035        for _ in 0..4 {
3036            pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3037        }
3038        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3039        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3040        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3041        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3042        pmx_buf.extend_from_slice(&1i32.to_le_bytes());
3043        pmx_buf.extend_from_slice(&(utf8_name.len() as i32).to_le_bytes());
3044        pmx_buf.extend_from_slice(utf8_name);
3045        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3046        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3047        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3048        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3049        pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
3050        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3051        pmx_buf.extend_from_slice(&0u16.to_le_bytes());
3052        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3053        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3054        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3055        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3056
3057        let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
3058
3059        let mut vmd_buf = build_vmd_header_bytes();
3060        vmd_buf.extend_from_slice(&1u32.to_le_bytes());
3061        let mut bone_name = [0u8; 15];
3062        bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
3063        vmd_buf.extend_from_slice(&bone_name);
3064        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3065        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3066        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3067        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3068        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3069        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3070        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3071        vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
3072        vmd_buf.extend_from_slice(&[20u8; 64]);
3073        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3074        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3075        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3076        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3077        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3078
3079        let vmd = import_vmd_motion(&vmd_buf).unwrap();
3080
3081        let normalized = normalize_vmd_name(sjis_name);
3082        assert_eq!(&normalized[..], utf8_name);
3083
3084        assert!(
3085            pmx.bone_name_to_index.contains_key(&normalized),
3086            "PMX should have normalized bone name key"
3087        );
3088
3089        let clip = build_pair_clip(
3090            &vmd,
3091            &pmx.bone_name_to_index,
3092            &pmx.morph_name_to_index,
3093            &pmx.ik_solver_bone_name_to_index,
3094            pmx.model.ik_count(),
3095        );
3096
3097        assert_eq!(clip.bone_track_count(), 1);
3098        assert_eq!(clip.morph_track_count(), 0);
3099        assert!(!clip.has_property_track());
3100    }
3101
3102    #[test]
3103    fn japanese_vmd_name_matches_pmx_utf16le_name_via_decoded_key() {
3104        let sjis_name: &[u8] = &[0x8D, 0xB6, 0x91, 0xAB];
3105        let utf8_name: &[u8] = &[0xE5, 0xB7, 0xA6, 0xE8, 0xB6, 0xB3];
3106        let utf16le_name: Vec<u8> = "\u{5DE6}\u{8DB3}"
3107            .encode_utf16()
3108            .flat_map(u16::to_le_bytes)
3109            .collect();
3110
3111        let mut pmx_buf = Vec::new();
3112        pmx_buf.extend_from_slice(b"PMX ");
3113        pmx_buf.extend_from_slice(&2.0f32.to_le_bytes());
3114        pmx_buf.push(8);
3115        pmx_buf.push(0);
3116        pmx_buf.push(0);
3117        pmx_buf.push(4);
3118        pmx_buf.push(1);
3119        pmx_buf.push(1);
3120        pmx_buf.push(2);
3121        pmx_buf.push(1);
3122        pmx_buf.push(1);
3123        for _ in 0..4 {
3124            pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3125        }
3126        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3127        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3128        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3129        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3130        pmx_buf.extend_from_slice(&1i32.to_le_bytes());
3131        pmx_buf.extend_from_slice(&(utf16le_name.len() as i32).to_le_bytes());
3132        pmx_buf.extend_from_slice(&utf16le_name);
3133        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3134        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3135        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3136        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3137        pmx_buf.extend_from_slice(&(-1i16).to_le_bytes());
3138        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3139        pmx_buf.extend_from_slice(&0u16.to_le_bytes());
3140        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3141        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3142        pmx_buf.extend_from_slice(&0.0f32.to_le_bytes());
3143        pmx_buf.extend_from_slice(&0i32.to_le_bytes());
3144
3145        let pmx = crate::pmx::import_pmx_runtime(&pmx_buf).unwrap();
3146
3147        let mut vmd_buf = build_vmd_header_bytes();
3148        vmd_buf.extend_from_slice(&1u32.to_le_bytes());
3149        let mut bone_name = [0u8; 15];
3150        bone_name[..sjis_name.len()].copy_from_slice(sjis_name);
3151        vmd_buf.extend_from_slice(&bone_name);
3152        vmd_buf.extend_from_slice(&0u32.to_le_bytes());
3153        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3154        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3155        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3156        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3157        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3158        vmd_buf.extend_from_slice(&0.0f32.to_le_bytes());
3159        vmd_buf.extend_from_slice(&1.0f32.to_le_bytes());
3160        vmd_buf.extend_from_slice(&[20u8; 64]);
3161
3162        let vmd = import_vmd_motion(&vmd_buf).unwrap();
3163        let normalized = normalize_vmd_name(sjis_name);
3164        assert_eq!(&normalized[..], utf8_name);
3165        assert!(
3166            pmx.bone_name_to_index.contains_key(&utf16le_name),
3167            "PMX should retain the raw UTF-16LE name key"
3168        );
3169        assert!(
3170            pmx.bone_name_to_index.contains_key(&normalized),
3171            "PMX should also index UTF-16LE names by decoded UTF-8 bytes"
3172        );
3173
3174        let clip = build_pair_clip(
3175            &vmd,
3176            &pmx.bone_name_to_index,
3177            &pmx.morph_name_to_index,
3178            &pmx.ik_solver_bone_name_to_index,
3179            pmx.model.ik_count(),
3180        );
3181
3182        assert_eq!(clip.bone_track_count(), 1);
3183    }
3184
3185    #[test]
3186    fn build_pair_clip_default_includes_property_ik() {
3187        // VMD with one property IK frame that disables "LeftLegIK" at frame 0.
3188        let result = VmdImportResult {
3189            bone_keyframes: Vec::new(),
3190            morph_keyframes: Vec::new(),
3191            property_keyframes: Vec::new(),
3192            property_ik_frames: vec![VmdPropertyIkFrame {
3193                frame: 0,
3194                show: 0,
3195                entries: vec![VmdIkEntry {
3196                    name_bytes: ik_name_bytes("LeftLegIK").to_vec(),
3197                    enabled: 0,
3198                    name_normalized: b"LeftLegIK".to_vec(),
3199                }],
3200            }],
3201        };
3202
3203        let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
3204            std::collections::HashMap::new();
3205        let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
3206            std::collections::HashMap::new();
3207        let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
3208            std::collections::HashMap::new();
3209        ik_solver_bone_name_to_index.insert(b"LeftLegIK".to_vec(), 0);
3210
3211        let clip = build_pair_clip(
3212            &result,
3213            &bone_name_to_index,
3214            &morph_name_to_index,
3215            &ik_solver_bone_name_to_index,
3216            2, // 2 solvers total
3217        );
3218
3219        assert!(
3220            clip.has_property_track(),
3221            "default build_pair_clip should include property IK track"
3222        );
3223    }
3224
3225    #[test]
3226    fn build_pair_clip_with_options_omits_property_ik() {
3227        // Same VMD data, but with honor_property_ik: false => no property track.
3228        let result = VmdImportResult {
3229            bone_keyframes: Vec::new(),
3230            morph_keyframes: Vec::new(),
3231            property_keyframes: Vec::new(),
3232            property_ik_frames: vec![VmdPropertyIkFrame {
3233                frame: 5,
3234                show: 0,
3235                entries: vec![VmdIkEntry {
3236                    name_bytes: ik_name_bytes("RightLegIK").to_vec(),
3237                    enabled: 0,
3238                    name_normalized: b"RightLegIK".to_vec(),
3239                }],
3240            }],
3241        };
3242
3243        let bone_name_to_index: std::collections::HashMap<Vec<u8>, BoneIndex> =
3244            std::collections::HashMap::new();
3245        let morph_name_to_index: std::collections::HashMap<Vec<u8>, MorphIndex> =
3246            std::collections::HashMap::new();
3247        let mut ik_solver_bone_name_to_index: std::collections::HashMap<Vec<u8>, usize> =
3248            std::collections::HashMap::new();
3249        ik_solver_bone_name_to_index.insert(b"RightLegIK".to_vec(), 1);
3250
3251        let clip = build_pair_clip_with_options(
3252            &result,
3253            &bone_name_to_index,
3254            &morph_name_to_index,
3255            &ik_solver_bone_name_to_index,
3256            2,
3257            VmdClipBuildOptions {
3258                honor_property_ik: false,
3259            },
3260        );
3261
3262        assert!(
3263            !clip.has_property_track(),
3264            "build_pair_clip_with_options(honor_property_ik: false) should omit property IK track"
3265        );
3266    }
3267
3268    fn fixed_axis_test_keyframe(rotation: Quat) -> VmdBoneKeyframeRaw {
3269        VmdBoneKeyframeRaw {
3270            bone_mode: VmdBoneImportMode::ByIndex(7),
3271            frame: 12,
3272            position: Vec3A::new(1.0, 2.0, 3.0),
3273            rotation,
3274            interpolation: [20u8; 64],
3275            bone_name_normalized: b"FixedAxis".to_vec(),
3276        }
3277    }
3278
3279    fn assert_quat_near(actual: Quat, expected: Quat) {
3280        for (actual, expected) in [actual.x, actual.y, actual.z, actual.w]
3281            .into_iter()
3282            .zip([expected.x, expected.y, expected.z, expected.w])
3283        {
3284            assert!((actual - expected).abs() < 1.0e-6);
3285        }
3286    }
3287
3288    fn test_model(fixed_axis: Option<Vec3A>) -> ModelArena {
3289        let mut bones: Vec<_> = (0..8).map(|_| BoneInit::new(None, Vec3A::ZERO)).collect();
3290        if let Some(axis) = fixed_axis {
3291            bones[7] = BoneInit::new(None, Vec3A::ZERO).with_fixed_axis(axis);
3292        }
3293        ModelArena::new(bones).unwrap()
3294    }
3295
3296    #[test]
3297    fn model_fixed_axis_projects_off_axis_positive_dot_and_preserves_keyframe_data() {
3298        let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.0, 0.8));
3299        let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
3300        let mapped = build_mapped_bone_keyframe(
3301            &source,
3302            BoneIndex(7),
3303            VmdRegistrationPolicy::MmdRegistered(&model),
3304        );
3305
3306        assert_quat_near(mapped.rotation, Quat::from_xyzw(0.0, 0.5, 0.0, 0.8));
3307        assert_eq!(mapped.frame, source.frame);
3308        assert_eq!(mapped.position, source.position);
3309        let (position_interpolation, rotation_interpolation) =
3310            decode_mmd_registered_bone_interpolation(&source.interpolation);
3311        assert_eq!(mapped.position_interpolation, position_interpolation);
3312        assert_eq!(mapped.rotation_interpolation, rotation_interpolation);
3313    }
3314
3315    #[test]
3316    fn model_fixed_axis_projects_off_axis_negative_dot_with_negative_sign() {
3317        let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, -0.4, 0.0, 0.8));
3318        let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
3319        let mapped = build_mapped_bone_keyframe(
3320            &source,
3321            BoneIndex(7),
3322            VmdRegistrationPolicy::MmdRegistered(&model),
3323        );
3324
3325        assert_quat_near(mapped.rotation, Quat::from_xyzw(0.0, -0.5, 0.0, 0.8));
3326    }
3327
3328    #[test]
3329    fn model_fixed_axis_leaves_already_axis_aligned_rotation_unchanged() {
3330        let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.0, -0.5, 0.0, 0.8));
3331        let model = test_model(Some(Vec3A::new(0.0, 3.0, 0.0)));
3332        let mapped = build_mapped_bone_keyframe(
3333            &source,
3334            BoneIndex(7),
3335            VmdRegistrationPolicy::MmdRegistered(&model),
3336        );
3337
3338        assert_quat_near(mapped.rotation, source.rotation);
3339    }
3340
3341    #[test]
3342    fn model_fixed_axis_leaves_non_fixed_bone_rotation_unchanged() {
3343        let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.1, 0.8));
3344        let model = test_model(None);
3345        let mapped = build_mapped_bone_keyframe(
3346            &source,
3347            BoneIndex(7),
3348            VmdRegistrationPolicy::MmdRegistered(&model),
3349        );
3350
3351        assert_eq!(mapped.rotation, source.rotation);
3352    }
3353
3354    #[test]
3355    fn model_aware_bone_keyframe_uses_registered_interpolation_for_all_mapped_bones() {
3356        let mut source = fixed_axis_test_keyframe(Quat::IDENTITY);
3357        for (index, value) in source.interpolation.iter_mut().enumerate() {
3358            *value = index as u8;
3359        }
3360        let model = test_model(None);
3361        let raw = build_mapped_bone_keyframe(&source, BoneIndex(7), VmdRegistrationPolicy::Raw);
3362        let model_aware = build_mapped_bone_keyframe(
3363            &source,
3364            BoneIndex(7),
3365            VmdRegistrationPolicy::MmdRegistered(&model),
3366        );
3367        let (raw_position, raw_rotation) = decode_bone_interpolation(&source.interpolation);
3368        let (registered_position, registered_rotation) =
3369            decode_mmd_registered_bone_interpolation(&source.interpolation);
3370
3371        assert_eq!(raw.position_interpolation, raw_position);
3372        assert_eq!(raw.rotation_interpolation, raw_rotation);
3373        assert_eq!(model_aware.position_interpolation, registered_position);
3374        assert_eq!(model_aware.rotation_interpolation, registered_rotation);
3375        assert_ne!(
3376            raw.position_interpolation,
3377            model_aware.position_interpolation
3378        );
3379        assert_ne!(
3380            raw.rotation_interpolation,
3381            model_aware.rotation_interpolation
3382        );
3383    }
3384
3385    #[test]
3386    fn public_mmd_registered_builder_projects_fixed_axis_rotation() {
3387        let source = fixed_axis_test_keyframe(Quat::from_xyzw(0.3, 0.4, 0.0, 0.8));
3388        let model = test_model(Some(Vec3A::Y));
3389        let result = VmdImportResult {
3390            bone_keyframes: vec![source],
3391            morph_keyframes: Vec::new(),
3392            property_keyframes: Vec::new(),
3393            property_ik_frames: Vec::new(),
3394        };
3395        let mut bone_name_to_index = std::collections::HashMap::new();
3396        bone_name_to_index.insert(b"FixedAxis".to_vec(), BoneIndex(7));
3397        let morph_name_to_index = std::collections::HashMap::new();
3398        let ik_solver_bone_name_to_index = std::collections::HashMap::new();
3399
3400        let clip = build_mmd_registered_pair_clip_with_options(
3401            &model,
3402            &result,
3403            &bone_name_to_index,
3404            &morph_name_to_index,
3405            &ik_solver_bone_name_to_index,
3406            0,
3407            VmdClipBuildOptions::default(),
3408        )
3409        .unwrap();
3410        let sample = clip.sample_at(12.0);
3411        let rotation = sample.bone_samples()[0].rotation;
3412        assert_quat_near(rotation, Quat::from_xyzw(0.0, 0.5, 0.0, 0.8).normalize());
3413    }
3414
3415    #[test]
3416    fn public_mmd_registered_builder_uses_registered_interpolation_layout() {
3417        let mut first = fixed_axis_test_keyframe(Quat::IDENTITY);
3418        first.frame = 0;
3419        first.position = Vec3A::ZERO;
3420        let mut second = fixed_axis_test_keyframe(Quat::IDENTITY);
3421        second.frame = 10;
3422        second.position = Vec3A::Y;
3423        for (index, value) in second.interpolation.iter_mut().enumerate() {
3424            *value = index as u8;
3425        }
3426        let result = VmdImportResult {
3427            bone_keyframes: vec![first, second],
3428            morph_keyframes: Vec::new(),
3429            property_keyframes: Vec::new(),
3430            property_ik_frames: Vec::new(),
3431        };
3432        let mut bone_name_to_index = std::collections::HashMap::new();
3433        bone_name_to_index.insert(b"FixedAxis".to_vec(), BoneIndex(7));
3434        let morph_name_to_index = std::collections::HashMap::new();
3435        let ik_solver_bone_name_to_index = std::collections::HashMap::new();
3436        let model = test_model(None);
3437
3438        let registered = build_mmd_registered_pair_clip(
3439            &model,
3440            &result,
3441            &bone_name_to_index,
3442            &morph_name_to_index,
3443            &ik_solver_bone_name_to_index,
3444            0,
3445        )
3446        .unwrap();
3447        let raw = build_pair_clip(
3448            &result,
3449            &bone_name_to_index,
3450            &morph_name_to_index,
3451            &ik_solver_bone_name_to_index,
3452            0,
3453        );
3454        let registered_position = registered.sample_at(5.0).bone_samples()[0].position;
3455        let raw_position = raw.sample_at(5.0).bone_samples()[0].position;
3456        assert_ne!(registered_position, raw_position);
3457    }
3458
3459    #[test]
3460    fn public_mmd_registered_builder_rejects_out_of_range_bone_map() {
3461        let model = test_model(None);
3462        let result = VmdImportResult {
3463            bone_keyframes: Vec::new(),
3464            morph_keyframes: Vec::new(),
3465            property_keyframes: Vec::new(),
3466            property_ik_frames: Vec::new(),
3467        };
3468        let mut bone_name_to_index = std::collections::HashMap::new();
3469        bone_name_to_index.insert(b"Invalid".to_vec(), BoneIndex(8));
3470        let morph_name_to_index = std::collections::HashMap::new();
3471        let ik_solver_bone_name_to_index = std::collections::HashMap::new();
3472
3473        let error = build_mmd_registered_pair_clip(
3474            &model,
3475            &result,
3476            &bone_name_to_index,
3477            &morph_name_to_index,
3478            &ik_solver_bone_name_to_index,
3479            0,
3480        )
3481        .unwrap_err();
3482        assert_eq!(
3483            error,
3484            VmdClipBuildError::BoneIndexOutOfRange {
3485                bone_index: BoneIndex(8),
3486                bone_count: 8,
3487            }
3488        );
3489    }
3490
3491    // ---------------------------------------------------------------------------
3492    // Synthetic roundtrip fixtures (CI gate for P0 Exporter Roundtrip Gate)
3493    // ---------------------------------------------------------------------------
3494
3495    /// Build a minimal VMD binary from plain components so tests carry no external files.
3496    type SyntheticBoneFrame<'a> = (&'a str, u32, [f32; 3], [f32; 4], [u8; 64]);
3497    type SyntheticMorphFrame<'a> = (&'a str, u32, f32);
3498    type SyntheticPropertyFrame<'a> = (&'a [(&'a str, bool)], u32, bool);
3499
3500    fn assert_near(actual: f32, expected: f32) {
3501        let delta = (actual - expected).abs();
3502        assert!(
3503            delta < 1.0e-4,
3504            "actual={actual:?} expected={expected:?} delta={delta:?}"
3505        );
3506    }
3507
3508    fn assert_vec3_near(actual: [f32; 3], expected: [f32; 3]) {
3509        for (actual, expected) in actual.into_iter().zip(expected) {
3510            assert_near(actual, expected);
3511        }
3512    }
3513
3514    fn simple_camera_vmd_fixture() -> &'static [u8] {
3515        include_bytes!("../../fixtures/vmd/simple_camera.vmd")
3516    }
3517
3518    fn make_vmd_bytes(
3519        model_name_ascii: &str,
3520        bone_frames: &[SyntheticBoneFrame<'_>],
3521        morph_frames: &[SyntheticMorphFrame<'_>],
3522        property_frames: &[SyntheticPropertyFrame<'_>],
3523    ) -> Vec<u8> {
3524        let mut v = Vec::new();
3525        v.extend_from_slice(&VMD_MAGIC);
3526
3527        let mut name_buf = [0u8; 20];
3528        let nb = model_name_ascii.as_bytes();
3529        name_buf[..nb.len().min(20)].copy_from_slice(&nb[..nb.len().min(20)]);
3530        v.extend_from_slice(&name_buf);
3531
3532        v.extend_from_slice(&(bone_frames.len() as u32).to_le_bytes());
3533        for (name, frame, trans, rot, interp) in bone_frames {
3534            let mut nb = [0u8; 15];
3535            let b = name.as_bytes();
3536            nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
3537            v.extend_from_slice(&nb);
3538            v.extend_from_slice(&frame.to_le_bytes());
3539            for &x in trans {
3540                v.extend_from_slice(&x.to_le_bytes());
3541            }
3542            for &x in rot {
3543                v.extend_from_slice(&x.to_le_bytes());
3544            }
3545            v.extend_from_slice(interp);
3546        }
3547
3548        v.extend_from_slice(&(morph_frames.len() as u32).to_le_bytes());
3549        for (name, frame, weight) in morph_frames {
3550            let mut nb = [0u8; 15];
3551            let b = name.as_bytes();
3552            nb[..b.len().min(15)].copy_from_slice(&b[..b.len().min(15)]);
3553            v.extend_from_slice(&nb);
3554            v.extend_from_slice(&frame.to_le_bytes());
3555            v.extend_from_slice(&weight.to_le_bytes());
3556        }
3557
3558        // camera / light / self-shadow: 0 frames each
3559        v.extend_from_slice(&0u32.to_le_bytes());
3560        v.extend_from_slice(&0u32.to_le_bytes());
3561        v.extend_from_slice(&0u32.to_le_bytes());
3562
3563        v.extend_from_slice(&(property_frames.len() as u32).to_le_bytes());
3564        for (ik_states, frame, visible) in property_frames {
3565            v.extend_from_slice(&frame.to_le_bytes());
3566            v.push(u8::from(*visible));
3567            v.extend_from_slice(&(ik_states.len() as u32).to_le_bytes());
3568            for (ik_name, enabled) in *ik_states {
3569                let mut nb = [0u8; 20];
3570                let b = ik_name.as_bytes();
3571                nb[..b.len().min(20)].copy_from_slice(&b[..b.len().min(20)]);
3572                v.extend_from_slice(&nb);
3573                v.push(u8::from(*enabled));
3574            }
3575        }
3576        v
3577    }
3578
3579    fn json_keys(value: &serde_json::Value) -> Vec<String> {
3580        let mut keys = value
3581            .as_object()
3582            .unwrap()
3583            .keys()
3584            .map(ToOwned::to_owned)
3585            .collect::<Vec<_>>();
3586        keys.sort();
3587        keys
3588    }
3589
3590    #[test]
3591    fn vmd_animation_json_top_level_schema_is_stable() {
3592        let vmd = make_vmd_bytes("miku", &[], &[], &[]);
3593        let parsed = parse_vmd_animation(&vmd).unwrap();
3594        let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
3595
3596        assert_eq!(
3597            keys,
3598            vec![
3599                "boneFrames",
3600                "cameraFrames",
3601                "kind",
3602                "lightFrames",
3603                "metadata",
3604                "morphFrames",
3605                "propertyFrames",
3606                "selfShadowFrames",
3607            ]
3608        );
3609    }
3610
3611    #[test]
3612    fn parses_simple_camera_vmd_fixture() {
3613        let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
3614
3615        assert_eq!(parsed.metadata.model_name, "camera_fixture");
3616        assert_eq!(parsed.metadata.counts.bones, 0);
3617        assert_eq!(parsed.metadata.counts.morphs, 0);
3618        assert_eq!(parsed.metadata.counts.cameras, 2);
3619        assert_eq!(parsed.metadata.counts.lights, 0);
3620        assert_eq!(parsed.metadata.counts.self_shadows, 0);
3621        assert_eq!(parsed.metadata.counts.properties, 0);
3622        assert_eq!(parsed.metadata.max_frame, 45);
3623        assert_eq!(parsed.camera_frames.len(), 2);
3624
3625        let first = &parsed.camera_frames[0];
3626        assert_eq!(first.frame, 0);
3627        assert_eq!(first.distance, -30.5);
3628        assert_eq!(first.position, [1.0, 2.0, 3.0]);
3629        assert_eq!(first.rotation, [0.1, -0.2, 0.3]);
3630        assert_eq!(
3631            first.interpolation,
3632            [
3633                20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
3634                41, 42, 43
3635            ]
3636        );
3637        assert_eq!(first.fov, 35);
3638        assert!(first.perspective);
3639
3640        let second = &parsed.camera_frames[1];
3641        assert_eq!(second.frame, 45);
3642        assert_eq!(second.distance, -50.0);
3643        assert_eq!(second.position, [-1.5, 10.0, 0.25]);
3644        assert_eq!(second.rotation, [-0.3, 0.0, 1.2]);
3645        assert_eq!(
3646            second.interpolation,
3647            [
3648                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 127, 127, 127, 127, 127, 127, 127, 127,
3649                127, 127, 127
3650            ]
3651        );
3652        assert_eq!(second.fov, 60);
3653        assert!(!second.perspective);
3654    }
3655
3656    #[test]
3657    fn roundtrip_simple_camera_vmd_fixture_parse_export_parse() {
3658        let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
3659        let exported = export_vmd_animation(&parsed);
3660        let reparsed = parse_vmd_animation(&exported).unwrap();
3661
3662        assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
3663        assert_eq!(reparsed.metadata.counts.cameras, 2);
3664        assert_eq!(reparsed.metadata.max_frame, 45);
3665        assert_eq!(reparsed.camera_frames.len(), parsed.camera_frames.len());
3666        for (left, right) in parsed.camera_frames.iter().zip(&reparsed.camera_frames) {
3667            assert_eq!(left.frame, right.frame);
3668            assert_eq!(left.distance, right.distance);
3669            assert_eq!(left.position, right.position);
3670            assert_eq!(left.rotation, right.rotation);
3671            assert_eq!(left.interpolation, right.interpolation);
3672            assert_eq!(left.fov, right.fov);
3673            assert_eq!(left.perspective, right.perspective);
3674        }
3675    }
3676
3677    #[test]
3678    fn samples_simple_camera_vmd_fixture_with_channel_interpolation() {
3679        let parsed = parse_vmd_animation(simple_camera_vmd_fixture()).unwrap();
3680        let camera = sample_vmd_camera_frames(&parsed.camera_frames, 22.5).unwrap();
3681
3682        assert_near(camera.distance, -40.25);
3683        assert_vec3_near(camera.position, [-0.25, 6.0, 1.625]);
3684        assert_vec3_near(camera.rotation, [-0.1, -0.1, 0.75]);
3685        assert_near(camera.fov, 47.5);
3686        assert!(camera.perspective);
3687
3688        let last = sample_vmd_camera_frames(&parsed.camera_frames, 45.0).unwrap();
3689        assert!(!last.perspective);
3690    }
3691
3692    #[test]
3693    fn samples_vmd_light_frames_linearly() {
3694        let frames = vec![
3695            VmdParsedLightFrame {
3696                frame: 30,
3697                color: [1.0, 0.5, 0.0],
3698                direction: [0.0, -1.0, 0.0],
3699            },
3700            VmdParsedLightFrame {
3701                frame: 10,
3702                color: [0.0, 0.0, 1.0],
3703                direction: [1.0, 0.0, 0.0],
3704            },
3705        ];
3706
3707        let light = sample_vmd_light_frames(&frames, 20.0).unwrap();
3708
3709        assert_vec3_near(light.color, [0.5, 0.25, 0.5]);
3710        assert_vec3_near(light.direction, [0.5, -0.5, 0.0]);
3711    }
3712
3713    #[test]
3714    fn samples_vmd_self_shadow_frames_linearly_with_stepped_mode() {
3715        let frames = vec![
3716            VmdParsedSelfShadowFrame {
3717                frame: 10,
3718                mode: 1,
3719                distance: 20.0,
3720            },
3721            VmdParsedSelfShadowFrame {
3722                frame: 30,
3723                mode: 2,
3724                distance: 60.0,
3725            },
3726        ];
3727
3728        let middle = sample_vmd_self_shadow_frames(&frames, 20.0).unwrap();
3729        assert_eq!(middle.mode, 1);
3730        assert_near(middle.distance, 40.0);
3731
3732        let last = sample_vmd_self_shadow_frames(&frames, 30.0).unwrap();
3733        assert_eq!(last.mode, 2);
3734        assert_near(last.distance, 60.0);
3735    }
3736
3737    #[test]
3738    fn roundtrip_bone_frame_parse_export_parse() {
3739        let interp = [20u8; 64];
3740        let vmd = make_vmd_bytes(
3741            "miku",
3742            &[("arm", 30, [1.0, 0.5, -0.5], [0.0, 0.0, 0.0, 1.0], interp)],
3743            &[],
3744            &[],
3745        );
3746        let parsed = parse_vmd_animation(&vmd).unwrap();
3747        assert_eq!(parsed.bone_frames.len(), 1);
3748        assert_eq!(parsed.bone_frames[0].bone_name, "arm");
3749        assert_eq!(parsed.bone_frames[0].frame, 30);
3750        assert_eq!(parsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
3751        assert_eq!(parsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
3752        assert_eq!(parsed.bone_frames[0].interpolation, vec![20u8; 64]);
3753
3754        let exported = export_vmd_animation(&parsed);
3755        let reparsed = parse_vmd_animation(&exported).unwrap();
3756        assert_eq!(reparsed.metadata.model_name, parsed.metadata.model_name);
3757        assert_eq!(reparsed.metadata.max_frame, 30);
3758        assert_eq!(reparsed.bone_frames.len(), 1);
3759        assert_eq!(reparsed.bone_frames[0].bone_name, "arm");
3760        assert_eq!(reparsed.bone_frames[0].frame, 30);
3761        assert_eq!(reparsed.bone_frames[0].translation, [1.0, 0.5, -0.5]);
3762        assert_eq!(reparsed.bone_frames[0].rotation, [0.0, 0.0, 0.0, 1.0]);
3763        assert_eq!(reparsed.bone_frames[0].interpolation, vec![20u8; 64]);
3764    }
3765
3766    #[test]
3767    fn roundtrip_morph_frame_parse_export_parse() {
3768        let vmd = make_vmd_bytes("miku", &[], &[("blink", 15, 0.75)], &[]);
3769        let parsed = parse_vmd_animation(&vmd).unwrap();
3770        let exported = export_vmd_animation(&parsed);
3771        let reparsed = parse_vmd_animation(&exported).unwrap();
3772        assert_eq!(reparsed.morph_frames.len(), 1);
3773        assert_eq!(reparsed.morph_frames[0].morph_name, "blink");
3774        assert_eq!(reparsed.morph_frames[0].frame, 15);
3775        assert!((reparsed.morph_frames[0].weight - 0.75f32).abs() < 1e-6);
3776    }
3777
3778    #[test]
3779    fn roundtrip_property_ik_frames_parse_export_parse() {
3780        let vmd = make_vmd_bytes(
3781            "camera",
3782            &[],
3783            &[],
3784            &[(&[("leftIK", true), ("rightIK", false)], 20, true)],
3785        );
3786        let parsed = parse_vmd_animation(&vmd).unwrap();
3787        let exported = export_vmd_animation(&parsed);
3788        let reparsed = parse_vmd_animation(&exported).unwrap();
3789        assert_eq!(reparsed.property_frames.len(), 1);
3790        assert_eq!(reparsed.property_frames[0].frame, 20);
3791        assert!(reparsed.property_frames[0].visible);
3792        assert_eq!(reparsed.property_frames[0].ik_states.len(), 2);
3793        assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "leftIK");
3794        assert!(reparsed.property_frames[0].ik_states[0].enabled);
3795        assert_eq!(
3796            reparsed.property_frames[0].ik_states[1].bone_name,
3797            "rightIK"
3798        );
3799        assert!(!reparsed.property_frames[0].ik_states[1].enabled);
3800    }
3801
3802    #[test]
3803    fn roundtrip_json_dto_bone_and_morph() {
3804        let interp = [20u8; 64];
3805        let vmd = make_vmd_bytes(
3806            "miku",
3807            &[("spine", 60, [0.0, 1.0, 0.0], [0.1, 0.2, 0.3, 0.9], interp)],
3808            &[("mouth", 30, 0.8)],
3809            &[],
3810        );
3811        let parsed = parse_vmd_animation(&vmd).unwrap();
3812        let json = serde_json::to_string(&parsed).unwrap();
3813        let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3814        let exported = export_vmd_animation(&from_json);
3815        let reparsed = parse_vmd_animation(&exported).unwrap();
3816
3817        assert_eq!(
3818            reparsed.bone_frames[0].bone_name,
3819            parsed.bone_frames[0].bone_name
3820        );
3821        assert_eq!(reparsed.bone_frames[0].frame, parsed.bone_frames[0].frame);
3822        assert_eq!(
3823            reparsed.bone_frames[0].translation,
3824            parsed.bone_frames[0].translation
3825        );
3826        assert_eq!(
3827            reparsed.bone_frames[0].rotation,
3828            parsed.bone_frames[0].rotation
3829        );
3830        assert_eq!(
3831            reparsed.morph_frames[0].morph_name,
3832            parsed.morph_frames[0].morph_name
3833        );
3834        assert_eq!(reparsed.morph_frames[0].frame, parsed.morph_frames[0].frame);
3835        assert!((reparsed.morph_frames[0].weight - parsed.morph_frames[0].weight).abs() < 1e-6);
3836    }
3837
3838    fn expected_sjis_name_bytes(value: &str, len: usize) -> Vec<u8> {
3839        let (encoded, _, _) = encoding_rs::SHIFT_JIS.encode(value);
3840        encoded.as_ref()[..encoded.len().min(len)].to_vec()
3841    }
3842
3843    #[test]
3844    fn export_json_dto_encodes_shift_jis_when_raw_name_bytes_are_missing() {
3845        let animation = vmd_parsed_animation(
3846            "初音ミク".to_owned(),
3847            Vec::new(),
3848            30,
3849            VmdParsedSections {
3850                bone_frames: vec![VmdParsedBoneFrame {
3851                    bone_name: "左足".to_owned(),
3852                    bone_name_bytes: Vec::new(),
3853                    frame: 10,
3854                    translation: [1.0, 2.0, 3.0],
3855                    rotation: [0.0, 0.0, 0.0, 1.0],
3856                    interpolation: vec![20; 64],
3857                }],
3858                morph_frames: vec![VmdParsedMorphFrame {
3859                    morph_name: "笑い".to_owned(),
3860                    morph_name_bytes: Vec::new(),
3861                    frame: 20,
3862                    weight: 0.75,
3863                }],
3864                camera_frames: Vec::new(),
3865                light_frames: Vec::new(),
3866                self_shadow_frames: Vec::new(),
3867                property_frames: vec![VmdParsedPropertyFrame {
3868                    frame: 30,
3869                    visible: true,
3870                    ik_states: vec![VmdParsedIkState {
3871                        bone_name: "右足IK".to_owned(),
3872                        bone_name_bytes: Vec::new(),
3873                        enabled: false,
3874                    }],
3875                }],
3876            },
3877        );
3878        let json = serde_json::to_string(&animation).unwrap();
3879        assert!(!json.contains("modelNameBytes"));
3880        assert!(!json.contains("boneNameBytes"));
3881        assert!(!json.contains("morphNameBytes"));
3882
3883        let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3884        let exported = export_vmd_animation(&from_json);
3885        let reparsed = parse_vmd_animation(&exported).unwrap();
3886
3887        assert_eq!(reparsed.metadata.model_name, "初音ミク");
3888        assert!(!reparsed.metadata.model_name_bytes.is_empty());
3889        assert_eq!(
3890            reparsed.metadata.model_name_bytes,
3891            expected_sjis_name_bytes("初音ミク", 20)
3892        );
3893        assert_eq!(reparsed.bone_frames[0].bone_name, "左足");
3894        assert!(!reparsed.bone_frames[0].bone_name_bytes.is_empty());
3895        assert_eq!(
3896            reparsed.bone_frames[0].bone_name_bytes,
3897            expected_sjis_name_bytes("左足", 15)
3898        );
3899        assert_eq!(reparsed.morph_frames[0].morph_name, "笑い");
3900        assert!(!reparsed.morph_frames[0].morph_name_bytes.is_empty());
3901        assert_eq!(
3902            reparsed.morph_frames[0].morph_name_bytes,
3903            expected_sjis_name_bytes("笑い", 15)
3904        );
3905        assert_eq!(reparsed.property_frames[0].ik_states[0].bone_name, "右足IK");
3906        assert!(
3907            !reparsed.property_frames[0].ik_states[0]
3908                .bone_name_bytes
3909                .is_empty()
3910        );
3911        assert_eq!(
3912            reparsed.property_frames[0].ik_states[0].bone_name_bytes,
3913            expected_sjis_name_bytes("右足IK", 20)
3914        );
3915        assert!(!reparsed.property_frames[0].ik_states[0].enabled);
3916    }
3917
3918    #[test]
3919    fn export_json_dto_uses_encoding_rs_replacement_for_non_shift_jis_names() {
3920        let animation = vmd_parsed_animation(
3921            "miku".to_owned(),
3922            Vec::new(),
3923            1,
3924            VmdParsedSections {
3925                bone_frames: vec![VmdParsedBoneFrame {
3926                    bone_name: "左足🧪".to_owned(),
3927                    bone_name_bytes: Vec::new(),
3928                    frame: 1,
3929                    translation: [0.0, 0.0, 0.0],
3930                    rotation: [0.0, 0.0, 0.0, 1.0],
3931                    interpolation: vec![20; 64],
3932                }],
3933                morph_frames: Vec::new(),
3934                camera_frames: Vec::new(),
3935                light_frames: Vec::new(),
3936                self_shadow_frames: Vec::new(),
3937                property_frames: Vec::new(),
3938            },
3939        );
3940        let json = serde_json::to_string(&animation).unwrap();
3941        let from_json: VmdParsedAnimation = serde_json::from_str(&json).unwrap();
3942        let exported = export_vmd_animation(&from_json);
3943        let reparsed = parse_vmd_animation(&exported).unwrap();
3944
3945        assert_eq!(
3946            reparsed.bone_frames[0].bone_name_bytes,
3947            expected_sjis_name_bytes("左足🧪", 15)
3948        );
3949        assert_eq!(
3950            reparsed.bone_frames[0].bone_name,
3951            encoding_rs::SHIFT_JIS
3952                .decode(&expected_sjis_name_bytes("左足🧪", 15))
3953                .0
3954                .trim()
3955        );
3956    }
3957}