Skip to main content

mmd_anim_format/vmd/
mod.rs

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