Skip to main content

mmd_anim_format/pmx/
mod.rs

1use std::collections::HashMap;
2
3use glam::{Mat4, Quat};
4use serde::{Deserialize, Serialize};
5
6use mmd_anim_runtime::{
7    AppendTransformInit, BoneIndex, BoneInit, BoneMorphOffset, GroupMorphOffset, IkAngleLimit,
8    IkLinkInit, IkSolverInit, LocalAxis, ModelArena, MorphIndex, MorphInit, MorphOffsetSpan,
9    RuntimeAppendTransformDescriptorV1, RuntimeBoneDescriptorV1,
10    RuntimeBoneMorphOffsetDescriptorV1, RuntimeGroupMorphOffsetDescriptorV1,
11    RuntimeIkLinkDescriptorV1, RuntimeIkSolverDescriptorV1, RuntimeModelDescriptorV1,
12    RuntimeMorphDescriptorV1, build_morph_init_from_offsets, compile_runtime_model_descriptor_v1,
13};
14
15use crate::binary::{
16    ByteReader, write_f32_le as write_f32, write_f32_slice_le as write_f32_slice,
17    write_i32_le as write_i32, write_u16_le as write_u16,
18};
19use crate::error::ImportError;
20use crate::sjis::encode_sjis;
21
22type Reader<'a> = ByteReader<'a>;
23
24mod json_f32;
25
26const PMX_MAGIC: [u8; 4] = [0x50, 0x4D, 0x58, 0x20];
27
28const BONE_FLAG_TAIL_INDEX: u16 = 0x0001;
29const BONE_FLAG_IK: u16 = 0x0020;
30const BONE_FLAG_LOCAL_APPEND: u16 = 0x0080;
31const BONE_FLAG_APPEND_ROTATE: u16 = 0x0100;
32const BONE_FLAG_APPEND_TRANSLATE: u16 = 0x0200;
33const BONE_FLAG_FIXED_AXIS: u16 = 0x0400;
34const BONE_FLAG_LOCAL_AXIS: u16 = 0x0800;
35const BONE_FLAG_TRANSFORM_AFTER_PHYSICS: u16 = 0x1000;
36const BONE_FLAG_EXTERNAL_PARENT: u16 = 0x2000;
37const PMX_SKINNING_MODE_BDEF1: &str = "bdef1";
38const PMX_SKINNING_MODE_BDEF2: &str = "bdef2";
39const PMX_SKINNING_MODE_BDEF4: &str = "bdef4";
40const PMX_SKINNING_MODE_SDEF: &str = "sdef";
41const PMX_SKINNING_MODE_QDEF: &str = "qdef";
42
43fn decode_utf16le_lossy(bytes: &[u8]) -> String {
44    let end = bytes.len().saturating_sub(bytes.len() % 2);
45    let units = bytes[..end]
46        .chunks_exact(2)
47        .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
48        .take_while(|&code| code != 0);
49    std::char::decode_utf16(units)
50        .map(|item| item.unwrap_or(char::REPLACEMENT_CHARACTER))
51        .collect()
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum TextEncoding {
56    Utf16Le = 0,
57    Utf8 = 1,
58}
59
60#[derive(Debug, Clone)]
61pub struct PmxHeader {
62    pub version: f32,
63    pub encoding: TextEncoding,
64    pub extra_uv_count: u8,
65    pub vertex_index_size: u8,
66    pub texture_index_size: u8,
67    pub material_index_size: u8,
68    pub bone_index_size: u8,
69    pub morph_index_size: u8,
70    pub rigidbody_index_size: u8,
71}
72
73impl<'a> ByteReader<'a> {
74    fn read_vec2_array(&mut self) -> Result<[f32; 2], ImportError> {
75        Ok([self.read_f32_le()?, self.read_f32_le()?])
76    }
77
78    fn read_vec3_array(&mut self) -> Result<[f32; 3], ImportError> {
79        Ok([
80            self.read_f32_le()?,
81            self.read_f32_le()?,
82            self.read_f32_le()?,
83        ])
84    }
85
86    fn read_vec4_array(&mut self) -> Result<[f32; 4], ImportError> {
87        Ok([
88            self.read_f32_le()?,
89            self.read_f32_le()?,
90            self.read_f32_le()?,
91            self.read_f32_le()?,
92        ])
93    }
94
95    fn read_sized_index(&mut self, size: u8) -> Result<i32, ImportError> {
96        match size {
97            1 => Ok(self.read_u8()? as i8 as i32),
98            2 => Ok({
99                let b = self.read_bytes(2)?;
100                i16::from_le_bytes([b[0], b[1]]) as i32
101            }),
102            4 => self.read_i32_le(),
103            _ => Err(ImportError::InvalidIndexSize(size)),
104        }
105    }
106
107    fn read_index(&mut self, header: &PmxHeader) -> Result<i32, ImportError> {
108        self.read_sized_index(header.bone_index_size)
109    }
110
111    fn read_vertex_index(&mut self, size: u8) -> Result<u32, ImportError> {
112        match size {
113            1 => Ok(self.read_u8()? as u32),
114            2 => Ok(self.read_u16_le()? as u32),
115            4 => {
116                let raw = self.read_i32_le()?;
117                if raw < 0 {
118                    Err(ImportError::SectionOverflow)
119                } else {
120                    Ok(raw as u32)
121                }
122            }
123            _ => Err(ImportError::InvalidIndexSize(size)),
124        }
125    }
126
127    fn read_string(&mut self, encoding: TextEncoding) -> Result<String, ImportError> {
128        let len = self.read_i32_le()?;
129        if len <= 0 {
130            return Ok(String::new());
131        }
132        let bytes = self.read_bytes(len as usize)?;
133        match encoding {
134            TextEncoding::Utf8 => {
135                let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
136                Ok(String::from_utf8_lossy(&bytes[..end]).into_owned())
137            }
138            TextEncoding::Utf16Le => Ok(decode_utf16le_lossy(bytes)),
139        }
140    }
141
142    fn read_string_owned(
143        &mut self,
144        encoding: TextEncoding,
145    ) -> Result<(Vec<u8>, String), ImportError> {
146        let len = self.read_i32_le()?;
147        if len <= 0 {
148            return Ok((Vec::new(), String::new()));
149        }
150        let bytes = self.read_bytes(len as usize)?.to_vec();
151        let decoded = match encoding {
152            TextEncoding::Utf8 => {
153                let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
154                String::from_utf8_lossy(&bytes[..end]).into_owned()
155            }
156            TextEncoding::Utf16Le => decode_utf16le_lossy(&bytes),
157        };
158        Ok((bytes, decoded))
159    }
160
161    fn read_bone_index(&mut self, header: &PmxHeader) -> Result<Option<BoneIndex>, ImportError> {
162        let raw = self.read_sized_index(header.bone_index_size)?;
163        if raw < 0 {
164            Ok(None)
165        } else {
166            Ok(Some(BoneIndex(raw as u32)))
167        }
168    }
169}
170
171pub fn read_header(data: &[u8]) -> Result<(PmxHeader, usize), ImportError> {
172    let mut r = Reader::new(data);
173
174    let magic = r.read_bytes(4)?;
175    if magic != PMX_MAGIC {
176        return Err(ImportError::InvalidPmxMagic);
177    }
178
179    let version = r.read_f32_le()?;
180    if !(2.0..=2.2).contains(&version) {
181        return Err(ImportError::UnsupportedPmxVersion(version));
182    }
183
184    let data_count = r.read_u8()?;
185    if data_count < 8 {
186        return Err(ImportError::InvalidEncoding(data_count));
187    }
188
189    let encoding_byte = r.read_u8()?;
190    let encoding = match encoding_byte {
191        0 => TextEncoding::Utf16Le,
192        1 => TextEncoding::Utf8,
193        _ => return Err(ImportError::InvalidEncoding(encoding_byte)),
194    };
195
196    let extra_uv_count = r.read_u8()?;
197    let vertex_index_size = r.read_u8()?;
198    let texture_index_size = r.read_u8()?;
199    let material_index_size = r.read_u8()?;
200    let bone_index_size = r.read_u8()?;
201    let morph_index_size = r.read_u8()?;
202    let rigidbody_index_size = r.read_u8()?;
203    for size in [
204        vertex_index_size,
205        texture_index_size,
206        material_index_size,
207        bone_index_size,
208        morph_index_size,
209        rigidbody_index_size,
210    ] {
211        if !matches!(size, 1 | 2 | 4) {
212            return Err(ImportError::InvalidIndexSize(size));
213        }
214    }
215    if data_count > 8 {
216        r.skip((data_count - 8) as usize)?;
217    }
218
219    let header = PmxHeader {
220        version,
221        encoding,
222        extra_uv_count,
223        vertex_index_size,
224        texture_index_size,
225        material_index_size,
226        bone_index_size,
227        morph_index_size,
228        rigidbody_index_size,
229    };
230
231    Ok((header, r.pos))
232}
233
234pub fn skip_model_info(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
235    let mut r = Reader { data, pos };
236    let _model_name = r.read_string(header.encoding)?;
237    let _english_name = r.read_string(header.encoding)?;
238    let _comment = r.read_string(header.encoding)?;
239    let _english_comment = r.read_string(header.encoding)?;
240    Ok(r.pos)
241}
242
243pub fn skip_vertices(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
244    let mut r = Reader { data, pos };
245    let count = r.read_i32_le()?;
246    if count < 0 {
247        return Err(ImportError::SectionOverflow);
248    }
249    let count = count as usize;
250
251    for _ in 0..count {
252        r.skip(12)?;
253        r.skip(12)?;
254        r.skip(8)?;
255        r.skip(header.extra_uv_count as usize * 16)?;
256        let vertex_deform_type = r.read_u8()?;
257
258        let bone_indices: usize = match vertex_deform_type {
259            0 => 1,
260            1 => 2,
261            2 | 4 => 4,
262            3 => 2,
263            _ => return Err(ImportError::SectionOverflow),
264        };
265        for _ in 0..bone_indices {
266            r.read_sized_index(header.bone_index_size)?;
267        }
268
269        let weight_count: usize = match vertex_deform_type {
270            0 => 0,
271            1 => 1,
272            2 | 4 => 4,
273            3 => 1,
274            _ => return Err(ImportError::SectionOverflow),
275        };
276        r.skip(weight_count * 4)?;
277
278        if vertex_deform_type == 3 {
279            r.skip(36)?;
280        }
281
282        r.skip(4)?;
283    }
284
285    Ok(r.pos)
286}
287
288pub fn skip_faces(data: &[u8], vertex_index_size: u8, pos: usize) -> Result<usize, ImportError> {
289    let mut r = Reader { data, pos };
290    let count = r.read_i32_le()?;
291    if count < 0 {
292        return Err(ImportError::SectionOverflow);
293    }
294    let count = count as usize;
295    let bytes = count
296        .checked_mul(vertex_index_size as usize)
297        .ok_or(ImportError::SectionOverflow)?;
298    r.skip(bytes)?;
299    Ok(r.pos)
300}
301
302pub fn skip_textures(
303    data: &[u8],
304    encoding: TextEncoding,
305    pos: usize,
306) -> Result<usize, ImportError> {
307    let mut r = Reader { data, pos };
308    let count = r.read_i32_le()?;
309    if count < 0 {
310        return Err(ImportError::SectionOverflow);
311    }
312    for _ in 0..count {
313        let _path = r.read_string(encoding)?;
314    }
315    Ok(r.pos)
316}
317
318pub fn skip_materials(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
319    let mut r = Reader { data, pos };
320    let count = r.read_i32_le()?;
321    if count < 0 {
322        return Err(ImportError::SectionOverflow);
323    }
324    for _ in 0..count {
325        let _name = r.read_string(header.encoding)?;
326        let _english_name = r.read_string(header.encoding)?;
327        r.skip(16)?;
328        r.skip(16)?;
329        r.skip(12)?;
330        r.skip(1)?;
331        r.skip(16)?;
332        r.skip(4)?;
333        let _texture = r.read_sized_index(header.texture_index_size)?;
334        let _sphere_tex = r.read_sized_index(header.texture_index_size)?;
335        r.skip(1)?;
336        let _toon = r.read_u8()?;
337        if _toon == 0 {
338            r.read_sized_index(header.texture_index_size)?;
339        } else {
340            r.skip(1)?;
341        }
342        let _memo = r.read_string(header.encoding)?;
343        let _face_count = r.read_i32_le()?;
344    }
345    Ok(r.pos)
346}
347
348pub fn skip_morphs(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
349    let mut r = Reader { data, pos };
350    let count = r.read_i32_le()?;
351    if count < 0 {
352        return Err(ImportError::SectionOverflow);
353    }
354    for _ in 0..count {
355        let _name = r.read_string(header.encoding)?;
356        let _english_name = r.read_string(header.encoding)?;
357        r.skip(1)?;
358        let _morph_type = r.read_u8()?;
359        let _offset_count = r.read_i32_le()?;
360        match _morph_type {
361            0 => {
362                for _ in 0.._offset_count {
363                    r.read_sized_index(header.morph_index_size)?;
364                    r.skip(4)?;
365                }
366            }
367            1 => {
368                for _ in 0.._offset_count {
369                    r.read_sized_index(header.vertex_index_size)?;
370                    r.skip(12)?;
371                }
372            }
373            2 => {
374                for _ in 0.._offset_count {
375                    r.read_sized_index(header.bone_index_size)?;
376                    r.skip(12)?;
377                    r.skip(16)?;
378                }
379            }
380            3..=7 => {
381                for _ in 0.._offset_count {
382                    r.read_sized_index(header.vertex_index_size)?;
383                    r.skip(16)?;
384                }
385            }
386            8 => {
387                for _ in 0.._offset_count {
388                    r.read_sized_index(header.material_index_size)?;
389                    r.skip(1)?;
390                    r.skip(16)?;
391                    r.skip(12)?;
392                    r.skip(4)?;
393                    r.skip(12)?;
394                    r.skip(16)?;
395                    r.skip(4)?;
396                    r.skip(16)?;
397                    r.skip(16)?;
398                    r.skip(16)?;
399                }
400            }
401            9 => {
402                for _ in 0.._offset_count {
403                    r.read_sized_index(header.rigidbody_index_size)?;
404                    r.skip(1)?;
405                    r.skip(12)?;
406                    r.skip(12)?;
407                }
408            }
409            _ => return Err(ImportError::SectionOverflow),
410        }
411    }
412    Ok(r.pos)
413}
414
415#[derive(Debug, Clone)]
416pub struct PmxMorphNames {
417    pub name_bytes: Vec<Vec<u8>>,
418    pub names: Vec<String>,
419}
420
421pub fn read_morph_names(
422    data: &[u8],
423    header: &PmxHeader,
424    pos: usize,
425) -> Result<(PmxMorphNames, usize), ImportError> {
426    let mut r = Reader { data, pos };
427    let count = r.read_i32_le()?;
428    if count < 0 {
429        return Err(ImportError::SectionOverflow);
430    }
431    let count = count as usize;
432    r.require_record_bytes(count, 1)?;
433
434    let mut name_bytes = Vec::with_capacity(count);
435    let mut names = Vec::with_capacity(count);
436
437    for _ in 0..count {
438        let (raw, name) = r.read_string_owned(header.encoding)?;
439        let _english_name = r.read_string(header.encoding)?;
440        name_bytes.push(raw);
441        names.push(name);
442
443        r.skip(1)?;
444        let morph_type = r.read_u8()?;
445        let offset_count = r.read_i32_le()?;
446        if offset_count < 0 {
447            return Err(ImportError::SectionOverflow);
448        }
449        let offset_count = offset_count as usize;
450
451        let per_offset = match morph_type {
452            0 => header.morph_index_size as usize + 4,
453            1 => header.vertex_index_size as usize + 12,
454            2 => header.bone_index_size as usize + 12 + 16,
455            3..=7 => header.vertex_index_size as usize + 16,
456            8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
457            9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
458            _ => return Err(ImportError::SectionOverflow),
459        };
460        r.skip(
461            offset_count
462                .checked_mul(per_offset)
463                .ok_or(ImportError::SectionOverflow)?,
464        )?;
465    }
466
467    Ok((PmxMorphNames { name_bytes, names }, r.pos))
468}
469
470/// Parse morph names and the runtime-relevant bone/group offset payload.
471/// Unsupported morph types (vertex, UV, material, flip) have their payload
472/// skipped, while the descriptor compiler remains the only production
473/// normalization/validation point.
474fn read_runtime_morph_descriptor(
475    data: &[u8],
476    header: &PmxHeader,
477    pos: usize,
478) -> Result<(PmxMorphNames, RuntimeMorphDescriptorV1, usize), ImportError> {
479    let mut r = Reader { data, pos };
480    let count = r.read_i32_le()?;
481    if count < 0 {
482        return Err(ImportError::SectionOverflow);
483    }
484    let count = count as usize;
485    r.require_record_bytes(count, 1)?;
486
487    let mut name_bytes = Vec::with_capacity(count);
488    let mut names = Vec::with_capacity(count);
489
490    let mut bone_offsets = Vec::new();
491    let mut group_offsets = Vec::new();
492
493    for morph_index in 0..count {
494        let (raw, name) = r.read_string_owned(header.encoding)?;
495        let _english_name = r.read_string(header.encoding)?;
496        name_bytes.push(raw);
497        names.push(name);
498
499        r.skip(1)?;
500        let morph_type = r.read_u8()?;
501        let offset_count = r.read_i32_le()?;
502        if offset_count < 0 {
503            return Err(ImportError::SectionOverflow);
504        }
505        let offset_count = offset_count as usize;
506
507        match morph_type {
508            0 => {
509                for _ in 0..offset_count {
510                    let child_raw = r.read_sized_index(header.morph_index_size)?;
511                    if child_raw < 0 {
512                        return Err(ImportError::SectionOverflow);
513                    }
514                    let ratio = r.read_f32_le()?;
515                    group_offsets.push(RuntimeGroupMorphOffsetDescriptorV1 {
516                        morph_index: MorphIndex(morph_index as u32),
517                        child_morph: MorphIndex(child_raw as u32),
518                        ratio,
519                    });
520                }
521            }
522            2 => {
523                for _ in 0..offset_count {
524                    let bone_raw = r.read_sized_index(header.bone_index_size)?;
525                    if bone_raw < 0 {
526                        return Err(ImportError::SectionOverflow);
527                    }
528                    let pos = r.read_vec3()?;
529                    let qx = r.read_f32_le()?;
530                    let qy = r.read_f32_le()?;
531                    let qz = r.read_f32_le()?;
532                    let qw = r.read_f32_le()?;
533                    bone_offsets.push(RuntimeBoneMorphOffsetDescriptorV1 {
534                        morph_index: MorphIndex(morph_index as u32),
535                        target_bone: BoneIndex(bone_raw as u32),
536                        position_offset: pos,
537                        rotation_offset: Quat::from_xyzw(qx, qy, qz, qw),
538                    });
539                }
540            }
541            _ => {
542                let per_offset = match morph_type {
543                    1 => header.vertex_index_size as usize + 12,
544                    3..=7 => header.vertex_index_size as usize + 16,
545                    8 => {
546                        header.material_index_size as usize
547                            + 1
548                            + 16
549                            + 12
550                            + 4
551                            + 12
552                            + 16
553                            + 4
554                            + 16
555                            + 16
556                            + 16
557                    }
558                    9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
559                    _ => return Err(ImportError::SectionOverflow),
560                };
561                r.skip(
562                    offset_count
563                        .checked_mul(per_offset)
564                        .ok_or(ImportError::SectionOverflow)?,
565                )?;
566            }
567        }
568    }
569
570    Ok((
571        PmxMorphNames { name_bytes, names },
572        RuntimeMorphDescriptorV1 {
573            morph_count: count as u32,
574            bone_offsets,
575            group_offsets,
576        },
577        r.pos,
578    ))
579}
580
581/// Read morph names and adapt the runtime descriptor to the historical
582/// `MorphInit` representation for parser-facing callers and tests. Runtime
583/// import itself calls `read_runtime_morph_descriptor` directly.
584pub fn read_morph_offsets(
585    data: &[u8],
586    header: &PmxHeader,
587    pos: usize,
588) -> Result<(PmxMorphNames, MorphInit, usize), ImportError> {
589    let (names, descriptor, pos) = read_runtime_morph_descriptor(data, header, pos)?;
590    let morph_count = descriptor.morph_count as usize;
591    let bone_offsets = descriptor
592        .bone_offsets
593        .iter()
594        .map(|offset| {
595            (
596                offset.morph_index,
597                BoneMorphOffset {
598                    target_bone: offset.target_bone,
599                    position_offset: offset.position_offset,
600                    rotation_offset: offset.rotation_offset,
601                },
602            )
603        })
604        .collect();
605    let group_offsets = descriptor
606        .group_offsets
607        .iter()
608        .map(|offset| {
609            (
610                offset.morph_index,
611                GroupMorphOffset {
612                    child_morph: offset.child_morph,
613                    ratio: offset.ratio,
614                },
615            )
616        })
617        .collect();
618    let mut morph =
619        build_morph_init_from_offsets(descriptor.morph_count, bone_offsets, group_offsets)
620            .map_err(ImportError::ModelBuildFailed)?;
621    morph.vertex_spans = vec![MorphOffsetSpan::default(); morph_count];
622    Ok((names, morph, pos))
623}
624
625#[derive(Debug, Clone)]
626pub struct PmxBoneImport {
627    pub bones: Vec<BoneInit>,
628    pub ik_solvers: Vec<IkSolverInit>,
629    pub append_transforms: Vec<AppendTransformInit>,
630    pub bone_name_bytes: Vec<Vec<u8>>,
631    pub bone_names: Vec<String>,
632}
633
634struct PmxBoneRead {
635    descriptor: RuntimeModelDescriptorV1,
636    bone_name_bytes: Vec<Vec<u8>>,
637    bone_names: Vec<String>,
638}
639
640fn read_bones_with_local_axes(
641    data: &[u8],
642    header: &PmxHeader,
643    pos: usize,
644) -> Result<(PmxBoneRead, usize), ImportError> {
645    let mut r = Reader { data, pos };
646    let count = r.read_i32_le()?;
647    if count < 0 {
648        return Err(ImportError::SectionOverflow);
649    }
650    let count = count as usize;
651    r.require_record_bytes(count, 1)?;
652
653    let mut bones = Vec::with_capacity(count);
654    let mut ik_solvers = Vec::new();
655    let mut append_transforms = Vec::new();
656
657    let mut bone_name_bytes = Vec::with_capacity(count);
658    let mut bone_names = Vec::with_capacity(count);
659
660    for _bone_i in 0..count {
661        let (name_bytes, name) = r.read_string_owned(header.encoding)?;
662        let _english_name = r.read_string(header.encoding)?;
663        let position = r.read_vec3()?;
664        let parent = r.read_bone_index(header)?;
665        let transform_order = r.read_i32_le()?;
666        let flags = r.read_u16_le()?;
667
668        if flags & BONE_FLAG_TAIL_INDEX != 0 {
669            r.read_index(header)?;
670        } else {
671            r.read_vec3()?;
672        }
673
674        let has_append =
675            flags & BONE_FLAG_APPEND_ROTATE != 0 || flags & BONE_FLAG_APPEND_TRANSLATE != 0;
676
677        if has_append {
678            let append_parent = r.read_bone_index(header)?;
679            let append_ratio = r.read_f32_le()?;
680
681            // Preserve the historical PMX import policy: malformed optional
682            // append metadata is ignored at the tolerant file-format boundary.
683            // Host-authored typed descriptors remain strictly validated.
684            if let Some(source) = append_parent
685                && append_ratio.is_finite()
686            {
687                let target_idx = bones.len();
688                append_transforms.push(RuntimeAppendTransformDescriptorV1 {
689                    target_bone: BoneIndex(target_idx as u32),
690                    source_bone: source,
691                    ratio: append_ratio,
692                    affect_rotation: flags & BONE_FLAG_APPEND_ROTATE != 0,
693                    affect_translation: flags & BONE_FLAG_APPEND_TRANSLATE != 0,
694                    local: flags & BONE_FLAG_LOCAL_APPEND != 0,
695                });
696            }
697        }
698
699        let fixed_axis = if flags & BONE_FLAG_FIXED_AXIS != 0 {
700            let axis = r.read_vec3()?;
701            (axis.is_finite() && axis.length_squared() > f32::EPSILON).then_some(axis)
702        } else {
703            None
704        };
705
706        let local_axis = if flags & BONE_FLAG_LOCAL_AXIS != 0 {
707            let x = r.read_vec3()?;
708            let z = r.read_vec3()?;
709            let axis = LocalAxis::new(x, z);
710            axis.basis_quat().is_some().then_some(axis)
711        } else {
712            None
713        };
714
715        if flags & BONE_FLAG_EXTERNAL_PARENT != 0 {
716            r.read_i32_le()?;
717        }
718
719        if flags & BONE_FLAG_IK != 0 {
720            let ik_target = r.read_bone_index(header)?;
721            let ik_loop_count = r.read_i32_le()?;
722            let ik_limit_angle = r.read_f32_le()?;
723            let link_count = r.read_i32_le()?;
724            if link_count < 0 {
725                return Err(ImportError::SectionOverflow);
726            }
727            r.require_record_bytes(link_count as usize, header.bone_index_size as usize + 1)?;
728
729            let mut links = Vec::with_capacity(link_count as usize);
730            for _ in 0..link_count {
731                // Match the legacy importer for malformed optional IK links.
732                // Bone 0 is the historical fallback and keeps such PMX files
733                // loadable without weakening direct descriptor validation.
734                let link_bone = r.read_bone_index(header)?.unwrap_or(BoneIndex(0));
735                let has_limit = r.read_u8()?;
736                let angle_limit = if has_limit != 0 {
737                    let min = r.read_vec3()?;
738                    let max = r.read_vec3()?;
739                    // Angle limits are optional metadata at the PMX boundary.
740                    // Keep the link, but drop a malformed limit before the
741                    // strict typed-descriptor compiler sees it.
742                    (min.is_finite() && max.is_finite() && min.cmple(max).bitmask() == 0b111)
743                        .then(|| IkAngleLimit::new(min, max))
744                } else {
745                    None
746                };
747                links.push(RuntimeIkLinkDescriptorV1 {
748                    bone: link_bone,
749                    angle_limit,
750                });
751            }
752
753            let ik_bone = BoneIndex(bones.len() as u32);
754            // A missing target was historically treated as "no solver". A
755            // non-positive loop count is likewise sanitized to no solver,
756            // avoiding the old negative-i32-to-u32 explosion while preserving
757            // PMX import success.
758            if let Some(target_bone) = ik_target
759                && ik_loop_count > 0
760                && ik_limit_angle.is_finite()
761                && ik_limit_angle >= 0.0
762            {
763                ik_solvers.push(RuntimeIkSolverDescriptorV1 {
764                    ik_bone,
765                    target_bone,
766                    links,
767                    iteration_count: ik_loop_count as u32,
768                    limit_angle: ik_limit_angle,
769                });
770            }
771        }
772
773        bone_name_bytes.push(name_bytes);
774        bone_names.push(name);
775
776        bones.push(RuntimeBoneDescriptorV1 {
777            parent,
778            rest_position: position,
779            transform_order,
780            transform_after_physics: flags & BONE_FLAG_TRANSFORM_AFTER_PHYSICS != 0,
781            fixed_axis,
782            local_axis,
783        });
784    }
785
786    Ok((
787        PmxBoneRead {
788            descriptor: RuntimeModelDescriptorV1 {
789                bones,
790                ik_solvers,
791                append_transforms,
792                ..RuntimeModelDescriptorV1::default()
793            },
794            bone_name_bytes,
795            bone_names,
796        },
797        r.pos,
798    ))
799}
800
801/// Adapt the host-independent descriptor into the historical low-level
802/// `PmxBoneImport` shape.  Runtime model construction must use the descriptor
803/// compiler; this adapter exists only for callers of the parser-facing API.
804fn legacy_bone_import(read: &PmxBoneRead) -> PmxBoneImport {
805    let bones = read
806        .descriptor
807        .bones
808        .iter()
809        .map(|descriptor| {
810            let rest_position = descriptor
811                .parent
812                .and_then(|parent| read.descriptor.bones.get(parent.as_usize()))
813                .map_or(descriptor.rest_position, |parent| {
814                    descriptor.rest_position - parent.rest_position
815                });
816            BoneInit {
817                parent: descriptor.parent,
818                rest_position,
819                inverse_bind_matrix: Mat4::from_translation((-descriptor.rest_position).into()),
820                transform_order: descriptor.transform_order,
821                transform_after_physics: descriptor.transform_after_physics,
822                fixed_axis: descriptor.fixed_axis,
823                // PMX fixed-axis is metadata and an IK constraint; ordinary
824                // pose evaluation does not project rotations onto it.
825                enforce_fixed_axis: false,
826            }
827        })
828        .collect();
829    let ik_solvers = read
830        .descriptor
831        .ik_solvers
832        .iter()
833        .map(|solver| IkSolverInit {
834            ik_bone: solver.ik_bone,
835            target_bone: solver.target_bone,
836            links: solver
837                .links
838                .iter()
839                .map(|link| IkLinkInit {
840                    bone: link.bone,
841                    angle_limit: link.angle_limit,
842                })
843                .collect(),
844            iteration_count: solver.iteration_count,
845            limit_angle: solver.limit_angle,
846        })
847        .collect();
848    let append_transforms = read
849        .descriptor
850        .append_transforms
851        .iter()
852        .map(|append| AppendTransformInit {
853            target_bone: append.target_bone,
854            source_bone: append.source_bone,
855            ratio: append.ratio,
856            affect_rotation: append.affect_rotation,
857            affect_translation: append.affect_translation,
858            local: append.local,
859        })
860        .collect();
861    PmxBoneImport {
862        bones,
863        ik_solvers,
864        append_transforms,
865        bone_name_bytes: read.bone_name_bytes.clone(),
866        bone_names: read.bone_names.clone(),
867    }
868}
869
870pub fn read_bones(
871    data: &[u8],
872    header: &PmxHeader,
873    pos: usize,
874) -> Result<(PmxBoneImport, usize), ImportError> {
875    let (bone_read, pos) = read_bones_with_local_axes(data, header, pos)?;
876    Ok((legacy_bone_import(&bone_read), pos))
877}
878
879pub fn import_pmx_model(data: &[u8]) -> Result<PmxBoneImport, ImportError> {
880    let (header, pos) = read_header(data)?;
881    let pos = skip_model_info(data, &header, pos)?;
882    let pos = skip_vertices(data, &header, pos)?;
883    let pos = skip_faces(data, header.vertex_index_size, pos)?;
884    let pos = skip_textures(data, header.encoding, pos)?;
885    let pos = skip_materials(data, &header, pos)?;
886    let (bones, _pos) = read_bones(data, &header, pos)?;
887    Ok(bones)
888}
889
890#[derive(Debug, Clone, Serialize, Deserialize)]
891#[serde(rename_all = "camelCase")]
892pub struct PmxParsedModel {
893    pub metadata: PmxParsedMetadata,
894    pub geometry: PmxParsedGeometry,
895    pub materials: Vec<PmxParsedMaterial>,
896    pub skeleton: PmxParsedSkeleton,
897    pub morphs: Vec<PmxParsedMorph>,
898    pub display_frames: Vec<PmxParsedDisplayFrame>,
899    pub rigid_bodies: Vec<PmxParsedRigidBody>,
900    pub joints: Vec<PmxParsedJoint>,
901    pub soft_bodies: Vec<PmxParsedSoftBody>,
902    pub diagnostics: Vec<PmxParserDiagnostic>,
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize)]
906#[serde(rename_all = "camelCase")]
907pub struct PmxParsedMetadata {
908    #[serde(default = "default_pmx_format")]
909    pub format: String,
910    #[serde(with = "json_f32")]
911    pub version: f32,
912    pub encoding: String,
913    pub name: String,
914    pub english_name: String,
915    pub comment: String,
916    pub english_comment: String,
917    pub counts: PmxParsedCounts,
918    pub index_sizes: PmxParsedIndexSizes,
919    pub additional_uv_count: u8,
920}
921
922#[derive(Debug, Clone, Serialize, Deserialize)]
923#[serde(rename_all = "camelCase")]
924pub struct PmxParsedCounts {
925    pub vertices: usize,
926    pub faces: usize,
927    pub materials: usize,
928    pub bones: usize,
929    pub morphs: usize,
930    pub display_frames: usize,
931    pub rigid_bodies: usize,
932    pub joints: usize,
933    pub soft_bodies: usize,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
937pub struct PmxParsedIndexSizes {
938    pub vertex: u8,
939    pub texture: u8,
940    pub material: u8,
941    pub bone: u8,
942    pub morph: u8,
943    #[serde(rename = "rigidBody")]
944    pub rigid_body: u8,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize, Default)]
948#[serde(rename_all = "camelCase")]
949pub struct PmxParsedSdef {
950    #[serde(default)]
951    pub skinning_modes: Vec<String>,
952    #[serde(with = "json_f32::vec_f32")]
953    pub enabled: Vec<f32>,
954    #[serde(with = "json_f32::vec_f32")]
955    pub c: Vec<f32>,
956    #[serde(with = "json_f32::vec_f32")]
957    pub r0: Vec<f32>,
958    #[serde(with = "json_f32::vec_f32")]
959    pub r1: Vec<f32>,
960    #[serde(with = "json_f32::vec_f32")]
961    pub rw0: Vec<f32>,
962    #[serde(with = "json_f32::vec_f32")]
963    pub rw1: Vec<f32>,
964}
965
966#[derive(Debug, Clone, Serialize, Deserialize, Default)]
967#[serde(rename_all = "camelCase")]
968pub struct PmxParsedQdef {
969    #[serde(with = "json_f32::vec_f32")]
970    pub enabled: Vec<f32>,
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize)]
974#[serde(rename_all = "camelCase")]
975pub struct PmxParsedGeometry {
976    #[serde(with = "json_f32::vec_f32")]
977    pub positions: Vec<f32>,
978    #[serde(with = "json_f32::vec_f32")]
979    pub normals: Vec<f32>,
980    #[serde(with = "json_f32::vec_f32")]
981    pub uvs: Vec<f32>,
982    #[serde(with = "json_f32::nested_vec")]
983    pub additional_uvs: Vec<Vec<f32>>,
984    pub indices: Vec<u32>,
985    pub skin_indices: Vec<u32>,
986    #[serde(with = "json_f32::vec_f32")]
987    pub skin_weights: Vec<f32>,
988    #[serde(with = "json_f32::vec_f32")]
989    pub edge_scale: Vec<f32>,
990    pub material_groups: Vec<PmxParsedMaterialGroup>,
991    pub sdef: PmxParsedSdef,
992    pub qdef: PmxParsedQdef,
993}
994
995#[derive(Debug, Clone, Serialize, Deserialize)]
996#[serde(rename_all = "camelCase")]
997pub struct PmxParsedMaterialGroup {
998    pub start: usize,
999    pub count: usize,
1000    pub material_index: usize,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[serde(rename_all = "camelCase")]
1005pub struct PmxParsedMaterial {
1006    pub name: String,
1007    pub english_name: String,
1008    pub texture_path: String,
1009    pub sphere_texture_path: String,
1010    pub sphere_mode: String,
1011    pub toon_texture_path: String,
1012    pub shared_toon_index: Option<u8>,
1013    #[serde(with = "json_f32::array4")]
1014    pub diffuse: [f32; 4],
1015    #[serde(with = "json_f32::array3")]
1016    pub specular: [f32; 3],
1017    #[serde(with = "json_f32")]
1018    pub specular_power: f32,
1019    #[serde(with = "json_f32::array3")]
1020    pub ambient: [f32; 3],
1021    #[serde(with = "json_f32::array4")]
1022    pub edge_color: [f32; 4],
1023    #[serde(with = "json_f32")]
1024    pub edge_size: f32,
1025    pub flags: PmxParsedMaterialFlags,
1026    pub face_count: i32,
1027}
1028
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1030#[serde(rename_all = "camelCase")]
1031pub struct PmxParsedMaterialFlags {
1032    pub double_sided: bool,
1033    pub ground_shadow: bool,
1034    pub self_shadow_map: bool,
1035    pub self_shadow: bool,
1036    pub edge: bool,
1037    pub vertex_color: bool,
1038    pub point_draw: bool,
1039    pub line_draw: bool,
1040}
1041
1042#[derive(Debug, Clone, Serialize, Deserialize)]
1043pub struct PmxParsedSkeleton {
1044    pub bones: Vec<PmxParsedBone>,
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct PmxParsedBone {
1050    pub name: String,
1051    pub english_name: String,
1052    pub parent_index: i32,
1053    pub layer: i32,
1054    #[serde(with = "json_f32::array3")]
1055    pub position: [f32; 3],
1056    pub tail_index: i32,
1057    #[serde(with = "json_f32::option_array3")]
1058    pub tail_position: Option<[f32; 3]>,
1059    pub flags: PmxParsedBoneFlags,
1060    pub append_transform: Option<PmxParsedAppendTransform>,
1061    #[serde(with = "json_f32::option_array3")]
1062    pub fixed_axis: Option<[f32; 3]>,
1063    pub local_axis: Option<PmxParsedLocalAxis>,
1064    pub external_parent_key: Option<i32>,
1065    pub ik: Option<PmxParsedIk>,
1066}
1067
1068#[derive(Debug, Clone, Serialize, Deserialize)]
1069#[serde(rename_all = "camelCase")]
1070pub struct PmxParsedBoneFlags {
1071    pub indexed_tail: bool,
1072    pub rotatable: bool,
1073    pub translatable: bool,
1074    pub visible: bool,
1075    pub enabled: bool,
1076    pub ik: bool,
1077    pub append_local: bool,
1078    pub append_rotate: bool,
1079    pub append_translate: bool,
1080    pub fixed_axis: bool,
1081    pub local_axis: bool,
1082    pub transform_after_physics: bool,
1083    pub external_parent_transform: bool,
1084}
1085
1086#[derive(Debug, Clone, Serialize, Deserialize)]
1087#[serde(rename_all = "camelCase")]
1088pub struct PmxParsedAppendTransform {
1089    pub parent_index: i32,
1090    #[serde(with = "json_f32")]
1091    pub weight: f32,
1092}
1093
1094#[derive(Debug, Clone, Serialize, Deserialize)]
1095pub struct PmxParsedLocalAxis {
1096    #[serde(with = "json_f32::array3")]
1097    pub x: [f32; 3],
1098    #[serde(with = "json_f32::array3")]
1099    pub z: [f32; 3],
1100}
1101
1102#[derive(Debug, Clone, Serialize, Deserialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct PmxParsedIk {
1105    pub target_index: i32,
1106    pub loop_count: i32,
1107    #[serde(with = "json_f32")]
1108    pub limit_angle: f32,
1109    pub links: Vec<PmxParsedIkLink>,
1110}
1111
1112#[derive(Debug, Clone, Serialize, Deserialize)]
1113#[serde(rename_all = "camelCase")]
1114pub struct PmxParsedIkLink {
1115    pub bone_index: i32,
1116    pub limits: Option<PmxParsedIkLimit>,
1117}
1118
1119#[derive(Debug, Clone, Serialize, Deserialize)]
1120pub struct PmxParsedIkLimit {
1121    #[serde(with = "json_f32::array3")]
1122    pub lower: [f32; 3],
1123    #[serde(with = "json_f32::array3")]
1124    pub upper: [f32; 3],
1125}
1126
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1128#[serde(rename_all = "camelCase")]
1129pub struct PmxRigSpec {
1130    pub bone_count: usize,
1131    pub ik_chain_count: usize,
1132    pub grant_count: usize,
1133    pub bones: Vec<PmxRigSpecBone>,
1134    pub ik_chains: Vec<PmxRigSpecIkChain>,
1135    pub grants: Vec<PmxRigSpecGrant>,
1136}
1137
1138#[derive(Debug, Clone, Serialize, Deserialize)]
1139#[serde(rename_all = "camelCase")]
1140pub struct PmxRigSpecBone {
1141    pub name: String,
1142    pub name_bytes: String,
1143    pub parent_index: i32,
1144    #[serde(with = "json_f32::array3")]
1145    pub rest_position: [f32; 3],
1146    pub deform_layer: i32,
1147    #[serde(with = "json_f32::option_array3")]
1148    pub fixed_axis: Option<[f32; 3]>,
1149    pub local_axis: Option<PmxRigSpecLocalAxis>,
1150    pub transform_after_physics: bool,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize)]
1154pub struct PmxRigSpecLocalAxis {
1155    #[serde(with = "json_f32::array3")]
1156    pub x: [f32; 3],
1157    #[serde(with = "json_f32::array3")]
1158    pub z: [f32; 3],
1159}
1160
1161#[derive(Debug, Clone, Serialize, Deserialize)]
1162#[serde(rename_all = "camelCase")]
1163pub struct PmxRigSpecIkChain {
1164    pub controller_bone_index: i32,
1165    pub target_bone_index: i32,
1166    pub iteration_count: u32,
1167    #[serde(with = "json_f32")]
1168    pub limit_angle: f32,
1169    pub links: Vec<PmxRigSpecIkLink>,
1170}
1171
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1173#[serde(rename_all = "camelCase")]
1174pub struct PmxRigSpecIkLink {
1175    pub bone_index: i32,
1176    pub has_angle_limit: bool,
1177    #[serde(with = "json_f32::array3")]
1178    pub angle_limit_min: [f32; 3],
1179    #[serde(with = "json_f32::array3")]
1180    pub angle_limit_max: [f32; 3],
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1184#[serde(rename_all = "camelCase")]
1185pub struct PmxRigSpecGrant {
1186    pub target_bone_index: i32,
1187    pub source_bone_index: i32,
1188    #[serde(with = "json_f32")]
1189    pub ratio: f32,
1190    pub affect_rotation: bool,
1191    pub affect_translation: bool,
1192    pub local: bool,
1193}
1194
1195pub fn parse_pmx_rig_spec(data: &[u8]) -> Result<PmxRigSpec, ImportError> {
1196    let model = parse_pmx_model(data)?;
1197    Ok(pmx_model_to_rig_spec(&model))
1198}
1199
1200pub fn pmx_model_to_rig_spec(model: &PmxParsedModel) -> PmxRigSpec {
1201    let mut bones = Vec::with_capacity(model.skeleton.bones.len());
1202    let mut ik_chains = Vec::new();
1203    let mut grants = Vec::new();
1204
1205    for (index, bone) in model.skeleton.bones.iter().enumerate() {
1206        bones.push(PmxRigSpecBone {
1207            name: bone.name.clone(),
1208            name_bytes: shift_jis_hex(&bone.name),
1209            parent_index: bone.parent_index,
1210            rest_position: bone.position,
1211            deform_layer: bone.layer,
1212            fixed_axis: bone.fixed_axis,
1213            local_axis: bone.local_axis.as_ref().map(|axis| PmxRigSpecLocalAxis {
1214                x: axis.x,
1215                z: axis.z,
1216            }),
1217            transform_after_physics: bone.flags.transform_after_physics,
1218        });
1219
1220        if let Some(ik) = &bone.ik {
1221            ik_chains.push(PmxRigSpecIkChain {
1222                controller_bone_index: index as i32,
1223                target_bone_index: ik.target_index,
1224                iteration_count: ik.loop_count.max(0) as u32,
1225                limit_angle: ik.limit_angle,
1226                links: ik
1227                    .links
1228                    .iter()
1229                    .map(|link| {
1230                        let (angle_limit_min, angle_limit_max) = link
1231                            .limits
1232                            .as_ref()
1233                            .map_or(([0.0; 3], [0.0; 3]), |limit| (limit.lower, limit.upper));
1234                        PmxRigSpecIkLink {
1235                            bone_index: link.bone_index,
1236                            has_angle_limit: link.limits.is_some(),
1237                            angle_limit_min,
1238                            angle_limit_max,
1239                        }
1240                    })
1241                    .collect(),
1242            });
1243        }
1244
1245        if let Some(append) = &bone.append_transform {
1246            grants.push(PmxRigSpecGrant {
1247                target_bone_index: index as i32,
1248                source_bone_index: append.parent_index,
1249                ratio: append.weight,
1250                affect_rotation: bone.flags.append_rotate,
1251                affect_translation: bone.flags.append_translate,
1252                local: bone.flags.append_local,
1253            });
1254        }
1255    }
1256
1257    PmxRigSpec {
1258        bone_count: bones.len(),
1259        ik_chain_count: ik_chains.len(),
1260        grant_count: grants.len(),
1261        bones,
1262        ik_chains,
1263        grants,
1264    }
1265}
1266
1267fn shift_jis_hex(text: &str) -> String {
1268    bytes_to_hex(&encode_sjis(text))
1269}
1270
1271fn bytes_to_hex(bytes: &[u8]) -> String {
1272    const HEX: &[u8; 16] = b"0123456789abcdef";
1273    let mut out = String::with_capacity(bytes.len() * 2);
1274    for &byte in bytes {
1275        out.push(HEX[(byte >> 4) as usize] as char);
1276        out.push(HEX[(byte & 0x0f) as usize] as char);
1277    }
1278    out
1279}
1280
1281#[derive(Debug, Clone, Serialize, Deserialize)]
1282#[serde(rename_all = "camelCase")]
1283pub struct PmxParsedMorph {
1284    pub name: String,
1285    pub english_name: String,
1286    #[serde(default = "default_pmx_morph_panel")]
1287    pub panel: String,
1288    #[serde(rename = "type")]
1289    pub kind: String,
1290    pub vertex_offsets: Vec<PmxParsedVertexMorphOffset>,
1291    pub group_offsets: Vec<PmxParsedGroupMorphOffset>,
1292    pub bone_offsets: Vec<PmxParsedBoneMorphOffset>,
1293    pub uv_offsets: Vec<PmxParsedUvMorphOffset>,
1294    pub additional_uv_offsets: Vec<PmxParsedAdditionalUvMorphOffset>,
1295    pub material_offsets: Vec<PmxParsedMaterialMorphOffset>,
1296    pub flip_offsets: Vec<PmxParsedGroupMorphOffset>,
1297    pub impulse_offsets: Vec<PmxParsedImpulseMorphOffset>,
1298}
1299
1300#[derive(Debug, Clone, Serialize, Deserialize)]
1301#[serde(rename_all = "camelCase")]
1302pub struct PmxParsedVertexMorphOffset {
1303    pub vertex_index: u32,
1304    #[serde(with = "json_f32::array3")]
1305    pub position: [f32; 3],
1306}
1307
1308#[derive(Debug, Clone, Serialize, Deserialize)]
1309#[serde(rename_all = "camelCase")]
1310pub struct PmxParsedGroupMorphOffset {
1311    pub morph_index: i32,
1312    #[serde(with = "json_f32")]
1313    pub weight: f32,
1314}
1315
1316#[derive(Debug, Clone, Serialize, Deserialize)]
1317#[serde(rename_all = "camelCase")]
1318pub struct PmxParsedBoneMorphOffset {
1319    pub bone_index: i32,
1320    #[serde(with = "json_f32::array3")]
1321    pub translation: [f32; 3],
1322    #[serde(with = "json_f32::array4")]
1323    pub rotation: [f32; 4],
1324}
1325
1326#[derive(Debug, Clone, Serialize, Deserialize)]
1327#[serde(rename_all = "camelCase")]
1328pub struct PmxParsedUvMorphOffset {
1329    pub vertex_index: u32,
1330    #[serde(with = "json_f32::array4")]
1331    pub uv: [f32; 4],
1332}
1333
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335#[serde(rename_all = "camelCase")]
1336pub struct PmxParsedAdditionalUvMorphOffset {
1337    pub vertex_index: u32,
1338    pub uv_index: u8,
1339    #[serde(with = "json_f32::array4")]
1340    pub uv: [f32; 4],
1341}
1342
1343#[derive(Debug, Clone, Serialize, Deserialize)]
1344#[serde(rename_all = "camelCase")]
1345pub struct PmxParsedMaterialMorphOffset {
1346    pub material_index: i32,
1347    pub operation: String,
1348    #[serde(with = "json_f32::array4")]
1349    pub diffuse: [f32; 4],
1350    #[serde(with = "json_f32::array3")]
1351    pub specular: [f32; 3],
1352    #[serde(with = "json_f32")]
1353    pub specular_power: f32,
1354    #[serde(with = "json_f32::array3")]
1355    pub ambient: [f32; 3],
1356    #[serde(with = "json_f32::array4")]
1357    pub edge_color: [f32; 4],
1358    #[serde(with = "json_f32")]
1359    pub edge_size: f32,
1360    #[serde(with = "json_f32::array4")]
1361    pub texture_factor: [f32; 4],
1362    #[serde(with = "json_f32::array4")]
1363    pub sphere_texture_factor: [f32; 4],
1364    #[serde(with = "json_f32::array4")]
1365    pub toon_texture_factor: [f32; 4],
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize)]
1369#[serde(rename_all = "camelCase")]
1370pub struct PmxParsedImpulseMorphOffset {
1371    pub rigid_body_index: i32,
1372    pub local: bool,
1373    #[serde(with = "json_f32::array3")]
1374    pub velocity: [f32; 3],
1375    #[serde(with = "json_f32::array3")]
1376    pub torque: [f32; 3],
1377}
1378
1379#[derive(Debug, Clone, Serialize, Deserialize)]
1380#[serde(rename_all = "camelCase")]
1381pub struct PmxMaterialSplitResult {
1382    pub meshes: Vec<PmxMaterialSplitMesh>,
1383    pub manifest: PmxMaterialSplitManifest,
1384}
1385
1386#[derive(Debug, Clone, Serialize, Deserialize)]
1387#[serde(rename_all = "camelCase")]
1388pub struct PmxMaterialSplitMesh {
1389    pub original_material_index: usize,
1390    pub original_vertex_indices: Vec<u32>,
1391    pub geometry: PmxParsedGeometry,
1392    pub material: PmxParsedMaterial,
1393    pub morphs: Vec<PmxParsedMorph>,
1394    pub morph_index_map: Vec<PmxMaterialSplitMorphIndexMap>,
1395    pub diagnostics: Vec<PmxParserDiagnostic>,
1396}
1397
1398#[derive(Debug, Clone, Serialize, Deserialize)]
1399#[serde(rename_all = "camelCase")]
1400pub struct PmxMaterialSplit {
1401    pub mesh: PmxMaterialSplitMesh,
1402}
1403
1404#[derive(Debug, Clone, Serialize, Deserialize)]
1405#[serde(rename_all = "camelCase")]
1406pub struct PmxMaterialSplitManifest {
1407    pub mesh_count: usize,
1408    pub meshes: Vec<PmxMaterialSplitManifestMesh>,
1409    pub diagnostics: Vec<PmxParserDiagnostic>,
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1413#[serde(rename_all = "camelCase")]
1414pub struct PmxMaterialSplitManifestMesh {
1415    pub mesh_index: usize,
1416    pub original_material_index: usize,
1417    pub original_vertex_indices: Vec<u32>,
1418    pub morph_index_map: Vec<PmxMaterialSplitMorphIndexMap>,
1419    pub diagnostics: Vec<PmxParserDiagnostic>,
1420}
1421
1422#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1423#[serde(rename_all = "camelCase")]
1424pub struct PmxMaterialSplitMorphIndexMap {
1425    pub original_index: usize,
1426    pub local_index: usize,
1427}
1428
1429#[derive(Debug, Clone, Serialize, Deserialize)]
1430#[serde(rename_all = "camelCase")]
1431pub struct PmxParsedDisplayFrame {
1432    pub name: String,
1433    pub english_name: String,
1434    pub special: bool,
1435    pub frames: Vec<PmxParsedDisplayFrameElement>,
1436}
1437
1438#[derive(Debug, Clone, Serialize, Deserialize)]
1439pub struct PmxParsedDisplayFrameElement {
1440    #[serde(rename = "type")]
1441    pub kind: String,
1442    pub index: i32,
1443}
1444
1445#[derive(Debug, Clone, Serialize, Deserialize)]
1446#[serde(rename_all = "camelCase")]
1447pub struct PmxParsedRigidBody {
1448    pub name: String,
1449    pub english_name: String,
1450    pub bone_index: i32,
1451    pub group: u8,
1452    /// PMX rigid-body collision mask, stored as a collide-with group bitmask.
1453    pub mask: u16,
1454    pub shape: String,
1455    #[serde(with = "json_f32::array3")]
1456    pub size: [f32; 3],
1457    #[serde(with = "json_f32::array3")]
1458    pub position: [f32; 3],
1459    #[serde(with = "json_f32::array3")]
1460    pub rotation: [f32; 3],
1461    #[serde(with = "json_f32")]
1462    pub mass: f32,
1463    #[serde(with = "json_f32")]
1464    pub linear_damping: f32,
1465    #[serde(with = "json_f32")]
1466    pub angular_damping: f32,
1467    #[serde(with = "json_f32")]
1468    pub restitution: f32,
1469    #[serde(with = "json_f32")]
1470    pub friction: f32,
1471    pub mode: String,
1472}
1473
1474#[derive(Debug, Clone, Serialize, Deserialize)]
1475#[serde(rename_all = "camelCase")]
1476pub struct PmxParsedJoint {
1477    pub name: String,
1478    pub english_name: String,
1479    #[serde(rename = "type")]
1480    pub kind: String,
1481    pub rigid_body_index_a: i32,
1482    pub rigid_body_index_b: i32,
1483    #[serde(with = "json_f32::array3")]
1484    pub position: [f32; 3],
1485    #[serde(with = "json_f32::array3")]
1486    pub rotation: [f32; 3],
1487    #[serde(with = "json_f32::array3")]
1488    pub translation_lower_limit: [f32; 3],
1489    #[serde(with = "json_f32::array3")]
1490    pub translation_upper_limit: [f32; 3],
1491    #[serde(with = "json_f32::array3")]
1492    pub rotation_lower_limit: [f32; 3],
1493    #[serde(with = "json_f32::array3")]
1494    pub rotation_upper_limit: [f32; 3],
1495    #[serde(with = "json_f32::array3")]
1496    pub spring_translation_factor: [f32; 3],
1497    #[serde(with = "json_f32::array3")]
1498    pub spring_rotation_factor: [f32; 3],
1499}
1500
1501#[derive(Debug, Clone, Serialize, Deserialize)]
1502#[serde(rename_all = "camelCase")]
1503pub struct PmxParsedSoftBody {
1504    pub name: String,
1505    pub english_name: String,
1506    #[serde(rename = "type")]
1507    pub kind: String,
1508    pub material_index: i32,
1509    pub collision_group: u8,
1510    pub collision_mask: u16,
1511    pub flags: u8,
1512    pub bending_constraints_distance: i32,
1513    pub cluster_count: i32,
1514    #[serde(with = "json_f32")]
1515    pub total_mass: f32,
1516    #[serde(with = "json_f32")]
1517    pub collision_margin: f32,
1518}
1519
1520#[derive(Debug, Clone, Serialize, Deserialize)]
1521pub struct PmxParserDiagnostic {
1522    pub level: String,
1523    pub code: String,
1524    pub message: String,
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize)]
1528#[serde(rename_all = "camelCase")]
1529pub struct PmxPartsDescriptor {
1530    #[serde(default = "default_pmx_parts_version")]
1531    #[serde(with = "json_f32")]
1532    pub version: f32,
1533    #[serde(default = "default_pmx_parts_encoding")]
1534    pub encoding: String,
1535    #[serde(default = "default_pmx_parts_name")]
1536    pub name: String,
1537    #[serde(default)]
1538    pub english_name: String,
1539    #[serde(default)]
1540    pub comment: String,
1541    #[serde(default)]
1542    pub english_comment: String,
1543    #[serde(default)]
1544    pub material_name: String,
1545    #[serde(default)]
1546    pub english_material_name: String,
1547    #[serde(default)]
1548    pub materials: Vec<PmxPartsMaterialDescriptor>,
1549    #[serde(default)]
1550    pub bones: Vec<PmxPartsBoneDescriptor>,
1551    #[serde(default)]
1552    pub morphs: Vec<PmxPartsMorphDescriptor>,
1553    #[serde(default)]
1554    pub display_frames: Vec<PmxPartsDisplayFrameDescriptor>,
1555    #[serde(default)]
1556    pub rigid_bodies: Vec<PmxPartsRigidBodyDescriptor>,
1557    #[serde(default)]
1558    pub joints: Vec<PmxPartsJointDescriptor>,
1559    #[serde(default)]
1560    pub index_sizes: PmxPartsIndexSizes,
1561}
1562
1563#[derive(Debug, Clone, Serialize, Deserialize)]
1564#[serde(rename_all = "camelCase")]
1565pub struct PmxPartsMaterialDescriptor {
1566    #[serde(default = "default_pmx_parts_material_name")]
1567    pub name: String,
1568    #[serde(default)]
1569    pub english_name: String,
1570    #[serde(default)]
1571    pub texture_path: String,
1572    #[serde(default)]
1573    pub sphere_texture_path: String,
1574    #[serde(default)]
1575    pub sphere_mode: String,
1576    #[serde(default)]
1577    pub toon_texture_path: String,
1578    #[serde(default = "default_pmx_parts_shared_toon_index")]
1579    pub shared_toon_index: Option<u8>,
1580    #[serde(default = "default_pmx_parts_diffuse")]
1581    #[serde(with = "json_f32::array4")]
1582    pub diffuse: [f32; 4],
1583    #[serde(default)]
1584    #[serde(with = "json_f32::array3")]
1585    pub specular: [f32; 3],
1586    #[serde(default = "default_pmx_parts_specular_power")]
1587    #[serde(with = "json_f32")]
1588    pub specular_power: f32,
1589    #[serde(default = "default_pmx_parts_ambient")]
1590    #[serde(with = "json_f32::array3")]
1591    pub ambient: [f32; 3],
1592    #[serde(default = "default_pmx_parts_edge_color")]
1593    #[serde(with = "json_f32::array4")]
1594    pub edge_color: [f32; 4],
1595    #[serde(default = "default_pmx_parts_edge_size")]
1596    #[serde(with = "json_f32")]
1597    pub edge_size: f32,
1598    #[serde(default)]
1599    pub flags: PmxPartsMaterialFlags,
1600    #[serde(default)]
1601    pub face_count: i32,
1602}
1603
1604#[derive(Debug, Clone, Serialize, Deserialize)]
1605#[serde(rename_all = "camelCase")]
1606pub struct PmxPartsMaterialFlags {
1607    #[serde(default = "default_true")]
1608    pub double_sided: bool,
1609    #[serde(default = "default_true")]
1610    pub ground_shadow: bool,
1611    #[serde(default = "default_true")]
1612    pub self_shadow_map: bool,
1613    #[serde(default = "default_true")]
1614    pub self_shadow: bool,
1615    #[serde(default)]
1616    pub edge: bool,
1617    #[serde(default)]
1618    pub vertex_color: bool,
1619    #[serde(default)]
1620    pub point_draw: bool,
1621    #[serde(default)]
1622    pub line_draw: bool,
1623}
1624
1625impl Default for PmxPartsMaterialFlags {
1626    fn default() -> Self {
1627        Self {
1628            double_sided: true,
1629            ground_shadow: true,
1630            self_shadow_map: true,
1631            self_shadow: true,
1632            edge: false,
1633            vertex_color: false,
1634            point_draw: false,
1635            line_draw: false,
1636        }
1637    }
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize)]
1641#[serde(rename_all = "camelCase")]
1642pub struct PmxPartsBoneDescriptor {
1643    #[serde(default = "default_pmx_parts_root_bone_name")]
1644    pub name: String,
1645    #[serde(default)]
1646    pub english_name: String,
1647    #[serde(default = "default_negative_index")]
1648    pub parent_index: i32,
1649    #[serde(default)]
1650    pub layer: i32,
1651    #[serde(default)]
1652    #[serde(with = "json_f32::array3")]
1653    pub position: [f32; 3],
1654    #[serde(default = "default_negative_index")]
1655    pub tail_index: i32,
1656    #[serde(default)]
1657    #[serde(with = "json_f32::option_array3")]
1658    pub tail_position: Option<[f32; 3]>,
1659    #[serde(default)]
1660    pub rotatable: bool,
1661    #[serde(default)]
1662    pub translatable: bool,
1663    #[serde(default = "default_true")]
1664    pub visible: bool,
1665    #[serde(default = "default_true")]
1666    pub enabled: bool,
1667}
1668
1669#[derive(Debug, Clone, Serialize, Deserialize)]
1670#[serde(rename_all = "camelCase")]
1671pub struct PmxPartsMorphDescriptor {
1672    #[serde(default)]
1673    pub name: String,
1674    #[serde(default)]
1675    pub english_name: String,
1676    #[serde(default = "default_pmx_morph_panel")]
1677    pub panel: String,
1678    #[serde(default = "default_pmx_parts_morph_kind", alias = "type")]
1679    pub kind: String,
1680    #[serde(default)]
1681    pub vertex_offsets: Vec<PmxPartsVertexMorphOffset>,
1682    #[serde(default)]
1683    pub group_offsets: Vec<PmxPartsGroupMorphOffset>,
1684}
1685
1686#[derive(Debug, Clone, Serialize, Deserialize)]
1687#[serde(rename_all = "camelCase")]
1688pub struct PmxPartsVertexMorphOffset {
1689    pub vertex_index: u32,
1690    #[serde(with = "json_f32::array3")]
1691    pub position: [f32; 3],
1692}
1693
1694#[derive(Debug, Clone, Serialize, Deserialize)]
1695#[serde(rename_all = "camelCase")]
1696pub struct PmxPartsGroupMorphOffset {
1697    pub morph_index: i32,
1698    #[serde(with = "json_f32")]
1699    pub weight: f32,
1700}
1701
1702#[derive(Debug, Clone, Serialize, Deserialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct PmxPartsDisplayFrameDescriptor {
1705    #[serde(default)]
1706    pub name: String,
1707    #[serde(default)]
1708    pub english_name: String,
1709    #[serde(default)]
1710    pub special: bool,
1711    #[serde(default)]
1712    pub frames: Vec<PmxPartsDisplayFrameItem>,
1713}
1714
1715#[derive(Debug, Clone, Serialize, Deserialize)]
1716#[serde(rename_all = "camelCase")]
1717pub struct PmxPartsDisplayFrameItem {
1718    #[serde(default = "default_pmx_parts_display_frame_kind")]
1719    pub kind: String,
1720    #[serde(default)]
1721    pub index: i32,
1722}
1723
1724#[derive(Debug, Clone, Serialize, Deserialize)]
1725#[serde(rename_all = "camelCase")]
1726pub struct PmxPartsRigidBodyDescriptor {
1727    #[serde(default)]
1728    pub name: String,
1729    #[serde(default)]
1730    pub english_name: String,
1731    #[serde(default = "default_negative_index")]
1732    pub bone_index: i32,
1733    #[serde(default)]
1734    pub group: u8,
1735    /// Collide-with group bitmask. Omitted masks collide with every group.
1736    #[serde(default = "default_collision_mask_all")]
1737    pub mask: u16,
1738    #[serde(default = "default_pmx_parts_rigid_body_shape")]
1739    pub shape: String,
1740    #[serde(default = "default_unit_vec3")]
1741    #[serde(with = "json_f32::array3")]
1742    pub size: [f32; 3],
1743    #[serde(default)]
1744    #[serde(with = "json_f32::array3")]
1745    pub position: [f32; 3],
1746    #[serde(default)]
1747    #[serde(with = "json_f32::array3")]
1748    pub rotation: [f32; 3],
1749    #[serde(default = "default_pmx_parts_mass")]
1750    #[serde(with = "json_f32")]
1751    pub mass: f32,
1752    #[serde(default)]
1753    #[serde(with = "json_f32")]
1754    pub linear_damping: f32,
1755    #[serde(default)]
1756    #[serde(with = "json_f32")]
1757    pub angular_damping: f32,
1758    #[serde(default)]
1759    #[serde(with = "json_f32")]
1760    pub restitution: f32,
1761    #[serde(default)]
1762    #[serde(with = "json_f32")]
1763    pub friction: f32,
1764    #[serde(default = "default_pmx_parts_rigid_body_mode")]
1765    pub mode: String,
1766}
1767
1768#[derive(Debug, Clone, Serialize, Deserialize)]
1769#[serde(rename_all = "camelCase")]
1770pub struct PmxPartsJointDescriptor {
1771    #[serde(default)]
1772    pub name: String,
1773    #[serde(default)]
1774    pub english_name: String,
1775    #[serde(
1776        default = "default_pmx_parts_joint_kind",
1777        rename = "type",
1778        alias = "kind"
1779    )]
1780    pub kind: String,
1781    #[serde(default = "default_negative_index")]
1782    pub rigid_body_index_a: i32,
1783    #[serde(default = "default_negative_index")]
1784    pub rigid_body_index_b: i32,
1785    #[serde(default)]
1786    #[serde(with = "json_f32::array3")]
1787    pub position: [f32; 3],
1788    #[serde(default)]
1789    #[serde(with = "json_f32::array3")]
1790    pub rotation: [f32; 3],
1791    #[serde(default)]
1792    #[serde(with = "json_f32::array3")]
1793    pub translation_lower_limit: [f32; 3],
1794    #[serde(default)]
1795    #[serde(with = "json_f32::array3")]
1796    pub translation_upper_limit: [f32; 3],
1797    #[serde(default)]
1798    #[serde(with = "json_f32::array3")]
1799    pub rotation_lower_limit: [f32; 3],
1800    #[serde(default)]
1801    #[serde(with = "json_f32::array3")]
1802    pub rotation_upper_limit: [f32; 3],
1803    #[serde(default)]
1804    #[serde(with = "json_f32::array3")]
1805    pub spring_translation_factor: [f32; 3],
1806    #[serde(default)]
1807    #[serde(with = "json_f32::array3")]
1808    pub spring_rotation_factor: [f32; 3],
1809}
1810
1811#[derive(Debug, Clone, Serialize, Deserialize)]
1812#[serde(rename_all = "camelCase")]
1813pub struct PmxPartsIndexSizes {
1814    #[serde(default = "default_pmx_vertex_index_size")]
1815    pub vertex: u8,
1816    #[serde(default = "default_pmx_small_index_size")]
1817    pub texture: u8,
1818    #[serde(default = "default_pmx_small_index_size")]
1819    pub material: u8,
1820    #[serde(default = "default_pmx_small_index_size")]
1821    pub bone: u8,
1822    #[serde(default = "default_pmx_small_index_size")]
1823    pub morph: u8,
1824    #[serde(default = "default_pmx_small_index_size")]
1825    pub rigid_body: u8,
1826}
1827
1828impl Default for PmxPartsIndexSizes {
1829    fn default() -> Self {
1830        Self {
1831            vertex: default_pmx_vertex_index_size(),
1832            texture: default_pmx_small_index_size(),
1833            material: default_pmx_small_index_size(),
1834            bone: default_pmx_small_index_size(),
1835            morph: default_pmx_small_index_size(),
1836            rigid_body: default_pmx_small_index_size(),
1837        }
1838    }
1839}
1840
1841pub struct PmxPartsInput<'a> {
1842    pub descriptor: PmxPartsDescriptor,
1843    pub positions_xyz: &'a [f32],
1844    pub normals_xyz: &'a [f32],
1845    pub uvs_xy: &'a [f32],
1846    pub indices: &'a [u32],
1847    pub skin_indices: &'a [u32],
1848    pub skin_weights: &'a [f32],
1849    pub edge_scale: &'a [f32],
1850}
1851
1852fn default_pmx_format() -> String {
1853    "pmx".to_owned()
1854}
1855
1856fn default_pmx_parts_version() -> f32 {
1857    2.0
1858}
1859
1860fn default_pmx_parts_encoding() -> String {
1861    "utf-8".to_owned()
1862}
1863
1864fn default_pmx_parts_name() -> String {
1865    "model".to_owned()
1866}
1867
1868fn default_pmx_parts_root_bone_name() -> String {
1869    "root".to_owned()
1870}
1871
1872fn default_negative_index() -> i32 {
1873    -1
1874}
1875
1876fn default_pmx_parts_display_frame_kind() -> String {
1877    "bone".to_owned()
1878}
1879
1880fn default_pmx_parts_morph_kind() -> String {
1881    "vertex".to_owned()
1882}
1883
1884fn default_pmx_morph_panel() -> String {
1885    "other".to_owned()
1886}
1887
1888fn default_pmx_parts_rigid_body_shape() -> String {
1889    "sphere".to_owned()
1890}
1891
1892fn default_pmx_parts_rigid_body_mode() -> String {
1893    "static".to_owned()
1894}
1895
1896fn default_pmx_parts_joint_kind() -> String {
1897    "generic6dofSpring".to_owned()
1898}
1899
1900fn default_collision_mask_all() -> u16 {
1901    0xffff
1902}
1903
1904fn default_unit_vec3() -> [f32; 3] {
1905    [1.0, 1.0, 1.0]
1906}
1907
1908fn default_pmx_parts_mass() -> f32 {
1909    1.0
1910}
1911
1912fn default_pmx_parts_material_name() -> String {
1913    "material".to_owned()
1914}
1915
1916fn default_pmx_parts_shared_toon_index() -> Option<u8> {
1917    Some(0)
1918}
1919
1920fn default_pmx_parts_diffuse() -> [f32; 4] {
1921    [0.8, 0.8, 0.8, 1.0]
1922}
1923
1924fn default_pmx_parts_specular_power() -> f32 {
1925    1.0
1926}
1927
1928fn default_pmx_parts_ambient() -> [f32; 3] {
1929    [0.2, 0.2, 0.2]
1930}
1931
1932fn default_pmx_parts_edge_color() -> [f32; 4] {
1933    [0.0, 0.0, 0.0, 1.0]
1934}
1935
1936fn default_pmx_parts_edge_size() -> f32 {
1937    1.0
1938}
1939
1940fn default_true() -> bool {
1941    true
1942}
1943
1944fn default_pmx_vertex_index_size() -> u8 {
1945    4
1946}
1947
1948fn default_pmx_small_index_size() -> u8 {
1949    1
1950}
1951
1952pub fn build_pmx_model_from_parts(input: PmxPartsInput<'_>) -> Result<PmxParsedModel, String> {
1953    let vertex_count = validate_pmx_parts_geometry(
1954        input.positions_xyz,
1955        input.normals_xyz,
1956        input.uvs_xy,
1957        input.indices,
1958        input.skin_indices,
1959        input.skin_weights,
1960        input.edge_scale,
1961    )?;
1962    validate_pmx_parts_descriptor(
1963        &input.descriptor,
1964        vertex_count,
1965        input.indices,
1966        input.skin_indices,
1967    )?;
1968
1969    let skin_indices = if input.skin_indices.is_empty() {
1970        vec![0; vertex_count * 4]
1971    } else {
1972        input.skin_indices.to_vec()
1973    };
1974    let skin_weights = if input.skin_weights.is_empty() {
1975        default_pmx_skin_weights(vertex_count)
1976    } else {
1977        input.skin_weights.to_vec()
1978    };
1979    let edge_scale = if input.edge_scale.is_empty() {
1980        vec![1.0; vertex_count]
1981    } else {
1982        input.edge_scale.to_vec()
1983    };
1984    let skinning_modes = vec![PMX_SKINNING_MODE_BDEF4.to_owned(); vertex_count];
1985    let face_count = input.indices.len() / 3;
1986    let (materials, material_groups) = build_pmx_parts_materials(&input.descriptor, face_count);
1987    let bones = build_pmx_parts_bones(&input.descriptor);
1988    let morphs = build_pmx_parts_morphs(&input.descriptor);
1989    let display_frames = build_pmx_parts_display_frames(&input.descriptor);
1990    let rigid_bodies = build_pmx_parts_rigid_bodies(&input.descriptor);
1991    let joints = build_pmx_parts_joints(&input.descriptor);
1992
1993    let model = PmxParsedModel {
1994        metadata: PmxParsedMetadata {
1995            format: "pmx".to_owned(),
1996            version: input.descriptor.version,
1997            encoding: input.descriptor.encoding,
1998            name: input.descriptor.name,
1999            english_name: input.descriptor.english_name,
2000            comment: input.descriptor.comment,
2001            english_comment: input.descriptor.english_comment,
2002            counts: PmxParsedCounts {
2003                vertices: vertex_count,
2004                faces: face_count,
2005                materials: materials.len(),
2006                bones: bones.len(),
2007                morphs: morphs.len(),
2008                display_frames: display_frames.len(),
2009                rigid_bodies: rigid_bodies.len(),
2010                joints: joints.len(),
2011                soft_bodies: 0,
2012            },
2013            index_sizes: PmxParsedIndexSizes {
2014                vertex: input.descriptor.index_sizes.vertex,
2015                texture: input.descriptor.index_sizes.texture,
2016                material: input.descriptor.index_sizes.material,
2017                bone: input.descriptor.index_sizes.bone,
2018                morph: input.descriptor.index_sizes.morph,
2019                rigid_body: input.descriptor.index_sizes.rigid_body,
2020            },
2021            additional_uv_count: 0,
2022        },
2023        geometry: PmxParsedGeometry {
2024            positions: input.positions_xyz.to_vec(),
2025            normals: input.normals_xyz.to_vec(),
2026            uvs: input.uvs_xy.to_vec(),
2027            additional_uvs: Vec::new(),
2028            indices: input.indices.to_vec(),
2029            skin_indices,
2030            skin_weights,
2031            edge_scale,
2032            material_groups,
2033            sdef: PmxParsedSdef {
2034                skinning_modes,
2035                ..PmxParsedSdef::default()
2036            },
2037            qdef: PmxParsedQdef::default(),
2038        },
2039        materials,
2040        skeleton: PmxParsedSkeleton { bones },
2041        morphs,
2042        display_frames,
2043        rigid_bodies,
2044        joints,
2045        soft_bodies: Vec::new(),
2046        diagnostics: Vec::new(),
2047    };
2048    validate_pmx_export_model(&model)?;
2049    Ok(model)
2050}
2051
2052pub fn validate_pmx_export_model(model: &PmxParsedModel) -> Result<(), String> {
2053    let geometry = &model.geometry;
2054    if !geometry.positions.len().is_multiple_of(3) {
2055        return Err(format!(
2056            "PMX export positions length must be divisible by 3: {}",
2057            geometry.positions.len()
2058        ));
2059    }
2060    let vertex_count = geometry.positions.len() / 3;
2061    if geometry.normals.len() != vertex_count * 3 {
2062        return Err(format!(
2063            "PMX export normals length mismatch: expected {}, got {}",
2064            vertex_count * 3,
2065            geometry.normals.len()
2066        ));
2067    }
2068    if geometry.uvs.len() != vertex_count * 2 {
2069        return Err(format!(
2070            "PMX export uvs length mismatch: expected {}, got {}",
2071            vertex_count * 2,
2072            geometry.uvs.len()
2073        ));
2074    }
2075    if geometry.additional_uvs.len() != model.metadata.additional_uv_count as usize {
2076        return Err(format!(
2077            "PMX export additionalUvs set count mismatch: expected {}, got {}",
2078            model.metadata.additional_uv_count,
2079            geometry.additional_uvs.len()
2080        ));
2081    }
2082    for (index, values) in geometry.additional_uvs.iter().enumerate() {
2083        if values.len() != vertex_count * 4 {
2084            return Err(format!(
2085                "PMX export additionalUvs[{}] length mismatch: expected {}, got {}",
2086                index,
2087                vertex_count * 4,
2088                values.len()
2089            ));
2090        }
2091    }
2092    if !geometry.indices.len().is_multiple_of(3) {
2093        return Err(format!(
2094            "PMX export indices length must be divisible by 3: {}",
2095            geometry.indices.len()
2096        ));
2097    }
2098    let expected_skin_len = vertex_count * 4;
2099    if geometry.skin_indices.is_empty() != geometry.skin_weights.is_empty() {
2100        return Err(
2101            "PMX export skinIndices and skinWeights must be both empty or both provided".to_owned(),
2102        );
2103    }
2104    if !geometry.skin_indices.is_empty() && geometry.skin_indices.len() != expected_skin_len {
2105        return Err(format!(
2106            "PMX export skinIndices length mismatch: expected {}, got {}",
2107            expected_skin_len,
2108            geometry.skin_indices.len()
2109        ));
2110    }
2111    if !geometry.skin_weights.is_empty() && geometry.skin_weights.len() != expected_skin_len {
2112        return Err(format!(
2113            "PMX export skinWeights length mismatch: expected {}, got {}",
2114            expected_skin_len,
2115            geometry.skin_weights.len()
2116        ));
2117    }
2118    if !geometry.sdef.skinning_modes.is_empty()
2119        && geometry.sdef.skinning_modes.len() != vertex_count
2120    {
2121        return Err(format!(
2122            "PMX export skinningModes length mismatch: expected {}, got {}",
2123            vertex_count,
2124            geometry.sdef.skinning_modes.len()
2125        ));
2126    }
2127    for (index, mode) in geometry.sdef.skinning_modes.iter().enumerate() {
2128        if !matches!(
2129            mode.as_str(),
2130            PMX_SKINNING_MODE_BDEF1
2131                | PMX_SKINNING_MODE_BDEF2
2132                | PMX_SKINNING_MODE_BDEF4
2133                | PMX_SKINNING_MODE_SDEF
2134                | PMX_SKINNING_MODE_QDEF
2135        ) {
2136            return Err(format!(
2137                "PMX export skinningModes[{index}] must be bdef1, bdef2, bdef4, sdef, or qdef, got {mode}"
2138            ));
2139        }
2140    }
2141    Ok(())
2142}
2143
2144fn validate_pmx_parts_geometry(
2145    positions_xyz: &[f32],
2146    normals_xyz: &[f32],
2147    uvs_xy: &[f32],
2148    indices: &[u32],
2149    skin_indices: &[u32],
2150    skin_weights: &[f32],
2151    edge_scale: &[f32],
2152) -> Result<usize, String> {
2153    if !positions_xyz.len().is_multiple_of(3) {
2154        return Err(format!(
2155            "positions_xyz must contain vertex_count * 3 values, got {}",
2156            positions_xyz.len()
2157        ));
2158    }
2159    let vertex_count = positions_xyz.len() / 3;
2160    if vertex_count == 0 {
2161        return Err("PMX parts export requires at least one vertex".to_owned());
2162    }
2163    if normals_xyz.len() != vertex_count * 3 {
2164        return Err(format!(
2165            "normals_xyz must contain vertex_count * 3 values, expected {}, got {}",
2166            vertex_count * 3,
2167            normals_xyz.len()
2168        ));
2169    }
2170    if uvs_xy.len() != vertex_count * 2 {
2171        return Err(format!(
2172            "uvs_xy must contain vertex_count * 2 values, expected {}, got {}",
2173            vertex_count * 2,
2174            uvs_xy.len()
2175        ));
2176    }
2177    if !indices.len().is_multiple_of(3) {
2178        return Err(format!(
2179            "indices must contain triangle indices and be divisible by 3, got {}",
2180            indices.len()
2181        ));
2182    }
2183    if let Some(&index) = indices
2184        .iter()
2185        .find(|&&index| index as usize >= vertex_count)
2186    {
2187        return Err(format!(
2188            "indices contains out-of-range vertex index {index} for vertex_count {vertex_count}"
2189        ));
2190    }
2191    if !skin_indices.is_empty() && skin_indices.len() != vertex_count * 4 {
2192        return Err(format!(
2193            "skin_indices must contain vertex_count * 4 values when provided, expected {}, got {}",
2194            vertex_count * 4,
2195            skin_indices.len()
2196        ));
2197    }
2198    if !skin_weights.is_empty() && skin_weights.len() != vertex_count * 4 {
2199        return Err(format!(
2200            "skin_weights must contain vertex_count * 4 values when provided, expected {}, got {}",
2201            vertex_count * 4,
2202            skin_weights.len()
2203        ));
2204    }
2205    if skin_indices.is_empty() != skin_weights.is_empty() {
2206        return Err(
2207            "skin_indices and skin_weights must either both be provided or both be empty"
2208                .to_owned(),
2209        );
2210    }
2211    if !edge_scale.is_empty() && edge_scale.len() != vertex_count {
2212        return Err(format!(
2213            "edge_scale must contain vertex_count values when provided, expected {}, got {}",
2214            vertex_count,
2215            edge_scale.len()
2216        ));
2217    }
2218    Ok(vertex_count)
2219}
2220
2221fn validate_pmx_parts_descriptor(
2222    descriptor: &PmxPartsDescriptor,
2223    vertex_count: usize,
2224    indices: &[u32],
2225    skin_indices: &[u32],
2226) -> Result<(), String> {
2227    if descriptor.version < 2.0 {
2228        return Err(format!(
2229            "PMX parts export supports PMX 2.0 or newer, got {}",
2230            descriptor.version
2231        ));
2232    }
2233    if descriptor.encoding != "utf-8" && descriptor.encoding != "utf-16-le" {
2234        return Err(format!(
2235            "PMX parts export encoding must be utf-8 or utf-16-le, got {}",
2236            descriptor.encoding
2237        ));
2238    }
2239    validate_pmx_index_size("vertex", descriptor.index_sizes.vertex)?;
2240    validate_pmx_index_size("texture", descriptor.index_sizes.texture)?;
2241    validate_pmx_index_size("material", descriptor.index_sizes.material)?;
2242    validate_pmx_index_size("bone", descriptor.index_sizes.bone)?;
2243    validate_pmx_index_size("morph", descriptor.index_sizes.morph)?;
2244    validate_pmx_index_size("rigidBody", descriptor.index_sizes.rigid_body)?;
2245    if indices.len() / 3 > i32::MAX as usize {
2246        return Err(format!(
2247            "face count {} exceeds PMX i32 material face count limit",
2248            indices.len() / 3
2249        ));
2250    }
2251    if !pmx_vertex_index_size_can_hold(descriptor.index_sizes.vertex, vertex_count) {
2252        return Err(format!(
2253            "vertex index size {} cannot hold vertex_count {}",
2254            descriptor.index_sizes.vertex, vertex_count
2255        ));
2256    }
2257    if !indices.is_empty()
2258        && !pmx_vertex_index_size_can_hold(
2259            descriptor.index_sizes.vertex,
2260            indices.iter().copied().max().unwrap_or(0) as usize + 1,
2261        )
2262    {
2263        return Err(format!(
2264            "vertex index size {} cannot hold max index {}",
2265            descriptor.index_sizes.vertex,
2266            indices.iter().copied().max().unwrap_or(0)
2267        ));
2268    }
2269    validate_pmx_parts_materials(descriptor, indices.len() / 3)?;
2270    validate_pmx_parts_bones(descriptor)?;
2271    validate_pmx_parts_morphs(descriptor, vertex_count)?;
2272    validate_pmx_parts_skin_indices(descriptor, skin_indices)?;
2273    validate_pmx_parts_display_frames(descriptor)?;
2274    validate_pmx_parts_physics(descriptor)?;
2275    Ok(())
2276}
2277
2278fn validate_pmx_parts_materials(
2279    descriptor: &PmxPartsDescriptor,
2280    face_count: usize,
2281) -> Result<(), String> {
2282    if descriptor.materials.is_empty() {
2283        return Ok(());
2284    }
2285    if !pmx_signed_index_size_can_hold_count(
2286        descriptor.index_sizes.material,
2287        descriptor.materials.len(),
2288    ) {
2289        return Err(format!(
2290            "material index size {} cannot hold material count {}",
2291            descriptor.index_sizes.material,
2292            descriptor.materials.len()
2293        ));
2294    }
2295    let mut total = 0usize;
2296    for (index, material) in descriptor.materials.iter().enumerate() {
2297        if material.face_count < 0 {
2298            return Err(format!(
2299                "materials[{index}].faceCount must be non-negative, got {}",
2300                material.face_count
2301            ));
2302        }
2303        total += material.face_count as usize;
2304    }
2305    if total != face_count {
2306        return Err(format!(
2307            "materials faceCount sum must match index face count, expected {}, got {}",
2308            face_count, total
2309        ));
2310    }
2311    Ok(())
2312}
2313
2314fn validate_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
2315    let bone_count = descriptor.bones.len().max(1);
2316    if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.bone, bone_count) {
2317        return Err(format!(
2318            "bone index size {} cannot hold bone count {}",
2319            descriptor.index_sizes.bone, bone_count
2320        ));
2321    }
2322    for (index, bone) in descriptor.bones.iter().enumerate() {
2323        if bone.parent_index < -1
2324            || (bone.parent_index != -1 && bone.parent_index as usize >= bone_count)
2325        {
2326            return Err(format!(
2327                "bones[{index}].parentIndex must be -1 or a valid bone index, got {}",
2328                bone.parent_index
2329            ));
2330        }
2331        if bone.tail_index < -1 || (bone.tail_index != -1 && bone.tail_index as usize >= bone_count)
2332        {
2333            return Err(format!(
2334                "bones[{index}].tailIndex must be -1 or a valid bone index, got {}",
2335                bone.tail_index
2336            ));
2337        }
2338    }
2339    Ok(())
2340}
2341
2342fn validate_pmx_parts_skin_indices(
2343    descriptor: &PmxPartsDescriptor,
2344    skin_indices: &[u32],
2345) -> Result<(), String> {
2346    if skin_indices.is_empty() {
2347        return Ok(());
2348    }
2349    let bone_count = descriptor.bones.len().max(1);
2350    if let Some(&index) = skin_indices
2351        .iter()
2352        .find(|&&index| index as usize >= bone_count)
2353    {
2354        return Err(format!(
2355            "skin_indices contains out-of-range bone index {index} for bone count {bone_count}"
2356        ));
2357    }
2358    Ok(())
2359}
2360
2361fn validate_pmx_parts_morphs(
2362    descriptor: &PmxPartsDescriptor,
2363    vertex_count: usize,
2364) -> Result<(), String> {
2365    if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.morph, descriptor.morphs.len())
2366    {
2367        return Err(format!(
2368            "morph index size {} cannot hold morph count {}",
2369            descriptor.index_sizes.morph,
2370            descriptor.morphs.len()
2371        ));
2372    }
2373    for (morph_index, morph) in descriptor.morphs.iter().enumerate() {
2374        if morph_panel_byte_checked(&morph.panel).is_none() {
2375            return Err(format!(
2376                "morphs[{morph_index}].panel must be system, brow, eye, mouth, or other, got {}",
2377                morph.panel
2378            ));
2379        }
2380        match morph.kind.as_str() {
2381            "vertex" => {
2382                for (offset_index, offset) in morph.vertex_offsets.iter().enumerate() {
2383                    if offset.vertex_index as usize >= vertex_count {
2384                        return Err(format!(
2385                            "morphs[{morph_index}].vertexOffsets[{offset_index}].vertexIndex out of range: {} for vertex_count {vertex_count}",
2386                            offset.vertex_index
2387                        ));
2388                    }
2389                }
2390            }
2391            "group" => {
2392                for (offset_index, offset) in morph.group_offsets.iter().enumerate() {
2393                    if offset.morph_index < 0
2394                        || offset.morph_index as usize >= descriptor.morphs.len()
2395                    {
2396                        return Err(format!(
2397                            "morphs[{morph_index}].groupOffsets[{offset_index}].morphIndex out of range: {}",
2398                            offset.morph_index
2399                        ));
2400                    }
2401                }
2402            }
2403            other => {
2404                return Err(format!(
2405                    "morphs[{morph_index}].kind must be vertex or group, got {other}"
2406                ));
2407            }
2408        }
2409    }
2410    Ok(())
2411}
2412
2413fn validate_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
2414    let bone_count = descriptor.bones.len().max(1);
2415    let morph_count = descriptor.morphs.len();
2416    for (frame_index, frame) in descriptor.display_frames.iter().enumerate() {
2417        for (item_index, item) in frame.frames.iter().enumerate() {
2418            match item.kind.as_str() {
2419                "bone" => {
2420                    if item.index < 0 || item.index as usize >= bone_count {
2421                        return Err(format!(
2422                            "displayFrames[{frame_index}].frames[{item_index}].index must reference an existing bone, got {}",
2423                            item.index
2424                        ));
2425                    }
2426                }
2427                "morph" => {
2428                    if item.index < 0 || item.index as usize >= morph_count {
2429                        return Err(format!(
2430                            "displayFrames[{frame_index}].frames[{item_index}].index must reference an existing morph, got {}",
2431                            item.index
2432                        ));
2433                    }
2434                }
2435                other => {
2436                    return Err(format!(
2437                        "displayFrames[{frame_index}].frames[{item_index}].kind must be bone or morph, got {other}"
2438                    ));
2439                }
2440            }
2441        }
2442    }
2443    Ok(())
2444}
2445
2446fn validate_pmx_parts_physics(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
2447    let bone_count = descriptor.bones.len().max(1);
2448    if !pmx_signed_index_size_can_hold_count(
2449        descriptor.index_sizes.rigid_body,
2450        descriptor.rigid_bodies.len(),
2451    ) {
2452        return Err(format!(
2453            "rigidBody index size {} cannot hold rigid body count {}",
2454            descriptor.index_sizes.rigid_body,
2455            descriptor.rigid_bodies.len()
2456        ));
2457    }
2458    for (index, body) in descriptor.rigid_bodies.iter().enumerate() {
2459        if body.bone_index < -1 || (body.bone_index != -1 && body.bone_index as usize >= bone_count)
2460        {
2461            return Err(format!(
2462                "rigidBodies[{index}].boneIndex must be -1 or a valid bone index, got {}",
2463                body.bone_index
2464            ));
2465        }
2466        if !matches!(body.shape.as_str(), "sphere" | "box" | "capsule") {
2467            return Err(format!(
2468                "rigidBodies[{index}].shape must be sphere, box, or capsule, got {}",
2469                body.shape
2470            ));
2471        }
2472        if !matches!(body.mode.as_str(), "static" | "dynamic" | "dynamicBone") {
2473            return Err(format!(
2474                "rigidBodies[{index}].mode must be static, dynamic, or dynamicBone, got {}",
2475                body.mode
2476            ));
2477        }
2478    }
2479    for (index, joint) in descriptor.joints.iter().enumerate() {
2480        validate_pmx_parts_rigid_body_ref(
2481            "rigidBodyIndexA",
2482            index,
2483            joint.rigid_body_index_a,
2484            descriptor.rigid_bodies.len(),
2485        )?;
2486        validate_pmx_parts_rigid_body_ref(
2487            "rigidBodyIndexB",
2488            index,
2489            joint.rigid_body_index_b,
2490            descriptor.rigid_bodies.len(),
2491        )?;
2492        if !matches!(
2493            joint.kind.as_str(),
2494            "generic6dofSpring" | "generic6dof" | "point2point" | "coneTwist" | "slider" | "hinge"
2495        ) {
2496            return Err(format!(
2497                "joints[{index}].kind has unsupported PMX joint type {}",
2498                joint.kind
2499            ));
2500        }
2501    }
2502    Ok(())
2503}
2504
2505fn validate_pmx_parts_rigid_body_ref(
2506    field: &str,
2507    joint_index: usize,
2508    value: i32,
2509    rigid_body_count: usize,
2510) -> Result<(), String> {
2511    if value < -1 || (value != -1 && value as usize >= rigid_body_count) {
2512        Err(format!(
2513            "joints[{joint_index}].{field} must be -1 or a valid rigid body index, got {value}"
2514        ))
2515    } else {
2516        Ok(())
2517    }
2518}
2519
2520fn validate_pmx_index_size(name: &str, value: u8) -> Result<(), String> {
2521    if matches!(value, 1 | 2 | 4) {
2522        Ok(())
2523    } else {
2524        Err(format!(
2525            "PMX {name} index size must be 1, 2, or 4, got {value}"
2526        ))
2527    }
2528}
2529
2530fn pmx_vertex_index_size_can_hold(size: u8, count: usize) -> bool {
2531    match size {
2532        1 => count <= u8::MAX as usize + 1,
2533        2 => count <= u16::MAX as usize + 1,
2534        4 => true,
2535        _ => false,
2536    }
2537}
2538
2539fn pmx_signed_index_size_can_hold_count(size: u8, count: usize) -> bool {
2540    match size {
2541        1 => count <= i8::MAX as usize + 1,
2542        2 => count <= i16::MAX as usize + 1,
2543        4 => true,
2544        _ => false,
2545    }
2546}
2547
2548fn default_pmx_skin_weights(vertex_count: usize) -> Vec<f32> {
2549    let mut weights = vec![0.0; vertex_count * 4];
2550    for vertex in 0..vertex_count {
2551        weights[vertex * 4] = 1.0;
2552    }
2553    weights
2554}
2555
2556fn build_pmx_parts_materials(
2557    descriptor: &PmxPartsDescriptor,
2558    face_count: usize,
2559) -> (Vec<PmxParsedMaterial>, Vec<PmxParsedMaterialGroup>) {
2560    if face_count == 0 {
2561        return (Vec::new(), Vec::new());
2562    }
2563    if descriptor.materials.is_empty() {
2564        return (
2565            vec![default_pmx_parts_material(
2566                &descriptor.material_name,
2567                &descriptor.english_material_name,
2568                face_count as i32,
2569            )],
2570            vec![PmxParsedMaterialGroup {
2571                start: 0,
2572                count: face_count * 3,
2573                material_index: 0,
2574            }],
2575        );
2576    }
2577
2578    let mut start = 0usize;
2579    let mut materials = Vec::with_capacity(descriptor.materials.len());
2580    let mut groups = Vec::with_capacity(descriptor.materials.len());
2581    for (index, material) in descriptor.materials.iter().enumerate() {
2582        let index_count = material.face_count as usize * 3;
2583        materials.push(pmx_parts_material_from_descriptor(material));
2584        groups.push(PmxParsedMaterialGroup {
2585            start,
2586            count: index_count,
2587            material_index: index,
2588        });
2589        start += index_count;
2590    }
2591    (materials, groups)
2592}
2593
2594fn pmx_parts_material_from_descriptor(material: &PmxPartsMaterialDescriptor) -> PmxParsedMaterial {
2595    PmxParsedMaterial {
2596        name: material.name.clone(),
2597        english_name: material.english_name.clone(),
2598        texture_path: material.texture_path.clone(),
2599        sphere_texture_path: material.sphere_texture_path.clone(),
2600        sphere_mode: if material.sphere_mode.is_empty() {
2601            "none".to_owned()
2602        } else {
2603            material.sphere_mode.clone()
2604        },
2605        toon_texture_path: material.toon_texture_path.clone(),
2606        shared_toon_index: material.shared_toon_index,
2607        diffuse: material.diffuse,
2608        specular: material.specular,
2609        specular_power: material.specular_power,
2610        ambient: material.ambient,
2611        edge_color: material.edge_color,
2612        edge_size: material.edge_size,
2613        flags: PmxParsedMaterialFlags {
2614            double_sided: material.flags.double_sided,
2615            ground_shadow: material.flags.ground_shadow,
2616            self_shadow_map: material.flags.self_shadow_map,
2617            self_shadow: material.flags.self_shadow,
2618            edge: material.flags.edge,
2619            vertex_color: material.flags.vertex_color,
2620            point_draw: material.flags.point_draw,
2621            line_draw: material.flags.line_draw,
2622        },
2623        face_count: material.face_count,
2624    }
2625}
2626
2627fn build_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedBone> {
2628    if descriptor.bones.is_empty() {
2629        return vec![default_pmx_parts_root_bone()];
2630    }
2631    descriptor
2632        .bones
2633        .iter()
2634        .map(pmx_parts_bone_from_descriptor)
2635        .collect()
2636}
2637
2638fn build_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedDisplayFrame> {
2639    descriptor
2640        .display_frames
2641        .iter()
2642        .map(|frame| PmxParsedDisplayFrame {
2643            name: frame.name.clone(),
2644            english_name: frame.english_name.clone(),
2645            special: frame.special,
2646            frames: frame
2647                .frames
2648                .iter()
2649                .map(|item| PmxParsedDisplayFrameElement {
2650                    kind: item.kind.clone(),
2651                    index: item.index,
2652                })
2653                .collect(),
2654        })
2655        .collect()
2656}
2657
2658fn build_pmx_parts_morphs(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedMorph> {
2659    descriptor
2660        .morphs
2661        .iter()
2662        .map(pmx_parts_morph_from_descriptor)
2663        .collect()
2664}
2665
2666fn build_pmx_parts_rigid_bodies(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedRigidBody> {
2667    descriptor
2668        .rigid_bodies
2669        .iter()
2670        .map(|body| PmxParsedRigidBody {
2671            name: body.name.clone(),
2672            english_name: body.english_name.clone(),
2673            bone_index: body.bone_index,
2674            group: body.group,
2675            mask: body.mask,
2676            shape: body.shape.clone(),
2677            size: body.size,
2678            position: body.position,
2679            rotation: body.rotation,
2680            mass: body.mass,
2681            linear_damping: body.linear_damping,
2682            angular_damping: body.angular_damping,
2683            restitution: body.restitution,
2684            friction: body.friction,
2685            mode: body.mode.clone(),
2686        })
2687        .collect()
2688}
2689
2690fn build_pmx_parts_joints(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedJoint> {
2691    descriptor
2692        .joints
2693        .iter()
2694        .map(|joint| PmxParsedJoint {
2695            name: joint.name.clone(),
2696            english_name: joint.english_name.clone(),
2697            kind: joint.kind.clone(),
2698            rigid_body_index_a: joint.rigid_body_index_a,
2699            rigid_body_index_b: joint.rigid_body_index_b,
2700            position: joint.position,
2701            rotation: joint.rotation,
2702            translation_lower_limit: joint.translation_lower_limit,
2703            translation_upper_limit: joint.translation_upper_limit,
2704            rotation_lower_limit: joint.rotation_lower_limit,
2705            rotation_upper_limit: joint.rotation_upper_limit,
2706            spring_translation_factor: joint.spring_translation_factor,
2707            spring_rotation_factor: joint.spring_rotation_factor,
2708        })
2709        .collect()
2710}
2711
2712fn pmx_parts_morph_from_descriptor(morph: &PmxPartsMorphDescriptor) -> PmxParsedMorph {
2713    let mut parsed = PmxParsedMorph {
2714        name: morph.name.clone(),
2715        english_name: morph.english_name.clone(),
2716        panel: morph.panel.clone(),
2717        kind: morph.kind.clone(),
2718        vertex_offsets: Vec::new(),
2719        group_offsets: Vec::new(),
2720        bone_offsets: Vec::new(),
2721        uv_offsets: Vec::new(),
2722        additional_uv_offsets: Vec::new(),
2723        material_offsets: Vec::new(),
2724        flip_offsets: Vec::new(),
2725        impulse_offsets: Vec::new(),
2726    };
2727    match morph.kind.as_str() {
2728        "vertex" => {
2729            parsed.vertex_offsets = morph
2730                .vertex_offsets
2731                .iter()
2732                .map(|offset| PmxParsedVertexMorphOffset {
2733                    vertex_index: offset.vertex_index,
2734                    position: offset.position,
2735                })
2736                .collect();
2737        }
2738        "group" => {
2739            parsed.group_offsets = morph
2740                .group_offsets
2741                .iter()
2742                .map(|offset| PmxParsedGroupMorphOffset {
2743                    morph_index: offset.morph_index,
2744                    weight: offset.weight,
2745                })
2746                .collect();
2747        }
2748        _ => unreachable!("PmxPartsMorphDescriptor was validated before build"),
2749    }
2750    parsed
2751}
2752
2753fn pmx_parts_bone_from_descriptor(bone: &PmxPartsBoneDescriptor) -> PmxParsedBone {
2754    let tail_position = if bone.tail_index >= 0 {
2755        None
2756    } else {
2757        Some(bone.tail_position.unwrap_or([0.0, 1.0, 0.0]))
2758    };
2759    PmxParsedBone {
2760        name: bone.name.clone(),
2761        english_name: bone.english_name.clone(),
2762        parent_index: bone.parent_index,
2763        layer: bone.layer,
2764        position: bone.position,
2765        tail_index: bone.tail_index,
2766        tail_position,
2767        flags: PmxParsedBoneFlags {
2768            indexed_tail: bone.tail_index >= 0,
2769            rotatable: bone.rotatable,
2770            translatable: bone.translatable,
2771            visible: bone.visible,
2772            enabled: bone.enabled,
2773            ik: false,
2774            append_local: false,
2775            append_rotate: false,
2776            append_translate: false,
2777            fixed_axis: false,
2778            local_axis: false,
2779            external_parent_transform: false,
2780            transform_after_physics: false,
2781        },
2782        append_transform: None,
2783        fixed_axis: None,
2784        local_axis: None,
2785        external_parent_key: None,
2786        ik: None,
2787    }
2788}
2789
2790fn default_pmx_parts_material(
2791    name: &str,
2792    english_name: &str,
2793    face_count: i32,
2794) -> PmxParsedMaterial {
2795    PmxParsedMaterial {
2796        name: if name.is_empty() {
2797            "material".to_owned()
2798        } else {
2799            name.to_owned()
2800        },
2801        english_name: english_name.to_owned(),
2802        texture_path: String::new(),
2803        sphere_texture_path: String::new(),
2804        sphere_mode: "none".to_owned(),
2805        toon_texture_path: String::new(),
2806        shared_toon_index: Some(0),
2807        diffuse: [0.8, 0.8, 0.8, 1.0],
2808        specular: [0.0, 0.0, 0.0],
2809        specular_power: 1.0,
2810        ambient: [0.2, 0.2, 0.2],
2811        edge_color: [0.0, 0.0, 0.0, 1.0],
2812        edge_size: 1.0,
2813        flags: PmxParsedMaterialFlags {
2814            double_sided: true,
2815            ground_shadow: true,
2816            self_shadow_map: true,
2817            self_shadow: true,
2818            edge: false,
2819            vertex_color: false,
2820            point_draw: false,
2821            line_draw: false,
2822        },
2823        face_count,
2824    }
2825}
2826
2827fn default_pmx_parts_root_bone() -> PmxParsedBone {
2828    PmxParsedBone {
2829        name: "root".to_owned(),
2830        english_name: "root".to_owned(),
2831        parent_index: -1,
2832        layer: 0,
2833        position: [0.0, 0.0, 0.0],
2834        tail_index: -1,
2835        tail_position: Some([0.0, 1.0, 0.0]),
2836        flags: PmxParsedBoneFlags {
2837            indexed_tail: false,
2838            rotatable: true,
2839            translatable: true,
2840            visible: true,
2841            enabled: true,
2842            ik: false,
2843            append_local: false,
2844            append_rotate: false,
2845            append_translate: false,
2846            fixed_axis: false,
2847            local_axis: false,
2848            transform_after_physics: false,
2849            external_parent_transform: false,
2850        },
2851        append_transform: None,
2852        fixed_axis: None,
2853        local_axis: None,
2854        external_parent_key: None,
2855        ik: None,
2856    }
2857}
2858
2859pub fn parse_pmx_model(data: &[u8]) -> Result<PmxParsedModel, ImportError> {
2860    let (header, pos) = read_header(data)?;
2861    let mut r = Reader { data, pos };
2862    let name = r.read_string(header.encoding)?;
2863    let english_name = r.read_string(header.encoding)?;
2864    let comment = r.read_string(header.encoding)?;
2865    let english_comment = r.read_string(header.encoding)?;
2866
2867    let vertex_count = read_section_count_with_min_record(&mut r, 12 + 12 + 8 + 1 + 4)?;
2868    let geometry = read_parsed_geometry(&mut r, &header, vertex_count)?;
2869    let index_count = geometry.indices.len();
2870    let textures = read_parsed_textures(&mut r, header.encoding)?;
2871    let materials = read_parsed_materials(&mut r, &header, &textures)?;
2872    let material_groups = build_material_groups(&materials);
2873    let bone_count = read_section_count_with_min_record(&mut r, 4 + 4 + 12)?;
2874    let skeleton = PmxParsedSkeleton {
2875        bones: read_parsed_bones(&mut r, &header, bone_count)?,
2876    };
2877    let morphs = read_parsed_morphs(&mut r, &header)?;
2878    let display_frames = read_parsed_display_frames(&mut r, &header)?;
2879    let rigid_bodies = if r.remaining() >= 4 {
2880        read_parsed_rigid_bodies(&mut r, &header)?
2881    } else {
2882        Vec::new()
2883    };
2884    let joints = if r.remaining() >= 4 {
2885        read_parsed_joints(&mut r, &header)?
2886    } else {
2887        Vec::new()
2888    };
2889    let soft_bodies = if header.version >= 2.05 && r.remaining() >= 4 {
2890        read_parsed_soft_bodies(&mut r, &header)?
2891    } else {
2892        Vec::new()
2893    };
2894
2895    let mut geometry = geometry;
2896    geometry.material_groups = material_groups;
2897    let mut diagnostics = Vec::new();
2898    if !soft_bodies.is_empty() {
2899        diagnostics.push(PmxParserDiagnostic {
2900            level: "warning".to_owned(),
2901            code: "PMX_SOFT_BODY_UNSUPPORTED".to_owned(),
2902            message: format!(
2903                "{} PMX soft bodies are parsed only as header records by mmd-anim.",
2904                soft_bodies.len()
2905            ),
2906        });
2907    }
2908    if r.remaining() > 0 {
2909        diagnostics.push(PmxParserDiagnostic {
2910            level: "warning".to_owned(),
2911            code: "PMX_TRAILING_DATA_UNPARSED".to_owned(),
2912            message: format!("{} trailing PMX bytes were left unparsed.", r.remaining()),
2913        });
2914    }
2915
2916    Ok(PmxParsedModel {
2917        metadata: PmxParsedMetadata {
2918            format: "pmx".to_owned(),
2919            version: header.version,
2920            encoding: match header.encoding {
2921                TextEncoding::Utf8 => "utf-8".to_owned(),
2922                TextEncoding::Utf16Le => "utf-16-le".to_owned(),
2923            },
2924            name,
2925            english_name,
2926            comment,
2927            english_comment,
2928            counts: PmxParsedCounts {
2929                vertices: vertex_count,
2930                faces: index_count / 3,
2931                materials: materials.len(),
2932                bones: skeleton.bones.len(),
2933                morphs: morphs.len(),
2934                display_frames: display_frames.len(),
2935                rigid_bodies: rigid_bodies.len(),
2936                joints: joints.len(),
2937                soft_bodies: soft_bodies.len(),
2938            },
2939            index_sizes: PmxParsedIndexSizes {
2940                vertex: header.vertex_index_size,
2941                texture: header.texture_index_size,
2942                material: header.material_index_size,
2943                bone: header.bone_index_size,
2944                morph: header.morph_index_size,
2945                rigid_body: header.rigidbody_index_size,
2946            },
2947            additional_uv_count: header.extra_uv_count,
2948        },
2949        geometry,
2950        materials,
2951        skeleton,
2952        morphs,
2953        display_frames,
2954        rigid_bodies,
2955        joints,
2956        soft_bodies,
2957        diagnostics,
2958    })
2959}
2960
2961pub fn export_pmx_model(model: &PmxParsedModel) -> Vec<u8> {
2962    let encoding = pmx_text_encoding(&model.metadata);
2963    let mut out = Vec::new();
2964    out.extend_from_slice(&PMX_MAGIC);
2965    write_f32(&mut out, model.metadata.version);
2966    out.push(8);
2967    out.push(encoding as u8);
2968    out.push(model.metadata.additional_uv_count);
2969    out.push(model.metadata.index_sizes.vertex);
2970    out.push(model.metadata.index_sizes.texture);
2971    out.push(model.metadata.index_sizes.material);
2972    out.push(model.metadata.index_sizes.bone);
2973    out.push(model.metadata.index_sizes.morph);
2974    out.push(model.metadata.index_sizes.rigid_body);
2975
2976    write_pmx_string(&mut out, &model.metadata.name, encoding);
2977    write_pmx_string(&mut out, &model.metadata.english_name, encoding);
2978    write_pmx_string(&mut out, &model.metadata.comment, encoding);
2979    write_pmx_string(&mut out, &model.metadata.english_comment, encoding);
2980
2981    let vertex_count = model.geometry.positions.len() / 3;
2982    write_i32(&mut out, vertex_count as i32);
2983    for index in 0..vertex_count {
2984        write_f32_slice(
2985            &mut out,
2986            &model.geometry.positions[index * 3..index * 3 + 3],
2987        );
2988        write_f32_slice(&mut out, &model.geometry.normals[index * 3..index * 3 + 3]);
2989        write_f32_slice(&mut out, &model.geometry.uvs[index * 2..index * 2 + 2]);
2990        for uv in &model.geometry.additional_uvs {
2991            write_f32_slice(&mut out, &uv[index * 4..index * 4 + 4]);
2992        }
2993        write_pmx_vertex_skinning(&mut out, model, index);
2994        write_f32(
2995            &mut out,
2996            model.geometry.edge_scale.get(index).copied().unwrap_or(1.0),
2997        );
2998    }
2999
3000    write_i32(&mut out, model.geometry.indices.len() as i32);
3001    for &index in &model.geometry.indices {
3002        write_vertex_index(&mut out, index, model.metadata.index_sizes.vertex);
3003    }
3004
3005    let textures = collect_pmx_textures(&model.materials);
3006    write_i32(&mut out, textures.len() as i32);
3007    for texture in &textures {
3008        write_pmx_string(&mut out, texture, encoding);
3009    }
3010
3011    write_i32(&mut out, model.materials.len() as i32);
3012    for material in &model.materials {
3013        write_pmx_string(&mut out, &material.name, encoding);
3014        write_pmx_string(&mut out, &material.english_name, encoding);
3015        write_f32_slice(&mut out, &material.diffuse);
3016        write_f32_slice(&mut out, &material.specular);
3017        write_f32(&mut out, material.specular_power);
3018        write_f32_slice(&mut out, &material.ambient);
3019        out.push(material_flag_bits(&material.flags));
3020        write_f32_slice(&mut out, &material.edge_color);
3021        write_f32(&mut out, material.edge_size);
3022        write_sized_index(
3023            &mut out,
3024            texture_index(&textures, &material.texture_path),
3025            model.metadata.index_sizes.texture,
3026        );
3027        write_sized_index(
3028            &mut out,
3029            texture_index(&textures, &material.sphere_texture_path),
3030            model.metadata.index_sizes.texture,
3031        );
3032        out.push(match material.sphere_mode.as_str() {
3033            "multiply" => 1,
3034            "add" => 2,
3035            "subTexture" => 3,
3036            _ => 0,
3037        });
3038        if let Some(shared) = material.shared_toon_index {
3039            out.push(1);
3040            out.push(shared);
3041        } else {
3042            out.push(0);
3043            write_sized_index(
3044                &mut out,
3045                texture_index(&textures, &material.toon_texture_path),
3046                model.metadata.index_sizes.texture,
3047            );
3048        }
3049        write_pmx_string(&mut out, "", encoding);
3050        write_i32(&mut out, material.face_count.saturating_mul(3));
3051    }
3052
3053    write_i32(&mut out, model.skeleton.bones.len() as i32);
3054    for bone in &model.skeleton.bones {
3055        write_pmx_string(&mut out, &bone.name, encoding);
3056        write_pmx_string(&mut out, &bone.english_name, encoding);
3057        write_f32_slice(&mut out, &bone.position);
3058        write_sized_index(&mut out, bone.parent_index, model.metadata.index_sizes.bone);
3059        write_i32(&mut out, bone.layer);
3060        let bits = bone_flag_bits(&bone.flags);
3061        write_u16(&mut out, bits);
3062        if bone.flags.indexed_tail {
3063            write_sized_index(&mut out, bone.tail_index, model.metadata.index_sizes.bone);
3064        } else {
3065            write_f32_slice(&mut out, &bone.tail_position.unwrap_or([0.0; 3]));
3066        }
3067        if bone.flags.append_rotate || bone.flags.append_translate {
3068            let append = bone.append_transform.as_ref();
3069            write_sized_index(
3070                &mut out,
3071                append.map(|value| value.parent_index).unwrap_or(-1),
3072                model.metadata.index_sizes.bone,
3073            );
3074            write_f32(&mut out, append.map(|value| value.weight).unwrap_or(0.0));
3075        }
3076        if bone.flags.fixed_axis {
3077            write_f32_slice(&mut out, &bone.fixed_axis.unwrap_or([0.0; 3]));
3078        }
3079        if bone.flags.local_axis {
3080            if let Some(axis) = &bone.local_axis {
3081                write_f32_slice(&mut out, &axis.x);
3082                write_f32_slice(&mut out, &axis.z);
3083            } else {
3084                write_f32_slice(&mut out, &[1.0, 0.0, 0.0]);
3085                write_f32_slice(&mut out, &[0.0, 0.0, 1.0]);
3086            }
3087        }
3088        if bone.flags.external_parent_transform {
3089            write_i32(&mut out, bone.external_parent_key.unwrap_or(0));
3090        }
3091        if bone.flags.ik {
3092            if let Some(ik) = &bone.ik {
3093                write_sized_index(&mut out, ik.target_index, model.metadata.index_sizes.bone);
3094                write_i32(&mut out, ik.loop_count);
3095                write_f32(&mut out, ik.limit_angle);
3096                write_i32(&mut out, ik.links.len() as i32);
3097                for link in &ik.links {
3098                    write_sized_index(&mut out, link.bone_index, model.metadata.index_sizes.bone);
3099                    out.push(u8::from(link.limits.is_some()));
3100                    if let Some(limits) = &link.limits {
3101                        write_f32_slice(&mut out, &limits.lower);
3102                        write_f32_slice(&mut out, &limits.upper);
3103                    }
3104                }
3105            } else {
3106                write_sized_index(&mut out, -1, model.metadata.index_sizes.bone);
3107                write_i32(&mut out, 0);
3108                write_f32(&mut out, 0.0);
3109                write_i32(&mut out, 0);
3110            }
3111        }
3112    }
3113
3114    write_i32(&mut out, model.morphs.len() as i32);
3115    for morph in &model.morphs {
3116        write_pmx_string(&mut out, &morph.name, encoding);
3117        write_pmx_string(&mut out, &morph.english_name, encoding);
3118        out.push(morph_panel_byte(&morph.panel));
3119        let morph_type = morph_type_byte(morph);
3120        out.push(morph_type);
3121        write_i32(&mut out, morph_offset_count(morph, morph_type) as i32);
3122        match morph_type {
3123            0 => write_group_morph_offsets(&mut out, &morph.group_offsets, model),
3124            1 => {
3125                for offset in &morph.vertex_offsets {
3126                    write_vertex_index(
3127                        &mut out,
3128                        offset.vertex_index,
3129                        model.metadata.index_sizes.vertex,
3130                    );
3131                    write_f32_slice(&mut out, &offset.position);
3132                }
3133            }
3134            2 => {
3135                for offset in &morph.bone_offsets {
3136                    write_sized_index(&mut out, offset.bone_index, model.metadata.index_sizes.bone);
3137                    write_f32_slice(&mut out, &offset.translation);
3138                    write_f32_slice(&mut out, &offset.rotation);
3139                }
3140            }
3141            3 => write_uv_morph_offsets(&mut out, &morph.uv_offsets, model),
3142            4..=7 => {
3143                for offset in &morph.additional_uv_offsets {
3144                    write_vertex_index(
3145                        &mut out,
3146                        offset.vertex_index,
3147                        model.metadata.index_sizes.vertex,
3148                    );
3149                    write_f32_slice(&mut out, &offset.uv);
3150                }
3151            }
3152            8 => {
3153                for offset in &morph.material_offsets {
3154                    write_sized_index(
3155                        &mut out,
3156                        offset.material_index,
3157                        model.metadata.index_sizes.material,
3158                    );
3159                    out.push(if offset.operation == "multiply" { 0 } else { 1 });
3160                    write_f32_slice(&mut out, &offset.diffuse);
3161                    write_f32_slice(&mut out, &offset.specular);
3162                    write_f32(&mut out, offset.specular_power);
3163                    write_f32_slice(&mut out, &offset.ambient);
3164                    write_f32_slice(&mut out, &offset.edge_color);
3165                    write_f32(&mut out, offset.edge_size);
3166                    write_f32_slice(&mut out, &offset.texture_factor);
3167                    write_f32_slice(&mut out, &offset.sphere_texture_factor);
3168                    write_f32_slice(&mut out, &offset.toon_texture_factor);
3169                }
3170            }
3171            9 => write_group_morph_offsets(&mut out, &morph.flip_offsets, model),
3172            10 => {
3173                for offset in &morph.impulse_offsets {
3174                    write_sized_index(
3175                        &mut out,
3176                        offset.rigid_body_index,
3177                        model.metadata.index_sizes.rigid_body,
3178                    );
3179                    out.push(u8::from(offset.local));
3180                    write_f32_slice(&mut out, &offset.velocity);
3181                    write_f32_slice(&mut out, &offset.torque);
3182                }
3183            }
3184            _ => {}
3185        }
3186    }
3187
3188    write_i32(&mut out, model.display_frames.len() as i32);
3189    for frame in &model.display_frames {
3190        write_pmx_string(&mut out, &frame.name, encoding);
3191        write_pmx_string(&mut out, &frame.english_name, encoding);
3192        out.push(u8::from(frame.special));
3193        write_i32(&mut out, frame.frames.len() as i32);
3194        for item in &frame.frames {
3195            if item.kind == "morph" {
3196                out.push(1);
3197                write_sized_index(&mut out, item.index, model.metadata.index_sizes.morph);
3198            } else {
3199                out.push(0);
3200                write_sized_index(&mut out, item.index, model.metadata.index_sizes.bone);
3201            }
3202        }
3203    }
3204
3205    write_i32(&mut out, model.rigid_bodies.len() as i32);
3206    for body in &model.rigid_bodies {
3207        write_pmx_string(&mut out, &body.name, encoding);
3208        write_pmx_string(&mut out, &body.english_name, encoding);
3209        write_sized_index(&mut out, body.bone_index, model.metadata.index_sizes.bone);
3210        out.push(body.group);
3211        write_u16(&mut out, body.mask);
3212        out.push(match body.shape.as_str() {
3213            "box" => 1,
3214            "capsule" => 2,
3215            _ => 0,
3216        });
3217        write_f32_slice(&mut out, &body.size);
3218        write_f32_slice(&mut out, &body.position);
3219        write_f32_slice(&mut out, &body.rotation);
3220        write_f32(&mut out, body.mass);
3221        write_f32(&mut out, body.linear_damping);
3222        write_f32(&mut out, body.angular_damping);
3223        write_f32(&mut out, body.restitution);
3224        write_f32(&mut out, body.friction);
3225        out.push(match body.mode.as_str() {
3226            "dynamic" => 1,
3227            "dynamicBone" => 2,
3228            _ => 0,
3229        });
3230    }
3231
3232    write_i32(&mut out, model.joints.len() as i32);
3233    for joint in &model.joints {
3234        write_pmx_string(&mut out, &joint.name, encoding);
3235        write_pmx_string(&mut out, &joint.english_name, encoding);
3236        out.push(match joint.kind.as_str() {
3237            "generic6dof" => 1,
3238            "point2point" => 2,
3239            "coneTwist" => 3,
3240            "slider" => 4,
3241            "hinge" => 5,
3242            _ => 0,
3243        });
3244        write_sized_index(
3245            &mut out,
3246            joint.rigid_body_index_a,
3247            model.metadata.index_sizes.rigid_body,
3248        );
3249        write_sized_index(
3250            &mut out,
3251            joint.rigid_body_index_b,
3252            model.metadata.index_sizes.rigid_body,
3253        );
3254        write_f32_slice(&mut out, &joint.position);
3255        write_f32_slice(&mut out, &joint.rotation);
3256        write_f32_slice(&mut out, &joint.translation_lower_limit);
3257        write_f32_slice(&mut out, &joint.translation_upper_limit);
3258        write_f32_slice(&mut out, &joint.rotation_lower_limit);
3259        write_f32_slice(&mut out, &joint.rotation_upper_limit);
3260        write_f32_slice(&mut out, &joint.spring_translation_factor);
3261        write_f32_slice(&mut out, &joint.spring_rotation_factor);
3262    }
3263
3264    if model.metadata.version >= 2.05 {
3265        write_i32(&mut out, model.soft_bodies.len() as i32);
3266        for soft_body in &model.soft_bodies {
3267            write_pmx_string(&mut out, &soft_body.name, encoding);
3268            write_pmx_string(&mut out, &soft_body.english_name, encoding);
3269            out.push(if soft_body.kind == "rope" { 1 } else { 0 });
3270            write_sized_index(
3271                &mut out,
3272                soft_body.material_index,
3273                model.metadata.index_sizes.material,
3274            );
3275            out.push(soft_body.collision_group);
3276            write_u16(&mut out, soft_body.collision_mask);
3277            out.push(soft_body.flags);
3278            write_i32(&mut out, soft_body.bending_constraints_distance);
3279            write_i32(&mut out, soft_body.cluster_count);
3280            write_f32(&mut out, soft_body.total_mass);
3281            write_f32(&mut out, soft_body.collision_margin);
3282            out.extend_from_slice(&[0u8; 4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4]);
3283            write_i32(&mut out, 0);
3284            write_i32(&mut out, 0);
3285        }
3286    }
3287
3288    out
3289}
3290
3291fn read_section_count(r: &mut Reader<'_>) -> Result<usize, ImportError> {
3292    let count = r.read_i32_le()?;
3293    if count < 0 {
3294        return Err(ImportError::SectionOverflow);
3295    }
3296    Ok(count as usize)
3297}
3298
3299fn read_section_count_with_min_record(
3300    r: &mut Reader<'_>,
3301    min_record_size: usize,
3302) -> Result<usize, ImportError> {
3303    let count = read_section_count(r)?;
3304    r.require_record_bytes(count, min_record_size)?;
3305    Ok(count)
3306}
3307
3308fn read_parsed_geometry(
3309    r: &mut Reader<'_>,
3310    header: &PmxHeader,
3311    vertex_count: usize,
3312) -> Result<PmxParsedGeometry, ImportError> {
3313    r.require_record_bytes(vertex_count, 12 + 12 + 8 + 1 + 4)?;
3314    let position_capacity = vertex_count
3315        .checked_mul(3)
3316        .ok_or(ImportError::SectionOverflow)?;
3317    let uv_capacity = vertex_count
3318        .checked_mul(2)
3319        .ok_or(ImportError::SectionOverflow)?;
3320    let skin_capacity = vertex_count
3321        .checked_mul(4)
3322        .ok_or(ImportError::SectionOverflow)?;
3323
3324    let mut positions = Vec::with_capacity(position_capacity);
3325    let mut normals = Vec::with_capacity(position_capacity);
3326    let mut uvs = Vec::with_capacity(uv_capacity);
3327    let mut additional_uvs =
3328        vec![Vec::with_capacity(skin_capacity); header.extra_uv_count as usize];
3329    let mut skin_indices = Vec::with_capacity(skin_capacity);
3330    let mut skin_weights = Vec::with_capacity(skin_capacity);
3331    let mut skinning_modes = Vec::with_capacity(vertex_count);
3332    let mut edge_scale = Vec::with_capacity(vertex_count);
3333    let mut sdef_enabled = vec![0.0f32; vertex_count];
3334    let mut sdef_c = vec![0.0f32; vertex_count * 3];
3335    let mut sdef_r0 = vec![0.0f32; vertex_count * 3];
3336    let mut sdef_r1 = vec![0.0f32; vertex_count * 3];
3337    let mut sdef_rw0 = vec![0.0f32; vertex_count * 3];
3338    let mut sdef_rw1 = vec![0.0f32; vertex_count * 3];
3339    let mut qdef_enabled = vec![0.0f32; vertex_count];
3340
3341    for vertex_i in 0..vertex_count {
3342        positions.extend_from_slice(&r.read_vec3_array()?);
3343        normals.extend_from_slice(&r.read_vec3_array()?);
3344        uvs.extend_from_slice(&r.read_vec2_array()?);
3345        for uv in additional_uvs.iter_mut() {
3346            uv.extend_from_slice(&r.read_vec4_array()?);
3347        }
3348        let weight_type = r.read_u8()?;
3349        let mut indices = [0u32; 4];
3350        let mut weights = [0.0f32; 4];
3351        match weight_type {
3352            0 => {
3353                skinning_modes.push(PMX_SKINNING_MODE_BDEF1.to_owned());
3354                indices[0] =
3355                    normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3356                weights[0] = 1.0;
3357            }
3358            1 | 3 => {
3359                skinning_modes.push(
3360                    if weight_type == 3 {
3361                        PMX_SKINNING_MODE_SDEF
3362                    } else {
3363                        PMX_SKINNING_MODE_BDEF2
3364                    }
3365                    .to_owned(),
3366                );
3367                indices[0] =
3368                    normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3369                indices[1] =
3370                    normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3371                let w = r.read_f32_le()?;
3372                weights[0] = w;
3373                weights[1] = 1.0 - w;
3374                if weight_type == 3 {
3375                    let c = r.read_vec3_array()?;
3376                    let r0 = r.read_vec3_array()?;
3377                    let r1 = r.read_vec3_array()?;
3378                    let w1 = 1.0 - w;
3379                    let offset = vertex_i * 3;
3380                    sdef_enabled[vertex_i] = 1.0;
3381                    sdef_c[offset..offset + 3].copy_from_slice(&c);
3382                    sdef_r0[offset..offset + 3].copy_from_slice(&r0);
3383                    sdef_r1[offset..offset + 3].copy_from_slice(&r1);
3384                    for k in 0..3 {
3385                        let rw = r0[k] * w + r1[k] * w1;
3386                        sdef_rw0[offset + k] = (c[k] * 2.0 + r0[k] - rw) * 0.5;
3387                        sdef_rw1[offset + k] = (c[k] * 2.0 + r1[k] - rw) * 0.5;
3388                    }
3389                }
3390            }
3391            2 => {
3392                skinning_modes.push(PMX_SKINNING_MODE_BDEF4.to_owned());
3393                for index in &mut indices {
3394                    *index =
3395                        normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3396                }
3397                for weight in &mut weights {
3398                    *weight = r.read_f32_le()?;
3399                }
3400            }
3401            4 => {
3402                skinning_modes.push(PMX_SKINNING_MODE_QDEF.to_owned());
3403                for index in &mut indices {
3404                    *index =
3405                        normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
3406                }
3407                for weight in &mut weights {
3408                    *weight = r.read_f32_le()?;
3409                }
3410                qdef_enabled[vertex_i] = 1.0;
3411            }
3412            _ => return Err(ImportError::SectionOverflow),
3413        }
3414        skin_indices.extend_from_slice(&indices);
3415        skin_weights.extend_from_slice(&weights);
3416        edge_scale.push(r.read_f32_le()?);
3417    }
3418
3419    let index_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
3420    let mut indices = Vec::with_capacity(index_count);
3421    for _ in 0..index_count {
3422        indices.push(r.read_vertex_index(header.vertex_index_size)?);
3423    }
3424
3425    Ok(PmxParsedGeometry {
3426        positions,
3427        normals,
3428        uvs,
3429        additional_uvs,
3430        indices,
3431        skin_indices,
3432        skin_weights,
3433        edge_scale,
3434        material_groups: Vec::new(),
3435        sdef: PmxParsedSdef {
3436            skinning_modes,
3437            enabled: sdef_enabled,
3438            c: sdef_c,
3439            r0: sdef_r0,
3440            r1: sdef_r1,
3441            rw0: sdef_rw0,
3442            rw1: sdef_rw1,
3443        },
3444        qdef: PmxParsedQdef {
3445            enabled: qdef_enabled,
3446        },
3447    })
3448}
3449
3450fn normalize_nonnegative_index(index: i32) -> u32 {
3451    if index < 0 { 0 } else { index as u32 }
3452}
3453
3454pub fn parse_pmx_material_split(
3455    data: &[u8],
3456    _flags: u32,
3457) -> Result<PmxMaterialSplitResult, ImportError> {
3458    let model = parse_pmx_model(data)?;
3459    Ok(split_pmx_model_by_material(&model))
3460}
3461
3462pub fn split_pmx_model_by_material(model: &PmxParsedModel) -> PmxMaterialSplitResult {
3463    let mut meshes = Vec::with_capacity(model.geometry.material_groups.len());
3464    let mut manifest_meshes = Vec::with_capacity(model.geometry.material_groups.len());
3465    let mut diagnostics = Vec::new();
3466
3467    for (mesh_index, group) in model.geometry.material_groups.iter().enumerate() {
3468        if group.material_index >= model.materials.len() {
3469            let diagnostic = pmx_material_split_diagnostic(
3470                "PMX_MATERIAL_SPLIT_MATERIAL_INDEX_OUT_OF_RANGE",
3471                format!(
3472                    "material group {mesh_index} references material {} but material count is {}",
3473                    group.material_index,
3474                    model.materials.len()
3475                ),
3476            );
3477            diagnostics.push(diagnostic);
3478            continue;
3479        }
3480
3481        let mesh = split_pmx_material_group(model, group, mesh_index);
3482        manifest_meshes.push(PmxMaterialSplitManifestMesh {
3483            mesh_index,
3484            original_material_index: mesh.original_material_index,
3485            original_vertex_indices: mesh.original_vertex_indices.clone(),
3486            morph_index_map: mesh.morph_index_map.clone(),
3487            diagnostics: mesh.diagnostics.clone(),
3488        });
3489        diagnostics.extend(mesh.diagnostics.iter().cloned());
3490        meshes.push(mesh);
3491    }
3492
3493    PmxMaterialSplitResult {
3494        manifest: PmxMaterialSplitManifest {
3495            mesh_count: meshes.len(),
3496            meshes: manifest_meshes,
3497            diagnostics,
3498        },
3499        meshes,
3500    }
3501}
3502
3503fn split_pmx_material_group(
3504    model: &PmxParsedModel,
3505    group: &PmxParsedMaterialGroup,
3506    mesh_index: usize,
3507) -> PmxMaterialSplitMesh {
3508    let mut diagnostics = Vec::new();
3509    let index_end = group.start.saturating_add(group.count);
3510    let index_slice = match model.geometry.indices.get(group.start..index_end) {
3511        Some(slice) => slice,
3512        None => {
3513            diagnostics.push(pmx_material_split_diagnostic(
3514                "PMX_MATERIAL_SPLIT_INDEX_RANGE_OUT_OF_RANGE",
3515                format!(
3516                    "material group {mesh_index} index range {}..{} exceeds index count {}",
3517                    group.start,
3518                    index_end,
3519                    model.geometry.indices.len()
3520                ),
3521            ));
3522            &[][..]
3523        }
3524    };
3525
3526    let vertex_count = model.geometry.positions.len() / 3;
3527    let mut original_vertex_indices = Vec::new();
3528    let mut vertex_remap = HashMap::<u32, u32>::new();
3529    let mut local_indices = Vec::with_capacity(index_slice.len());
3530    for &original_index in index_slice {
3531        if original_index as usize >= vertex_count {
3532            diagnostics.push(pmx_material_split_diagnostic(
3533                "PMX_MATERIAL_SPLIT_VERTEX_INDEX_OUT_OF_RANGE",
3534                format!(
3535                    "material group {mesh_index} references vertex {original_index} but vertex count is {vertex_count}"
3536                ),
3537            ));
3538            continue;
3539        }
3540        let local_index = match vertex_remap.get(&original_index) {
3541            Some(&local_index) => local_index,
3542            None => {
3543                let local_index = original_vertex_indices.len() as u32;
3544                vertex_remap.insert(original_index, local_index);
3545                original_vertex_indices.push(original_index);
3546                local_index
3547            }
3548        };
3549        local_indices.push(local_index);
3550    }
3551
3552    let local_index_count = local_indices.len();
3553    let geometry = remap_pmx_geometry(
3554        &model.geometry,
3555        &original_vertex_indices,
3556        local_indices,
3557        local_index_count,
3558    );
3559    let material = {
3560        let mut material = model.materials[group.material_index].clone();
3561        material.face_count = (geometry.indices.len() / 3) as i32;
3562        material
3563    };
3564    let (morphs, morph_index_map, morph_diagnostics) =
3565        remap_pmx_material_split_morphs(model, &vertex_remap, group.material_index, mesh_index);
3566    diagnostics.extend(morph_diagnostics);
3567
3568    PmxMaterialSplitMesh {
3569        original_material_index: group.material_index,
3570        original_vertex_indices,
3571        geometry,
3572        material,
3573        morphs,
3574        morph_index_map,
3575        diagnostics,
3576    }
3577}
3578
3579fn remap_pmx_geometry(
3580    geometry: &PmxParsedGeometry,
3581    original_vertex_indices: &[u32],
3582    indices: Vec<u32>,
3583    local_index_count: usize,
3584) -> PmxParsedGeometry {
3585    PmxParsedGeometry {
3586        positions: gather_vertex_f32(&geometry.positions, 3, original_vertex_indices),
3587        normals: gather_vertex_f32(&geometry.normals, 3, original_vertex_indices),
3588        uvs: gather_vertex_f32(&geometry.uvs, 2, original_vertex_indices),
3589        additional_uvs: geometry
3590            .additional_uvs
3591            .iter()
3592            .map(|uvs| gather_vertex_f32(uvs, 4, original_vertex_indices))
3593            .collect(),
3594        indices,
3595        skin_indices: gather_vertex_u32(&geometry.skin_indices, 4, original_vertex_indices),
3596        skin_weights: gather_vertex_f32(&geometry.skin_weights, 4, original_vertex_indices),
3597        edge_scale: gather_vertex_f32(&geometry.edge_scale, 1, original_vertex_indices),
3598        material_groups: vec![PmxParsedMaterialGroup {
3599            start: 0,
3600            count: local_index_count,
3601            material_index: 0,
3602        }],
3603        sdef: PmxParsedSdef {
3604            skinning_modes: gather_vertex_string(
3605                &geometry.sdef.skinning_modes,
3606                original_vertex_indices,
3607            ),
3608            enabled: gather_vertex_f32(&geometry.sdef.enabled, 1, original_vertex_indices),
3609            c: gather_vertex_f32(&geometry.sdef.c, 3, original_vertex_indices),
3610            r0: gather_vertex_f32(&geometry.sdef.r0, 3, original_vertex_indices),
3611            r1: gather_vertex_f32(&geometry.sdef.r1, 3, original_vertex_indices),
3612            rw0: gather_vertex_f32(&geometry.sdef.rw0, 3, original_vertex_indices),
3613            rw1: gather_vertex_f32(&geometry.sdef.rw1, 3, original_vertex_indices),
3614        },
3615        qdef: PmxParsedQdef {
3616            enabled: gather_vertex_f32(&geometry.qdef.enabled, 1, original_vertex_indices),
3617        },
3618    }
3619}
3620
3621fn gather_vertex_f32(values: &[f32], stride: usize, original_vertex_indices: &[u32]) -> Vec<f32> {
3622    let mut out = Vec::with_capacity(original_vertex_indices.len() * stride);
3623    for &index in original_vertex_indices {
3624        let start = index as usize * stride;
3625        let end = start + stride;
3626        if let Some(slice) = values.get(start..end) {
3627            out.extend_from_slice(slice);
3628        }
3629    }
3630    out
3631}
3632
3633fn gather_vertex_string(values: &[String], original_vertex_indices: &[u32]) -> Vec<String> {
3634    let mut out = Vec::with_capacity(original_vertex_indices.len());
3635    for &index in original_vertex_indices {
3636        if let Some(value) = values.get(index as usize) {
3637            out.push(value.clone());
3638        }
3639    }
3640    out
3641}
3642
3643fn gather_vertex_u32(values: &[u32], stride: usize, original_vertex_indices: &[u32]) -> Vec<u32> {
3644    let mut out = Vec::with_capacity(original_vertex_indices.len() * stride);
3645    for &index in original_vertex_indices {
3646        let start = index as usize * stride;
3647        let end = start + stride;
3648        if let Some(slice) = values.get(start..end) {
3649            out.extend_from_slice(slice);
3650        }
3651    }
3652    out
3653}
3654
3655fn remap_pmx_material_split_morphs(
3656    model: &PmxParsedModel,
3657    vertex_remap: &HashMap<u32, u32>,
3658    original_material_index: usize,
3659    mesh_index: usize,
3660) -> (
3661    Vec<PmxParsedMorph>,
3662    Vec<PmxMaterialSplitMorphIndexMap>,
3663    Vec<PmxParserDiagnostic>,
3664) {
3665    let mut diagnostics = Vec::new();
3666    let mut direct_morphs = vec![None; model.morphs.len()];
3667    let mut survives = vec![false; model.morphs.len()];
3668    let mut morph_remap = HashMap::<usize, usize>::new();
3669
3670    for (original_index, morph) in model.morphs.iter().enumerate() {
3671        match morph.kind.as_str() {
3672            "vertex" | "uv" | "additionalUv" | "material" => {
3673                let remapped = remap_direct_pmx_material_split_morph(
3674                    morph,
3675                    vertex_remap,
3676                    original_material_index,
3677                );
3678                if pmx_material_split_morph_has_offsets(&remapped) {
3679                    direct_morphs[original_index] = Some(remapped);
3680                    survives[original_index] = true;
3681                }
3682            }
3683            "bone" => {
3684                if !morph.bone_offsets.is_empty() {
3685                    diagnostics.push(pmx_material_split_diagnostic(
3686                        "PMX_MATERIAL_SPLIT_BONE_MORPH_SHARED_SKELETON",
3687                        format!(
3688                            "mesh {mesh_index} excludes bone morph {original_index} because skeleton morphs are shared"
3689                        ),
3690                    ));
3691                }
3692            }
3693            "impulse" => {
3694                if !morph.impulse_offsets.is_empty() {
3695                    diagnostics.push(pmx_material_split_diagnostic(
3696                        "PMX_MATERIAL_SPLIT_IMPULSE_MORPH_UNSUPPORTED",
3697                        format!(
3698                            "mesh {mesh_index} excludes impulse morph {original_index} because physics morphs are not exposed in the initial material split ABI"
3699                        ),
3700                    ));
3701                }
3702            }
3703            "group" | "flip" => {}
3704            _ => {
3705                diagnostics.push(pmx_material_split_diagnostic(
3706                    "PMX_MATERIAL_SPLIT_MORPH_KIND_UNSUPPORTED",
3707                    format!(
3708                        "mesh {mesh_index} excludes morph {original_index} with unsupported kind {}",
3709                        morph.kind
3710                    ),
3711                ));
3712            }
3713        }
3714    }
3715
3716    let mut changed = true;
3717    while changed {
3718        changed = false;
3719        for (original_index, morph) in model.morphs.iter().enumerate() {
3720            if survives[original_index] || (morph.kind != "group" && morph.kind != "flip") {
3721                continue;
3722            }
3723            if pmx_material_split_reference_offsets(morph)
3724                .iter()
3725                .any(|offset| {
3726                    offset.morph_index >= 0
3727                        && survives
3728                            .get(offset.morph_index as usize)
3729                            .copied()
3730                            .unwrap_or(false)
3731                })
3732            {
3733                survives[original_index] = true;
3734                changed = true;
3735            }
3736        }
3737    }
3738
3739    for (original_index, survives) in survives.iter().copied().enumerate() {
3740        if survives {
3741            let local_index = morph_remap.len();
3742            morph_remap.insert(original_index, local_index);
3743        }
3744    }
3745
3746    let mut morphs = Vec::with_capacity(morph_remap.len());
3747    for (original_index, morph) in model.morphs.iter().enumerate() {
3748        if !morph_remap.contains_key(&original_index) {
3749            continue;
3750        }
3751        let remapped = match direct_morphs[original_index].take() {
3752            Some(remapped) => remapped,
3753            None => remap_reference_pmx_material_split_morph(morph, &morph_remap),
3754        };
3755        morphs.push((original_index, remapped));
3756    }
3757
3758    let morph_index_map = morphs
3759        .iter()
3760        .enumerate()
3761        .map(
3762            |(local_index, (original_index, _))| PmxMaterialSplitMorphIndexMap {
3763                original_index: *original_index,
3764                local_index,
3765            },
3766        )
3767        .collect();
3768    let morphs = morphs
3769        .into_iter()
3770        .map(|(_, morph)| morph)
3771        .collect::<Vec<_>>();
3772
3773    (morphs, morph_index_map, diagnostics)
3774}
3775
3776fn remap_direct_pmx_material_split_morph(
3777    morph: &PmxParsedMorph,
3778    vertex_remap: &HashMap<u32, u32>,
3779    original_material_index: usize,
3780) -> PmxParsedMorph {
3781    let mut remapped = empty_pmx_material_split_morph_like(morph);
3782    remapped.vertex_offsets = morph
3783        .vertex_offsets
3784        .iter()
3785        .filter_map(|offset| {
3786            vertex_remap
3787                .get(&offset.vertex_index)
3788                .map(|&vertex_index| PmxParsedVertexMorphOffset {
3789                    vertex_index,
3790                    position: offset.position,
3791                })
3792        })
3793        .collect();
3794    remapped.uv_offsets = morph
3795        .uv_offsets
3796        .iter()
3797        .filter_map(|offset| {
3798            vertex_remap
3799                .get(&offset.vertex_index)
3800                .map(|&vertex_index| PmxParsedUvMorphOffset {
3801                    vertex_index,
3802                    uv: offset.uv,
3803                })
3804        })
3805        .collect();
3806    remapped.additional_uv_offsets = morph
3807        .additional_uv_offsets
3808        .iter()
3809        .filter_map(|offset| {
3810            vertex_remap.get(&offset.vertex_index).map(|&vertex_index| {
3811                PmxParsedAdditionalUvMorphOffset {
3812                    vertex_index,
3813                    uv_index: offset.uv_index,
3814                    uv: offset.uv,
3815                }
3816            })
3817        })
3818        .collect();
3819    remapped.material_offsets = morph
3820        .material_offsets
3821        .iter()
3822        .filter_map(|offset| {
3823            if offset.material_index == -1
3824                || offset.material_index as usize == original_material_index
3825            {
3826                let mut remapped = offset.clone();
3827                if remapped.material_index >= 0 {
3828                    remapped.material_index = 0;
3829                }
3830                Some(remapped)
3831            } else {
3832                None
3833            }
3834        })
3835        .collect();
3836    remapped
3837}
3838
3839fn remap_reference_pmx_material_split_morph(
3840    morph: &PmxParsedMorph,
3841    morph_remap: &HashMap<usize, usize>,
3842) -> PmxParsedMorph {
3843    let mut remapped = empty_pmx_material_split_morph_like(morph);
3844    if morph.kind == "group" {
3845        remapped.group_offsets =
3846            remap_pmx_material_split_morph_refs(&morph.group_offsets, morph_remap);
3847    } else {
3848        remapped.flip_offsets =
3849            remap_pmx_material_split_morph_refs(&morph.flip_offsets, morph_remap);
3850    }
3851    remapped
3852}
3853
3854fn pmx_material_split_reference_offsets(morph: &PmxParsedMorph) -> &[PmxParsedGroupMorphOffset] {
3855    if morph.kind == "group" {
3856        &morph.group_offsets
3857    } else {
3858        &morph.flip_offsets
3859    }
3860}
3861
3862fn remap_pmx_material_split_morph_refs(
3863    offsets: &[PmxParsedGroupMorphOffset],
3864    morph_remap: &HashMap<usize, usize>,
3865) -> Vec<PmxParsedGroupMorphOffset> {
3866    offsets
3867        .iter()
3868        .filter_map(|offset| {
3869            if offset.morph_index < 0 {
3870                return None;
3871            }
3872            morph_remap
3873                .get(&(offset.morph_index as usize))
3874                .map(|&morph_index| PmxParsedGroupMorphOffset {
3875                    morph_index: morph_index as i32,
3876                    weight: offset.weight,
3877                })
3878        })
3879        .collect()
3880}
3881
3882fn empty_pmx_material_split_morph_like(morph: &PmxParsedMorph) -> PmxParsedMorph {
3883    PmxParsedMorph {
3884        name: morph.name.clone(),
3885        english_name: morph.english_name.clone(),
3886        panel: morph.panel.clone(),
3887        kind: morph.kind.clone(),
3888        vertex_offsets: Vec::new(),
3889        group_offsets: Vec::new(),
3890        bone_offsets: Vec::new(),
3891        uv_offsets: Vec::new(),
3892        additional_uv_offsets: Vec::new(),
3893        material_offsets: Vec::new(),
3894        flip_offsets: Vec::new(),
3895        impulse_offsets: Vec::new(),
3896    }
3897}
3898
3899fn pmx_material_split_morph_has_offsets(morph: &PmxParsedMorph) -> bool {
3900    !morph.vertex_offsets.is_empty()
3901        || !morph.group_offsets.is_empty()
3902        || !morph.bone_offsets.is_empty()
3903        || !morph.uv_offsets.is_empty()
3904        || !morph.additional_uv_offsets.is_empty()
3905        || !morph.material_offsets.is_empty()
3906        || !morph.flip_offsets.is_empty()
3907        || !morph.impulse_offsets.is_empty()
3908}
3909
3910fn pmx_material_split_diagnostic(
3911    code: impl Into<String>,
3912    message: impl Into<String>,
3913) -> PmxParserDiagnostic {
3914    PmxParserDiagnostic {
3915        level: "info".to_owned(),
3916        code: code.into(),
3917        message: message.into(),
3918    }
3919}
3920
3921fn read_parsed_textures(
3922    r: &mut Reader<'_>,
3923    encoding: TextEncoding,
3924) -> Result<Vec<String>, ImportError> {
3925    let count = read_section_count_with_min_record(r, 4)?;
3926    let mut textures = Vec::with_capacity(count);
3927    for _ in 0..count {
3928        textures.push(r.read_string(encoding)?);
3929    }
3930    Ok(textures)
3931}
3932
3933fn read_parsed_materials(
3934    r: &mut Reader<'_>,
3935    header: &PmxHeader,
3936    textures: &[String],
3937) -> Result<Vec<PmxParsedMaterial>, ImportError> {
3938    let count = read_section_count_with_min_record(r, 4 + 4 + 16 + 16 + 12 + 1 + 16 + 4)?;
3939    let mut materials = Vec::with_capacity(count);
3940    for _ in 0..count {
3941        let name = r.read_string(header.encoding)?;
3942        let english_name = r.read_string(header.encoding)?;
3943        let diffuse = r.read_vec4_array()?;
3944        let specular = r.read_vec3_array()?;
3945        let specular_power = r.read_f32_le()?;
3946        let ambient = r.read_vec3_array()?;
3947        let flag_bits = r.read_u8()?;
3948        let edge_color = r.read_vec4_array()?;
3949        let edge_size = r.read_f32_le()?;
3950        let texture_index = r.read_sized_index(header.texture_index_size)?;
3951        let sphere_texture_index = r.read_sized_index(header.texture_index_size)?;
3952        let sphere_mode_raw = r.read_u8()?;
3953        let toon_flag = r.read_u8()?;
3954        let (toon_texture_path, shared_toon_index) = if toon_flag == 0 {
3955            let toon_texture_index = r.read_sized_index(header.texture_index_size)?;
3956            (texture_path(textures, toon_texture_index), None)
3957        } else {
3958            (String::new(), Some(r.read_u8()?))
3959        };
3960        let _memo = r.read_string(header.encoding)?;
3961        let face_count = r.read_i32_le()? / 3;
3962        materials.push(PmxParsedMaterial {
3963            name,
3964            english_name,
3965            texture_path: texture_path(textures, texture_index),
3966            sphere_texture_path: texture_path(textures, sphere_texture_index),
3967            sphere_mode: match sphere_mode_raw {
3968                1 => "multiply",
3969                2 => "add",
3970                3 => "subTexture",
3971                _ => "none",
3972            }
3973            .to_owned(),
3974            toon_texture_path,
3975            shared_toon_index,
3976            diffuse,
3977            specular,
3978            specular_power,
3979            ambient,
3980            edge_color,
3981            edge_size,
3982            flags: PmxParsedMaterialFlags {
3983                double_sided: flag_bits & 0x01 != 0,
3984                ground_shadow: flag_bits & 0x02 != 0,
3985                self_shadow_map: flag_bits & 0x04 != 0,
3986                self_shadow: flag_bits & 0x08 != 0,
3987                edge: flag_bits & 0x10 != 0,
3988                vertex_color: flag_bits & 0x20 != 0,
3989                point_draw: flag_bits & 0x40 != 0,
3990                line_draw: flag_bits & 0x80 != 0,
3991            },
3992            face_count,
3993        });
3994    }
3995    Ok(materials)
3996}
3997
3998fn texture_path(textures: &[String], index: i32) -> String {
3999    if index < 0 {
4000        String::new()
4001    } else {
4002        textures.get(index as usize).cloned().unwrap_or_default()
4003    }
4004}
4005
4006fn build_material_groups(materials: &[PmxParsedMaterial]) -> Vec<PmxParsedMaterialGroup> {
4007    let mut start = 0usize;
4008    materials
4009        .iter()
4010        .enumerate()
4011        .map(|(material_index, material)| {
4012            let count = (material.face_count.max(0) as usize) * 3;
4013            let group = PmxParsedMaterialGroup {
4014                start,
4015                count,
4016                material_index,
4017            };
4018            start += count;
4019            group
4020        })
4021        .collect()
4022}
4023
4024fn pmx_text_encoding(metadata: &PmxParsedMetadata) -> TextEncoding {
4025    match metadata.encoding.as_str() {
4026        "utf-16-le" => TextEncoding::Utf16Le,
4027        _ => TextEncoding::Utf8,
4028    }
4029}
4030
4031fn write_pmx_string(out: &mut Vec<u8>, text: &str, encoding: TextEncoding) {
4032    match encoding {
4033        TextEncoding::Utf8 => {
4034            write_i32(out, text.len() as i32);
4035            out.extend_from_slice(text.as_bytes());
4036        }
4037        TextEncoding::Utf16Le => {
4038            let bytes = text
4039                .encode_utf16()
4040                .flat_map(u16::to_le_bytes)
4041                .collect::<Vec<_>>();
4042            write_i32(out, bytes.len() as i32);
4043            out.extend_from_slice(&bytes);
4044        }
4045    }
4046}
4047
4048fn write_sized_index(out: &mut Vec<u8>, value: i32, size: u8) {
4049    match size {
4050        1 => out.push(value as i8 as u8),
4051        2 => out.extend_from_slice(&(value as i16).to_le_bytes()),
4052        4 => out.extend_from_slice(&value.to_le_bytes()),
4053        _ => {}
4054    }
4055}
4056
4057fn write_vertex_index(out: &mut Vec<u8>, value: u32, size: u8) {
4058    match size {
4059        1 => out.push(value as u8),
4060        2 => out.extend_from_slice(&(value as u16).to_le_bytes()),
4061        4 => out.extend_from_slice(&(value as i32).to_le_bytes()),
4062        _ => {}
4063    }
4064}
4065
4066fn write_pmx_vertex_skinning(out: &mut Vec<u8>, model: &PmxParsedModel, vertex_index: usize) {
4067    match pmx_vertex_skinning_mode(&model.geometry, vertex_index) {
4068        PMX_SKINNING_MODE_BDEF1 => {
4069            out.push(0);
4070            write_pmx_vertex_bone_index(out, model, vertex_index, 0);
4071        }
4072        PMX_SKINNING_MODE_BDEF2 => {
4073            out.push(1);
4074            write_pmx_vertex_bone_index(out, model, vertex_index, 0);
4075            write_pmx_vertex_bone_index(out, model, vertex_index, 1);
4076            write_f32(
4077                out,
4078                pmx_vertex_skin_weight(&model.geometry, vertex_index, 0),
4079            );
4080        }
4081        PMX_SKINNING_MODE_SDEF => {
4082            out.push(3);
4083            write_pmx_vertex_bone_index(out, model, vertex_index, 0);
4084            write_pmx_vertex_bone_index(out, model, vertex_index, 1);
4085            write_f32(
4086                out,
4087                pmx_vertex_skin_weight(&model.geometry, vertex_index, 0),
4088            );
4089            write_f32_slice(
4090                out,
4091                &pmx_vertex_sdef_vec3(&model.geometry.sdef.c, vertex_index),
4092            );
4093            write_f32_slice(
4094                out,
4095                &pmx_vertex_sdef_vec3(&model.geometry.sdef.r0, vertex_index),
4096            );
4097            write_f32_slice(
4098                out,
4099                &pmx_vertex_sdef_vec3(&model.geometry.sdef.r1, vertex_index),
4100            );
4101        }
4102        PMX_SKINNING_MODE_QDEF => {
4103            out.push(4);
4104            write_pmx_vertex_bdef4_skinning(out, model, vertex_index);
4105        }
4106        _ => {
4107            out.push(2);
4108            write_pmx_vertex_bdef4_skinning(out, model, vertex_index);
4109        }
4110    }
4111}
4112
4113fn pmx_vertex_skinning_mode(geometry: &PmxParsedGeometry, vertex_index: usize) -> &str {
4114    if let Some(mode) = geometry.sdef.skinning_modes.get(vertex_index) {
4115        return mode.as_str();
4116    }
4117    if geometry
4118        .sdef
4119        .enabled
4120        .get(vertex_index)
4121        .copied()
4122        .unwrap_or(0.0)
4123        > 0.5
4124    {
4125        PMX_SKINNING_MODE_SDEF
4126    } else if geometry
4127        .qdef
4128        .enabled
4129        .get(vertex_index)
4130        .copied()
4131        .unwrap_or(0.0)
4132        > 0.5
4133    {
4134        PMX_SKINNING_MODE_QDEF
4135    } else {
4136        PMX_SKINNING_MODE_BDEF4
4137    }
4138}
4139
4140fn write_pmx_vertex_bdef4_skinning(out: &mut Vec<u8>, model: &PmxParsedModel, vertex_index: usize) {
4141    for slot in 0..4 {
4142        write_pmx_vertex_bone_index(out, model, vertex_index, slot);
4143    }
4144    for slot in 0..4 {
4145        write_f32(
4146            out,
4147            pmx_vertex_skin_weight(&model.geometry, vertex_index, slot),
4148        );
4149    }
4150}
4151
4152fn write_pmx_vertex_bone_index(
4153    out: &mut Vec<u8>,
4154    model: &PmxParsedModel,
4155    vertex_index: usize,
4156    slot: usize,
4157) {
4158    write_sized_index(
4159        out,
4160        model
4161            .geometry
4162            .skin_indices
4163            .get(vertex_index * 4 + slot)
4164            .copied()
4165            .unwrap_or(0) as i32,
4166        model.metadata.index_sizes.bone,
4167    );
4168}
4169
4170fn pmx_vertex_skin_weight(geometry: &PmxParsedGeometry, vertex_index: usize, slot: usize) -> f32 {
4171    geometry
4172        .skin_weights
4173        .get(vertex_index * 4 + slot)
4174        .copied()
4175        .unwrap_or(if slot == 0 { 1.0 } else { 0.0 })
4176}
4177
4178fn pmx_vertex_sdef_vec3(values: &[f32], vertex_index: usize) -> [f32; 3] {
4179    let start = vertex_index * 3;
4180    [
4181        values.get(start).copied().unwrap_or(0.0),
4182        values.get(start + 1).copied().unwrap_or(0.0),
4183        values.get(start + 2).copied().unwrap_or(0.0),
4184    ]
4185}
4186
4187fn material_flag_bits(flags: &PmxParsedMaterialFlags) -> u8 {
4188    u8::from(flags.double_sided)
4189        | (u8::from(flags.ground_shadow) << 1)
4190        | (u8::from(flags.self_shadow_map) << 2)
4191        | (u8::from(flags.self_shadow) << 3)
4192        | (u8::from(flags.edge) << 4)
4193        | (u8::from(flags.vertex_color) << 5)
4194        | (u8::from(flags.point_draw) << 6)
4195        | (u8::from(flags.line_draw) << 7)
4196}
4197
4198fn bone_flag_bits(flags: &PmxParsedBoneFlags) -> u16 {
4199    u16::from(flags.indexed_tail)
4200        | (u16::from(flags.rotatable) << 1)
4201        | (u16::from(flags.translatable) << 2)
4202        | (u16::from(flags.visible) << 3)
4203        | (u16::from(flags.enabled) << 4)
4204        | if flags.ik { BONE_FLAG_IK } else { 0 }
4205        | if flags.append_local {
4206            BONE_FLAG_LOCAL_APPEND
4207        } else {
4208            0
4209        }
4210        | if flags.append_rotate {
4211            BONE_FLAG_APPEND_ROTATE
4212        } else {
4213            0
4214        }
4215        | if flags.append_translate {
4216            BONE_FLAG_APPEND_TRANSLATE
4217        } else {
4218            0
4219        }
4220        | if flags.fixed_axis {
4221            BONE_FLAG_FIXED_AXIS
4222        } else {
4223            0
4224        }
4225        | if flags.local_axis {
4226            BONE_FLAG_LOCAL_AXIS
4227        } else {
4228            0
4229        }
4230        | if flags.transform_after_physics {
4231            BONE_FLAG_TRANSFORM_AFTER_PHYSICS
4232        } else {
4233            0
4234        }
4235        | if flags.external_parent_transform {
4236            BONE_FLAG_EXTERNAL_PARENT
4237        } else {
4238            0
4239        }
4240}
4241
4242fn collect_pmx_textures(materials: &[PmxParsedMaterial]) -> Vec<String> {
4243    let mut textures = Vec::new();
4244    for texture in materials.iter().flat_map(|material| {
4245        [
4246            material.texture_path.as_str(),
4247            material.sphere_texture_path.as_str(),
4248            material.toon_texture_path.as_str(),
4249        ]
4250    }) {
4251        if !texture.is_empty() && !textures.iter().any(|existing| existing == texture) {
4252            textures.push(texture.to_owned());
4253        }
4254    }
4255    textures
4256}
4257
4258fn texture_index(textures: &[String], texture: &str) -> i32 {
4259    if texture.is_empty() {
4260        -1
4261    } else {
4262        textures
4263            .iter()
4264            .position(|candidate| candidate == texture)
4265            .map(|index| index as i32)
4266            .unwrap_or(-1)
4267    }
4268}
4269
4270fn morph_type_byte(morph: &PmxParsedMorph) -> u8 {
4271    match morph.kind.as_str() {
4272        "group" => 0,
4273        "vertex" => 1,
4274        "bone" => 2,
4275        "uv" => 3,
4276        "additionalUv" => morph
4277            .additional_uv_offsets
4278            .first()
4279            .map(|offset| offset.uv_index.saturating_add(4).min(7))
4280            .unwrap_or(4),
4281        "material" => 8,
4282        "flip" => 9,
4283        "impulse" => 10,
4284        _ => 0,
4285    }
4286}
4287
4288fn morph_panel_name(panel: u8) -> String {
4289    match panel {
4290        0 => "system",
4291        1 => "brow",
4292        2 => "eye",
4293        3 => "mouth",
4294        4 => "other",
4295        _ => "unknown",
4296    }
4297    .to_owned()
4298}
4299
4300fn morph_panel_byte(panel: &str) -> u8 {
4301    morph_panel_byte_checked(panel).unwrap_or(4)
4302}
4303
4304fn morph_panel_byte_checked(panel: &str) -> Option<u8> {
4305    match panel {
4306        "system" => Some(0),
4307        "brow" => Some(1),
4308        "eye" => Some(2),
4309        "mouth" => Some(3),
4310        "other" => Some(4),
4311        _ => None,
4312    }
4313}
4314
4315fn morph_offset_count(morph: &PmxParsedMorph, morph_type: u8) -> usize {
4316    match morph_type {
4317        0 => morph.group_offsets.len(),
4318        1 => morph.vertex_offsets.len(),
4319        2 => morph.bone_offsets.len(),
4320        3 => morph.uv_offsets.len(),
4321        4..=7 => morph.additional_uv_offsets.len(),
4322        8 => morph.material_offsets.len(),
4323        9 => morph.flip_offsets.len(),
4324        10 => morph.impulse_offsets.len(),
4325        _ => 0,
4326    }
4327}
4328
4329fn write_group_morph_offsets(
4330    out: &mut Vec<u8>,
4331    offsets: &[PmxParsedGroupMorphOffset],
4332    model: &PmxParsedModel,
4333) {
4334    for offset in offsets {
4335        write_sized_index(out, offset.morph_index, model.metadata.index_sizes.morph);
4336        write_f32(out, offset.weight);
4337    }
4338}
4339
4340fn write_uv_morph_offsets(
4341    out: &mut Vec<u8>,
4342    offsets: &[PmxParsedUvMorphOffset],
4343    model: &PmxParsedModel,
4344) {
4345    for offset in offsets {
4346        write_vertex_index(out, offset.vertex_index, model.metadata.index_sizes.vertex);
4347        write_f32_slice(out, &offset.uv);
4348    }
4349}
4350
4351fn read_parsed_bones(
4352    r: &mut Reader<'_>,
4353    header: &PmxHeader,
4354    count: usize,
4355) -> Result<Vec<PmxParsedBone>, ImportError> {
4356    r.require_record_bytes(count, 4 + 4 + 12)?;
4357    let mut bones = Vec::with_capacity(count);
4358    for _ in 0..count {
4359        let name = r.read_string(header.encoding)?;
4360        let english_name = r.read_string(header.encoding)?;
4361        let position = r.read_vec3_array()?;
4362        let parent_index = r.read_sized_index(header.bone_index_size)?;
4363        let layer = r.read_i32_le()?;
4364        let bits = r.read_u16_le()?;
4365        let flags = parsed_bone_flags(bits);
4366        let (tail_index, tail_position) = if flags.indexed_tail {
4367            (r.read_sized_index(header.bone_index_size)?, None)
4368        } else {
4369            (-1, Some(r.read_vec3_array()?))
4370        };
4371        let append_transform = if flags.append_rotate || flags.append_translate {
4372            Some(PmxParsedAppendTransform {
4373                parent_index: r.read_sized_index(header.bone_index_size)?,
4374                weight: r.read_f32_le()?,
4375            })
4376        } else {
4377            None
4378        };
4379        let fixed_axis = if flags.fixed_axis {
4380            Some(r.read_vec3_array()?)
4381        } else {
4382            None
4383        };
4384        let local_axis = if flags.local_axis {
4385            Some(PmxParsedLocalAxis {
4386                x: r.read_vec3_array()?,
4387                z: r.read_vec3_array()?,
4388            })
4389        } else {
4390            None
4391        };
4392        let external_parent_key = if flags.external_parent_transform {
4393            Some(r.read_i32_le()?)
4394        } else {
4395            None
4396        };
4397        let ik = if flags.ik {
4398            let target_index = r.read_sized_index(header.bone_index_size)?;
4399            let loop_count = r.read_i32_le()?;
4400            let limit_angle = r.read_f32_le()?;
4401            let link_count =
4402                read_section_count_with_min_record(r, header.bone_index_size as usize + 1)?;
4403            let mut links = Vec::with_capacity(link_count);
4404            for _ in 0..link_count {
4405                let bone_index = r.read_sized_index(header.bone_index_size)?;
4406                let has_limit = r.read_u8()? != 0;
4407                links.push(PmxParsedIkLink {
4408                    bone_index,
4409                    limits: if has_limit {
4410                        Some(PmxParsedIkLimit {
4411                            lower: r.read_vec3_array()?,
4412                            upper: r.read_vec3_array()?,
4413                        })
4414                    } else {
4415                        None
4416                    },
4417                });
4418            }
4419            Some(PmxParsedIk {
4420                target_index,
4421                loop_count,
4422                limit_angle,
4423                links,
4424            })
4425        } else {
4426            None
4427        };
4428        bones.push(PmxParsedBone {
4429            name,
4430            english_name,
4431            parent_index,
4432            layer,
4433            position,
4434            tail_index,
4435            tail_position,
4436            flags,
4437            append_transform,
4438            fixed_axis,
4439            local_axis,
4440            external_parent_key,
4441            ik,
4442        });
4443    }
4444    Ok(bones)
4445}
4446
4447fn parsed_bone_flags(bits: u16) -> PmxParsedBoneFlags {
4448    PmxParsedBoneFlags {
4449        indexed_tail: bits & BONE_FLAG_TAIL_INDEX != 0,
4450        rotatable: bits & 0x0002 != 0,
4451        translatable: bits & 0x0004 != 0,
4452        visible: bits & 0x0008 != 0,
4453        enabled: bits & 0x0010 != 0,
4454        ik: bits & BONE_FLAG_IK != 0,
4455        append_local: bits & BONE_FLAG_LOCAL_APPEND != 0,
4456        append_rotate: bits & BONE_FLAG_APPEND_ROTATE != 0,
4457        append_translate: bits & BONE_FLAG_APPEND_TRANSLATE != 0,
4458        fixed_axis: bits & BONE_FLAG_FIXED_AXIS != 0,
4459        local_axis: bits & BONE_FLAG_LOCAL_AXIS != 0,
4460        transform_after_physics: bits & 0x1000 != 0,
4461        external_parent_transform: bits & BONE_FLAG_EXTERNAL_PARENT != 0,
4462    }
4463}
4464
4465fn read_parsed_morphs(
4466    r: &mut Reader<'_>,
4467    header: &PmxHeader,
4468) -> Result<Vec<PmxParsedMorph>, ImportError> {
4469    let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 1 + 4)?;
4470    let mut morphs = Vec::with_capacity(count);
4471    for _ in 0..count {
4472        let name = r.read_string(header.encoding)?;
4473        let english_name = r.read_string(header.encoding)?;
4474        let panel = morph_panel_name(r.read_u8()?);
4475        let morph_type = r.read_u8()?;
4476        let min_offset_size = match morph_type {
4477            0 => header.morph_index_size as usize + 4,
4478            1 => header.vertex_index_size as usize + 12,
4479            2 => header.bone_index_size as usize + 12 + 16,
4480            3..=7 => header.vertex_index_size as usize + 16,
4481            8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
4482            9 => header.morph_index_size as usize + 4,
4483            10 => header.rigidbody_index_size as usize + 1 + 12 + 12,
4484            _ => return Err(ImportError::SectionOverflow),
4485        };
4486        let offset_count = read_section_count_with_min_record(r, min_offset_size)?;
4487        let mut morph = PmxParsedMorph {
4488            name,
4489            english_name,
4490            panel,
4491            kind: match morph_type {
4492                0 => "group",
4493                1 => "vertex",
4494                2 => "bone",
4495                3 => "uv",
4496                4..=7 => "additionalUv",
4497                8 => "material",
4498                9 => "flip",
4499                10 => "impulse",
4500                _ => "unknown",
4501            }
4502            .to_owned(),
4503            vertex_offsets: Vec::new(),
4504            group_offsets: Vec::new(),
4505            bone_offsets: Vec::new(),
4506            uv_offsets: Vec::new(),
4507            additional_uv_offsets: Vec::new(),
4508            material_offsets: Vec::new(),
4509            flip_offsets: Vec::new(),
4510            impulse_offsets: Vec::new(),
4511        };
4512        for _ in 0..offset_count {
4513            match morph_type {
4514                0 => morph.group_offsets.push(PmxParsedGroupMorphOffset {
4515                    morph_index: r.read_sized_index(header.morph_index_size)?,
4516                    weight: r.read_f32_le()?,
4517                }),
4518                1 => morph.vertex_offsets.push(PmxParsedVertexMorphOffset {
4519                    vertex_index: r.read_vertex_index(header.vertex_index_size)?,
4520                    position: r.read_vec3_array()?,
4521                }),
4522                2 => morph.bone_offsets.push(PmxParsedBoneMorphOffset {
4523                    bone_index: r.read_sized_index(header.bone_index_size)?,
4524                    translation: r.read_vec3_array()?,
4525                    rotation: r.read_vec4_array()?,
4526                }),
4527                3 => morph.uv_offsets.push(PmxParsedUvMorphOffset {
4528                    vertex_index: r.read_vertex_index(header.vertex_index_size)?,
4529                    uv: r.read_vec4_array()?,
4530                }),
4531                4..=7 => morph
4532                    .additional_uv_offsets
4533                    .push(PmxParsedAdditionalUvMorphOffset {
4534                        vertex_index: r.read_vertex_index(header.vertex_index_size)?,
4535                        uv_index: morph_type - 4,
4536                        uv: r.read_vec4_array()?,
4537                    }),
4538                8 => morph.material_offsets.push(PmxParsedMaterialMorphOffset {
4539                    material_index: r.read_sized_index(header.material_index_size)?,
4540                    operation: if r.read_u8()? == 0 {
4541                        "multiply".to_owned()
4542                    } else {
4543                        "add".to_owned()
4544                    },
4545                    diffuse: r.read_vec4_array()?,
4546                    specular: r.read_vec3_array()?,
4547                    specular_power: r.read_f32_le()?,
4548                    ambient: r.read_vec3_array()?,
4549                    edge_color: r.read_vec4_array()?,
4550                    edge_size: r.read_f32_le()?,
4551                    texture_factor: r.read_vec4_array()?,
4552                    sphere_texture_factor: r.read_vec4_array()?,
4553                    toon_texture_factor: r.read_vec4_array()?,
4554                }),
4555                9 => morph.flip_offsets.push(PmxParsedGroupMorphOffset {
4556                    morph_index: r.read_sized_index(header.morph_index_size)?,
4557                    weight: r.read_f32_le()?,
4558                }),
4559                10 => morph.impulse_offsets.push(PmxParsedImpulseMorphOffset {
4560                    rigid_body_index: r.read_sized_index(header.rigidbody_index_size)?,
4561                    local: r.read_u8()? != 0,
4562                    velocity: r.read_vec3_array()?,
4563                    torque: r.read_vec3_array()?,
4564                }),
4565                _ => return Err(ImportError::SectionOverflow),
4566            }
4567        }
4568        morphs.push(morph);
4569    }
4570    Ok(morphs)
4571}
4572
4573fn read_parsed_display_frames(
4574    r: &mut Reader<'_>,
4575    header: &PmxHeader,
4576) -> Result<Vec<PmxParsedDisplayFrame>, ImportError> {
4577    let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 4)?;
4578    let mut frames = Vec::with_capacity(count);
4579    for _ in 0..count {
4580        let name = r.read_string(header.encoding)?;
4581        let english_name = r.read_string(header.encoding)?;
4582        let special = r.read_u8()? == 1;
4583        let item_count = read_section_count_with_min_record(r, 1)?;
4584        let mut items = Vec::with_capacity(item_count);
4585        for _ in 0..item_count {
4586            let kind = r.read_u8()?;
4587            let index = match kind {
4588                0 => r.read_sized_index(header.bone_index_size)?,
4589                1 => r.read_sized_index(header.morph_index_size)?,
4590                _ => return Err(ImportError::SectionOverflow),
4591            };
4592            items.push(PmxParsedDisplayFrameElement {
4593                kind: if kind == 0 {
4594                    "bone".to_owned()
4595                } else {
4596                    "morph".to_owned()
4597                },
4598                index,
4599            });
4600        }
4601        frames.push(PmxParsedDisplayFrame {
4602            name,
4603            english_name,
4604            special,
4605            frames: items,
4606        });
4607    }
4608    Ok(frames)
4609}
4610
4611fn read_parsed_rigid_bodies(
4612    r: &mut Reader<'_>,
4613    header: &PmxHeader,
4614) -> Result<Vec<PmxParsedRigidBody>, ImportError> {
4615    let count = read_section_count_with_min_record(
4616        r,
4617        4 + 4 + header.bone_index_size as usize + 1 + 2 + 1 + 12 + 12 + 12 + 4 + 4 + 4 + 4 + 4 + 1,
4618    )?;
4619    let mut bodies = Vec::with_capacity(count);
4620    for _ in 0..count {
4621        bodies.push(PmxParsedRigidBody {
4622            name: r.read_string(header.encoding)?,
4623            english_name: r.read_string(header.encoding)?,
4624            bone_index: r.read_sized_index(header.bone_index_size)?,
4625            group: r.read_u8()?,
4626            mask: r.read_u16_le()?,
4627            shape: match r.read_u8()? {
4628                0 => "sphere",
4629                1 => "box",
4630                2 => "capsule",
4631                _ => "unknown",
4632            }
4633            .to_owned(),
4634            size: r.read_vec3_array()?,
4635            position: r.read_vec3_array()?,
4636            rotation: r.read_vec3_array()?,
4637            mass: r.read_f32_le()?,
4638            linear_damping: r.read_f32_le()?,
4639            angular_damping: r.read_f32_le()?,
4640            restitution: r.read_f32_le()?,
4641            friction: r.read_f32_le()?,
4642            mode: match r.read_u8()? {
4643                0 => "static",
4644                1 => "dynamic",
4645                2 => "dynamicBone",
4646                _ => "unknown",
4647            }
4648            .to_owned(),
4649        });
4650    }
4651    Ok(bodies)
4652}
4653
4654fn read_parsed_joints(
4655    r: &mut Reader<'_>,
4656    header: &PmxHeader,
4657) -> Result<Vec<PmxParsedJoint>, ImportError> {
4658    let count = read_section_count_with_min_record(
4659        r,
4660        4 + 4 + 1 + header.rigidbody_index_size as usize * 2 + 12 * 8,
4661    )?;
4662    let mut joints = Vec::with_capacity(count);
4663    for _ in 0..count {
4664        joints.push(PmxParsedJoint {
4665            name: r.read_string(header.encoding)?,
4666            english_name: r.read_string(header.encoding)?,
4667            kind: match r.read_u8()? {
4668                0 => "generic6dofSpring",
4669                1 => "generic6dof",
4670                2 => "point2point",
4671                3 => "coneTwist",
4672                4 => "slider",
4673                5 => "hinge",
4674                _ => "unknown",
4675            }
4676            .to_owned(),
4677            rigid_body_index_a: r.read_sized_index(header.rigidbody_index_size)?,
4678            rigid_body_index_b: r.read_sized_index(header.rigidbody_index_size)?,
4679            position: r.read_vec3_array()?,
4680            rotation: r.read_vec3_array()?,
4681            translation_lower_limit: r.read_vec3_array()?,
4682            translation_upper_limit: r.read_vec3_array()?,
4683            rotation_lower_limit: r.read_vec3_array()?,
4684            rotation_upper_limit: r.read_vec3_array()?,
4685            spring_translation_factor: r.read_vec3_array()?,
4686            spring_rotation_factor: r.read_vec3_array()?,
4687        });
4688    }
4689    Ok(joints)
4690}
4691
4692fn read_parsed_soft_bodies(
4693    r: &mut Reader<'_>,
4694    header: &PmxHeader,
4695) -> Result<Vec<PmxParsedSoftBody>, ImportError> {
4696    let count = read_section_count_with_min_record(
4697        r,
4698        4 + 4
4699            + 1
4700            + header.material_index_size as usize
4701            + 1
4702            + 2
4703            + 1
4704            + 4
4705            + 4
4706            + 4
4707            + 4
4708            + 4
4709            + 12 * 4
4710            + 6 * 4
4711            + 4 * 4
4712            + 3 * 4
4713            + 4
4714            + 4,
4715    )?;
4716    let mut soft_bodies = Vec::with_capacity(count);
4717    for _ in 0..count {
4718        let name = r.read_string(header.encoding)?;
4719        let english_name = r.read_string(header.encoding)?;
4720        let kind = match r.read_u8()? {
4721            0 => "triMesh",
4722            1 => "rope",
4723            _ => "unknown",
4724        }
4725        .to_owned();
4726        let material_index = r.read_sized_index(header.material_index_size)?;
4727        let collision_group = r.read_u8()?;
4728        let collision_mask = r.read_u16_le()?;
4729        let flags = r.read_u8()?;
4730        let bending_constraints_distance = r.read_i32_le()?;
4731        let cluster_count = r.read_i32_le()?;
4732        let total_mass = r.read_f32_le()?;
4733        let collision_margin = r.read_f32_le()?;
4734        r.skip(4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4)?;
4735        let anchor_count = read_section_count_with_min_record(
4736            r,
4737            header.rigidbody_index_size as usize + header.vertex_index_size as usize + 1,
4738        )?;
4739        for _ in 0..anchor_count {
4740            r.read_sized_index(header.rigidbody_index_size)?;
4741            r.read_vertex_index(header.vertex_index_size)?;
4742            r.skip(1)?;
4743        }
4744        let pin_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
4745        for _ in 0..pin_count {
4746            r.read_vertex_index(header.vertex_index_size)?;
4747        }
4748        soft_bodies.push(PmxParsedSoftBody {
4749            name,
4750            english_name,
4751            kind,
4752            material_index,
4753            collision_group,
4754            collision_mask,
4755            flags,
4756            bending_constraints_distance,
4757            cluster_count,
4758            total_mass,
4759            collision_margin,
4760        });
4761    }
4762    Ok(soft_bodies)
4763}
4764
4765#[derive(Debug)]
4766pub struct PmxRuntimeImport {
4767    pub model: ModelArena,
4768    pub bone_names: Vec<String>,
4769    pub bone_name_to_index: HashMap<Vec<u8>, BoneIndex>,
4770    pub morph_name_to_index: HashMap<Vec<u8>, MorphIndex>,
4771    pub ik_solver_bone_name_to_index: HashMap<Vec<u8>, usize>,
4772}
4773
4774pub fn import_pmx_runtime(data: &[u8]) -> Result<PmxRuntimeImport, ImportError> {
4775    let (header, pos) = read_header(data)?;
4776    let pos = skip_model_info(data, &header, pos)?;
4777    let pos = skip_vertices(data, &header, pos)?;
4778    let pos = skip_faces(data, header.vertex_index_size, pos)?;
4779    let pos = skip_textures(data, header.encoding, pos)?;
4780    let pos = skip_materials(data, &header, pos)?;
4781    let (bone_read, pos) = read_bones_with_local_axes(data, &header, pos)?;
4782    let PmxBoneRead {
4783        descriptor: bone_descriptor,
4784        bone_name_bytes,
4785        bone_names,
4786    } = bone_read;
4787    let (morph_names, morph_descriptor, _pos) = read_runtime_morph_descriptor(data, &header, pos)?;
4788
4789    let mut bone_name_to_index = HashMap::with_capacity(bone_name_bytes.len() * 2);
4790    for (i, bytes) in bone_name_bytes.iter().enumerate() {
4791        let index = BoneIndex(i as u32);
4792        bone_name_to_index.insert(bytes.clone(), index);
4793        let decoded = bone_names[i].as_bytes();
4794        if decoded != bytes.as_slice() && !decoded.is_empty() {
4795            bone_name_to_index.insert(decoded.to_vec(), index);
4796        }
4797    }
4798
4799    let mut morph_name_to_index = HashMap::with_capacity(morph_names.name_bytes.len() * 2);
4800    for (i, bytes) in morph_names.name_bytes.iter().enumerate() {
4801        let index = MorphIndex(i as u32);
4802        morph_name_to_index.insert(bytes.clone(), index);
4803        let decoded = morph_names.names[i].as_bytes();
4804        if decoded != bytes.as_slice() && !decoded.is_empty() {
4805            morph_name_to_index.insert(decoded.to_vec(), index);
4806        }
4807    }
4808
4809    let mut ik_solver_bone_name_to_index =
4810        HashMap::with_capacity(bone_descriptor.ik_solvers.len() * 2);
4811    for (solver_idx, solver) in bone_descriptor.ik_solvers.iter().enumerate() {
4812        let bone_idx = solver.ik_bone.as_usize();
4813        if bone_idx < bone_name_bytes.len() {
4814            let bytes = &bone_name_bytes[bone_idx];
4815            ik_solver_bone_name_to_index.insert(bytes.clone(), solver_idx);
4816            let decoded = bone_names[bone_idx].as_bytes();
4817            if decoded != bytes.as_slice() && !decoded.is_empty() {
4818                ik_solver_bone_name_to_index.insert(decoded.to_vec(), solver_idx);
4819            }
4820        }
4821    }
4822
4823    let descriptor = RuntimeModelDescriptorV1 {
4824        morphs: morph_descriptor,
4825        ..bone_descriptor
4826    };
4827    let model = compile_runtime_model_descriptor_v1(&descriptor).map_err(|error| {
4828        ImportError::ModelBuildFailed(
4829            mmd_anim_runtime::ModelBuildError::InvalidRuntimeDescriptor {
4830                path: error.path,
4831                reason: error.kind.to_string(),
4832            },
4833        )
4834    })?;
4835
4836    Ok(PmxRuntimeImport {
4837        model,
4838        bone_names,
4839        bone_name_to_index,
4840        morph_name_to_index,
4841        ik_solver_bone_name_to_index,
4842    })
4843}
4844
4845#[cfg(test)]
4846mod tests {
4847    use super::*;
4848    use glam::{Vec3, Vec3A};
4849    use mmd_anim_runtime::RuntimeInstance;
4850    use std::sync::Arc;
4851
4852    fn build_small_pmx_header_bytes(bone_index_size: u8, encoding: TextEncoding) -> Vec<u8> {
4853        let mut buf = Vec::new();
4854        buf.extend_from_slice(b"PMX ");
4855        buf.extend_from_slice(&2.0f32.to_le_bytes());
4856        buf.push(8);
4857        buf.push(encoding as u8);
4858        buf.push(0);
4859        buf.push(4);
4860        buf.push(1);
4861        buf.push(1);
4862        buf.push(bone_index_size);
4863        buf.push(1);
4864        buf.push(1);
4865        buf
4866    }
4867
4868    fn build_empty_model_info(_encoding: TextEncoding) -> Vec<u8> {
4869        let mut buf = Vec::new();
4870        for _ in 0..4 {
4871            buf.extend_from_slice(&0i32.to_le_bytes());
4872        }
4873        buf
4874    }
4875
4876    fn build_empty_vertex_section() -> Vec<u8> {
4877        vec![0u8, 0, 0, 0]
4878    }
4879
4880    fn build_empty_face_section() -> Vec<u8> {
4881        vec![0u8, 0, 0, 0]
4882    }
4883
4884    fn build_empty_texture_section() -> Vec<u8> {
4885        vec![0u8, 0, 0, 0]
4886    }
4887
4888    fn build_empty_material_section() -> Vec<u8> {
4889        vec![0u8, 0, 0, 0]
4890    }
4891
4892    #[test]
4893    fn rejects_impossible_pmx_vertex_count_before_allocation() {
4894        let mut buf = build_small_pmx_header_bytes(1, TextEncoding::Utf8);
4895        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4896        buf.extend_from_slice(&i32::MAX.to_le_bytes());
4897
4898        assert!(matches!(
4899            parse_pmx_model(&buf),
4900            Err(ImportError::UnexpectedEof(_))
4901        ));
4902        assert!(matches!(
4903            import_pmx_runtime(&buf),
4904            Err(ImportError::UnexpectedEof(_))
4905        ));
4906    }
4907
4908    #[test]
4909    fn rejects_invalid_pmx_index_sizes_in_header() {
4910        let buf = build_small_pmx_header_bytes(3, TextEncoding::Utf8);
4911
4912        assert!(matches!(
4913            read_header(&buf),
4914            Err(ImportError::InvalidIndexSize(3))
4915        ));
4916    }
4917
4918    fn build_bone_name_bytes(name: &str) -> Vec<u8> {
4919        let mut buf = Vec::new();
4920        let name_bytes = name.as_bytes();
4921        buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
4922        buf.extend_from_slice(name_bytes);
4923        buf
4924    }
4925
4926    fn build_bone_section_header(count: u32) -> Vec<u8> {
4927        let mut buf = Vec::new();
4928        buf.extend_from_slice(&count.to_le_bytes());
4929        buf
4930    }
4931
4932    fn build_morph_name_bytes(name: &str) -> Vec<u8> {
4933        let mut buf = Vec::new();
4934        let name_bytes = name.as_bytes();
4935        buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
4936        buf.extend_from_slice(name_bytes);
4937        buf
4938    }
4939
4940    fn empty_pmx_flags() -> PmxParsedMaterialFlags {
4941        PmxParsedMaterialFlags {
4942            double_sided: false,
4943            ground_shadow: false,
4944            self_shadow_map: false,
4945            self_shadow: false,
4946            edge: false,
4947            vertex_color: false,
4948            point_draw: false,
4949            line_draw: false,
4950        }
4951    }
4952
4953    fn basic_bone_flags(indexed_tail: bool) -> PmxParsedBoneFlags {
4954        PmxParsedBoneFlags {
4955            indexed_tail,
4956            rotatable: true,
4957            translatable: false,
4958            visible: true,
4959            enabled: true,
4960            ik: false,
4961            append_local: false,
4962            append_rotate: false,
4963            append_translate: false,
4964            fixed_axis: false,
4965            local_axis: false,
4966            transform_after_physics: false,
4967            external_parent_transform: false,
4968        }
4969    }
4970
4971    fn parsed_pmx_fixture() -> PmxParsedModel {
4972        PmxParsedModel {
4973            metadata: PmxParsedMetadata {
4974                format: "pmx".to_owned(),
4975                version: 2.0,
4976                encoding: "utf-8".to_owned(),
4977                name: "model".to_owned(),
4978                english_name: "model-en".to_owned(),
4979                comment: "comment".to_owned(),
4980                english_comment: "comment-en".to_owned(),
4981                counts: PmxParsedCounts {
4982                    vertices: 1,
4983                    faces: 1,
4984                    materials: 1,
4985                    bones: 1,
4986                    morphs: 1,
4987                    display_frames: 1,
4988                    rigid_bodies: 1,
4989                    joints: 1,
4990                    soft_bodies: 0,
4991                },
4992                index_sizes: PmxParsedIndexSizes {
4993                    vertex: 4,
4994                    texture: 1,
4995                    material: 1,
4996                    bone: 2,
4997                    morph: 1,
4998                    rigid_body: 1,
4999                },
5000                additional_uv_count: 1,
5001            },
5002            geometry: PmxParsedGeometry {
5003                positions: vec![1.0, 2.0, 3.0],
5004                normals: vec![0.0, 1.0, 0.0],
5005                uvs: vec![0.25, 0.75],
5006                additional_uvs: vec![vec![1.0, 2.0, 3.0, 4.0]],
5007                indices: vec![0, 0, 0],
5008                skin_indices: vec![0, 0, 0, 0],
5009                skin_weights: vec![1.0, 0.0, 0.0, 0.0],
5010                edge_scale: vec![1.25],
5011                material_groups: vec![PmxParsedMaterialGroup {
5012                    start: 0,
5013                    count: 3,
5014                    material_index: 0,
5015                }],
5016                sdef: PmxParsedSdef {
5017                    skinning_modes: vec![PMX_SKINNING_MODE_BDEF4.to_owned()],
5018                    enabled: vec![0.0],
5019                    c: vec![0.0, 0.0, 0.0],
5020                    r0: vec![0.0, 0.0, 0.0],
5021                    r1: vec![0.0, 0.0, 0.0],
5022                    rw0: vec![0.0, 0.0, 0.0],
5023                    rw1: vec![0.0, 0.0, 0.0],
5024                },
5025                qdef: PmxParsedQdef { enabled: vec![0.0] },
5026            },
5027            materials: vec![PmxParsedMaterial {
5028                name: "mat".to_owned(),
5029                english_name: "mat-en".to_owned(),
5030                texture_path: "tex.png".to_owned(),
5031                sphere_texture_path: "sphere.spa".to_owned(),
5032                sphere_mode: "add".to_owned(),
5033                toon_texture_path: "toon.bmp".to_owned(),
5034                shared_toon_index: None,
5035                diffuse: [0.1, 0.2, 0.3, 0.4],
5036                specular: [0.5, 0.6, 0.7],
5037                specular_power: 8.0,
5038                ambient: [0.8, 0.9, 1.0],
5039                edge_color: [0.0, 0.1, 0.2, 0.3],
5040                edge_size: 1.5,
5041                flags: PmxParsedMaterialFlags {
5042                    double_sided: true,
5043                    edge: true,
5044                    ..empty_pmx_flags()
5045                },
5046                face_count: 1,
5047            }],
5048            skeleton: PmxParsedSkeleton {
5049                bones: vec![PmxParsedBone {
5050                    name: "Root".to_owned(),
5051                    english_name: "RootEn".to_owned(),
5052                    parent_index: -1,
5053                    layer: 0,
5054                    position: [0.0, 1.0, 2.0],
5055                    tail_index: -1,
5056                    tail_position: Some([0.0, 2.0, 0.0]),
5057                    flags: basic_bone_flags(false),
5058                    append_transform: None,
5059                    fixed_axis: None,
5060                    local_axis: None,
5061                    external_parent_key: None,
5062                    ik: None,
5063                }],
5064            },
5065            morphs: vec![PmxParsedMorph {
5066                name: "Smile".to_owned(),
5067                english_name: "SmileEn".to_owned(),
5068                panel: "eye".to_owned(),
5069                kind: "vertex".to_owned(),
5070                vertex_offsets: vec![PmxParsedVertexMorphOffset {
5071                    vertex_index: 0,
5072                    position: [0.1, 0.2, 0.3],
5073                }],
5074                group_offsets: Vec::new(),
5075                bone_offsets: Vec::new(),
5076                uv_offsets: Vec::new(),
5077                additional_uv_offsets: Vec::new(),
5078                material_offsets: Vec::new(),
5079                flip_offsets: Vec::new(),
5080                impulse_offsets: Vec::new(),
5081            }],
5082            display_frames: vec![PmxParsedDisplayFrame {
5083                name: "RootFrame".to_owned(),
5084                english_name: "RootFrameEn".to_owned(),
5085                special: true,
5086                frames: vec![
5087                    PmxParsedDisplayFrameElement {
5088                        kind: "bone".to_owned(),
5089                        index: 0,
5090                    },
5091                    PmxParsedDisplayFrameElement {
5092                        kind: "morph".to_owned(),
5093                        index: 0,
5094                    },
5095                ],
5096            }],
5097            rigid_bodies: vec![PmxParsedRigidBody {
5098                name: "body".to_owned(),
5099                english_name: "body-en".to_owned(),
5100                bone_index: 0,
5101                group: 1,
5102                mask: 2,
5103                shape: "box".to_owned(),
5104                size: [1.0, 2.0, 3.0],
5105                position: [4.0, 5.0, 6.0],
5106                rotation: [0.1, 0.2, 0.3],
5107                mass: 10.0,
5108                linear_damping: 0.4,
5109                angular_damping: 0.5,
5110                restitution: 0.6,
5111                friction: 0.7,
5112                mode: "dynamicBone".to_owned(),
5113            }],
5114            joints: vec![PmxParsedJoint {
5115                name: "joint".to_owned(),
5116                english_name: "joint-en".to_owned(),
5117                kind: "generic6dofSpring".to_owned(),
5118                rigid_body_index_a: 0,
5119                rigid_body_index_b: -1,
5120                position: [1.0, 1.1, 1.2],
5121                rotation: [2.0, 2.1, 2.2],
5122                translation_lower_limit: [-1.0, -1.1, -1.2],
5123                translation_upper_limit: [1.0, 1.1, 1.2],
5124                rotation_lower_limit: [-0.1, -0.2, -0.3],
5125                rotation_upper_limit: [0.1, 0.2, 0.3],
5126                spring_translation_factor: [3.0, 3.1, 3.2],
5127                spring_rotation_factor: [4.0, 4.1, 4.2],
5128            }],
5129            soft_bodies: Vec::new(),
5130            diagnostics: Vec::new(),
5131        }
5132    }
5133
5134    fn material_morph_offset(material_index: i32) -> PmxParsedMaterialMorphOffset {
5135        PmxParsedMaterialMorphOffset {
5136            material_index,
5137            operation: "add".to_owned(),
5138            diffuse: [0.1, 0.2, 0.3, 0.4],
5139            specular: [0.5, 0.6, 0.7],
5140            specular_power: 0.8,
5141            ambient: [0.9, 1.0, 1.1],
5142            edge_color: [0.0, 0.1, 0.2, 0.3],
5143            edge_size: 0.4,
5144            texture_factor: [1.0, 0.0, 0.0, 1.0],
5145            sphere_texture_factor: [0.0, 1.0, 0.0, 1.0],
5146            toon_texture_factor: [0.0, 0.0, 1.0, 1.0],
5147        }
5148    }
5149
5150    fn material_split_fixture() -> PmxParsedModel {
5151        let mut model = parsed_pmx_fixture();
5152        model.metadata.counts.vertices = 4;
5153        model.metadata.counts.faces = 2;
5154        model.metadata.counts.materials = 2;
5155        model.metadata.counts.morphs = 5;
5156        model.metadata.additional_uv_count = 1;
5157        model.geometry = PmxParsedGeometry {
5158            positions: vec![
5159                0.0, 0.0, 0.0, //
5160                1.0, 0.0, 0.0, //
5161                0.0, 1.0, 0.0, //
5162                1.0, 1.0, 0.0,
5163            ],
5164            normals: vec![
5165                0.0, 0.0, 1.0, //
5166                0.0, 0.0, 1.0, //
5167                0.0, 0.0, 1.0, //
5168                0.0, 0.0, 1.0,
5169            ],
5170            uvs: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
5171            additional_uvs: vec![vec![
5172                0.0, 0.0, 0.0, 0.0, //
5173                1.0, 0.0, 0.0, 0.0, //
5174                0.0, 1.0, 0.0, 0.0, //
5175                1.0, 1.0, 0.0, 0.0,
5176            ]],
5177            indices: vec![0, 1, 2, 2, 3, 0],
5178            skin_indices: vec![
5179                0, 0, 0, 0, //
5180                0, 0, 0, 0, //
5181                0, 0, 0, 0, //
5182                0, 0, 0, 0,
5183            ],
5184            skin_weights: vec![
5185                1.0, 0.0, 0.0, 0.0, //
5186                1.0, 0.0, 0.0, 0.0, //
5187                1.0, 0.0, 0.0, 0.0, //
5188                1.0, 0.0, 0.0, 0.0,
5189            ],
5190            edge_scale: vec![1.0, 1.1, 1.2, 1.3],
5191            material_groups: vec![
5192                PmxParsedMaterialGroup {
5193                    start: 0,
5194                    count: 3,
5195                    material_index: 0,
5196                },
5197                PmxParsedMaterialGroup {
5198                    start: 3,
5199                    count: 3,
5200                    material_index: 1,
5201                },
5202            ],
5203            sdef: PmxParsedSdef {
5204                skinning_modes: vec![
5205                    PMX_SKINNING_MODE_BDEF4.to_owned(),
5206                    PMX_SKINNING_MODE_SDEF.to_owned(),
5207                    PMX_SKINNING_MODE_BDEF4.to_owned(),
5208                    PMX_SKINNING_MODE_QDEF.to_owned(),
5209                ],
5210                enabled: vec![0.0, 1.0, 0.0, 0.0],
5211                c: vec![0.0; 12],
5212                r0: vec![0.0; 12],
5213                r1: vec![0.0; 12],
5214                rw0: vec![0.0; 12],
5215                rw1: vec![0.0; 12],
5216            },
5217            qdef: PmxParsedQdef {
5218                enabled: vec![0.0, 0.0, 0.0, 1.0],
5219            },
5220        };
5221        let mut material_b = model.materials[0].clone();
5222        material_b.name = "mat-b".to_owned();
5223        material_b.english_name = "mat-b-en".to_owned();
5224        model.materials = vec![model.materials[0].clone(), material_b];
5225        model.morphs = vec![
5226            PmxParsedMorph {
5227                name: "left-raise".to_owned(),
5228                english_name: "left-raise-en".to_owned(),
5229                panel: "brow".to_owned(),
5230                kind: "vertex".to_owned(),
5231                vertex_offsets: vec![PmxParsedVertexMorphOffset {
5232                    vertex_index: 1,
5233                    position: [0.0, 0.5, 0.0],
5234                }],
5235                group_offsets: Vec::new(),
5236                bone_offsets: Vec::new(),
5237                uv_offsets: Vec::new(),
5238                additional_uv_offsets: Vec::new(),
5239                material_offsets: Vec::new(),
5240                flip_offsets: Vec::new(),
5241                impulse_offsets: Vec::new(),
5242            },
5243            PmxParsedMorph {
5244                name: "right-raise".to_owned(),
5245                english_name: "right-raise-en".to_owned(),
5246                panel: "eye".to_owned(),
5247                kind: "vertex".to_owned(),
5248                vertex_offsets: vec![PmxParsedVertexMorphOffset {
5249                    vertex_index: 3,
5250                    position: [0.0, 0.75, 0.0],
5251                }],
5252                group_offsets: Vec::new(),
5253                bone_offsets: Vec::new(),
5254                uv_offsets: Vec::new(),
5255                additional_uv_offsets: Vec::new(),
5256                material_offsets: Vec::new(),
5257                flip_offsets: Vec::new(),
5258                impulse_offsets: Vec::new(),
5259            },
5260            PmxParsedMorph {
5261                name: "right-material".to_owned(),
5262                english_name: "right-material-en".to_owned(),
5263                panel: "other".to_owned(),
5264                kind: "material".to_owned(),
5265                vertex_offsets: Vec::new(),
5266                group_offsets: Vec::new(),
5267                bone_offsets: Vec::new(),
5268                uv_offsets: Vec::new(),
5269                additional_uv_offsets: Vec::new(),
5270                material_offsets: vec![material_morph_offset(1)],
5271                flip_offsets: Vec::new(),
5272                impulse_offsets: Vec::new(),
5273            },
5274            PmxParsedMorph {
5275                name: "all-material".to_owned(),
5276                english_name: "all-material-en".to_owned(),
5277                panel: "other".to_owned(),
5278                kind: "material".to_owned(),
5279                vertex_offsets: Vec::new(),
5280                group_offsets: Vec::new(),
5281                bone_offsets: Vec::new(),
5282                uv_offsets: Vec::new(),
5283                additional_uv_offsets: Vec::new(),
5284                material_offsets: vec![material_morph_offset(-1)],
5285                flip_offsets: Vec::new(),
5286                impulse_offsets: Vec::new(),
5287            },
5288            PmxParsedMorph {
5289                name: "combo".to_owned(),
5290                english_name: "combo-en".to_owned(),
5291                panel: "other".to_owned(),
5292                kind: "group".to_owned(),
5293                vertex_offsets: Vec::new(),
5294                group_offsets: vec![
5295                    PmxParsedGroupMorphOffset {
5296                        morph_index: 0,
5297                        weight: 0.25,
5298                    },
5299                    PmxParsedGroupMorphOffset {
5300                        morph_index: 1,
5301                        weight: 0.5,
5302                    },
5303                    PmxParsedGroupMorphOffset {
5304                        morph_index: 2,
5305                        weight: 0.75,
5306                    },
5307                    PmxParsedGroupMorphOffset {
5308                        morph_index: 3,
5309                        weight: 1.0,
5310                    },
5311                ],
5312                bone_offsets: Vec::new(),
5313                uv_offsets: Vec::new(),
5314                additional_uv_offsets: Vec::new(),
5315                material_offsets: Vec::new(),
5316                flip_offsets: Vec::new(),
5317                impulse_offsets: Vec::new(),
5318            },
5319        ];
5320        model
5321    }
5322
5323    fn ik_multi_axis_limit_pmx_fixture() -> &'static [u8] {
5324        include_bytes!("../../fixtures/pmx/ik_multi_axis_limit.pmx")
5325    }
5326
5327    fn model_descriptor_parity_pmx_fixture() -> &'static [u8] {
5328        include_bytes!("../../fixtures/pmx/model_descriptor_parity.pmx")
5329    }
5330
5331    fn assert_pmx_roundtrip_eq(left: &PmxParsedModel, right: &PmxParsedModel) {
5332        assert_eq!(
5333            serde_json::to_value(left).unwrap(),
5334            serde_json::to_value(right).unwrap()
5335        );
5336    }
5337
5338    fn json_keys(value: &serde_json::Value) -> Vec<String> {
5339        let mut keys = value
5340            .as_object()
5341            .unwrap()
5342            .keys()
5343            .map(ToOwned::to_owned)
5344            .collect::<Vec<_>>();
5345        keys.sort();
5346        keys
5347    }
5348
5349    #[test]
5350    fn pmx_material_split_remaps_geometry_and_prunes_morphs() {
5351        let model = material_split_fixture();
5352        let split = split_pmx_model_by_material(&model);
5353
5354        assert_eq!(split.meshes.len(), 2);
5355        assert_eq!(split.manifest.mesh_count, 2);
5356        assert_eq!(split.manifest.meshes[0].original_material_index, 0);
5357        assert_eq!(
5358            split.manifest.meshes[0].original_vertex_indices,
5359            vec![0, 1, 2]
5360        );
5361        assert_eq!(split.manifest.meshes[1].original_material_index, 1);
5362        assert_eq!(
5363            split.manifest.meshes[1].original_vertex_indices,
5364            vec![2, 3, 0]
5365        );
5366
5367        let left = &split.meshes[0];
5368        assert_eq!(left.geometry.indices, vec![0, 1, 2]);
5369        assert_eq!(
5370            left.geometry.positions,
5371            vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
5372        );
5373        assert_eq!(left.geometry.uvs, vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0]);
5374        assert_eq!(left.geometry.edge_scale, vec![1.0, 1.1, 1.2]);
5375        assert_eq!(left.geometry.sdef.enabled, vec![0.0, 1.0, 0.0]);
5376        assert_eq!(left.geometry.qdef.enabled, vec![0.0, 0.0, 0.0]);
5377        assert_eq!(
5378            left.morph_index_map,
5379            vec![
5380                PmxMaterialSplitMorphIndexMap {
5381                    original_index: 0,
5382                    local_index: 0,
5383                },
5384                PmxMaterialSplitMorphIndexMap {
5385                    original_index: 3,
5386                    local_index: 1,
5387                },
5388                PmxMaterialSplitMorphIndexMap {
5389                    original_index: 4,
5390                    local_index: 2,
5391                },
5392            ]
5393        );
5394        assert_eq!(left.morphs.len(), 3);
5395        assert_eq!(left.morphs[0].name, "left-raise");
5396        assert_eq!(left.morphs[0].vertex_offsets[0].vertex_index, 1);
5397        assert_eq!(left.morphs[1].name, "all-material");
5398        assert_eq!(left.morphs[1].material_offsets[0].material_index, -1);
5399        assert_eq!(left.morphs[2].name, "combo");
5400        assert_eq!(
5401            left.morphs[2]
5402                .group_offsets
5403                .iter()
5404                .map(|offset| (offset.morph_index, offset.weight))
5405                .collect::<Vec<_>>(),
5406            vec![(0, 0.25), (1, 1.0)]
5407        );
5408
5409        let right = &split.meshes[1];
5410        assert_eq!(right.geometry.indices, vec![0, 1, 2]);
5411        assert_eq!(
5412            right.geometry.positions,
5413            vec![0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]
5414        );
5415        assert_eq!(right.geometry.uvs, vec![0.0, 1.0, 1.0, 1.0, 0.0, 0.0]);
5416        assert_eq!(right.geometry.edge_scale, vec![1.2, 1.3, 1.0]);
5417        assert_eq!(right.geometry.sdef.enabled, vec![0.0, 0.0, 0.0]);
5418        assert_eq!(right.geometry.qdef.enabled, vec![0.0, 1.0, 0.0]);
5419        assert_eq!(
5420            right.morph_index_map,
5421            vec![
5422                PmxMaterialSplitMorphIndexMap {
5423                    original_index: 1,
5424                    local_index: 0,
5425                },
5426                PmxMaterialSplitMorphIndexMap {
5427                    original_index: 2,
5428                    local_index: 1,
5429                },
5430                PmxMaterialSplitMorphIndexMap {
5431                    original_index: 3,
5432                    local_index: 2,
5433                },
5434                PmxMaterialSplitMorphIndexMap {
5435                    original_index: 4,
5436                    local_index: 3,
5437                },
5438            ]
5439        );
5440        assert_eq!(right.morphs.len(), 4);
5441        assert_eq!(right.morphs[0].name, "right-raise");
5442        assert_eq!(right.morphs[0].vertex_offsets[0].vertex_index, 1);
5443        assert_eq!(right.morphs[1].name, "right-material");
5444        assert_eq!(right.morphs[1].material_offsets[0].material_index, 0);
5445        assert_eq!(right.morphs[2].name, "all-material");
5446        assert_eq!(right.morphs[2].material_offsets[0].material_index, -1);
5447        assert_eq!(
5448            right.morphs[3]
5449                .group_offsets
5450                .iter()
5451                .map(|offset| (offset.morph_index, offset.weight))
5452                .collect::<Vec<_>>(),
5453            vec![(0, 0.5), (1, 0.75), (2, 1.0)]
5454        );
5455    }
5456
5457    #[test]
5458    fn pmx_material_split_preserves_forward_group_morph_references() {
5459        let mut model = material_split_fixture();
5460        model.morphs.push(PmxParsedMorph {
5461            name: "forward-combo".to_owned(),
5462            english_name: "forward-combo-en".to_owned(),
5463            panel: "other".to_owned(),
5464            kind: "group".to_owned(),
5465            vertex_offsets: Vec::new(),
5466            group_offsets: vec![PmxParsedGroupMorphOffset {
5467                morph_index: 6,
5468                weight: 0.8,
5469            }],
5470            bone_offsets: Vec::new(),
5471            uv_offsets: Vec::new(),
5472            additional_uv_offsets: Vec::new(),
5473            material_offsets: Vec::new(),
5474            flip_offsets: Vec::new(),
5475            impulse_offsets: Vec::new(),
5476        });
5477        model.morphs.push(PmxParsedMorph {
5478            name: "later-combo".to_owned(),
5479            english_name: "later-combo-en".to_owned(),
5480            panel: "other".to_owned(),
5481            kind: "group".to_owned(),
5482            vertex_offsets: Vec::new(),
5483            group_offsets: vec![PmxParsedGroupMorphOffset {
5484                morph_index: 0,
5485                weight: 0.4,
5486            }],
5487            bone_offsets: Vec::new(),
5488            uv_offsets: Vec::new(),
5489            additional_uv_offsets: Vec::new(),
5490            material_offsets: Vec::new(),
5491            flip_offsets: Vec::new(),
5492            impulse_offsets: Vec::new(),
5493        });
5494        model.metadata.counts.morphs = model.morphs.len();
5495
5496        let split = split_pmx_model_by_material(&model);
5497        let left = &split.meshes[0];
5498
5499        assert_eq!(
5500            left.morph_index_map,
5501            vec![
5502                PmxMaterialSplitMorphIndexMap {
5503                    original_index: 0,
5504                    local_index: 0,
5505                },
5506                PmxMaterialSplitMorphIndexMap {
5507                    original_index: 3,
5508                    local_index: 1,
5509                },
5510                PmxMaterialSplitMorphIndexMap {
5511                    original_index: 4,
5512                    local_index: 2,
5513                },
5514                PmxMaterialSplitMorphIndexMap {
5515                    original_index: 5,
5516                    local_index: 3,
5517                },
5518                PmxMaterialSplitMorphIndexMap {
5519                    original_index: 6,
5520                    local_index: 4,
5521                },
5522            ]
5523        );
5524        assert_eq!(left.morphs[3].name, "forward-combo");
5525        assert_eq!(
5526            left.morphs[3]
5527                .group_offsets
5528                .iter()
5529                .map(|offset| (offset.morph_index, offset.weight))
5530                .collect::<Vec<_>>(),
5531            vec![(4, 0.8)]
5532        );
5533        assert_eq!(left.morphs[4].name, "later-combo");
5534        assert_eq!(
5535            left.morphs[4]
5536                .group_offsets
5537                .iter()
5538                .map(|offset| (offset.morph_index, offset.weight))
5539                .collect::<Vec<_>>(),
5540            vec![(0, 0.4)]
5541        );
5542    }
5543
5544    #[test]
5545    fn pmx_model_json_top_level_schema_is_stable() {
5546        let parsed = parsed_pmx_fixture();
5547        let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
5548
5549        assert_eq!(
5550            keys,
5551            vec![
5552                "diagnostics",
5553                "displayFrames",
5554                "geometry",
5555                "joints",
5556                "materials",
5557                "metadata",
5558                "morphs",
5559                "rigidBodies",
5560                "skeleton",
5561                "softBodies",
5562            ]
5563        );
5564    }
5565
5566    #[test]
5567    fn parses_pmx_header() {
5568        let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
5569        let (header, pos) = read_header(&header_bytes).unwrap();
5570
5571        assert_eq!(header.version, 2.0);
5572        assert_eq!(header.encoding, TextEncoding::Utf8);
5573        assert_eq!(header.bone_index_size, 4);
5574        assert_eq!(header.vertex_index_size, 4);
5575        assert!(pos > 0);
5576    }
5577
5578    #[test]
5579    fn pmx_bone_flag_values_match_reference_layout() {
5580        assert_eq!(BONE_FLAG_TAIL_INDEX, 0x0001);
5581        assert_eq!(BONE_FLAG_IK, 0x0020);
5582        assert_eq!(BONE_FLAG_LOCAL_APPEND, 0x0080);
5583        assert_eq!(BONE_FLAG_APPEND_ROTATE, 0x0100);
5584        assert_eq!(BONE_FLAG_APPEND_TRANSLATE, 0x0200);
5585        assert_eq!(BONE_FLAG_FIXED_AXIS, 0x0400);
5586        assert_eq!(BONE_FLAG_LOCAL_AXIS, 0x0800);
5587        assert_eq!(BONE_FLAG_TRANSFORM_AFTER_PHYSICS, 0x1000);
5588        assert_eq!(BONE_FLAG_EXTERNAL_PARENT, 0x2000);
5589    }
5590
5591    #[test]
5592    fn parses_ik_multi_axis_limit_pmx_fixture() {
5593        let parsed = parse_pmx_model(ik_multi_axis_limit_pmx_fixture()).unwrap();
5594        assert_eq!(parsed.metadata.name, "ik_multi_axis_limit_fixture");
5595        assert_eq!(parsed.metadata.counts.bones, 3);
5596        assert_eq!(parsed.skeleton.bones[2].name, "ik_controller");
5597        let ik = parsed.skeleton.bones[2].ik.as_ref().unwrap();
5598        assert_eq!(ik.target_index, 1);
5599        assert_eq!(ik.loop_count, 1);
5600        assert_eq!(ik.links.len(), 1);
5601        assert_eq!(ik.links[0].bone_index, 0);
5602        let limits = ik.links[0].limits.as_ref().unwrap();
5603        assert_eq!(limits.lower, [0.0, -1.0, -1.0]);
5604        assert_eq!(limits.upper, [0.0, 1.0, 1.0]);
5605
5606        let runtime = import_pmx_runtime(ik_multi_axis_limit_pmx_fixture()).unwrap();
5607        assert_eq!(runtime.model.bone_count(), 3);
5608        assert_eq!(runtime.model.ik_count(), 1);
5609        let solver = &runtime.model.ik_solvers()[0];
5610        assert_eq!(solver.ik_bone.as_usize(), 2);
5611        assert_eq!(solver.target_bone.as_usize(), 1);
5612        let limit = solver.links[0].angle_limit.unwrap();
5613        assert_eq!(limit.min.to_array(), [0.0, -1.0, -1.0]);
5614        assert_eq!(limit.max.to_array(), [0.0, 1.0, 1.0]);
5615    }
5616
5617    #[test]
5618    fn runtime_descriptor_compiler_matches_legacy_pmx_payload() {
5619        let data = model_descriptor_parity_pmx_fixture();
5620        let parsed = parse_pmx_model(data).unwrap();
5621        assert_eq!(parsed.metadata.counts.bones, 3);
5622        assert_eq!(parsed.metadata.counts.morphs, 2);
5623        let imported = import_pmx_runtime(data).unwrap();
5624        assert_eq!(imported.bone_names.len(), 3);
5625        assert_eq!(
5626            imported.bone_name_to_index.get(b"root".as_slice()),
5627            Some(&BoneIndex(0))
5628        );
5629        assert_eq!(
5630            imported.bone_name_to_index.get(b"append_after".as_slice()),
5631            Some(&BoneIndex(1))
5632        );
5633        assert_eq!(
5634            imported.bone_name_to_index.get(b"ik_controller".as_slice()),
5635            Some(&BoneIndex(2))
5636        );
5637        assert_eq!(
5638            imported.morph_name_to_index.get(b"bone_morph".as_slice()),
5639            Some(&MorphIndex(0))
5640        );
5641        assert_eq!(
5642            imported.morph_name_to_index.get(b"group_morph".as_slice()),
5643            Some(&MorphIndex(1))
5644        );
5645        assert_eq!(
5646            imported
5647                .ik_solver_bone_name_to_index
5648                .get(b"ik_controller".as_slice()),
5649            Some(&0)
5650        );
5651        assert_eq!(imported.model.ik_count(), 1);
5652        assert_eq!(imported.model.append_transforms().len(), 1);
5653        assert_eq!(imported.model.morph_count(), 2);
5654        assert_eq!(imported.model.bone_morph_offsets().len(), 1);
5655        assert_eq!(imported.model.group_morph_offsets().len(), 1);
5656        assert!(imported.model.transform_after_physics(BoneIndex(1)));
5657        assert!(imported.model.fixed_axis(BoneIndex(0)).is_some());
5658        assert_eq!(imported.model.local_axis_count(), 2);
5659        let append = imported.model.append_transforms()[0];
5660        assert!(append.affect_rotation);
5661        assert!(append.affect_translation);
5662        assert!(append.local);
5663        let solver = &imported.model.ik_solvers()[0];
5664        let limit = solver.links[0].angle_limit.unwrap();
5665        assert_eq!(limit.min.to_array(), [0.0, -1.0, -1.0]);
5666        assert_eq!(limit.max.to_array(), [0.0, 1.0, 1.0]);
5667
5668        // Test-only baseline for the pre-descriptor construction path. The
5669        // production importer must use the shared descriptor compiler above.
5670        let (header, pos) = read_header(data).unwrap();
5671        let pos = skip_model_info(data, &header, pos).unwrap();
5672        let pos = skip_vertices(data, &header, pos).unwrap();
5673        let pos = skip_faces(data, header.vertex_index_size, pos).unwrap();
5674        let pos = skip_textures(data, header.encoding, pos).unwrap();
5675        let pos = skip_materials(data, &header, pos).unwrap();
5676        let (bone_read, pos) = read_bones_with_local_axes(data, &header, pos).unwrap();
5677        let local_axes = bone_read
5678            .descriptor
5679            .bones
5680            .iter()
5681            .map(|bone| bone.local_axis)
5682            .collect::<Vec<_>>();
5683        let legacy_import = legacy_bone_import(&bone_read);
5684        let (_, morph, _) = read_morph_offsets(data, &header, pos).unwrap();
5685        let legacy_model = ModelArena::new_with_morphs(
5686            legacy_import.bones,
5687            legacy_import.ik_solvers,
5688            legacy_import.append_transforms,
5689            morph,
5690        )
5691        .unwrap()
5692        .with_local_axes(local_axes);
5693
5694        assert_eq!(imported.model.bone_count(), legacy_model.bone_count());
5695        assert_eq!(imported.model.ik_count(), legacy_model.ik_count());
5696        assert_eq!(
5697            imported.model.append_transforms(),
5698            legacy_model.append_transforms()
5699        );
5700        assert_eq!(imported.model.morph_count(), legacy_model.morph_count());
5701        assert_eq!(
5702            imported.model.bone_morph_spans(),
5703            legacy_model.bone_morph_spans()
5704        );
5705        assert_eq!(
5706            imported.model.bone_morph_offsets(),
5707            legacy_model.bone_morph_offsets()
5708        );
5709        assert_eq!(
5710            imported.model.group_morph_spans(),
5711            legacy_model.group_morph_spans()
5712        );
5713        assert_eq!(
5714            imported.model.group_morph_offsets(),
5715            legacy_model.group_morph_offsets()
5716        );
5717        assert_eq!(imported.model.ik_solvers(), legacy_model.ik_solvers());
5718        for bone_index in 0..imported.model.bone_count() {
5719            let bone = BoneIndex(bone_index as u32);
5720            assert_eq!(
5721                imported.model.rest_position(bone),
5722                legacy_model.rest_position(bone)
5723            );
5724            assert_eq!(
5725                imported.model.inverse_bind_matrix(bone),
5726                legacy_model.inverse_bind_matrix(bone)
5727            );
5728            assert_eq!(
5729                imported.model.transform_order(bone),
5730                legacy_model.transform_order(bone)
5731            );
5732            assert_eq!(
5733                imported.model.transform_after_physics(bone),
5734                legacy_model.transform_after_physics(bone)
5735            );
5736            assert_eq!(
5737                imported.model.fixed_axis(bone),
5738                legacy_model.fixed_axis(bone)
5739            );
5740            assert_eq!(
5741                imported.model.local_axis(bone),
5742                legacy_model.local_axis(bone)
5743            );
5744            assert_eq!(
5745                imported.model.local_axis_basis(bone),
5746                legacy_model.local_axis_basis(bone)
5747            );
5748        }
5749
5750        let mut shared_runtime = RuntimeInstance::new(Arc::new(imported.model));
5751        let mut legacy_runtime = RuntimeInstance::new(Arc::new(legacy_model));
5752        shared_runtime.evaluate_current_pose();
5753        legacy_runtime.evaluate_current_pose();
5754        assert_eq!(
5755            shared_runtime.world_matrices(),
5756            legacy_runtime.world_matrices()
5757        );
5758        assert_eq!(
5759            shared_runtime.skinning_matrices(),
5760            legacy_runtime.skinning_matrices()
5761        );
5762    }
5763
5764    #[test]
5765    fn rejects_bad_magic() {
5766        let mut header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
5767        header_bytes[0] = 0xFF;
5768        assert_eq!(
5769            read_header(&header_bytes).unwrap_err(),
5770            ImportError::InvalidPmxMagic
5771        );
5772    }
5773
5774    #[test]
5775    fn reads_single_bone_no_parent() {
5776        let mut buf = Vec::new();
5777        buf.extend_from_slice(&build_small_pmx_header_bytes(4, TextEncoding::Utf8));
5778        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5779        buf.extend_from_slice(&build_empty_vertex_section());
5780        buf.extend_from_slice(&build_empty_face_section());
5781        buf.extend_from_slice(&build_empty_texture_section());
5782        buf.extend_from_slice(&build_empty_material_section());
5783        buf.extend_from_slice(&build_bone_section_header(1));
5784
5785        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5786        buf.extend_from_slice(&build_bone_name_bytes(""));
5787        buf.extend_from_slice(&0.0f32.to_le_bytes());
5788        buf.extend_from_slice(&1.0f32.to_le_bytes());
5789        buf.extend_from_slice(&2.0f32.to_le_bytes());
5790        buf.extend_from_slice(&(-1i32).to_le_bytes());
5791        buf.extend_from_slice(&0i32.to_le_bytes());
5792        buf.extend_from_slice(&0u16.to_le_bytes());
5793        buf.extend_from_slice(&0.0f32.to_le_bytes());
5794        buf.extend_from_slice(&0.0f32.to_le_bytes());
5795        buf.extend_from_slice(&0.0f32.to_le_bytes());
5796
5797        let result = import_pmx_model(&buf).unwrap();
5798        assert_eq!(result.bones.len(), 1);
5799        assert_eq!(result.bone_names.len(), 1);
5800        assert_eq!(result.bone_names[0], "Root");
5801        assert_eq!(&result.bone_name_bytes[0][..], b"Root");
5802        assert_eq!(result.bones[0].parent, None);
5803        assert!((result.bones[0].rest_position.x - 0.0).abs() < 0.001);
5804        assert!((result.bones[0].rest_position.y - 1.0).abs() < 0.001);
5805        assert!((result.bones[0].rest_position.z - 2.0).abs() < 0.001);
5806        assert!(result.ik_solvers.is_empty());
5807        assert!(result.append_transforms.is_empty());
5808    }
5809
5810    #[test]
5811    fn reads_bone_with_parent() {
5812        let mut buf = Vec::new();
5813        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5814        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5815        buf.extend_from_slice(&build_empty_vertex_section());
5816        buf.extend_from_slice(&build_empty_face_section());
5817        buf.extend_from_slice(&build_empty_texture_section());
5818        buf.extend_from_slice(&build_empty_material_section());
5819        buf.extend_from_slice(&build_bone_section_header(2));
5820
5821        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5822        buf.extend_from_slice(&build_bone_name_bytes(""));
5823        buf.extend_from_slice(&0.0f32.to_le_bytes());
5824        buf.extend_from_slice(&0.0f32.to_le_bytes());
5825        buf.extend_from_slice(&0.0f32.to_le_bytes());
5826        buf.extend_from_slice(&(-1i16).to_le_bytes());
5827        buf.extend_from_slice(&0i32.to_le_bytes());
5828        buf.extend_from_slice(&0u16.to_le_bytes());
5829        buf.extend_from_slice(&0.0f32.to_le_bytes());
5830        buf.extend_from_slice(&0.0f32.to_le_bytes());
5831        buf.extend_from_slice(&0.0f32.to_le_bytes());
5832
5833        buf.extend_from_slice(&build_bone_name_bytes("Child"));
5834        buf.extend_from_slice(&build_bone_name_bytes(""));
5835        buf.extend_from_slice(&1.0f32.to_le_bytes());
5836        buf.extend_from_slice(&0.0f32.to_le_bytes());
5837        buf.extend_from_slice(&0.0f32.to_le_bytes());
5838        buf.extend_from_slice(&0i16.to_le_bytes());
5839        buf.extend_from_slice(&0i32.to_le_bytes());
5840        buf.extend_from_slice(&0u16.to_le_bytes());
5841        buf.extend_from_slice(&0.0f32.to_le_bytes());
5842        buf.extend_from_slice(&0.0f32.to_le_bytes());
5843        buf.extend_from_slice(&0.0f32.to_le_bytes());
5844
5845        let result = import_pmx_model(&buf).unwrap();
5846        assert_eq!(result.bones.len(), 2);
5847        assert_eq!(result.bone_names[0], "Root");
5848        assert_eq!(result.bone_names[1], "Child");
5849        assert_eq!(result.bones[0].parent, None);
5850        assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
5851    }
5852
5853    #[test]
5854    fn reads_bone_with_append_transform() {
5855        let mut buf = Vec::new();
5856        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5857        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5858        buf.extend_from_slice(&build_empty_vertex_section());
5859        buf.extend_from_slice(&build_empty_face_section());
5860        buf.extend_from_slice(&build_empty_texture_section());
5861        buf.extend_from_slice(&build_empty_material_section());
5862        buf.extend_from_slice(&build_bone_section_header(2));
5863
5864        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5865        buf.extend_from_slice(&build_bone_name_bytes(""));
5866        buf.extend_from_slice(&0.0f32.to_le_bytes());
5867        buf.extend_from_slice(&0.0f32.to_le_bytes());
5868        buf.extend_from_slice(&0.0f32.to_le_bytes());
5869        buf.extend_from_slice(&(-1i16).to_le_bytes());
5870        buf.extend_from_slice(&0i32.to_le_bytes());
5871        buf.extend_from_slice(&0u16.to_le_bytes());
5872        buf.extend_from_slice(&0.0f32.to_le_bytes());
5873        buf.extend_from_slice(&0.0f32.to_le_bytes());
5874        buf.extend_from_slice(&0.0f32.to_le_bytes());
5875
5876        buf.extend_from_slice(&build_bone_name_bytes("AppendBone"));
5877        buf.extend_from_slice(&build_bone_name_bytes(""));
5878        buf.extend_from_slice(&1.0f32.to_le_bytes());
5879        buf.extend_from_slice(&0.0f32.to_le_bytes());
5880        buf.extend_from_slice(&0.0f32.to_le_bytes());
5881        buf.extend_from_slice(&0i16.to_le_bytes());
5882        buf.extend_from_slice(&0i32.to_le_bytes());
5883        buf.extend_from_slice(
5884            &(BONE_FLAG_LOCAL_APPEND | BONE_FLAG_APPEND_ROTATE | BONE_FLAG_APPEND_TRANSLATE)
5885                .to_le_bytes(),
5886        );
5887        buf.extend_from_slice(&0.0f32.to_le_bytes());
5888        buf.extend_from_slice(&0.0f32.to_le_bytes());
5889        buf.extend_from_slice(&0.0f32.to_le_bytes());
5890        buf.extend_from_slice(&0i16.to_le_bytes());
5891        buf.extend_from_slice(&0.5f32.to_le_bytes());
5892
5893        let result = import_pmx_model(&buf).unwrap();
5894        assert_eq!(result.bones.len(), 2);
5895        assert_eq!(result.append_transforms.len(), 1);
5896        assert_eq!(result.append_transforms[0].target_bone, BoneIndex(1));
5897        assert_eq!(result.append_transforms[0].source_bone, BoneIndex(0));
5898        assert!((result.append_transforms[0].ratio - 0.5).abs() < 0.001);
5899        assert!(result.append_transforms[0].affect_rotation);
5900        assert!(result.append_transforms[0].affect_translation);
5901        assert!(result.append_transforms[0].local);
5902    }
5903
5904    #[test]
5905    fn reads_ik_bone_with_links_and_angle_limits() {
5906        let mut buf = Vec::new();
5907        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5908        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5909        buf.extend_from_slice(&build_empty_vertex_section());
5910        buf.extend_from_slice(&build_empty_face_section());
5911        buf.extend_from_slice(&build_empty_texture_section());
5912        buf.extend_from_slice(&build_empty_material_section());
5913        buf.extend_from_slice(&build_bone_section_header(3));
5914
5915        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5916        buf.extend_from_slice(&build_bone_name_bytes(""));
5917        buf.extend_from_slice(&0.0f32.to_le_bytes());
5918        buf.extend_from_slice(&0.0f32.to_le_bytes());
5919        buf.extend_from_slice(&0.0f32.to_le_bytes());
5920        buf.extend_from_slice(&(-1i16).to_le_bytes());
5921        buf.extend_from_slice(&0i32.to_le_bytes());
5922        buf.extend_from_slice(&0u16.to_le_bytes());
5923        buf.extend_from_slice(&0.0f32.to_le_bytes());
5924        buf.extend_from_slice(&0.0f32.to_le_bytes());
5925        buf.extend_from_slice(&0.0f32.to_le_bytes());
5926
5927        buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
5928        buf.extend_from_slice(&build_bone_name_bytes(""));
5929        buf.extend_from_slice(&2.0f32.to_le_bytes());
5930        buf.extend_from_slice(&0.0f32.to_le_bytes());
5931        buf.extend_from_slice(&0.0f32.to_le_bytes());
5932        buf.extend_from_slice(&0i16.to_le_bytes());
5933        buf.extend_from_slice(&0i32.to_le_bytes());
5934        buf.extend_from_slice(&0u16.to_le_bytes());
5935        buf.extend_from_slice(&0.0f32.to_le_bytes());
5936        buf.extend_from_slice(&0.0f32.to_le_bytes());
5937        buf.extend_from_slice(&0.0f32.to_le_bytes());
5938
5939        buf.extend_from_slice(&build_bone_name_bytes("IKBone"));
5940        buf.extend_from_slice(&build_bone_name_bytes(""));
5941        buf.extend_from_slice(&4.0f32.to_le_bytes());
5942        buf.extend_from_slice(&0.0f32.to_le_bytes());
5943        buf.extend_from_slice(&0.0f32.to_le_bytes());
5944        buf.extend_from_slice(&0i16.to_le_bytes());
5945        buf.extend_from_slice(&0i32.to_le_bytes());
5946        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
5947        buf.extend_from_slice(&1i16.to_le_bytes());
5948        buf.extend_from_slice(&1i16.to_le_bytes());
5949        buf.extend_from_slice(&10i32.to_le_bytes());
5950        buf.extend_from_slice(&0.5f32.to_le_bytes());
5951        buf.extend_from_slice(&1i32.to_le_bytes());
5952        buf.extend_from_slice(&1i16.to_le_bytes());
5953        buf.push(1u8);
5954        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5955        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5956        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5957        buf.extend_from_slice(&1.0f32.to_le_bytes());
5958        buf.extend_from_slice(&1.0f32.to_le_bytes());
5959        buf.extend_from_slice(&1.0f32.to_le_bytes());
5960
5961        let result = import_pmx_model(&buf).unwrap();
5962        assert_eq!(result.bones.len(), 3);
5963        assert_eq!(result.ik_solvers.len(), 1);
5964        assert_eq!(result.ik_solvers[0].ik_bone, BoneIndex(2));
5965        assert_eq!(result.ik_solvers[0].target_bone, BoneIndex(1));
5966        assert_eq!(result.ik_solvers[0].links.len(), 1);
5967        assert_eq!(result.ik_solvers[0].links[0].bone, BoneIndex(1));
5968        assert!(result.ik_solvers[0].links[0].angle_limit.is_some());
5969        assert_eq!(result.ik_solvers[0].iteration_count, 10);
5970        assert!((result.ik_solvers[0].limit_angle - 0.5).abs() < 0.001);
5971    }
5972
5973    #[test]
5974    fn handles_utf16le_encoding() {
5975        let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf16Le);
5976        let (header, _) = read_header(&header_bytes).unwrap();
5977        assert_eq!(header.encoding, TextEncoding::Utf16Le);
5978    }
5979
5980    #[test]
5981    fn decodes_utf16le_surrogate_pairs() {
5982        let bytes = [0x3D, 0xD8, 0x00, 0xDE, 0x00, 0x00];
5983        assert_eq!(decode_utf16le_lossy(&bytes), "\u{1F600}");
5984    }
5985
5986    #[test]
5987    fn reports_eof_on_truncated_data() {
5988        let buf = vec![0x50, 0x4D, 0x58]; // incomplete magic
5989        assert!(matches!(
5990            read_header(&buf).unwrap_err(),
5991            ImportError::UnexpectedEof(_)
5992        ));
5993    }
5994
5995    #[test]
5996    fn bone_names_retained_and_mapped_to_indices() {
5997        let mut buf = Vec::new();
5998        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5999        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6000        buf.extend_from_slice(&build_empty_vertex_section());
6001        buf.extend_from_slice(&build_empty_face_section());
6002        buf.extend_from_slice(&build_empty_texture_section());
6003        buf.extend_from_slice(&build_empty_material_section());
6004        buf.extend_from_slice(&build_bone_section_header(2));
6005
6006        buf.extend_from_slice(&build_bone_name_bytes("Hips"));
6007        buf.extend_from_slice(&build_bone_name_bytes(""));
6008        buf.extend_from_slice(&0.0f32.to_le_bytes());
6009        buf.extend_from_slice(&0.0f32.to_le_bytes());
6010        buf.extend_from_slice(&0.0f32.to_le_bytes());
6011        buf.extend_from_slice(&(-1i16).to_le_bytes());
6012        buf.extend_from_slice(&0i32.to_le_bytes());
6013        buf.extend_from_slice(&0u16.to_le_bytes());
6014        buf.extend_from_slice(&0.0f32.to_le_bytes());
6015        buf.extend_from_slice(&0.0f32.to_le_bytes());
6016        buf.extend_from_slice(&0.0f32.to_le_bytes());
6017
6018        buf.extend_from_slice(&build_bone_name_bytes("Spine"));
6019        buf.extend_from_slice(&build_bone_name_bytes(""));
6020        buf.extend_from_slice(&1.0f32.to_le_bytes());
6021        buf.extend_from_slice(&0.0f32.to_le_bytes());
6022        buf.extend_from_slice(&0.0f32.to_le_bytes());
6023        buf.extend_from_slice(&0i16.to_le_bytes());
6024        buf.extend_from_slice(&0i32.to_le_bytes());
6025        buf.extend_from_slice(&0u16.to_le_bytes());
6026        buf.extend_from_slice(&0.0f32.to_le_bytes());
6027        buf.extend_from_slice(&0.0f32.to_le_bytes());
6028        buf.extend_from_slice(&0.0f32.to_le_bytes());
6029
6030        let result = import_pmx_model(&buf).unwrap();
6031        assert_eq!(result.bone_name_bytes.len(), 2);
6032        assert_eq!(&result.bone_name_bytes[0][..], b"Hips");
6033        assert_eq!(&result.bone_name_bytes[1][..], b"Spine");
6034        assert_eq!(result.bone_names[0], "Hips");
6035        assert_eq!(result.bone_names[1], "Spine");
6036
6037        let bone_map: HashMap<Vec<u8>, BoneIndex> = result
6038            .bone_name_bytes
6039            .iter()
6040            .enumerate()
6041            .map(|(i, b)| (b.clone(), BoneIndex(i as u32)))
6042            .collect();
6043
6044        assert_eq!(bone_map.get(b"Hips".as_slice()), Some(&BoneIndex(0)));
6045        assert_eq!(bone_map.get(b"Spine".as_slice()), Some(&BoneIndex(1)));
6046        assert_eq!(bone_map.get(b"Unknown".as_slice()), None);
6047    }
6048
6049    #[test]
6050    fn ik_solver_name_mapped_to_solver_index() {
6051        let mut buf = Vec::new();
6052        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6053        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6054        buf.extend_from_slice(&build_empty_vertex_section());
6055        buf.extend_from_slice(&build_empty_face_section());
6056        buf.extend_from_slice(&build_empty_texture_section());
6057        buf.extend_from_slice(&build_empty_material_section());
6058        buf.extend_from_slice(&build_bone_section_header(3));
6059
6060        buf.extend_from_slice(&build_bone_name_bytes("Root"));
6061        buf.extend_from_slice(&build_bone_name_bytes(""));
6062        buf.extend_from_slice(&0.0f32.to_le_bytes());
6063        buf.extend_from_slice(&0.0f32.to_le_bytes());
6064        buf.extend_from_slice(&0.0f32.to_le_bytes());
6065        buf.extend_from_slice(&(-1i16).to_le_bytes());
6066        buf.extend_from_slice(&0i32.to_le_bytes());
6067        buf.extend_from_slice(&0u16.to_le_bytes());
6068        buf.extend_from_slice(&0.0f32.to_le_bytes());
6069        buf.extend_from_slice(&0.0f32.to_le_bytes());
6070        buf.extend_from_slice(&0.0f32.to_le_bytes());
6071
6072        buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
6073        buf.extend_from_slice(&build_bone_name_bytes(""));
6074        buf.extend_from_slice(&2.0f32.to_le_bytes());
6075        buf.extend_from_slice(&0.0f32.to_le_bytes());
6076        buf.extend_from_slice(&0.0f32.to_le_bytes());
6077        buf.extend_from_slice(&0i16.to_le_bytes());
6078        buf.extend_from_slice(&0i32.to_le_bytes());
6079        buf.extend_from_slice(&0u16.to_le_bytes());
6080        buf.extend_from_slice(&0.0f32.to_le_bytes());
6081        buf.extend_from_slice(&0.0f32.to_le_bytes());
6082        buf.extend_from_slice(&0.0f32.to_le_bytes());
6083
6084        buf.extend_from_slice(&build_bone_name_bytes("LeftLegIK"));
6085        buf.extend_from_slice(&build_bone_name_bytes(""));
6086        buf.extend_from_slice(&4.0f32.to_le_bytes());
6087        buf.extend_from_slice(&0.0f32.to_le_bytes());
6088        buf.extend_from_slice(&0.0f32.to_le_bytes());
6089        buf.extend_from_slice(&0i16.to_le_bytes());
6090        buf.extend_from_slice(&0i32.to_le_bytes());
6091        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
6092        buf.extend_from_slice(&1i16.to_le_bytes());
6093        buf.extend_from_slice(&1i16.to_le_bytes());
6094        buf.extend_from_slice(&10i32.to_le_bytes());
6095        buf.extend_from_slice(&0.5f32.to_le_bytes());
6096        buf.extend_from_slice(&1i32.to_le_bytes());
6097        buf.extend_from_slice(&1i16.to_le_bytes());
6098        buf.push(1u8);
6099        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6100        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6101        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6102        buf.extend_from_slice(&1.0f32.to_le_bytes());
6103        buf.extend_from_slice(&1.0f32.to_le_bytes());
6104        buf.extend_from_slice(&1.0f32.to_le_bytes());
6105
6106        let bone_import = import_pmx_model(&buf).unwrap();
6107        assert_eq!(bone_import.ik_solvers.len(), 1);
6108        assert_eq!(bone_import.ik_solvers[0].ik_bone, BoneIndex(2));
6109        assert_eq!(
6110            bone_import.bone_names[bone_import.ik_solvers[0].ik_bone.as_usize()],
6111            "LeftLegIK"
6112        );
6113
6114        let ik_name_map: HashMap<Vec<u8>, usize> = bone_import
6115            .ik_solvers
6116            .iter()
6117            .enumerate()
6118            .filter_map(|(solver_idx, solver)| {
6119                let bone_idx = solver.ik_bone.as_usize();
6120                if bone_idx < bone_import.bone_name_bytes.len() {
6121                    Some((bone_import.bone_name_bytes[bone_idx].clone(), solver_idx))
6122                } else {
6123                    None
6124                }
6125            })
6126            .collect();
6127
6128        assert_eq!(ik_name_map.get(b"LeftLegIK".as_slice()), Some(&0));
6129    }
6130
6131    #[test]
6132    fn reads_morph_names_without_payloads() {
6133        let (header, pos) =
6134            read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
6135
6136        let mut buf = vec![0u8; pos];
6137        buf.extend_from_slice(&1i32.to_le_bytes());
6138        buf.extend_from_slice(&build_morph_name_bytes("Smile"));
6139        buf.extend_from_slice(&build_morph_name_bytes(""));
6140        buf.push(4u8);
6141        buf.push(0u8);
6142        buf.extend_from_slice(&0i32.to_le_bytes());
6143
6144        let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
6145        assert_eq!(morphs.name_bytes.len(), 1);
6146        assert_eq!(&morphs.name_bytes[0][..], b"Smile");
6147        assert_eq!(morphs.names[0], "Smile");
6148        assert!(new_pos > pos);
6149    }
6150
6151    #[test]
6152    fn reads_morph_type_zero_with_vertex_group_offsets() {
6153        let (header, pos) =
6154            read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
6155        assert_eq!(header.vertex_index_size, 4);
6156        assert_eq!(header.morph_index_size, 1);
6157
6158        let mut buf = vec![0u8; pos];
6159        buf.extend_from_slice(&1i32.to_le_bytes());
6160        buf.extend_from_slice(&build_morph_name_bytes("Blink"));
6161        buf.extend_from_slice(&build_morph_name_bytes(""));
6162        buf.push(0u8);
6163        buf.push(0u8);
6164        buf.extend_from_slice(&2i32.to_le_bytes());
6165        buf.push(0u8);
6166        buf.extend_from_slice(&[0u8; 4]);
6167        buf.push(1u8);
6168        buf.extend_from_slice(&[0u8; 4]);
6169
6170        let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
6171        assert_eq!(morphs.name_bytes.len(), 1);
6172        assert_eq!(&morphs.name_bytes[0][..], b"Blink");
6173        assert_eq!(morphs.names[0], "Blink");
6174        assert_eq!(new_pos, buf.len());
6175    }
6176
6177    #[test]
6178    fn runtime_import_builds_model_arena_with_name_lookup() {
6179        let mut buf = Vec::new();
6180        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6181        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6182        buf.extend_from_slice(&build_empty_vertex_section());
6183        buf.extend_from_slice(&build_empty_face_section());
6184        buf.extend_from_slice(&build_empty_texture_section());
6185        buf.extend_from_slice(&build_empty_material_section());
6186        buf.extend_from_slice(&build_bone_section_header(2));
6187
6188        buf.extend_from_slice(&build_bone_name_bytes("Hips"));
6189        buf.extend_from_slice(&build_bone_name_bytes(""));
6190        buf.extend_from_slice(&0.0f32.to_le_bytes());
6191        buf.extend_from_slice(&0.0f32.to_le_bytes());
6192        buf.extend_from_slice(&0.0f32.to_le_bytes());
6193        buf.extend_from_slice(&(-1i16).to_le_bytes());
6194        buf.extend_from_slice(&0i32.to_le_bytes());
6195        buf.extend_from_slice(&0u16.to_le_bytes());
6196        buf.extend_from_slice(&0.0f32.to_le_bytes());
6197        buf.extend_from_slice(&0.0f32.to_le_bytes());
6198        buf.extend_from_slice(&0.0f32.to_le_bytes());
6199
6200        buf.extend_from_slice(&build_bone_name_bytes("Spine"));
6201        buf.extend_from_slice(&build_bone_name_bytes(""));
6202        buf.extend_from_slice(&0.0f32.to_le_bytes());
6203        buf.extend_from_slice(&1.0f32.to_le_bytes());
6204        buf.extend_from_slice(&0.0f32.to_le_bytes());
6205        buf.extend_from_slice(&0i16.to_le_bytes());
6206        buf.extend_from_slice(&0i32.to_le_bytes());
6207        buf.extend_from_slice(&0u16.to_le_bytes());
6208        buf.extend_from_slice(&0.0f32.to_le_bytes());
6209        buf.extend_from_slice(&0.0f32.to_le_bytes());
6210        buf.extend_from_slice(&0.0f32.to_le_bytes());
6211        buf.extend_from_slice(&0i32.to_le_bytes());
6212
6213        let rt = import_pmx_runtime(&buf).unwrap();
6214        assert_eq!(rt.model.bone_count(), 2);
6215        assert_eq!(rt.model.parent_index(BoneIndex(1)), Some(BoneIndex(0)));
6216        assert_eq!(
6217            rt.bone_name_to_index.get(b"Hips".as_slice()),
6218            Some(&BoneIndex(0))
6219        );
6220        assert_eq!(
6221            rt.bone_name_to_index.get(b"Spine".as_slice()),
6222            Some(&BoneIndex(1))
6223        );
6224        assert!(rt.morph_name_to_index.is_empty());
6225        assert!(rt.ik_solver_bone_name_to_index.is_empty());
6226    }
6227
6228    #[test]
6229    fn parse_pmx_model_exposes_three_loader_shaped_sections() {
6230        let mut buf = Vec::new();
6231        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6232        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6233        buf.extend_from_slice(&build_empty_vertex_section());
6234        buf.extend_from_slice(&build_empty_face_section());
6235        buf.extend_from_slice(&build_empty_texture_section());
6236        buf.extend_from_slice(&build_empty_material_section());
6237        buf.extend_from_slice(&build_bone_section_header(2));
6238
6239        buf.extend_from_slice(&build_bone_name_bytes("Root"));
6240        buf.extend_from_slice(&build_bone_name_bytes(""));
6241        buf.extend_from_slice(&0.0f32.to_le_bytes());
6242        buf.extend_from_slice(&0.0f32.to_le_bytes());
6243        buf.extend_from_slice(&0.0f32.to_le_bytes());
6244        buf.extend_from_slice(&(-1i16).to_le_bytes());
6245        buf.extend_from_slice(&0i32.to_le_bytes());
6246        buf.extend_from_slice(&0u16.to_le_bytes());
6247        buf.extend_from_slice(&0.0f32.to_le_bytes());
6248        buf.extend_from_slice(&0.0f32.to_le_bytes());
6249        buf.extend_from_slice(&0.0f32.to_le_bytes());
6250
6251        buf.extend_from_slice(&build_bone_name_bytes("Child"));
6252        buf.extend_from_slice(&build_bone_name_bytes(""));
6253        buf.extend_from_slice(&0.0f32.to_le_bytes());
6254        buf.extend_from_slice(&1.0f32.to_le_bytes());
6255        buf.extend_from_slice(&0.0f32.to_le_bytes());
6256        buf.extend_from_slice(&0i16.to_le_bytes());
6257        buf.extend_from_slice(&0i32.to_le_bytes());
6258        buf.extend_from_slice(&0u16.to_le_bytes());
6259        buf.extend_from_slice(&0.0f32.to_le_bytes());
6260        buf.extend_from_slice(&0.0f32.to_le_bytes());
6261        buf.extend_from_slice(&0.0f32.to_le_bytes());
6262
6263        buf.extend_from_slice(&0i32.to_le_bytes()); // morphs
6264        buf.extend_from_slice(&1i32.to_le_bytes()); // display frames
6265        buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
6266        buf.extend_from_slice(&build_bone_name_bytes(""));
6267        buf.push(1);
6268        buf.extend_from_slice(&1i32.to_le_bytes());
6269        buf.push(0);
6270        buf.extend_from_slice(&1i16.to_le_bytes());
6271        buf.extend_from_slice(&0i32.to_le_bytes()); // rigid bodies
6272        buf.extend_from_slice(&0i32.to_le_bytes()); // joints
6273
6274        let parsed = parse_pmx_model(&buf).unwrap();
6275        assert_eq!(parsed.metadata.format, "pmx");
6276        assert_eq!(parsed.metadata.counts.bones, 2);
6277        assert_eq!(parsed.skeleton.bones[0].name, "Root");
6278        assert_eq!(parsed.skeleton.bones[1].parent_index, 0);
6279        assert_eq!(parsed.display_frames.len(), 1);
6280        assert_eq!(parsed.display_frames[0].frames[0].kind, "bone");
6281        assert_eq!(parsed.display_frames[0].frames[0].index, 1);
6282        assert!(parsed.diagnostics.is_empty());
6283    }
6284
6285    #[test]
6286    fn exports_pmx_semantic_roundtrip() {
6287        let parsed = parsed_pmx_fixture();
6288        let exported = export_pmx_model(&parsed);
6289        let reparsed = parse_pmx_model(&exported).unwrap();
6290
6291        assert_pmx_roundtrip_eq(&parsed, &reparsed);
6292    }
6293
6294    #[test]
6295    fn exports_all_pmx_morph_panels_without_falling_back_to_other() {
6296        for panel in ["system", "brow", "eye", "mouth", "other"] {
6297            let mut parsed = parsed_pmx_fixture();
6298            parsed.morphs[0].panel = panel.to_owned();
6299
6300            let exported = export_pmx_model(&parsed);
6301            let reparsed = parse_pmx_model(&exported).unwrap();
6302
6303            assert_eq!(reparsed.morphs[0].panel, panel);
6304        }
6305    }
6306
6307    #[test]
6308    fn exports_pmx_json_dto_semantic_roundtrip() {
6309        let parsed = parsed_pmx_fixture();
6310        let json = serde_json::to_string(&parsed).unwrap();
6311        let from_json: PmxParsedModel = serde_json::from_str(&json).unwrap();
6312        let exported = export_pmx_model(&from_json);
6313        let reparsed = parse_pmx_model(&exported).unwrap();
6314
6315        assert_pmx_roundtrip_eq(&parsed, &reparsed);
6316    }
6317
6318    #[test]
6319    fn pmx_json_dto_roundtrips_non_finite_f32_values() {
6320        let mut parsed = parsed_pmx_fixture();
6321        let finite_position = parsed.geometry.positions[1];
6322        parsed.geometry.positions[0] = f32::NAN;
6323        parsed.geometry.normals[1] = f32::INFINITY;
6324        parsed.geometry.uvs[0] = f32::NEG_INFINITY;
6325        parsed.materials[0].diffuse[0] = f32::INFINITY;
6326        parsed.materials[0].edge_size = f32::NAN;
6327        parsed.skeleton.bones[0].position[0] = f32::NEG_INFINITY;
6328        parsed.morphs[0].vertex_offsets[0].position[1] = f32::NAN;
6329
6330        let json = serde_json::to_string(&parsed).unwrap();
6331        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
6332
6333        assert!(json.contains("\"NaN\""));
6334        assert!(json.contains("\"Infinity\""));
6335        assert!(json.contains("\"-Infinity\""));
6336        assert_eq!(value["geometry"]["positions"][0], serde_json::json!("NaN"));
6337        assert_eq!(
6338            value["geometry"]["normals"][1],
6339            serde_json::json!("Infinity")
6340        );
6341        assert_eq!(value["geometry"]["uvs"][0], serde_json::json!("-Infinity"));
6342        assert_eq!(
6343            value["geometry"]["positions"][1],
6344            serde_json::json!(finite_position)
6345        );
6346
6347        let from_json: PmxParsedModel = serde_json::from_str(&json).unwrap();
6348
6349        assert!(from_json.geometry.positions[0].is_nan());
6350        assert_eq!(from_json.geometry.normals[1], f32::INFINITY);
6351        assert_eq!(from_json.geometry.uvs[0], f32::NEG_INFINITY);
6352        assert_eq!(from_json.materials[0].diffuse[0], f32::INFINITY);
6353        assert!(from_json.materials[0].edge_size.is_nan());
6354        assert_eq!(from_json.skeleton.bones[0].position[0], f32::NEG_INFINITY);
6355        assert!(from_json.morphs[0].vertex_offsets[0].position[1].is_nan());
6356        assert_eq!(from_json.geometry.positions[1], finite_position);
6357    }
6358
6359    #[test]
6360    fn builds_pmx_parts_with_material_descriptors() {
6361        let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6362            "name": "material-parts",
6363            "materials": [
6364                {
6365                    "name": "red",
6366                    "englishName": "red-en",
6367                    "diffuse": [1.0, 0.0, 0.0, 1.0],
6368                    "ambient": [0.5, 0.0, 0.0],
6369                    "flags": { "edge": true },
6370                    "faceCount": 1
6371                },
6372                {
6373                    "name": "blue",
6374                    "diffuse": [0.0, 0.0, 1.0, 1.0],
6375                    "faceCount": 1
6376                }
6377            ],
6378            "bones": [
6379                { "name": "root", "englishName": "root-en", "tailIndex": 1 },
6380                { "name": "child", "parentIndex": 0, "position": [0.0, 1.0, 0.0], "rotatable": true }
6381            ],
6382            "morphs": [
6383                {
6384                    "name": "raise",
6385                    "englishName": "raise-en",
6386                    "kind": "vertex",
6387                    "vertexOffsets": [{ "vertexIndex": 0, "position": [0.0, 0.1, 0.0] }]
6388                },
6389                {
6390                    "name": "combo",
6391                    "kind": "group",
6392                    "groupOffsets": [{ "morphIndex": 0, "weight": 0.5 }]
6393                }
6394            ],
6395            "displayFrames": [
6396                {
6397                    "name": "Root",
6398                    "englishName": "Root-en",
6399                    "special": true,
6400                    "frames": [
6401                        { "kind": "bone", "index": 0 },
6402                        { "kind": "bone", "index": 1 },
6403                        { "kind": "morph", "index": 0 }
6404                    ]
6405                }
6406            ],
6407            "rigidBodies": [
6408                {
6409                    "name": "body",
6410                    "englishName": "body-en",
6411                    "boneIndex": 1,
6412                    "group": 2,
6413                    "mask": 3,
6414                    "shape": "box",
6415                    "size": [1.0, 2.0, 3.0],
6416                    "position": [0.0, 1.0, 0.0],
6417                    "rotation": [0.1, 0.2, 0.3],
6418                    "mass": 2.0,
6419                    "linearDamping": 0.4,
6420                    "angularDamping": 0.5,
6421                    "restitution": 0.6,
6422                    "friction": 0.7,
6423                    "mode": "dynamicBone"
6424                }
6425            ],
6426            "joints": [
6427                {
6428                    "name": "joint",
6429                    "type": "generic6dofSpring",
6430                    "rigidBodyIndexA": 0,
6431                    "rigidBodyIndexB": -1,
6432                    "position": [0.0, 1.0, 0.0],
6433                    "translationLowerLimit": [-1.0, -1.0, -1.0],
6434                    "translationUpperLimit": [1.0, 1.0, 1.0],
6435                    "springTranslationFactor": [0.1, 0.2, 0.3]
6436                }
6437            ],
6438            "indexSizes": {
6439                "vertex": 1,
6440                "material": 1,
6441                "texture": 1,
6442                "bone": 1,
6443                "morph": 1,
6444                "rigidBody": 1
6445            }
6446        }))
6447        .unwrap();
6448        let model = build_pmx_model_from_parts(PmxPartsInput {
6449            descriptor,
6450            positions_xyz: &[
6451                0.0, 0.0, 0.0, //
6452                1.0, 0.0, 0.0, //
6453                1.0, 1.0, 0.0, //
6454                0.0, 1.0, 0.0,
6455            ],
6456            normals_xyz: &[
6457                0.0, 0.0, 1.0, //
6458                0.0, 0.0, 1.0, //
6459                0.0, 0.0, 1.0, //
6460                0.0, 0.0, 1.0,
6461            ],
6462            uvs_xy: &[0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],
6463            indices: &[0, 1, 2, 0, 2, 3],
6464            skin_indices: &[0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0],
6465            skin_weights: &[
6466                0.75, 0.25, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
6467            ],
6468            edge_scale: &[],
6469        })
6470        .unwrap();
6471        let exported = export_pmx_model(&model);
6472        let reparsed = parse_pmx_model(&exported).unwrap();
6473
6474        assert_eq!(reparsed.metadata.name, "material-parts");
6475        assert_eq!(reparsed.metadata.counts.materials, 2);
6476        assert_eq!(reparsed.materials[0].name, "red");
6477        assert_eq!(reparsed.materials[0].english_name, "red-en");
6478        assert_eq!(reparsed.materials[0].face_count, 1);
6479        assert!(reparsed.materials[0].flags.edge);
6480        assert_eq!(reparsed.materials[1].name, "blue");
6481        assert_eq!(reparsed.materials[1].face_count, 1);
6482        assert_eq!(reparsed.geometry.material_groups.len(), 2);
6483        assert_eq!(reparsed.geometry.material_groups[0].start, 0);
6484        assert_eq!(reparsed.geometry.material_groups[0].count, 3);
6485        assert_eq!(reparsed.geometry.material_groups[1].start, 3);
6486        assert_eq!(reparsed.geometry.material_groups[1].count, 3);
6487        assert_eq!(reparsed.metadata.counts.bones, 2);
6488        assert_eq!(reparsed.skeleton.bones[0].name, "root");
6489        assert_eq!(reparsed.skeleton.bones[0].english_name, "root-en");
6490        assert_eq!(reparsed.skeleton.bones[0].tail_index, 1);
6491        assert!(reparsed.skeleton.bones[0].flags.indexed_tail);
6492        assert_eq!(reparsed.skeleton.bones[1].name, "child");
6493        assert_eq!(reparsed.skeleton.bones[1].parent_index, 0);
6494        assert!(reparsed.skeleton.bones[1].flags.rotatable);
6495        assert_eq!(reparsed.geometry.skin_indices[1], 1);
6496        assert_eq!(reparsed.geometry.skin_weights[1], 0.25);
6497        assert_eq!(reparsed.metadata.counts.morphs, 2);
6498        assert_eq!(reparsed.morphs[0].name, "raise");
6499        assert_eq!(reparsed.morphs[0].english_name, "raise-en");
6500        assert_eq!(reparsed.morphs[0].kind, "vertex");
6501        assert_eq!(reparsed.morphs[0].vertex_offsets[0].vertex_index, 0);
6502        assert_eq!(
6503            reparsed.morphs[0].vertex_offsets[0].position,
6504            [0.0, 0.1, 0.0]
6505        );
6506        assert_eq!(reparsed.morphs[1].name, "combo");
6507        assert_eq!(reparsed.morphs[1].kind, "group");
6508        assert_eq!(reparsed.morphs[1].group_offsets[0].morph_index, 0);
6509        assert_eq!(reparsed.morphs[1].group_offsets[0].weight, 0.5);
6510        assert_eq!(reparsed.display_frames.len(), 1);
6511        assert_eq!(reparsed.display_frames[0].name, "Root");
6512        assert_eq!(reparsed.display_frames[0].english_name, "Root-en");
6513        assert!(reparsed.display_frames[0].special);
6514        assert_eq!(reparsed.display_frames[0].frames.len(), 3);
6515        assert_eq!(reparsed.display_frames[0].frames[1].kind, "bone");
6516        assert_eq!(reparsed.display_frames[0].frames[1].index, 1);
6517        assert_eq!(reparsed.display_frames[0].frames[2].kind, "morph");
6518        assert_eq!(reparsed.display_frames[0].frames[2].index, 0);
6519        assert_eq!(reparsed.rigid_bodies.len(), 1);
6520        assert_eq!(reparsed.rigid_bodies[0].name, "body");
6521        assert_eq!(reparsed.rigid_bodies[0].english_name, "body-en");
6522        assert_eq!(reparsed.rigid_bodies[0].bone_index, 1);
6523        assert_eq!(reparsed.rigid_bodies[0].group, 2);
6524        assert_eq!(reparsed.rigid_bodies[0].mask, 3);
6525        assert_eq!(reparsed.rigid_bodies[0].shape, "box");
6526        assert_eq!(reparsed.rigid_bodies[0].mode, "dynamicBone");
6527        assert_eq!(reparsed.joints.len(), 1);
6528        assert_eq!(reparsed.joints[0].name, "joint");
6529        assert_eq!(reparsed.joints[0].kind, "generic6dofSpring");
6530        assert_eq!(reparsed.joints[0].rigid_body_index_a, 0);
6531        assert_eq!(reparsed.joints[0].rigid_body_index_b, -1);
6532    }
6533
6534    #[test]
6535    fn pmx_parts_rigidbody_mask_defaults_to_collide_with_all_groups() {
6536        let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6537            "bones": [{ "name": "root" }],
6538            "rigidBodies": [
6539                {
6540                    "name": "body",
6541                    "boneIndex": 0
6542                }
6543            ]
6544        }))
6545        .unwrap();
6546
6547        assert_eq!(descriptor.rigid_bodies[0].mask, 0xffff);
6548    }
6549
6550    #[test]
6551    fn rejects_pmx_parts_material_face_count_mismatch() {
6552        let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6553            "materials": [{ "name": "bad", "faceCount": 2 }]
6554        }))
6555        .unwrap();
6556        let error = build_pmx_model_from_parts(PmxPartsInput {
6557            descriptor,
6558            positions_xyz: &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
6559            normals_xyz: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
6560            uvs_xy: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
6561            indices: &[0, 1, 2],
6562            skin_indices: &[],
6563            skin_weights: &[],
6564            edge_scale: &[],
6565        })
6566        .unwrap_err();
6567
6568        assert!(error.contains("materials faceCount sum"));
6569    }
6570
6571    #[test]
6572    fn rejects_invalid_pmx_parts_morph_panel() {
6573        let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
6574            "morphs": [{ "name": "typo", "kind": "vertex", "panel": "moutn" }]
6575        }))
6576        .unwrap();
6577        let error = build_pmx_model_from_parts(PmxPartsInput {
6578            descriptor,
6579            positions_xyz: &[0.0, 0.0, 0.0],
6580            normals_xyz: &[0.0, 1.0, 0.0],
6581            uvs_xy: &[0.0, 0.0],
6582            indices: &[],
6583            skin_indices: &[],
6584            skin_weights: &[],
6585            edge_scale: &[],
6586        })
6587        .unwrap_err();
6588
6589        assert_eq!(
6590            error,
6591            "morphs[0].panel must be system, brow, eye, mouth, or other, got moutn"
6592        );
6593    }
6594
6595    #[test]
6596    fn rejects_pmx_export_model_partial_skin_arrays() {
6597        let mut parsed = parsed_pmx_fixture();
6598        parsed.geometry.skin_weights.clear();
6599
6600        let error = validate_pmx_export_model(&parsed).unwrap_err();
6601
6602        assert!(error.contains("skinIndices and skinWeights"));
6603    }
6604
6605    // Synthetic PMX 2.2 gate — no real PMX 2.2 fixture exists in the scan set yet.
6606    // Verifies that version=2.2 survives export_pmx_model -> parse_pmx_model unchanged.
6607    #[test]
6608    fn exports_pmx_22_synthetic_roundtrip() {
6609        let mut parsed = parsed_pmx_fixture();
6610        parsed.metadata.version = 2.2;
6611
6612        let exported = export_pmx_model(&parsed);
6613        let reparsed = parse_pmx_model(&exported).unwrap();
6614
6615        assert_eq!(
6616            reparsed.metadata.version, 2.2,
6617            "PMX 2.2 version must survive the export-parse cycle"
6618        );
6619        assert_pmx_roundtrip_eq(&parsed, &reparsed);
6620    }
6621
6622    #[test]
6623    fn exports_pmx_soft_body_header_fields_with_diagnostic_for_pmx_21_and_22() {
6624        for version in [2.1, 2.2] {
6625            let mut parsed = parsed_pmx_fixture();
6626            parsed.metadata.version = version;
6627            parsed.soft_bodies = vec![PmxParsedSoftBody {
6628                name: "soft".to_owned(),
6629                english_name: "soft-en".to_owned(),
6630                kind: "rope".to_owned(),
6631                material_index: 0,
6632                collision_group: 1,
6633                collision_mask: 2,
6634                flags: 3,
6635                bending_constraints_distance: 4,
6636                cluster_count: 5,
6637                total_mass: 6.0,
6638                collision_margin: 0.7,
6639            }];
6640            let exported = export_pmx_model(&parsed);
6641            let reparsed = parse_pmx_model(&exported).unwrap();
6642
6643            assert_eq!(reparsed.metadata.version, version);
6644            assert_eq!(reparsed.metadata.counts.soft_bodies, 1);
6645            assert_eq!(reparsed.soft_bodies.len(), 1);
6646            assert_eq!(reparsed.soft_bodies[0].name, "soft");
6647            assert_eq!(reparsed.soft_bodies[0].english_name, "soft-en");
6648            assert_eq!(reparsed.soft_bodies[0].kind, "rope");
6649            assert_eq!(reparsed.soft_bodies[0].material_index, 0);
6650            assert_eq!(reparsed.soft_bodies[0].collision_group, 1);
6651            assert_eq!(reparsed.soft_bodies[0].collision_mask, 2);
6652            assert_eq!(reparsed.soft_bodies[0].flags, 3);
6653            assert_eq!(reparsed.soft_bodies[0].bending_constraints_distance, 4);
6654            assert_eq!(reparsed.soft_bodies[0].cluster_count, 5);
6655            assert_eq!(reparsed.soft_bodies[0].total_mass, 6.0);
6656            assert_eq!(reparsed.soft_bodies[0].collision_margin, 0.7);
6657            assert_eq!(reparsed.diagnostics.len(), 1);
6658            assert_eq!(reparsed.diagnostics[0].code, "PMX_SOFT_BODY_UNSUPPORTED");
6659        }
6660    }
6661
6662    #[test]
6663    fn exports_parsed_pmx_skeleton_display_roundtrip() {
6664        let mut buf = Vec::new();
6665        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6666        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6667        buf.extend_from_slice(&build_empty_vertex_section());
6668        buf.extend_from_slice(&build_empty_face_section());
6669        buf.extend_from_slice(&build_empty_texture_section());
6670        buf.extend_from_slice(&build_empty_material_section());
6671        buf.extend_from_slice(&build_bone_section_header(2));
6672
6673        buf.extend_from_slice(&build_bone_name_bytes("Root"));
6674        buf.extend_from_slice(&build_bone_name_bytes(""));
6675        buf.extend_from_slice(&0.0f32.to_le_bytes());
6676        buf.extend_from_slice(&0.0f32.to_le_bytes());
6677        buf.extend_from_slice(&0.0f32.to_le_bytes());
6678        buf.extend_from_slice(&(-1i16).to_le_bytes());
6679        buf.extend_from_slice(&0i32.to_le_bytes());
6680        buf.extend_from_slice(&0u16.to_le_bytes());
6681        buf.extend_from_slice(&0.0f32.to_le_bytes());
6682        buf.extend_from_slice(&0.0f32.to_le_bytes());
6683        buf.extend_from_slice(&0.0f32.to_le_bytes());
6684
6685        buf.extend_from_slice(&build_bone_name_bytes("Child"));
6686        buf.extend_from_slice(&build_bone_name_bytes(""));
6687        buf.extend_from_slice(&0.0f32.to_le_bytes());
6688        buf.extend_from_slice(&1.0f32.to_le_bytes());
6689        buf.extend_from_slice(&0.0f32.to_le_bytes());
6690        buf.extend_from_slice(&0i16.to_le_bytes());
6691        buf.extend_from_slice(&0i32.to_le_bytes());
6692        buf.extend_from_slice(&0u16.to_le_bytes());
6693        buf.extend_from_slice(&0.0f32.to_le_bytes());
6694        buf.extend_from_slice(&0.0f32.to_le_bytes());
6695        buf.extend_from_slice(&0.0f32.to_le_bytes());
6696
6697        buf.extend_from_slice(&0i32.to_le_bytes()); // morphs
6698        buf.extend_from_slice(&1i32.to_le_bytes()); // display frames
6699        buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
6700        buf.extend_from_slice(&build_bone_name_bytes(""));
6701        buf.push(1);
6702        buf.extend_from_slice(&1i32.to_le_bytes());
6703        buf.push(0);
6704        buf.extend_from_slice(&1i16.to_le_bytes());
6705        buf.extend_from_slice(&0i32.to_le_bytes()); // rigid bodies
6706        buf.extend_from_slice(&0i32.to_le_bytes()); // joints
6707
6708        let parsed = parse_pmx_model(&buf).unwrap();
6709        let exported = export_pmx_model(&parsed);
6710        let reparsed = parse_pmx_model(&exported).unwrap();
6711
6712        assert_pmx_roundtrip_eq(&parsed, &reparsed);
6713    }
6714
6715    #[test]
6716    fn runtime_import_ik_solver_name_lookup() {
6717        let mut buf = Vec::new();
6718        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6719        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6720        buf.extend_from_slice(&build_empty_vertex_section());
6721        buf.extend_from_slice(&build_empty_face_section());
6722        buf.extend_from_slice(&build_empty_texture_section());
6723        buf.extend_from_slice(&build_empty_material_section());
6724        buf.extend_from_slice(&build_bone_section_header(3));
6725
6726        buf.extend_from_slice(&build_bone_name_bytes("Root"));
6727        buf.extend_from_slice(&build_bone_name_bytes(""));
6728        buf.extend_from_slice(&0.0f32.to_le_bytes());
6729        buf.extend_from_slice(&0.0f32.to_le_bytes());
6730        buf.extend_from_slice(&0.0f32.to_le_bytes());
6731        buf.extend_from_slice(&(-1i16).to_le_bytes());
6732        buf.extend_from_slice(&0i32.to_le_bytes());
6733        buf.extend_from_slice(&0u16.to_le_bytes());
6734        buf.extend_from_slice(&0.0f32.to_le_bytes());
6735        buf.extend_from_slice(&0.0f32.to_le_bytes());
6736        buf.extend_from_slice(&0.0f32.to_le_bytes());
6737
6738        buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
6739        buf.extend_from_slice(&build_bone_name_bytes(""));
6740        buf.extend_from_slice(&2.0f32.to_le_bytes());
6741        buf.extend_from_slice(&0.0f32.to_le_bytes());
6742        buf.extend_from_slice(&0.0f32.to_le_bytes());
6743        buf.extend_from_slice(&0i16.to_le_bytes());
6744        buf.extend_from_slice(&0i32.to_le_bytes());
6745        buf.extend_from_slice(&0u16.to_le_bytes());
6746        buf.extend_from_slice(&0.0f32.to_le_bytes());
6747        buf.extend_from_slice(&0.0f32.to_le_bytes());
6748        buf.extend_from_slice(&0.0f32.to_le_bytes());
6749
6750        buf.extend_from_slice(&build_bone_name_bytes("LeftLegIK"));
6751        buf.extend_from_slice(&build_bone_name_bytes(""));
6752        buf.extend_from_slice(&4.0f32.to_le_bytes());
6753        buf.extend_from_slice(&0.0f32.to_le_bytes());
6754        buf.extend_from_slice(&0.0f32.to_le_bytes());
6755        buf.extend_from_slice(&0i16.to_le_bytes());
6756        buf.extend_from_slice(&0i32.to_le_bytes());
6757        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
6758        buf.extend_from_slice(&1i16.to_le_bytes());
6759        buf.extend_from_slice(&1i16.to_le_bytes());
6760        buf.extend_from_slice(&10i32.to_le_bytes());
6761        buf.extend_from_slice(&0.5f32.to_le_bytes());
6762        buf.extend_from_slice(&1i32.to_le_bytes());
6763        buf.extend_from_slice(&1i16.to_le_bytes());
6764        buf.push(1u8);
6765        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6766        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6767        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
6768        buf.extend_from_slice(&1.0f32.to_le_bytes());
6769        buf.extend_from_slice(&1.0f32.to_le_bytes());
6770        buf.extend_from_slice(&1.0f32.to_le_bytes());
6771        buf.extend_from_slice(&0i32.to_le_bytes());
6772
6773        let rt = import_pmx_runtime(&buf).unwrap();
6774        assert_eq!(rt.model.bone_count(), 3);
6775        assert_eq!(rt.model.ik_count(), 1);
6776        assert_eq!(
6777            rt.ik_solver_bone_name_to_index.get(b"LeftLegIK".as_slice()),
6778            Some(&0)
6779        );
6780        assert_eq!(
6781            rt.bone_name_to_index.get(b"LeftLegIK".as_slice()),
6782            Some(&BoneIndex(2))
6783        );
6784        assert_eq!(
6785            rt.ik_solver_bone_name_to_index.get(b"NoSuchIK".as_slice()),
6786            None
6787        );
6788    }
6789
6790    #[test]
6791    fn converts_absolute_pmx_positions_to_local_rest_positions() {
6792        let mut buf = Vec::new();
6793        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6794        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6795        buf.extend_from_slice(&build_empty_vertex_section());
6796        buf.extend_from_slice(&build_empty_face_section());
6797        buf.extend_from_slice(&build_empty_texture_section());
6798        buf.extend_from_slice(&build_empty_material_section());
6799        buf.extend_from_slice(&build_bone_section_header(2));
6800
6801        buf.extend_from_slice(&build_bone_name_bytes("Parent"));
6802        buf.extend_from_slice(&build_bone_name_bytes(""));
6803        buf.extend_from_slice(&1.0f32.to_le_bytes());
6804        buf.extend_from_slice(&2.0f32.to_le_bytes());
6805        buf.extend_from_slice(&0.0f32.to_le_bytes());
6806        buf.extend_from_slice(&(-1i16).to_le_bytes());
6807        buf.extend_from_slice(&0i32.to_le_bytes());
6808        buf.extend_from_slice(&0u16.to_le_bytes());
6809        buf.extend_from_slice(&0.0f32.to_le_bytes());
6810        buf.extend_from_slice(&0.0f32.to_le_bytes());
6811        buf.extend_from_slice(&0.0f32.to_le_bytes());
6812
6813        buf.extend_from_slice(&build_bone_name_bytes("Child"));
6814        buf.extend_from_slice(&build_bone_name_bytes(""));
6815        buf.extend_from_slice(&1.0f32.to_le_bytes());
6816        buf.extend_from_slice(&3.0f32.to_le_bytes());
6817        buf.extend_from_slice(&0.0f32.to_le_bytes());
6818        buf.extend_from_slice(&0i16.to_le_bytes());
6819        buf.extend_from_slice(&0i32.to_le_bytes());
6820        buf.extend_from_slice(&0u16.to_le_bytes());
6821        buf.extend_from_slice(&0.0f32.to_le_bytes());
6822        buf.extend_from_slice(&0.0f32.to_le_bytes());
6823        buf.extend_from_slice(&0.0f32.to_le_bytes());
6824
6825        let result = import_pmx_model(&buf).unwrap();
6826        assert_eq!(result.bones.len(), 2);
6827        assert_eq!(result.bones[0].parent, None);
6828        assert!((result.bones[0].rest_position.x - 1.0).abs() < 0.001);
6829        assert!((result.bones[0].rest_position.y - 2.0).abs() < 0.001);
6830        assert!((result.bones[0].rest_position.z - 0.0).abs() < 0.001);
6831        assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
6832        assert!((result.bones[1].rest_position.x - 0.0).abs() < 0.001);
6833        assert!((result.bones[1].rest_position.y - 1.0).abs() < 0.001);
6834        assert!((result.bones[1].rest_position.z - 0.0).abs() < 0.001);
6835    }
6836
6837    #[test]
6838    fn root_inverse_bind_cancels_rest_world() {
6839        let mut buf = Vec::new();
6840        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6841        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6842        buf.extend_from_slice(&build_empty_vertex_section());
6843        buf.extend_from_slice(&build_empty_face_section());
6844        buf.extend_from_slice(&build_empty_texture_section());
6845        buf.extend_from_slice(&build_empty_material_section());
6846        buf.extend_from_slice(&build_bone_section_header(1));
6847
6848        buf.extend_from_slice(&build_bone_name_bytes("Root"));
6849        buf.extend_from_slice(&build_bone_name_bytes(""));
6850        buf.extend_from_slice(&1.0f32.to_le_bytes());
6851        buf.extend_from_slice(&2.0f32.to_le_bytes());
6852        buf.extend_from_slice(&3.0f32.to_le_bytes());
6853        buf.extend_from_slice(&(-1i16).to_le_bytes());
6854        buf.extend_from_slice(&0i32.to_le_bytes());
6855        buf.extend_from_slice(&0u16.to_le_bytes());
6856        buf.extend_from_slice(&0.0f32.to_le_bytes());
6857        buf.extend_from_slice(&0.0f32.to_le_bytes());
6858        buf.extend_from_slice(&0.0f32.to_le_bytes());
6859
6860        let result = import_pmx_model(&buf).unwrap();
6861        let ibm = result.bones[0].inverse_bind_matrix;
6862        let rest_world = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
6863        assert_eq!(rest_world * ibm, Mat4::IDENTITY);
6864    }
6865
6866    #[test]
6867    fn reads_pmx_fixed_axis_bone_descriptor() {
6868        let mut buf = Vec::new();
6869        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6870        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6871        buf.extend_from_slice(&build_empty_vertex_section());
6872        buf.extend_from_slice(&build_empty_face_section());
6873        buf.extend_from_slice(&build_empty_texture_section());
6874        buf.extend_from_slice(&build_empty_material_section());
6875        buf.extend_from_slice(&build_bone_section_header(1));
6876
6877        buf.extend_from_slice(&build_bone_name_bytes("Fixed"));
6878        buf.extend_from_slice(&build_bone_name_bytes(""));
6879        buf.extend_from_slice(&0.0f32.to_le_bytes());
6880        buf.extend_from_slice(&0.0f32.to_le_bytes());
6881        buf.extend_from_slice(&0.0f32.to_le_bytes());
6882        buf.extend_from_slice(&(-1i16).to_le_bytes());
6883        buf.extend_from_slice(&0i32.to_le_bytes());
6884        buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.to_le_bytes());
6885        buf.extend_from_slice(&0.0f32.to_le_bytes());
6886        buf.extend_from_slice(&0.0f32.to_le_bytes());
6887        buf.extend_from_slice(&0.0f32.to_le_bytes());
6888        buf.extend_from_slice(&0.0f32.to_le_bytes());
6889        buf.extend_from_slice(&1.0f32.to_le_bytes());
6890        buf.extend_from_slice(&0.0f32.to_le_bytes());
6891
6892        let result = import_pmx_model(&buf).unwrap();
6893        assert_eq!(result.bones[0].fixed_axis, Some(Vec3A::Y));
6894        assert!(!result.bones[0].enforce_fixed_axis);
6895    }
6896
6897    #[test]
6898    fn reads_pmx_local_axis_bone_descriptor_into_runtime_init() {
6899        let mut buf = Vec::new();
6900        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6901        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6902        buf.extend_from_slice(&build_empty_vertex_section());
6903        buf.extend_from_slice(&build_empty_face_section());
6904        buf.extend_from_slice(&build_empty_texture_section());
6905        buf.extend_from_slice(&build_empty_material_section());
6906        buf.extend_from_slice(&build_bone_section_header(1));
6907
6908        buf.extend_from_slice(&build_bone_name_bytes("Local"));
6909        buf.extend_from_slice(&build_bone_name_bytes(""));
6910        buf.extend_from_slice(&0.0f32.to_le_bytes());
6911        buf.extend_from_slice(&0.0f32.to_le_bytes());
6912        buf.extend_from_slice(&0.0f32.to_le_bytes());
6913        buf.extend_from_slice(&(-1i16).to_le_bytes());
6914        buf.extend_from_slice(&0i32.to_le_bytes());
6915        buf.extend_from_slice(&BONE_FLAG_LOCAL_AXIS.to_le_bytes());
6916        // Tail offset (BONE_FLAG_TAIL_INDEX clear) then localAxis x/z.
6917        buf.extend_from_slice(&0.0f32.to_le_bytes());
6918        buf.extend_from_slice(&0.0f32.to_le_bytes());
6919        buf.extend_from_slice(&0.0f32.to_le_bytes());
6920        // localAxis.x = (0, 1, 0), localAxis.z = (0, 0, 1)
6921        buf.extend_from_slice(&0.0f32.to_le_bytes());
6922        buf.extend_from_slice(&1.0f32.to_le_bytes());
6923        buf.extend_from_slice(&0.0f32.to_le_bytes());
6924        buf.extend_from_slice(&0.0f32.to_le_bytes());
6925        buf.extend_from_slice(&0.0f32.to_le_bytes());
6926        buf.extend_from_slice(&1.0f32.to_le_bytes());
6927        // Empty morph section: import_pmx_runtime stops after this section.
6928        buf.extend_from_slice(&0i32.to_le_bytes());
6929
6930        let result = import_pmx_runtime(&buf).expect("localAxis PMX must import for runtime");
6931        let model = result.model;
6932        let local = model
6933            .local_axis(BoneIndex(0))
6934            .expect("runtime import must retain PMX localAxis");
6935        assert_eq!(local.x, Vec3A::Y);
6936        assert_eq!(local.z, Vec3A::Z);
6937        let basis = model
6938            .local_axis_basis(BoneIndex(0))
6939            .expect("runtime import must precompute a localAxis basis");
6940        assert!((basis.mul_vec3a(Vec3A::X) - Vec3A::Y).length() < 1.0e-5);
6941        assert!((basis.mul_vec3a(Vec3A::Y) + Vec3A::X).length() < 1.0e-5);
6942        assert!((basis.mul_vec3a(Vec3A::Z) - Vec3A::Z).length() < 1.0e-5);
6943    }
6944
6945    #[test]
6946    fn runtime_import_drops_degenerate_local_axis() {
6947        let mut buf = Vec::new();
6948        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6949        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6950        buf.extend_from_slice(&build_empty_vertex_section());
6951        buf.extend_from_slice(&build_empty_face_section());
6952        buf.extend_from_slice(&build_empty_texture_section());
6953        buf.extend_from_slice(&build_empty_material_section());
6954        buf.extend_from_slice(&build_bone_section_header(1));
6955        buf.extend_from_slice(&build_bone_name_bytes("DegenerateLocal"));
6956        buf.extend_from_slice(&build_bone_name_bytes(""));
6957        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6958        buf.extend_from_slice(&(-1i16).to_le_bytes());
6959        buf.extend_from_slice(&0i32.to_le_bytes());
6960        buf.extend_from_slice(&BONE_FLAG_LOCAL_AXIS.to_le_bytes());
6961        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6962        // localAxis.x is zero, so tolerant PMX normalization drops the axis
6963        // before invoking the strict descriptor compiler.
6964        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6965        buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
6966        buf.extend_from_slice(&0i32.to_le_bytes());
6967
6968        let imported = import_pmx_runtime(&buf).expect("degenerate PMX axis is tolerated");
6969        assert!(imported.model.local_axis(BoneIndex(0)).is_none());
6970    }
6971
6972    #[test]
6973    fn runtime_import_drops_degenerate_fixed_axis() {
6974        let mut buf = Vec::new();
6975        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
6976        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
6977        buf.extend_from_slice(&build_empty_vertex_section());
6978        buf.extend_from_slice(&build_empty_face_section());
6979        buf.extend_from_slice(&build_empty_texture_section());
6980        buf.extend_from_slice(&build_empty_material_section());
6981        buf.extend_from_slice(&build_bone_section_header(1));
6982        buf.extend_from_slice(&build_bone_name_bytes("DegenerateFixed"));
6983        buf.extend_from_slice(&build_bone_name_bytes(""));
6984        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6985        buf.extend_from_slice(&(-1i16).to_le_bytes());
6986        buf.extend_from_slice(&0i32.to_le_bytes());
6987        buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.to_le_bytes());
6988        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6989        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
6990        buf.extend_from_slice(&0i32.to_le_bytes());
6991
6992        let imported = import_pmx_runtime(&buf).expect("degenerate PMX axis is tolerated");
6993        assert!(imported.model.fixed_axis(BoneIndex(0)).is_none());
6994    }
6995
6996    #[test]
6997    fn nil_append_parent_is_sanitized_to_no_append() {
6998        let mut buf = Vec::new();
6999        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7000        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7001        buf.extend_from_slice(&build_empty_vertex_section());
7002        buf.extend_from_slice(&build_empty_face_section());
7003        buf.extend_from_slice(&build_empty_texture_section());
7004        buf.extend_from_slice(&build_empty_material_section());
7005        buf.extend_from_slice(&build_bone_section_header(1));
7006        buf.extend_from_slice(&build_bone_name_bytes("NilAppend"));
7007        buf.extend_from_slice(&build_bone_name_bytes(""));
7008        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7009        buf.extend_from_slice(&(-1i16).to_le_bytes());
7010        buf.extend_from_slice(&0i32.to_le_bytes());
7011        buf.extend_from_slice(&BONE_FLAG_APPEND_ROTATE.to_le_bytes());
7012        // Tail offset, then the malformed optional append parent and ratio.
7013        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7014        buf.extend_from_slice(&(-1i16).to_le_bytes());
7015        buf.extend_from_slice(&0.5f32.to_le_bytes());
7016        buf.extend_from_slice(&0i32.to_le_bytes());
7017
7018        let imported = import_pmx_runtime(&buf).expect("nil append parent is tolerated");
7019        assert!(imported.model.append_transforms().is_empty());
7020    }
7021
7022    #[test]
7023    fn non_finite_append_ratio_is_sanitized_to_no_append() {
7024        for ratio in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
7025            let mut buf = Vec::new();
7026            buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7027            buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7028            buf.extend_from_slice(&build_empty_vertex_section());
7029            buf.extend_from_slice(&build_empty_face_section());
7030            buf.extend_from_slice(&build_empty_texture_section());
7031            buf.extend_from_slice(&build_empty_material_section());
7032            buf.extend_from_slice(&build_bone_section_header(1));
7033            buf.extend_from_slice(&build_bone_name_bytes("NonFiniteAppend"));
7034            buf.extend_from_slice(&build_bone_name_bytes(""));
7035            buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7036            buf.extend_from_slice(&(-1i16).to_le_bytes());
7037            buf.extend_from_slice(&0i32.to_le_bytes());
7038            buf.extend_from_slice(&BONE_FLAG_APPEND_ROTATE.to_le_bytes());
7039            buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7040            // The source index is structurally valid; only the optional ratio is malformed.
7041            buf.extend_from_slice(&0i16.to_le_bytes());
7042            buf.extend_from_slice(&ratio.to_le_bytes());
7043            buf.extend_from_slice(&0i32.to_le_bytes());
7044
7045            let imported = import_pmx_runtime(&buf).expect("non-finite append ratio is tolerated");
7046            assert!(imported.model.append_transforms().is_empty());
7047        }
7048    }
7049
7050    #[test]
7051    fn pmx_fixed_axis_constrains_ik_end_to_end() {
7052        let mut buf = Vec::new();
7053        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7054        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7055        buf.extend_from_slice(&build_empty_vertex_section());
7056        buf.extend_from_slice(&build_empty_face_section());
7057        buf.extend_from_slice(&build_empty_texture_section());
7058        buf.extend_from_slice(&build_empty_material_section());
7059        buf.extend_from_slice(&build_bone_section_header(3));
7060
7061        // Link: rest direction is +Y. A +Z target needs an X rotation, so it
7062        // is reachable without a constraint but impossible for a Z-only link.
7063        buf.extend_from_slice(&build_bone_name_bytes("link"));
7064        buf.extend_from_slice(&build_bone_name_bytes(""));
7065        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7066        buf.extend_from_slice(&(-1i16).to_le_bytes());
7067        buf.extend_from_slice(&0i32.to_le_bytes());
7068        buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.to_le_bytes());
7069        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7070        buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7071
7072        buf.extend_from_slice(&build_bone_name_bytes("tip"));
7073        buf.extend_from_slice(&build_bone_name_bytes(""));
7074        buf.extend_from_slice(&[0.0f32, 1.0, 0.0].map(f32::to_le_bytes).concat());
7075        buf.extend_from_slice(&0i16.to_le_bytes());
7076        buf.extend_from_slice(&0i32.to_le_bytes());
7077        buf.extend_from_slice(&0u16.to_le_bytes());
7078        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7079
7080        buf.extend_from_slice(&build_bone_name_bytes("ik"));
7081        buf.extend_from_slice(&build_bone_name_bytes(""));
7082        buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7083        buf.extend_from_slice(&(-1i16).to_le_bytes());
7084        buf.extend_from_slice(&0i32.to_le_bytes());
7085        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
7086        buf.extend_from_slice(&1i16.to_le_bytes());
7087        buf.extend_from_slice(&1i16.to_le_bytes());
7088        buf.extend_from_slice(&16i32.to_le_bytes());
7089        buf.extend_from_slice(&std::f32::consts::PI.to_le_bytes());
7090        buf.extend_from_slice(&1i32.to_le_bytes());
7091        buf.extend_from_slice(&0i16.to_le_bytes());
7092        buf.push(0);
7093        buf.extend_from_slice(&0i32.to_le_bytes());
7094
7095        let imported = import_pmx_runtime(&buf).expect("fixed-axis IK PMX must import");
7096        let mut runtime = RuntimeInstance::new(Arc::new(imported.model));
7097        runtime.evaluate_current_pose();
7098
7099        let tip = Vec3A::from_vec4(runtime.world_matrices()[1].w_axis);
7100        assert!((tip - Vec3A::Y).length() < 1.0e-3, "tip={tip:?}");
7101        let rotation = runtime.pose().local_rotation(BoneIndex(0));
7102        assert!(rotation.x.abs() < 1.0e-4 && rotation.y.abs() < 1.0e-4);
7103        assert!(rotation.z.abs() < 1.0e-4, "rotation={rotation:?}");
7104    }
7105
7106    #[test]
7107    fn pmx_local_axis_rotates_ik_limit_frame() {
7108        let with_local_axis = import_pmx_runtime(&build_local_axis_limit_pmx(true)).unwrap();
7109        let without_local_axis = import_pmx_runtime(&build_local_axis_limit_pmx(false)).unwrap();
7110
7111        let mut local_runtime = RuntimeInstance::new(Arc::new(with_local_axis.model));
7112        local_runtime.evaluate_current_pose();
7113        let local_tip = Vec3A::from_vec4(local_runtime.world_matrices()[1].w_axis);
7114        assert!(
7115            (local_tip - Vec3A::Z).length() < 1.0e-3,
7116            "local-axis limit should reach +Z; tip={local_tip:?}"
7117        );
7118
7119        let mut unit_runtime = RuntimeInstance::new(Arc::new(without_local_axis.model));
7120        unit_runtime.evaluate_current_pose();
7121        let unit_tip = Vec3A::from_vec4(unit_runtime.world_matrices()[1].w_axis);
7122        assert!(
7123            (unit_tip - Vec3A::Z).length() > 0.2,
7124            "unit XYZ limit must not use the PMX local-axis frame; tip={unit_tip:?}"
7125        );
7126    }
7127
7128    #[test]
7129    fn non_positive_ik_loop_count_is_sanitized_to_no_solver() {
7130        for loop_count in [0, -1] {
7131            let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_loop_count(
7132                false, loop_count,
7133            ))
7134            .expect("malformed optional IK is tolerated");
7135            assert_eq!(imported.model.ik_count(), 0);
7136        }
7137    }
7138
7139    #[test]
7140    fn nil_ik_target_is_sanitized_to_no_solver() {
7141        let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_ik_indices(
7142            false, 16, -1, 0,
7143        ))
7144        .expect("nil optional IK target is tolerated");
7145        assert_eq!(imported.model.ik_count(), 0);
7146    }
7147
7148    #[test]
7149    fn nil_ik_link_preserves_legacy_bone_zero_fallback() {
7150        let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_ik_indices(
7151            false, 16, 1, -1,
7152        ))
7153        .expect("nil optional IK link is tolerated");
7154        assert_eq!(imported.model.ik_count(), 1);
7155        assert_eq!(imported.model.ik_solvers()[0].links[0].bone, BoneIndex(0));
7156    }
7157
7158    #[test]
7159    fn invalid_ik_limit_angle_is_sanitized_to_no_solver() {
7160        for limit_angle in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
7161            let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_metadata(
7162                false,
7163                16,
7164                1,
7165                0,
7166                limit_angle,
7167                [-std::f32::consts::FRAC_PI_2, 0.0, 0.0],
7168                [0.0, 0.0, 0.0],
7169            ))
7170            .expect("invalid optional IK limit angle is tolerated");
7171            assert_eq!(imported.model.ik_count(), 0);
7172        }
7173    }
7174
7175    #[test]
7176    fn invalid_ik_angle_limit_is_dropped_but_solver_is_retained() {
7177        let cases = [
7178            ([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0]),
7179            ([1.0, 0.0, 0.0], [0.0, 1.0, 1.0]),
7180        ];
7181        for (min, max) in cases {
7182            let imported = import_pmx_runtime(&build_local_axis_limit_pmx_with_metadata(
7183                false,
7184                16,
7185                1,
7186                0,
7187                std::f32::consts::PI,
7188                min,
7189                max,
7190            ))
7191            .expect("invalid optional IK angle limit is tolerated");
7192            assert_eq!(imported.model.ik_count(), 1);
7193            assert!(
7194                imported.model.ik_solvers()[0].links[0]
7195                    .angle_limit
7196                    .is_none()
7197            );
7198        }
7199    }
7200
7201    #[test]
7202    fn runtime_import_rejects_non_finite_derived_local_rest_position() {
7203        let mut buf = Vec::new();
7204        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7205        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7206        buf.extend_from_slice(&build_empty_vertex_section());
7207        buf.extend_from_slice(&build_empty_face_section());
7208        buf.extend_from_slice(&build_empty_texture_section());
7209        buf.extend_from_slice(&build_empty_material_section());
7210        buf.extend_from_slice(&build_bone_section_header(2));
7211
7212        for (name, position, parent) in [("Parent", f32::MAX, -1i16), ("Child", -f32::MAX, 0i16)] {
7213            buf.extend_from_slice(&build_bone_name_bytes(name));
7214            buf.extend_from_slice(&build_bone_name_bytes(""));
7215            buf.extend_from_slice(&[position, 0.0, 0.0].map(f32::to_le_bytes).concat());
7216            buf.extend_from_slice(&parent.to_le_bytes());
7217            buf.extend_from_slice(&0i32.to_le_bytes());
7218            buf.extend_from_slice(&0u16.to_le_bytes());
7219            buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7220        }
7221        buf.extend_from_slice(&0i32.to_le_bytes());
7222
7223        let result = import_pmx_runtime(&buf);
7224        assert!(matches!(
7225            result,
7226            Err(ImportError::ModelBuildFailed(
7227                mmd_anim_runtime::ModelBuildError::InvalidRuntimeDescriptor { path, .. }
7228            )) if path == "bones[1].rest_position"
7229        ));
7230    }
7231
7232    fn build_local_axis_limit_pmx(has_local_axis: bool) -> Vec<u8> {
7233        build_local_axis_limit_pmx_with_loop_count(has_local_axis, 16)
7234    }
7235
7236    fn build_local_axis_limit_pmx_with_loop_count(
7237        has_local_axis: bool,
7238        ik_loop_count: i32,
7239    ) -> Vec<u8> {
7240        build_local_axis_limit_pmx_with_ik_indices(has_local_axis, ik_loop_count, 1, 0)
7241    }
7242
7243    fn build_local_axis_limit_pmx_with_ik_indices(
7244        has_local_axis: bool,
7245        ik_loop_count: i32,
7246        ik_target: i16,
7247        ik_link: i16,
7248    ) -> Vec<u8> {
7249        build_local_axis_limit_pmx_with_metadata(
7250            has_local_axis,
7251            ik_loop_count,
7252            ik_target,
7253            ik_link,
7254            std::f32::consts::PI,
7255            [-std::f32::consts::FRAC_PI_2, 0.0, 0.0],
7256            [0.0, 0.0, 0.0],
7257        )
7258    }
7259
7260    fn build_local_axis_limit_pmx_with_metadata(
7261        has_local_axis: bool,
7262        ik_loop_count: i32,
7263        ik_target: i16,
7264        ik_link: i16,
7265        ik_limit_angle: f32,
7266        angle_limit_min: [f32; 3],
7267        angle_limit_max: [f32; 3],
7268    ) -> Vec<u8> {
7269        let mut buf = Vec::new();
7270        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7271        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7272        buf.extend_from_slice(&build_empty_vertex_section());
7273        buf.extend_from_slice(&build_empty_face_section());
7274        buf.extend_from_slice(&build_empty_texture_section());
7275        buf.extend_from_slice(&build_empty_material_section());
7276        buf.extend_from_slice(&build_bone_section_header(3));
7277
7278        buf.extend_from_slice(&build_bone_name_bytes("link"));
7279        buf.extend_from_slice(&build_bone_name_bytes(""));
7280        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7281        buf.extend_from_slice(&(-1i16).to_le_bytes());
7282        buf.extend_from_slice(&0i32.to_le_bytes());
7283        buf.extend_from_slice(
7284            &(if has_local_axis {
7285                BONE_FLAG_LOCAL_AXIS
7286            } else {
7287                0
7288            })
7289            .to_le_bytes(),
7290        );
7291        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7292        if has_local_axis {
7293            // Basis X=+Y, Z=+Z: local X limit is a bone-local Y rotation.
7294            buf.extend_from_slice(&[0.0f32, 1.0, 0.0].map(f32::to_le_bytes).concat());
7295            buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7296        }
7297
7298        buf.extend_from_slice(&build_bone_name_bytes("tip"));
7299        buf.extend_from_slice(&build_bone_name_bytes(""));
7300        buf.extend_from_slice(&[1.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7301        buf.extend_from_slice(&0i16.to_le_bytes());
7302        buf.extend_from_slice(&0i32.to_le_bytes());
7303        buf.extend_from_slice(&0u16.to_le_bytes());
7304        buf.extend_from_slice(&[0.0f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
7305
7306        buf.extend_from_slice(&build_bone_name_bytes("ik"));
7307        buf.extend_from_slice(&build_bone_name_bytes(""));
7308        buf.extend_from_slice(&[0.0f32, 0.0, 1.0].map(f32::to_le_bytes).concat());
7309        buf.extend_from_slice(&(-1i16).to_le_bytes());
7310        buf.extend_from_slice(&0i32.to_le_bytes());
7311        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
7312        buf.extend_from_slice(&1i16.to_le_bytes());
7313        buf.extend_from_slice(&ik_target.to_le_bytes());
7314        buf.extend_from_slice(&ik_loop_count.to_le_bytes());
7315        buf.extend_from_slice(&ik_limit_angle.to_le_bytes());
7316        buf.extend_from_slice(&1i32.to_le_bytes());
7317        buf.extend_from_slice(&ik_link.to_le_bytes());
7318        buf.push(1);
7319        for value in angle_limit_min {
7320            buf.extend_from_slice(&value.to_le_bytes());
7321        }
7322        for value in angle_limit_max {
7323            buf.extend_from_slice(&value.to_le_bytes());
7324        }
7325        buf.extend_from_slice(&0i32.to_le_bytes());
7326        buf
7327    }
7328
7329    #[test]
7330    fn child_inverse_bind_uses_absolute_not_local_position() {
7331        let mut buf = Vec::new();
7332        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7333        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7334        buf.extend_from_slice(&build_empty_vertex_section());
7335        buf.extend_from_slice(&build_empty_face_section());
7336        buf.extend_from_slice(&build_empty_texture_section());
7337        buf.extend_from_slice(&build_empty_material_section());
7338        buf.extend_from_slice(&build_bone_section_header(2));
7339
7340        buf.extend_from_slice(&build_bone_name_bytes("Root"));
7341        buf.extend_from_slice(&build_bone_name_bytes(""));
7342        buf.extend_from_slice(&1.0f32.to_le_bytes());
7343        buf.extend_from_slice(&0.0f32.to_le_bytes());
7344        buf.extend_from_slice(&0.0f32.to_le_bytes());
7345        buf.extend_from_slice(&(-1i16).to_le_bytes());
7346        buf.extend_from_slice(&0i32.to_le_bytes());
7347        buf.extend_from_slice(&0u16.to_le_bytes());
7348        buf.extend_from_slice(&0.0f32.to_le_bytes());
7349        buf.extend_from_slice(&0.0f32.to_le_bytes());
7350        buf.extend_from_slice(&0.0f32.to_le_bytes());
7351
7352        buf.extend_from_slice(&build_bone_name_bytes("Child"));
7353        buf.extend_from_slice(&build_bone_name_bytes(""));
7354        buf.extend_from_slice(&5.0f32.to_le_bytes());
7355        buf.extend_from_slice(&0.0f32.to_le_bytes());
7356        buf.extend_from_slice(&0.0f32.to_le_bytes());
7357        buf.extend_from_slice(&0i16.to_le_bytes());
7358        buf.extend_from_slice(&0i32.to_le_bytes());
7359        buf.extend_from_slice(&0u16.to_le_bytes());
7360        buf.extend_from_slice(&0.0f32.to_le_bytes());
7361        buf.extend_from_slice(&0.0f32.to_le_bytes());
7362        buf.extend_from_slice(&0.0f32.to_le_bytes());
7363
7364        let result = import_pmx_model(&buf).unwrap();
7365        assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
7366        assert_eq!(result.bones[1].rest_position, Vec3A::new(4.0, 0.0, 0.0));
7367        let ibm = result.bones[1].inverse_bind_matrix;
7368        let rest_world = Mat4::from_translation(Vec3::new(5.0, 0.0, 0.0));
7369        assert_eq!(rest_world * ibm, Mat4::IDENTITY);
7370    }
7371
7372    #[test]
7373    fn runtime_rest_pose_skinning_matrices_are_identity() {
7374        let mut buf = Vec::new();
7375        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
7376        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7377        buf.extend_from_slice(&build_empty_vertex_section());
7378        buf.extend_from_slice(&build_empty_face_section());
7379        buf.extend_from_slice(&build_empty_texture_section());
7380        buf.extend_from_slice(&build_empty_material_section());
7381        buf.extend_from_slice(&build_bone_section_header(2));
7382
7383        buf.extend_from_slice(&build_bone_name_bytes("Parent"));
7384        buf.extend_from_slice(&build_bone_name_bytes(""));
7385        buf.extend_from_slice(&1.0f32.to_le_bytes());
7386        buf.extend_from_slice(&2.0f32.to_le_bytes());
7387        buf.extend_from_slice(&0.0f32.to_le_bytes());
7388        buf.extend_from_slice(&(-1i16).to_le_bytes());
7389        buf.extend_from_slice(&0i32.to_le_bytes());
7390        buf.extend_from_slice(&0u16.to_le_bytes());
7391        buf.extend_from_slice(&0.0f32.to_le_bytes());
7392        buf.extend_from_slice(&0.0f32.to_le_bytes());
7393        buf.extend_from_slice(&0.0f32.to_le_bytes());
7394
7395        buf.extend_from_slice(&build_bone_name_bytes("Child"));
7396        buf.extend_from_slice(&build_bone_name_bytes(""));
7397        buf.extend_from_slice(&1.0f32.to_le_bytes());
7398        buf.extend_from_slice(&3.0f32.to_le_bytes());
7399        buf.extend_from_slice(&0.0f32.to_le_bytes());
7400        buf.extend_from_slice(&0i16.to_le_bytes());
7401        buf.extend_from_slice(&0i32.to_le_bytes());
7402        buf.extend_from_slice(&0u16.to_le_bytes());
7403        buf.extend_from_slice(&0.0f32.to_le_bytes());
7404        buf.extend_from_slice(&0.0f32.to_le_bytes());
7405        buf.extend_from_slice(&0.0f32.to_le_bytes());
7406
7407        buf.extend_from_slice(&0i32.to_le_bytes());
7408
7409        let rt = import_pmx_runtime(&buf).unwrap();
7410        let model = Arc::new(rt.model);
7411        let mut instance = RuntimeInstance::new(model);
7412        instance.evaluate_current_pose();
7413        let matrices = instance.skinning_matrices();
7414        assert_eq!(matrices.len(), 2);
7415        assert_eq!(matrices[0], Mat4::IDENTITY);
7416        assert_eq!(matrices[1], Mat4::IDENTITY);
7417    }
7418
7419    fn build_sdef_vertex_pmx() -> Vec<u8> {
7420        // Header: PMX 2.0, UTF-8, extra_uv=0, vertex_idx=1, tex_idx=1, mat_idx=1,
7421        //         bone_idx=1, morph_idx=1, rigidBody_idx=1
7422        let mut buf = Vec::new();
7423        buf.extend_from_slice(b"PMX ");
7424        buf.extend_from_slice(&2.0f32.to_le_bytes());
7425        buf.push(8); // data count
7426        buf.push(1); // UTF-8
7427        buf.push(0); // extra_uv_count
7428        buf.push(1); // vertex_index_size
7429        buf.push(1); // texture_index_size
7430        buf.push(1); // material_index_size
7431        buf.push(1); // bone_index_size
7432        buf.push(1); // morph_index_size
7433        buf.push(1); // rigidbody_index_size
7434        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7435        // 1 vertex, SDEF (type 3): bone indices 0 and 0 (i8 size=1), weight 0.25
7436        // SDEF params: c=[1,2,3], r0=[4,5,6], r1=[7,8,9]
7437        buf.extend_from_slice(&1i32.to_le_bytes());
7438        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position x
7439        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position y
7440        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position z
7441        buf.extend_from_slice(&0.0f32.to_le_bytes()); // normal x
7442        buf.extend_from_slice(&1.0f32.to_le_bytes()); // normal y
7443        buf.extend_from_slice(&0.0f32.to_le_bytes()); // normal z
7444        buf.extend_from_slice(&0.0f32.to_le_bytes()); // uv u
7445        buf.extend_from_slice(&0.0f32.to_le_bytes()); // uv v
7446        buf.push(3u8); // weight type = 3 (SDEF)
7447        buf.push(0u8); // bone_index_0 (1 byte)
7448        buf.push(0u8); // bone_index_1 (1 byte)
7449        buf.extend_from_slice(&0.25f32.to_le_bytes()); // weight
7450        buf.extend_from_slice(&1.0f32.to_le_bytes()); // c.x
7451        buf.extend_from_slice(&2.0f32.to_le_bytes()); // c.y
7452        buf.extend_from_slice(&3.0f32.to_le_bytes()); // c.z
7453        buf.extend_from_slice(&4.0f32.to_le_bytes()); // r0.x
7454        buf.extend_from_slice(&5.0f32.to_le_bytes()); // r0.y
7455        buf.extend_from_slice(&6.0f32.to_le_bytes()); // r0.z
7456        buf.extend_from_slice(&7.0f32.to_le_bytes()); // r1.x
7457        buf.extend_from_slice(&8.0f32.to_le_bytes()); // r1.y
7458        buf.extend_from_slice(&9.0f32.to_le_bytes()); // r1.z
7459        buf.extend_from_slice(&1.0f32.to_le_bytes()); // edge_scale
7460        for _ in 0..8 {
7461            buf.extend_from_slice(&0i32.to_le_bytes()); // 0 faces/textures/materials/bones/morphs/displayFrames/rigidBodies/joints
7462        }
7463        buf
7464    }
7465
7466    fn build_qdef_vertex_pmx() -> Vec<u8> {
7467        // Header: PMX 2.0, UTF-8, all index sizes = 1
7468        let mut buf = Vec::new();
7469        buf.extend_from_slice(b"PMX ");
7470        buf.extend_from_slice(&2.0f32.to_le_bytes());
7471        buf.push(8);
7472        buf.push(1); // UTF-8
7473        buf.push(0); // extra_uv_count
7474        buf.push(1); // vertex_index_size
7475        buf.push(1); // texture_index_size
7476        buf.push(1); // material_index_size
7477        buf.push(1); // bone_index_size
7478        buf.push(1); // morph_index_size
7479        buf.push(1); // rigidbody_index_size
7480        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
7481        // 1 vertex, QDEF (type 4): 4 bone indices (i8 each), 4 weights
7482        buf.extend_from_slice(&1i32.to_le_bytes());
7483        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position
7484        buf.extend_from_slice(&0.0f32.to_le_bytes());
7485        buf.extend_from_slice(&0.0f32.to_le_bytes());
7486        buf.extend_from_slice(&0.0f32.to_le_bytes()); // normal
7487        buf.extend_from_slice(&1.0f32.to_le_bytes());
7488        buf.extend_from_slice(&0.0f32.to_le_bytes());
7489        buf.extend_from_slice(&0.0f32.to_le_bytes()); // uv
7490        buf.extend_from_slice(&0.0f32.to_le_bytes());
7491        buf.push(4u8); // weight type = 4 (QDEF)
7492        buf.push(0u8);
7493        buf.push(0u8);
7494        buf.push(0u8);
7495        buf.push(0u8); // 4 bone indices
7496        buf.extend_from_slice(&1.0f32.to_le_bytes()); // w0
7497        buf.extend_from_slice(&0.0f32.to_le_bytes()); // w1
7498        buf.extend_from_slice(&0.0f32.to_le_bytes()); // w2
7499        buf.extend_from_slice(&0.0f32.to_le_bytes()); // w3
7500        buf.extend_from_slice(&1.0f32.to_le_bytes()); // edge_scale
7501        for _ in 0..8 {
7502            buf.extend_from_slice(&0i32.to_le_bytes());
7503        }
7504        buf
7505    }
7506
7507    #[test]
7508    fn pmx_sdef_geometry_json_includes_sdef_fields() {
7509        let buf = build_sdef_vertex_pmx();
7510        let model = parse_pmx_model(&buf).unwrap();
7511        assert_eq!(
7512            model.geometry.sdef.skinning_modes,
7513            vec![PMX_SKINNING_MODE_SDEF.to_owned()]
7514        );
7515        assert_eq!(model.geometry.sdef.enabled.len(), 1);
7516        assert!((model.geometry.sdef.enabled[0] - 1.0).abs() < 1e-5);
7517        assert_eq!(model.geometry.sdef.c, vec![1.0, 2.0, 3.0]);
7518        assert_eq!(model.geometry.sdef.r0, vec![4.0, 5.0, 6.0]);
7519        assert_eq!(model.geometry.sdef.r1, vec![7.0, 8.0, 9.0]);
7520        // rw values: w=0.25, w1=0.75
7521        // rw[k] = r0[k]*0.25 + r1[k]*0.75
7522        // rw0[k] = (c[k]*2 + r0[k] - rw[k]) * 0.5
7523        // rw1[k] = (c[k]*2 + r1[k] - rw[k]) * 0.5
7524        let w = 0.25f32;
7525        let w1 = 0.75f32;
7526        let c = [1.0f32, 2.0, 3.0];
7527        let r0 = [4.0f32, 5.0, 6.0];
7528        let r1 = [7.0f32, 8.0, 9.0];
7529        for k in 0..3 {
7530            let rw = r0[k] * w + r1[k] * w1;
7531            let expected_rw0 = (c[k] * 2.0 + r0[k] - rw) * 0.5;
7532            let expected_rw1 = (c[k] * 2.0 + r1[k] - rw) * 0.5;
7533            assert!(
7534                (model.geometry.sdef.rw0[k] - expected_rw0).abs() < 1e-4,
7535                "rw0[{k}]: expected {expected_rw0}, got {}",
7536                model.geometry.sdef.rw0[k]
7537            );
7538            assert!(
7539                (model.geometry.sdef.rw1[k] - expected_rw1).abs() < 1e-4,
7540                "rw1[{k}]: expected {expected_rw1}, got {}",
7541                model.geometry.sdef.rw1[k]
7542            );
7543        }
7544        assert_eq!(model.geometry.qdef.enabled[0], 0.0);
7545        // Verify roundtrip through JSON preserves the sdef key
7546        let json = serde_json::to_string(&model).unwrap();
7547        assert!(json.contains("\"sdef\""), "JSON must contain sdef key");
7548        assert!(json.contains("\"qdef\""), "JSON must contain qdef key");
7549        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
7550        assert_eq!(parsed["geometry"]["sdef"]["enabled"][0], 1.0);
7551    }
7552
7553    #[test]
7554    fn pmx_qdef_vertex_marks_enabled() {
7555        let buf = build_qdef_vertex_pmx();
7556        let model = parse_pmx_model(&buf).unwrap();
7557        assert_eq!(
7558            model.geometry.sdef.skinning_modes,
7559            vec![PMX_SKINNING_MODE_QDEF.to_owned()]
7560        );
7561        assert_eq!(model.geometry.qdef.enabled.len(), 1);
7562        assert!((model.geometry.qdef.enabled[0] - 1.0).abs() < 1e-5);
7563        assert_eq!(model.geometry.sdef.enabled[0], 0.0);
7564    }
7565
7566    #[test]
7567    fn exports_parsed_pmx_deform_type_binary_roundtrip() {
7568        for original in [build_sdef_vertex_pmx(), build_qdef_vertex_pmx()] {
7569            let parsed = parse_pmx_model(&original).unwrap();
7570            let exported = export_pmx_model(&parsed);
7571            assert_eq!(exported, original);
7572        }
7573    }
7574}