Skip to main content

mmd_anim_format/pmx/
mod.rs

1use std::collections::HashMap;
2
3use glam::{Mat4, Quat, Vec3A};
4use serde::{Deserialize, Serialize};
5
6use mmd_anim_runtime::{
7    AppendTransformInit, BoneIndex, BoneInit, BoneMorphOffset, GroupMorphOffset, IkAngleLimit,
8    IkLinkInit, IkSolverInit, ModelArena, MorphIndex, MorphInit, MorphOffsetSpan,
9};
10
11use crate::error::ImportError;
12
13const PMX_MAGIC: [u8; 4] = [0x50, 0x4D, 0x58, 0x20];
14
15const BONE_FLAG_TAIL_INDEX: u16 = 0x0001;
16const BONE_FLAG_IK: u16 = 0x0020;
17const BONE_FLAG_LOCAL_APPEND: u16 = 0x0080;
18const BONE_FLAG_APPEND_ROTATE: u16 = 0x0100;
19const BONE_FLAG_APPEND_TRANSLATE: u16 = 0x0200;
20const BONE_FLAG_FIXED_AXIS: u16 = 0x0400;
21const BONE_FLAG_LOCAL_AXIS: u16 = 0x0800;
22const BONE_FLAG_EXTERNAL_PARENT: u16 = 0x2000;
23
24fn decode_utf16le_lossy(bytes: &[u8]) -> String {
25    let end = bytes.len().saturating_sub(bytes.len() % 2);
26    let units = bytes[..end]
27        .chunks_exact(2)
28        .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
29        .take_while(|&code| code != 0);
30    std::char::decode_utf16(units)
31        .map(|item| item.unwrap_or(char::REPLACEMENT_CHARACTER))
32        .collect()
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum TextEncoding {
37    Utf16Le = 0,
38    Utf8 = 1,
39}
40
41#[derive(Debug, Clone)]
42pub struct PmxHeader {
43    pub version: f32,
44    pub encoding: TextEncoding,
45    pub extra_uv_count: u8,
46    pub vertex_index_size: u8,
47    pub texture_index_size: u8,
48    pub material_index_size: u8,
49    pub bone_index_size: u8,
50    pub morph_index_size: u8,
51    pub rigidbody_index_size: u8,
52}
53
54struct Reader<'a> {
55    data: &'a [u8],
56    pos: usize,
57}
58
59impl<'a> Reader<'a> {
60    fn new(data: &'a [u8]) -> Self {
61        Self { data, pos: 0 }
62    }
63
64    fn remaining(&self) -> usize {
65        self.data.len().saturating_sub(self.pos)
66    }
67
68    fn require(&self, n: usize) -> Result<(), ImportError> {
69        if self.remaining() >= n {
70            Ok(())
71        } else {
72            Err(ImportError::UnexpectedEof(
73                n.saturating_sub(self.remaining()),
74            ))
75        }
76    }
77
78    fn require_record_bytes(&self, count: usize, record_size: usize) -> Result<(), ImportError> {
79        let bytes = count
80            .checked_mul(record_size)
81            .ok_or(ImportError::SectionOverflow)?;
82        self.require(bytes)
83    }
84
85    fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], ImportError> {
86        self.require(n)?;
87        let slice = &self.data[self.pos..self.pos + n];
88        self.pos += n;
89        Ok(slice)
90    }
91
92    fn read_u8(&mut self) -> Result<u8, ImportError> {
93        Ok(self.read_bytes(1)?[0])
94    }
95
96    fn read_u16_le(&mut self) -> Result<u16, ImportError> {
97        let b = self.read_bytes(2)?;
98        Ok(u16::from_le_bytes([b[0], b[1]]))
99    }
100
101    fn read_i32_le(&mut self) -> Result<i32, ImportError> {
102        let b = self.read_bytes(4)?;
103        Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
104    }
105
106    fn read_f32_le(&mut self) -> Result<f32, ImportError> {
107        let b = self.read_bytes(4)?;
108        Ok(f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
109    }
110
111    fn read_vec3(&mut self) -> Result<Vec3A, ImportError> {
112        Ok(Vec3A::new(
113            self.read_f32_le()?,
114            self.read_f32_le()?,
115            self.read_f32_le()?,
116        ))
117    }
118
119    fn read_vec2_array(&mut self) -> Result<[f32; 2], ImportError> {
120        Ok([self.read_f32_le()?, self.read_f32_le()?])
121    }
122
123    fn read_vec3_array(&mut self) -> Result<[f32; 3], ImportError> {
124        Ok([
125            self.read_f32_le()?,
126            self.read_f32_le()?,
127            self.read_f32_le()?,
128        ])
129    }
130
131    fn read_vec4_array(&mut self) -> Result<[f32; 4], ImportError> {
132        Ok([
133            self.read_f32_le()?,
134            self.read_f32_le()?,
135            self.read_f32_le()?,
136            self.read_f32_le()?,
137        ])
138    }
139
140    fn read_sized_index(&mut self, size: u8) -> Result<i32, ImportError> {
141        match size {
142            1 => Ok(self.read_u8()? as i8 as i32),
143            2 => Ok({
144                let b = self.read_bytes(2)?;
145                i16::from_le_bytes([b[0], b[1]]) as i32
146            }),
147            4 => self.read_i32_le(),
148            _ => Err(ImportError::InvalidIndexSize(size)),
149        }
150    }
151
152    fn read_index(&mut self, header: &PmxHeader) -> Result<i32, ImportError> {
153        self.read_sized_index(header.bone_index_size)
154    }
155
156    fn read_vertex_index(&mut self, size: u8) -> Result<u32, ImportError> {
157        match size {
158            1 => Ok(self.read_u8()? as u32),
159            2 => Ok(self.read_u16_le()? as u32),
160            4 => {
161                let raw = self.read_i32_le()?;
162                if raw < 0 {
163                    Err(ImportError::SectionOverflow)
164                } else {
165                    Ok(raw as u32)
166                }
167            }
168            _ => Err(ImportError::InvalidIndexSize(size)),
169        }
170    }
171
172    fn skip(&mut self, n: usize) -> Result<(), ImportError> {
173        self.require(n)?;
174        self.pos += n;
175        Ok(())
176    }
177
178    fn read_string(&mut self, encoding: TextEncoding) -> Result<String, ImportError> {
179        let len = self.read_i32_le()?;
180        if len <= 0 {
181            return Ok(String::new());
182        }
183        let bytes = self.read_bytes(len as usize)?;
184        match encoding {
185            TextEncoding::Utf8 => {
186                let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
187                Ok(String::from_utf8_lossy(&bytes[..end]).into_owned())
188            }
189            TextEncoding::Utf16Le => Ok(decode_utf16le_lossy(bytes)),
190        }
191    }
192
193    fn read_string_owned(
194        &mut self,
195        encoding: TextEncoding,
196    ) -> Result<(Vec<u8>, String), ImportError> {
197        let len = self.read_i32_le()?;
198        if len <= 0 {
199            return Ok((Vec::new(), String::new()));
200        }
201        let bytes = self.read_bytes(len as usize)?.to_vec();
202        let decoded = match encoding {
203            TextEncoding::Utf8 => {
204                let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
205                String::from_utf8_lossy(&bytes[..end]).into_owned()
206            }
207            TextEncoding::Utf16Le => decode_utf16le_lossy(&bytes),
208        };
209        Ok((bytes, decoded))
210    }
211
212    fn read_bone_index(&mut self, header: &PmxHeader) -> Result<Option<BoneIndex>, ImportError> {
213        let raw = self.read_sized_index(header.bone_index_size)?;
214        if raw < 0 {
215            Ok(None)
216        } else {
217            Ok(Some(BoneIndex(raw as u32)))
218        }
219    }
220}
221
222pub fn read_header(data: &[u8]) -> Result<(PmxHeader, usize), ImportError> {
223    let mut r = Reader::new(data);
224
225    let magic = r.read_bytes(4)?;
226    if magic != PMX_MAGIC {
227        return Err(ImportError::InvalidPmxMagic);
228    }
229
230    let version = r.read_f32_le()?;
231    if !(2.0..=2.2).contains(&version) {
232        return Err(ImportError::UnsupportedPmxVersion(version));
233    }
234
235    let data_count = r.read_u8()?;
236    if data_count < 8 {
237        return Err(ImportError::InvalidEncoding(data_count));
238    }
239
240    let encoding_byte = r.read_u8()?;
241    let encoding = match encoding_byte {
242        0 => TextEncoding::Utf16Le,
243        1 => TextEncoding::Utf8,
244        _ => return Err(ImportError::InvalidEncoding(encoding_byte)),
245    };
246
247    let extra_uv_count = r.read_u8()?;
248    let vertex_index_size = r.read_u8()?;
249    let texture_index_size = r.read_u8()?;
250    let material_index_size = r.read_u8()?;
251    let bone_index_size = r.read_u8()?;
252    let morph_index_size = r.read_u8()?;
253    let rigidbody_index_size = r.read_u8()?;
254    for size in [
255        vertex_index_size,
256        texture_index_size,
257        material_index_size,
258        bone_index_size,
259        morph_index_size,
260        rigidbody_index_size,
261    ] {
262        if !matches!(size, 1 | 2 | 4) {
263            return Err(ImportError::InvalidIndexSize(size));
264        }
265    }
266    if data_count > 8 {
267        r.skip((data_count - 8) as usize)?;
268    }
269
270    let header = PmxHeader {
271        version,
272        encoding,
273        extra_uv_count,
274        vertex_index_size,
275        texture_index_size,
276        material_index_size,
277        bone_index_size,
278        morph_index_size,
279        rigidbody_index_size,
280    };
281
282    Ok((header, r.pos))
283}
284
285pub fn skip_model_info(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
286    let mut r = Reader { data, pos };
287    let _model_name = r.read_string(header.encoding)?;
288    let _english_name = r.read_string(header.encoding)?;
289    let _comment = r.read_string(header.encoding)?;
290    let _english_comment = r.read_string(header.encoding)?;
291    Ok(r.pos)
292}
293
294pub fn skip_vertices(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
295    let mut r = Reader { data, pos };
296    let count = r.read_i32_le()?;
297    if count < 0 {
298        return Err(ImportError::SectionOverflow);
299    }
300    let count = count as usize;
301
302    for _ in 0..count {
303        r.skip(12)?;
304        r.skip(12)?;
305        r.skip(8)?;
306        r.skip(header.extra_uv_count as usize * 16)?;
307        let vertex_deform_type = r.read_u8()?;
308
309        let bone_indices: usize = match vertex_deform_type {
310            0 => 1,
311            1 => 2,
312            2 | 4 => 4,
313            3 => 2,
314            _ => return Err(ImportError::SectionOverflow),
315        };
316        for _ in 0..bone_indices {
317            r.read_sized_index(header.bone_index_size)?;
318        }
319
320        let weight_count: usize = match vertex_deform_type {
321            0 => 0,
322            1 => 1,
323            2 | 4 => 4,
324            3 => 1,
325            _ => return Err(ImportError::SectionOverflow),
326        };
327        r.skip(weight_count * 4)?;
328
329        if vertex_deform_type == 3 {
330            r.skip(36)?;
331        }
332
333        r.skip(4)?;
334    }
335
336    Ok(r.pos)
337}
338
339pub fn skip_faces(data: &[u8], vertex_index_size: u8, pos: usize) -> Result<usize, ImportError> {
340    let mut r = Reader { data, pos };
341    let count = r.read_i32_le()?;
342    if count < 0 {
343        return Err(ImportError::SectionOverflow);
344    }
345    let count = count as usize;
346    let bytes = count
347        .checked_mul(vertex_index_size as usize)
348        .ok_or(ImportError::SectionOverflow)?;
349    r.skip(bytes)?;
350    Ok(r.pos)
351}
352
353pub fn skip_textures(
354    data: &[u8],
355    encoding: TextEncoding,
356    pos: usize,
357) -> Result<usize, ImportError> {
358    let mut r = Reader { data, pos };
359    let count = r.read_i32_le()?;
360    if count < 0 {
361        return Err(ImportError::SectionOverflow);
362    }
363    for _ in 0..count {
364        let _path = r.read_string(encoding)?;
365    }
366    Ok(r.pos)
367}
368
369pub fn skip_materials(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
370    let mut r = Reader { data, pos };
371    let count = r.read_i32_le()?;
372    if count < 0 {
373        return Err(ImportError::SectionOverflow);
374    }
375    for _ in 0..count {
376        let _name = r.read_string(header.encoding)?;
377        let _english_name = r.read_string(header.encoding)?;
378        r.skip(16)?;
379        r.skip(16)?;
380        r.skip(12)?;
381        r.skip(1)?;
382        r.skip(16)?;
383        r.skip(4)?;
384        let _texture = r.read_sized_index(header.texture_index_size)?;
385        let _sphere_tex = r.read_sized_index(header.texture_index_size)?;
386        r.skip(1)?;
387        let _toon = r.read_u8()?;
388        if _toon == 0 {
389            r.read_sized_index(header.texture_index_size)?;
390        } else {
391            r.skip(1)?;
392        }
393        let _memo = r.read_string(header.encoding)?;
394        let _face_count = r.read_i32_le()?;
395    }
396    Ok(r.pos)
397}
398
399pub fn skip_morphs(data: &[u8], header: &PmxHeader, pos: usize) -> Result<usize, ImportError> {
400    let mut r = Reader { data, pos };
401    let count = r.read_i32_le()?;
402    if count < 0 {
403        return Err(ImportError::SectionOverflow);
404    }
405    for _ in 0..count {
406        let _name = r.read_string(header.encoding)?;
407        let _english_name = r.read_string(header.encoding)?;
408        r.skip(1)?;
409        let _morph_type = r.read_u8()?;
410        let _offset_count = r.read_i32_le()?;
411        match _morph_type {
412            0 => {
413                for _ in 0.._offset_count {
414                    r.read_sized_index(header.morph_index_size)?;
415                    r.skip(4)?;
416                }
417            }
418            1 => {
419                for _ in 0.._offset_count {
420                    r.read_sized_index(header.vertex_index_size)?;
421                    r.skip(12)?;
422                }
423            }
424            2 => {
425                for _ in 0.._offset_count {
426                    r.read_sized_index(header.bone_index_size)?;
427                    r.skip(12)?;
428                    r.skip(16)?;
429                }
430            }
431            3..=7 => {
432                for _ in 0.._offset_count {
433                    r.read_sized_index(header.vertex_index_size)?;
434                    r.skip(16)?;
435                }
436            }
437            8 => {
438                for _ in 0.._offset_count {
439                    r.read_sized_index(header.material_index_size)?;
440                    r.skip(1)?;
441                    r.skip(16)?;
442                    r.skip(12)?;
443                    r.skip(4)?;
444                    r.skip(12)?;
445                    r.skip(16)?;
446                    r.skip(4)?;
447                    r.skip(16)?;
448                    r.skip(16)?;
449                    r.skip(16)?;
450                }
451            }
452            9 => {
453                for _ in 0.._offset_count {
454                    r.read_sized_index(header.rigidbody_index_size)?;
455                    r.skip(1)?;
456                    r.skip(12)?;
457                    r.skip(12)?;
458                }
459            }
460            _ => return Err(ImportError::SectionOverflow),
461        }
462    }
463    Ok(r.pos)
464}
465
466#[derive(Debug, Clone)]
467pub struct PmxMorphNames {
468    pub name_bytes: Vec<Vec<u8>>,
469    pub names: Vec<String>,
470}
471
472pub fn read_morph_names(
473    data: &[u8],
474    header: &PmxHeader,
475    pos: usize,
476) -> Result<(PmxMorphNames, usize), ImportError> {
477    let mut r = Reader { data, pos };
478    let count = r.read_i32_le()?;
479    if count < 0 {
480        return Err(ImportError::SectionOverflow);
481    }
482    let count = count as usize;
483    r.require_record_bytes(count, 1)?;
484
485    let mut name_bytes = Vec::with_capacity(count);
486    let mut names = Vec::with_capacity(count);
487
488    for _ in 0..count {
489        let (raw, name) = r.read_string_owned(header.encoding)?;
490        let _english_name = r.read_string(header.encoding)?;
491        name_bytes.push(raw);
492        names.push(name);
493
494        r.skip(1)?;
495        let morph_type = r.read_u8()?;
496        let offset_count = r.read_i32_le()?;
497        if offset_count < 0 {
498            return Err(ImportError::SectionOverflow);
499        }
500        let offset_count = offset_count as usize;
501
502        let per_offset = match morph_type {
503            0 => header.morph_index_size as usize + 4,
504            1 => header.vertex_index_size as usize + 12,
505            2 => header.bone_index_size as usize + 12 + 16,
506            3..=7 => header.vertex_index_size as usize + 16,
507            8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
508            9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
509            _ => return Err(ImportError::SectionOverflow),
510        };
511        r.skip(
512            offset_count
513                .checked_mul(per_offset)
514                .ok_or(ImportError::SectionOverflow)?,
515        )?;
516    }
517
518    Ok((PmxMorphNames { name_bytes, names }, r.pos))
519}
520
521/// Read morph names and extract bone/group offset payloads.
522///
523/// Returns names (same layout as [`read_morph_names`]) plus a [`MorphInit`]
524/// suitable for [`ModelArena::new_with_morphs`]. Unsupported morph types
525/// (vertex, UV, material, flip) have their payload skipped.
526pub fn read_morph_offsets(
527    data: &[u8],
528    header: &PmxHeader,
529    pos: usize,
530) -> Result<(PmxMorphNames, MorphInit, usize), ImportError> {
531    let mut r = Reader { data, pos };
532    let count = r.read_i32_le()?;
533    if count < 0 {
534        return Err(ImportError::SectionOverflow);
535    }
536    let count = count as usize;
537    r.require_record_bytes(count, 1)?;
538
539    let mut name_bytes = Vec::with_capacity(count);
540    let mut names = Vec::with_capacity(count);
541
542    let mut bone_offsets_all: Vec<BoneMorphOffset> = Vec::new();
543    let mut bone_spans: Vec<MorphOffsetSpan> = Vec::with_capacity(count);
544    let mut group_offsets_all: Vec<GroupMorphOffset> = Vec::new();
545    let mut group_spans: Vec<MorphOffsetSpan> = Vec::with_capacity(count);
546
547    for _ in 0..count {
548        let (raw, name) = r.read_string_owned(header.encoding)?;
549        let _english_name = r.read_string(header.encoding)?;
550        name_bytes.push(raw);
551        names.push(name);
552
553        r.skip(1)?;
554        let morph_type = r.read_u8()?;
555        let offset_count = r.read_i32_le()?;
556        if offset_count < 0 {
557            return Err(ImportError::SectionOverflow);
558        }
559        let offset_count = offset_count as usize;
560
561        match morph_type {
562            0 => {
563                let start = group_offsets_all.len() as u32;
564                for _ in 0..offset_count {
565                    let child_raw = r.read_sized_index(header.morph_index_size)?;
566                    if child_raw < 0 {
567                        return Err(ImportError::SectionOverflow);
568                    }
569                    let ratio = r.read_f32_le()?;
570                    group_offsets_all.push(GroupMorphOffset {
571                        child_morph: MorphIndex(child_raw as u32),
572                        ratio,
573                    });
574                }
575                group_spans.push(MorphOffsetSpan {
576                    start,
577                    count: offset_count as u32,
578                });
579                bone_spans.push(MorphOffsetSpan::default());
580            }
581            2 => {
582                let start = bone_offsets_all.len() as u32;
583                for _ in 0..offset_count {
584                    let bone_raw = r.read_sized_index(header.bone_index_size)?;
585                    if bone_raw < 0 {
586                        return Err(ImportError::SectionOverflow);
587                    }
588                    let pos = r.read_vec3()?;
589                    let qx = r.read_f32_le()?;
590                    let qy = r.read_f32_le()?;
591                    let qz = r.read_f32_le()?;
592                    let qw = r.read_f32_le()?;
593                    bone_offsets_all.push(BoneMorphOffset {
594                        target_bone: BoneIndex(bone_raw as u32),
595                        position_offset: pos,
596                        rotation_offset: Quat::from_xyzw(qx, qy, qz, qw),
597                    });
598                }
599                bone_spans.push(MorphOffsetSpan {
600                    start,
601                    count: offset_count as u32,
602                });
603                group_spans.push(MorphOffsetSpan::default());
604            }
605            _ => {
606                let per_offset = match morph_type {
607                    1 => header.vertex_index_size as usize + 12,
608                    3..=7 => header.vertex_index_size as usize + 16,
609                    8 => {
610                        header.material_index_size as usize
611                            + 1
612                            + 16
613                            + 12
614                            + 4
615                            + 12
616                            + 16
617                            + 4
618                            + 16
619                            + 16
620                            + 16
621                    }
622                    9 => header.rigidbody_index_size as usize + 1 + 12 + 12,
623                    _ => return Err(ImportError::SectionOverflow),
624                };
625                r.skip(
626                    offset_count
627                        .checked_mul(per_offset)
628                        .ok_or(ImportError::SectionOverflow)?,
629                )?;
630                bone_spans.push(MorphOffsetSpan::default());
631                group_spans.push(MorphOffsetSpan::default());
632            }
633        }
634    }
635
636    Ok((
637        PmxMorphNames { name_bytes, names },
638        MorphInit {
639            morph_count: count as u32,
640            vertex_spans: vec![MorphOffsetSpan::default(); count],
641            bone_offsets: bone_offsets_all,
642            bone_spans,
643            group_offsets: group_offsets_all,
644            group_spans,
645            ..MorphInit::default()
646        },
647        r.pos,
648    ))
649}
650
651#[derive(Debug, Clone)]
652pub struct PmxBoneImport {
653    pub bones: Vec<BoneInit>,
654    pub ik_solvers: Vec<IkSolverInit>,
655    pub append_transforms: Vec<AppendTransformInit>,
656    pub bone_name_bytes: Vec<Vec<u8>>,
657    pub bone_names: Vec<String>,
658}
659
660pub fn read_bones(
661    data: &[u8],
662    header: &PmxHeader,
663    pos: usize,
664) -> Result<(PmxBoneImport, usize), ImportError> {
665    let mut r = Reader { data, pos };
666    let count = r.read_i32_le()?;
667    if count < 0 {
668        return Err(ImportError::SectionOverflow);
669    }
670    let count = count as usize;
671    r.require_record_bytes(count, 1)?;
672
673    let mut bones = Vec::with_capacity(count);
674    let mut ik_solvers = Vec::new();
675    let mut append_transforms = Vec::new();
676
677    let mut bone_name_bytes = Vec::with_capacity(count);
678    let mut bone_names = Vec::with_capacity(count);
679
680    let mut absolute_positions: Vec<Vec3A> = Vec::with_capacity(count);
681
682    for _bone_i in 0..count {
683        let (name_bytes, name) = r.read_string_owned(header.encoding)?;
684        let _english_name = r.read_string(header.encoding)?;
685        let position = r.read_vec3()?;
686        absolute_positions.push(position);
687        let parent = r.read_bone_index(header)?;
688        let transform_order = r.read_i32_le()?;
689        let flags = r.read_u16_le()?;
690
691        if flags & BONE_FLAG_TAIL_INDEX != 0 {
692            r.read_index(header)?;
693        } else {
694            r.read_vec3()?;
695        }
696
697        let has_append =
698            flags & BONE_FLAG_APPEND_ROTATE != 0 || flags & BONE_FLAG_APPEND_TRANSLATE != 0;
699
700        if has_append {
701            let append_parent = r.read_bone_index(header)?;
702            let append_ratio = r.read_f32_le()?;
703
704            if let Some(source) = append_parent {
705                let target_idx = bones.len();
706                append_transforms.push(
707                    AppendTransformInit::new(BoneIndex(target_idx as u32), source, append_ratio)
708                        .with_rotation_if(flags & BONE_FLAG_APPEND_ROTATE != 0)
709                        .with_translation_if(flags & BONE_FLAG_APPEND_TRANSLATE != 0)
710                        .with_local_if(flags & BONE_FLAG_LOCAL_APPEND != 0),
711                );
712            }
713        }
714
715        let fixed_axis = if flags & BONE_FLAG_FIXED_AXIS != 0 {
716            Some(r.read_vec3()?)
717        } else {
718            None
719        };
720
721        if flags & BONE_FLAG_LOCAL_AXIS != 0 {
722            r.read_vec3()?;
723            r.read_vec3()?;
724        }
725
726        if flags & BONE_FLAG_EXTERNAL_PARENT != 0 {
727            r.read_i32_le()?;
728        }
729
730        if flags & BONE_FLAG_IK != 0 {
731            let ik_target = r.read_bone_index(header)?;
732            let ik_loop_count = r.read_i32_le()?;
733            let ik_limit_angle = r.read_f32_le()?;
734            let link_count = r.read_i32_le()?;
735            if link_count < 0 {
736                return Err(ImportError::SectionOverflow);
737            }
738            r.require_record_bytes(link_count as usize, header.bone_index_size as usize + 1)?;
739
740            let mut links = Vec::with_capacity(link_count as usize);
741            for _ in 0..link_count {
742                let link_bone = r.read_bone_index(header)?.unwrap_or(BoneIndex(0));
743                let has_limit = r.read_u8()?;
744                let angle_limit = if has_limit != 0 {
745                    let min = r.read_vec3()?;
746                    let max = r.read_vec3()?;
747                    Some(IkAngleLimit::new(min, max))
748                } else {
749                    None
750                };
751                links.push(match angle_limit {
752                    Some(lim) => IkLinkInit::new(link_bone).with_angle_limit(lim),
753                    None => IkLinkInit::new(link_bone),
754                });
755            }
756
757            let ik_bone = BoneIndex(bones.len() as u32);
758            if let Some(target) = ik_target {
759                ik_solvers.push(IkSolverInit {
760                    ik_bone,
761                    target_bone: target,
762                    links,
763                    iteration_count: ik_loop_count as u32,
764                    limit_angle: ik_limit_angle,
765                });
766            }
767        }
768
769        bone_name_bytes.push(name_bytes);
770        bone_names.push(name);
771
772        bones.push(BoneInit {
773            parent,
774            rest_position: position,
775            inverse_bind_matrix: Mat4::from_translation((-position).into()),
776            transform_order,
777            fixed_axis,
778        });
779    }
780
781    for bone in bones.iter_mut() {
782        if let Some(parent_idx) = bone.parent
783            && let Some(parent_abs) = absolute_positions.get(parent_idx.as_usize())
784        {
785            bone.rest_position -= parent_abs;
786        }
787    }
788
789    Ok((
790        PmxBoneImport {
791            bones,
792            ik_solvers,
793            append_transforms,
794            bone_name_bytes,
795            bone_names,
796        },
797        r.pos,
798    ))
799}
800
801pub fn import_pmx_model(data: &[u8]) -> Result<PmxBoneImport, ImportError> {
802    let (header, pos) = read_header(data)?;
803    let pos = skip_model_info(data, &header, pos)?;
804    let pos = skip_vertices(data, &header, pos)?;
805    let pos = skip_faces(data, header.vertex_index_size, pos)?;
806    let pos = skip_textures(data, header.encoding, pos)?;
807    let pos = skip_materials(data, &header, pos)?;
808    let (bones, _pos) = read_bones(data, &header, pos)?;
809    Ok(bones)
810}
811
812#[derive(Debug, Clone, Serialize, Deserialize)]
813#[serde(rename_all = "camelCase")]
814pub struct PmxParsedModel {
815    pub metadata: PmxParsedMetadata,
816    pub geometry: PmxParsedGeometry,
817    pub materials: Vec<PmxParsedMaterial>,
818    pub skeleton: PmxParsedSkeleton,
819    pub morphs: Vec<PmxParsedMorph>,
820    pub display_frames: Vec<PmxParsedDisplayFrame>,
821    pub rigid_bodies: Vec<PmxParsedRigidBody>,
822    pub joints: Vec<PmxParsedJoint>,
823    pub soft_bodies: Vec<PmxParsedSoftBody>,
824    pub diagnostics: Vec<PmxParserDiagnostic>,
825}
826
827#[derive(Debug, Clone, Serialize, Deserialize)]
828#[serde(rename_all = "camelCase")]
829pub struct PmxParsedMetadata {
830    #[serde(default = "default_pmx_format")]
831    pub format: String,
832    pub version: f32,
833    pub encoding: String,
834    pub name: String,
835    pub english_name: String,
836    pub comment: String,
837    pub english_comment: String,
838    pub counts: PmxParsedCounts,
839    pub index_sizes: PmxParsedIndexSizes,
840    pub additional_uv_count: u8,
841}
842
843#[derive(Debug, Clone, Serialize, Deserialize)]
844#[serde(rename_all = "camelCase")]
845pub struct PmxParsedCounts {
846    pub vertices: usize,
847    pub faces: usize,
848    pub materials: usize,
849    pub bones: usize,
850    pub morphs: usize,
851    pub display_frames: usize,
852    pub rigid_bodies: usize,
853    pub joints: usize,
854    pub soft_bodies: usize,
855}
856
857#[derive(Debug, Clone, Serialize, Deserialize)]
858pub struct PmxParsedIndexSizes {
859    pub vertex: u8,
860    pub texture: u8,
861    pub material: u8,
862    pub bone: u8,
863    pub morph: u8,
864    #[serde(rename = "rigidBody")]
865    pub rigid_body: u8,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize, Default)]
869#[serde(rename_all = "camelCase")]
870pub struct PmxParsedSdef {
871    pub enabled: Vec<f32>,
872    pub c: Vec<f32>,
873    pub r0: Vec<f32>,
874    pub r1: Vec<f32>,
875    pub rw0: Vec<f32>,
876    pub rw1: Vec<f32>,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize, Default)]
880#[serde(rename_all = "camelCase")]
881pub struct PmxParsedQdef {
882    pub enabled: Vec<f32>,
883}
884
885#[derive(Debug, Clone, Serialize, Deserialize)]
886#[serde(rename_all = "camelCase")]
887pub struct PmxParsedGeometry {
888    pub positions: Vec<f32>,
889    pub normals: Vec<f32>,
890    pub uvs: Vec<f32>,
891    pub additional_uvs: Vec<Vec<f32>>,
892    pub indices: Vec<u32>,
893    pub skin_indices: Vec<u32>,
894    pub skin_weights: Vec<f32>,
895    pub edge_scale: Vec<f32>,
896    pub material_groups: Vec<PmxParsedMaterialGroup>,
897    pub sdef: PmxParsedSdef,
898    pub qdef: PmxParsedQdef,
899}
900
901#[derive(Debug, Clone, Serialize, Deserialize)]
902#[serde(rename_all = "camelCase")]
903pub struct PmxParsedMaterialGroup {
904    pub start: usize,
905    pub count: usize,
906    pub material_index: usize,
907}
908
909#[derive(Debug, Clone, Serialize, Deserialize)]
910#[serde(rename_all = "camelCase")]
911pub struct PmxParsedMaterial {
912    pub name: String,
913    pub english_name: String,
914    pub texture_path: String,
915    pub sphere_texture_path: String,
916    pub sphere_mode: String,
917    pub toon_texture_path: String,
918    pub shared_toon_index: Option<u8>,
919    pub diffuse: [f32; 4],
920    pub specular: [f32; 3],
921    pub specular_power: f32,
922    pub ambient: [f32; 3],
923    pub edge_color: [f32; 4],
924    pub edge_size: f32,
925    pub flags: PmxParsedMaterialFlags,
926    pub face_count: i32,
927}
928
929#[derive(Debug, Clone, Serialize, Deserialize)]
930#[serde(rename_all = "camelCase")]
931pub struct PmxParsedMaterialFlags {
932    pub double_sided: bool,
933    pub ground_shadow: bool,
934    pub self_shadow_map: bool,
935    pub self_shadow: bool,
936    pub edge: bool,
937    pub vertex_color: bool,
938    pub point_draw: bool,
939    pub line_draw: bool,
940}
941
942#[derive(Debug, Clone, Serialize, Deserialize)]
943pub struct PmxParsedSkeleton {
944    pub bones: Vec<PmxParsedBone>,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
948#[serde(rename_all = "camelCase")]
949pub struct PmxParsedBone {
950    pub name: String,
951    pub english_name: String,
952    pub parent_index: i32,
953    pub layer: i32,
954    pub position: [f32; 3],
955    pub tail_index: i32,
956    pub tail_position: Option<[f32; 3]>,
957    pub flags: PmxParsedBoneFlags,
958    pub append_transform: Option<PmxParsedAppendTransform>,
959    pub fixed_axis: Option<[f32; 3]>,
960    pub local_axis: Option<PmxParsedLocalAxis>,
961    pub external_parent_key: Option<i32>,
962    pub ik: Option<PmxParsedIk>,
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize)]
966#[serde(rename_all = "camelCase")]
967pub struct PmxParsedBoneFlags {
968    pub indexed_tail: bool,
969    pub rotatable: bool,
970    pub translatable: bool,
971    pub visible: bool,
972    pub enabled: bool,
973    pub ik: bool,
974    pub append_local: bool,
975    pub append_rotate: bool,
976    pub append_translate: bool,
977    pub fixed_axis: bool,
978    pub local_axis: bool,
979    pub transform_after_physics: bool,
980    pub external_parent_transform: bool,
981}
982
983#[derive(Debug, Clone, Serialize, Deserialize)]
984#[serde(rename_all = "camelCase")]
985pub struct PmxParsedAppendTransform {
986    pub parent_index: i32,
987    pub weight: f32,
988}
989
990#[derive(Debug, Clone, Serialize, Deserialize)]
991pub struct PmxParsedLocalAxis {
992    pub x: [f32; 3],
993    pub z: [f32; 3],
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
997#[serde(rename_all = "camelCase")]
998pub struct PmxParsedIk {
999    pub target_index: i32,
1000    pub loop_count: i32,
1001    pub limit_angle: f32,
1002    pub links: Vec<PmxParsedIkLink>,
1003}
1004
1005#[derive(Debug, Clone, Serialize, Deserialize)]
1006#[serde(rename_all = "camelCase")]
1007pub struct PmxParsedIkLink {
1008    pub bone_index: i32,
1009    pub limits: Option<PmxParsedIkLimit>,
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct PmxParsedIkLimit {
1014    pub lower: [f32; 3],
1015    pub upper: [f32; 3],
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1019#[serde(rename_all = "camelCase")]
1020pub struct PmxParsedMorph {
1021    pub name: String,
1022    pub english_name: String,
1023    #[serde(rename = "type")]
1024    pub kind: String,
1025    pub vertex_offsets: Vec<PmxParsedVertexMorphOffset>,
1026    pub group_offsets: Vec<PmxParsedGroupMorphOffset>,
1027    pub bone_offsets: Vec<PmxParsedBoneMorphOffset>,
1028    pub uv_offsets: Vec<PmxParsedUvMorphOffset>,
1029    pub additional_uv_offsets: Vec<PmxParsedAdditionalUvMorphOffset>,
1030    pub material_offsets: Vec<PmxParsedMaterialMorphOffset>,
1031    pub flip_offsets: Vec<PmxParsedGroupMorphOffset>,
1032    pub impulse_offsets: Vec<PmxParsedImpulseMorphOffset>,
1033}
1034
1035#[derive(Debug, Clone, Serialize, Deserialize)]
1036#[serde(rename_all = "camelCase")]
1037pub struct PmxParsedVertexMorphOffset {
1038    pub vertex_index: u32,
1039    pub position: [f32; 3],
1040}
1041
1042#[derive(Debug, Clone, Serialize, Deserialize)]
1043#[serde(rename_all = "camelCase")]
1044pub struct PmxParsedGroupMorphOffset {
1045    pub morph_index: i32,
1046    pub weight: f32,
1047}
1048
1049#[derive(Debug, Clone, Serialize, Deserialize)]
1050#[serde(rename_all = "camelCase")]
1051pub struct PmxParsedBoneMorphOffset {
1052    pub bone_index: i32,
1053    pub translation: [f32; 3],
1054    pub rotation: [f32; 4],
1055}
1056
1057#[derive(Debug, Clone, Serialize, Deserialize)]
1058#[serde(rename_all = "camelCase")]
1059pub struct PmxParsedUvMorphOffset {
1060    pub vertex_index: u32,
1061    pub uv: [f32; 4],
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize)]
1065#[serde(rename_all = "camelCase")]
1066pub struct PmxParsedAdditionalUvMorphOffset {
1067    pub vertex_index: u32,
1068    pub uv_index: u8,
1069    pub uv: [f32; 4],
1070}
1071
1072#[derive(Debug, Clone, Serialize, Deserialize)]
1073#[serde(rename_all = "camelCase")]
1074pub struct PmxParsedMaterialMorphOffset {
1075    pub material_index: i32,
1076    pub operation: String,
1077    pub diffuse: [f32; 4],
1078    pub specular: [f32; 3],
1079    pub specular_power: f32,
1080    pub ambient: [f32; 3],
1081    pub edge_color: [f32; 4],
1082    pub edge_size: f32,
1083    pub texture_factor: [f32; 4],
1084    pub sphere_texture_factor: [f32; 4],
1085    pub toon_texture_factor: [f32; 4],
1086}
1087
1088#[derive(Debug, Clone, Serialize, Deserialize)]
1089#[serde(rename_all = "camelCase")]
1090pub struct PmxParsedImpulseMorphOffset {
1091    pub rigid_body_index: i32,
1092    pub local: bool,
1093    pub velocity: [f32; 3],
1094    pub torque: [f32; 3],
1095}
1096
1097#[derive(Debug, Clone, Serialize, Deserialize)]
1098#[serde(rename_all = "camelCase")]
1099pub struct PmxParsedDisplayFrame {
1100    pub name: String,
1101    pub english_name: String,
1102    pub special: bool,
1103    pub frames: Vec<PmxParsedDisplayFrameElement>,
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1107pub struct PmxParsedDisplayFrameElement {
1108    #[serde(rename = "type")]
1109    pub kind: String,
1110    pub index: i32,
1111}
1112
1113#[derive(Debug, Clone, Serialize, Deserialize)]
1114#[serde(rename_all = "camelCase")]
1115pub struct PmxParsedRigidBody {
1116    pub name: String,
1117    pub english_name: String,
1118    pub bone_index: i32,
1119    pub group: u8,
1120    pub mask: u16,
1121    pub shape: String,
1122    pub size: [f32; 3],
1123    pub position: [f32; 3],
1124    pub rotation: [f32; 3],
1125    pub mass: f32,
1126    pub linear_damping: f32,
1127    pub angular_damping: f32,
1128    pub restitution: f32,
1129    pub friction: f32,
1130    pub mode: String,
1131}
1132
1133#[derive(Debug, Clone, Serialize, Deserialize)]
1134#[serde(rename_all = "camelCase")]
1135pub struct PmxParsedJoint {
1136    pub name: String,
1137    pub english_name: String,
1138    #[serde(rename = "type")]
1139    pub kind: String,
1140    pub rigid_body_index_a: i32,
1141    pub rigid_body_index_b: i32,
1142    pub position: [f32; 3],
1143    pub rotation: [f32; 3],
1144    pub translation_lower_limit: [f32; 3],
1145    pub translation_upper_limit: [f32; 3],
1146    pub rotation_lower_limit: [f32; 3],
1147    pub rotation_upper_limit: [f32; 3],
1148    pub spring_translation_factor: [f32; 3],
1149    pub spring_rotation_factor: [f32; 3],
1150}
1151
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154pub struct PmxParsedSoftBody {
1155    pub name: String,
1156    pub english_name: String,
1157    #[serde(rename = "type")]
1158    pub kind: String,
1159    pub material_index: i32,
1160    pub collision_group: u8,
1161    pub collision_mask: u16,
1162    pub flags: u8,
1163    pub bending_constraints_distance: i32,
1164    pub cluster_count: i32,
1165    pub total_mass: f32,
1166    pub collision_margin: f32,
1167}
1168
1169#[derive(Debug, Clone, Serialize, Deserialize)]
1170pub struct PmxParserDiagnostic {
1171    pub level: String,
1172    pub code: String,
1173    pub message: String,
1174}
1175
1176#[derive(Debug, Clone, Serialize, Deserialize)]
1177#[serde(rename_all = "camelCase")]
1178pub struct PmxPartsDescriptor {
1179    #[serde(default = "default_pmx_parts_version")]
1180    pub version: f32,
1181    #[serde(default = "default_pmx_parts_encoding")]
1182    pub encoding: String,
1183    #[serde(default = "default_pmx_parts_name")]
1184    pub name: String,
1185    #[serde(default)]
1186    pub english_name: String,
1187    #[serde(default)]
1188    pub comment: String,
1189    #[serde(default)]
1190    pub english_comment: String,
1191    #[serde(default)]
1192    pub material_name: String,
1193    #[serde(default)]
1194    pub english_material_name: String,
1195    #[serde(default)]
1196    pub materials: Vec<PmxPartsMaterialDescriptor>,
1197    #[serde(default)]
1198    pub bones: Vec<PmxPartsBoneDescriptor>,
1199    #[serde(default)]
1200    pub morphs: Vec<PmxPartsMorphDescriptor>,
1201    #[serde(default)]
1202    pub display_frames: Vec<PmxPartsDisplayFrameDescriptor>,
1203    #[serde(default)]
1204    pub rigid_bodies: Vec<PmxPartsRigidBodyDescriptor>,
1205    #[serde(default)]
1206    pub joints: Vec<PmxPartsJointDescriptor>,
1207    #[serde(default)]
1208    pub index_sizes: PmxPartsIndexSizes,
1209}
1210
1211#[derive(Debug, Clone, Serialize, Deserialize)]
1212#[serde(rename_all = "camelCase")]
1213pub struct PmxPartsMaterialDescriptor {
1214    #[serde(default = "default_pmx_parts_material_name")]
1215    pub name: String,
1216    #[serde(default)]
1217    pub english_name: String,
1218    #[serde(default)]
1219    pub texture_path: String,
1220    #[serde(default)]
1221    pub sphere_texture_path: String,
1222    #[serde(default)]
1223    pub sphere_mode: String,
1224    #[serde(default)]
1225    pub toon_texture_path: String,
1226    #[serde(default = "default_pmx_parts_shared_toon_index")]
1227    pub shared_toon_index: Option<u8>,
1228    #[serde(default = "default_pmx_parts_diffuse")]
1229    pub diffuse: [f32; 4],
1230    #[serde(default)]
1231    pub specular: [f32; 3],
1232    #[serde(default = "default_pmx_parts_specular_power")]
1233    pub specular_power: f32,
1234    #[serde(default = "default_pmx_parts_ambient")]
1235    pub ambient: [f32; 3],
1236    #[serde(default = "default_pmx_parts_edge_color")]
1237    pub edge_color: [f32; 4],
1238    #[serde(default = "default_pmx_parts_edge_size")]
1239    pub edge_size: f32,
1240    #[serde(default)]
1241    pub flags: PmxPartsMaterialFlags,
1242    #[serde(default)]
1243    pub face_count: i32,
1244}
1245
1246#[derive(Debug, Clone, Serialize, Deserialize)]
1247#[serde(rename_all = "camelCase")]
1248pub struct PmxPartsMaterialFlags {
1249    #[serde(default = "default_true")]
1250    pub double_sided: bool,
1251    #[serde(default = "default_true")]
1252    pub ground_shadow: bool,
1253    #[serde(default = "default_true")]
1254    pub self_shadow_map: bool,
1255    #[serde(default = "default_true")]
1256    pub self_shadow: bool,
1257    #[serde(default)]
1258    pub edge: bool,
1259    #[serde(default)]
1260    pub vertex_color: bool,
1261    #[serde(default)]
1262    pub point_draw: bool,
1263    #[serde(default)]
1264    pub line_draw: bool,
1265}
1266
1267impl Default for PmxPartsMaterialFlags {
1268    fn default() -> Self {
1269        Self {
1270            double_sided: true,
1271            ground_shadow: true,
1272            self_shadow_map: true,
1273            self_shadow: true,
1274            edge: false,
1275            vertex_color: false,
1276            point_draw: false,
1277            line_draw: false,
1278        }
1279    }
1280}
1281
1282#[derive(Debug, Clone, Serialize, Deserialize)]
1283#[serde(rename_all = "camelCase")]
1284pub struct PmxPartsBoneDescriptor {
1285    #[serde(default = "default_pmx_parts_root_bone_name")]
1286    pub name: String,
1287    #[serde(default)]
1288    pub english_name: String,
1289    #[serde(default = "default_negative_index")]
1290    pub parent_index: i32,
1291    #[serde(default)]
1292    pub layer: i32,
1293    #[serde(default)]
1294    pub position: [f32; 3],
1295    #[serde(default = "default_negative_index")]
1296    pub tail_index: i32,
1297    #[serde(default)]
1298    pub tail_position: Option<[f32; 3]>,
1299    #[serde(default)]
1300    pub rotatable: bool,
1301    #[serde(default)]
1302    pub translatable: bool,
1303    #[serde(default = "default_true")]
1304    pub visible: bool,
1305    #[serde(default = "default_true")]
1306    pub enabled: bool,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1310#[serde(rename_all = "camelCase")]
1311pub struct PmxPartsMorphDescriptor {
1312    #[serde(default)]
1313    pub name: String,
1314    #[serde(default)]
1315    pub english_name: String,
1316    #[serde(default = "default_pmx_parts_morph_kind", alias = "type")]
1317    pub kind: String,
1318    #[serde(default)]
1319    pub vertex_offsets: Vec<PmxPartsVertexMorphOffset>,
1320    #[serde(default)]
1321    pub group_offsets: Vec<PmxPartsGroupMorphOffset>,
1322}
1323
1324#[derive(Debug, Clone, Serialize, Deserialize)]
1325#[serde(rename_all = "camelCase")]
1326pub struct PmxPartsVertexMorphOffset {
1327    pub vertex_index: u32,
1328    pub position: [f32; 3],
1329}
1330
1331#[derive(Debug, Clone, Serialize, Deserialize)]
1332#[serde(rename_all = "camelCase")]
1333pub struct PmxPartsGroupMorphOffset {
1334    pub morph_index: i32,
1335    pub weight: f32,
1336}
1337
1338#[derive(Debug, Clone, Serialize, Deserialize)]
1339#[serde(rename_all = "camelCase")]
1340pub struct PmxPartsDisplayFrameDescriptor {
1341    #[serde(default)]
1342    pub name: String,
1343    #[serde(default)]
1344    pub english_name: String,
1345    #[serde(default)]
1346    pub special: bool,
1347    #[serde(default)]
1348    pub frames: Vec<PmxPartsDisplayFrameItem>,
1349}
1350
1351#[derive(Debug, Clone, Serialize, Deserialize)]
1352#[serde(rename_all = "camelCase")]
1353pub struct PmxPartsDisplayFrameItem {
1354    #[serde(default = "default_pmx_parts_display_frame_kind")]
1355    pub kind: String,
1356    #[serde(default)]
1357    pub index: i32,
1358}
1359
1360#[derive(Debug, Clone, Serialize, Deserialize)]
1361#[serde(rename_all = "camelCase")]
1362pub struct PmxPartsRigidBodyDescriptor {
1363    #[serde(default)]
1364    pub name: String,
1365    #[serde(default)]
1366    pub english_name: String,
1367    #[serde(default = "default_negative_index")]
1368    pub bone_index: i32,
1369    #[serde(default)]
1370    pub group: u8,
1371    #[serde(default)]
1372    pub mask: u16,
1373    #[serde(default = "default_pmx_parts_rigid_body_shape")]
1374    pub shape: String,
1375    #[serde(default = "default_unit_vec3")]
1376    pub size: [f32; 3],
1377    #[serde(default)]
1378    pub position: [f32; 3],
1379    #[serde(default)]
1380    pub rotation: [f32; 3],
1381    #[serde(default = "default_pmx_parts_mass")]
1382    pub mass: f32,
1383    #[serde(default)]
1384    pub linear_damping: f32,
1385    #[serde(default)]
1386    pub angular_damping: f32,
1387    #[serde(default)]
1388    pub restitution: f32,
1389    #[serde(default)]
1390    pub friction: f32,
1391    #[serde(default = "default_pmx_parts_rigid_body_mode")]
1392    pub mode: String,
1393}
1394
1395#[derive(Debug, Clone, Serialize, Deserialize)]
1396#[serde(rename_all = "camelCase")]
1397pub struct PmxPartsJointDescriptor {
1398    #[serde(default)]
1399    pub name: String,
1400    #[serde(default)]
1401    pub english_name: String,
1402    #[serde(
1403        default = "default_pmx_parts_joint_kind",
1404        rename = "type",
1405        alias = "kind"
1406    )]
1407    pub kind: String,
1408    #[serde(default = "default_negative_index")]
1409    pub rigid_body_index_a: i32,
1410    #[serde(default = "default_negative_index")]
1411    pub rigid_body_index_b: i32,
1412    #[serde(default)]
1413    pub position: [f32; 3],
1414    #[serde(default)]
1415    pub rotation: [f32; 3],
1416    #[serde(default)]
1417    pub translation_lower_limit: [f32; 3],
1418    #[serde(default)]
1419    pub translation_upper_limit: [f32; 3],
1420    #[serde(default)]
1421    pub rotation_lower_limit: [f32; 3],
1422    #[serde(default)]
1423    pub rotation_upper_limit: [f32; 3],
1424    #[serde(default)]
1425    pub spring_translation_factor: [f32; 3],
1426    #[serde(default)]
1427    pub spring_rotation_factor: [f32; 3],
1428}
1429
1430#[derive(Debug, Clone, Serialize, Deserialize)]
1431#[serde(rename_all = "camelCase")]
1432pub struct PmxPartsIndexSizes {
1433    #[serde(default = "default_pmx_vertex_index_size")]
1434    pub vertex: u8,
1435    #[serde(default = "default_pmx_small_index_size")]
1436    pub texture: u8,
1437    #[serde(default = "default_pmx_small_index_size")]
1438    pub material: u8,
1439    #[serde(default = "default_pmx_small_index_size")]
1440    pub bone: u8,
1441    #[serde(default = "default_pmx_small_index_size")]
1442    pub morph: u8,
1443    #[serde(default = "default_pmx_small_index_size")]
1444    pub rigid_body: u8,
1445}
1446
1447impl Default for PmxPartsIndexSizes {
1448    fn default() -> Self {
1449        Self {
1450            vertex: default_pmx_vertex_index_size(),
1451            texture: default_pmx_small_index_size(),
1452            material: default_pmx_small_index_size(),
1453            bone: default_pmx_small_index_size(),
1454            morph: default_pmx_small_index_size(),
1455            rigid_body: default_pmx_small_index_size(),
1456        }
1457    }
1458}
1459
1460pub struct PmxPartsInput<'a> {
1461    pub descriptor: PmxPartsDescriptor,
1462    pub positions_xyz: &'a [f32],
1463    pub normals_xyz: &'a [f32],
1464    pub uvs_xy: &'a [f32],
1465    pub indices: &'a [u32],
1466    pub skin_indices: &'a [u32],
1467    pub skin_weights: &'a [f32],
1468    pub edge_scale: &'a [f32],
1469}
1470
1471fn default_pmx_format() -> String {
1472    "pmx".to_owned()
1473}
1474
1475fn default_pmx_parts_version() -> f32 {
1476    2.0
1477}
1478
1479fn default_pmx_parts_encoding() -> String {
1480    "utf-8".to_owned()
1481}
1482
1483fn default_pmx_parts_name() -> String {
1484    "model".to_owned()
1485}
1486
1487fn default_pmx_parts_root_bone_name() -> String {
1488    "root".to_owned()
1489}
1490
1491fn default_negative_index() -> i32 {
1492    -1
1493}
1494
1495fn default_pmx_parts_display_frame_kind() -> String {
1496    "bone".to_owned()
1497}
1498
1499fn default_pmx_parts_morph_kind() -> String {
1500    "vertex".to_owned()
1501}
1502
1503fn default_pmx_parts_rigid_body_shape() -> String {
1504    "sphere".to_owned()
1505}
1506
1507fn default_pmx_parts_rigid_body_mode() -> String {
1508    "static".to_owned()
1509}
1510
1511fn default_pmx_parts_joint_kind() -> String {
1512    "generic6dofSpring".to_owned()
1513}
1514
1515fn default_unit_vec3() -> [f32; 3] {
1516    [1.0, 1.0, 1.0]
1517}
1518
1519fn default_pmx_parts_mass() -> f32 {
1520    1.0
1521}
1522
1523fn default_pmx_parts_material_name() -> String {
1524    "material".to_owned()
1525}
1526
1527fn default_pmx_parts_shared_toon_index() -> Option<u8> {
1528    Some(0)
1529}
1530
1531fn default_pmx_parts_diffuse() -> [f32; 4] {
1532    [0.8, 0.8, 0.8, 1.0]
1533}
1534
1535fn default_pmx_parts_specular_power() -> f32 {
1536    1.0
1537}
1538
1539fn default_pmx_parts_ambient() -> [f32; 3] {
1540    [0.2, 0.2, 0.2]
1541}
1542
1543fn default_pmx_parts_edge_color() -> [f32; 4] {
1544    [0.0, 0.0, 0.0, 1.0]
1545}
1546
1547fn default_pmx_parts_edge_size() -> f32 {
1548    1.0
1549}
1550
1551fn default_true() -> bool {
1552    true
1553}
1554
1555fn default_pmx_vertex_index_size() -> u8 {
1556    4
1557}
1558
1559fn default_pmx_small_index_size() -> u8 {
1560    1
1561}
1562
1563pub fn build_pmx_model_from_parts(input: PmxPartsInput<'_>) -> Result<PmxParsedModel, String> {
1564    let vertex_count = validate_pmx_parts_geometry(
1565        input.positions_xyz,
1566        input.normals_xyz,
1567        input.uvs_xy,
1568        input.indices,
1569        input.skin_indices,
1570        input.skin_weights,
1571        input.edge_scale,
1572    )?;
1573    validate_pmx_parts_descriptor(
1574        &input.descriptor,
1575        vertex_count,
1576        input.indices,
1577        input.skin_indices,
1578    )?;
1579
1580    let skin_indices = if input.skin_indices.is_empty() {
1581        vec![0; vertex_count * 4]
1582    } else {
1583        input.skin_indices.to_vec()
1584    };
1585    let skin_weights = if input.skin_weights.is_empty() {
1586        default_pmx_skin_weights(vertex_count)
1587    } else {
1588        input.skin_weights.to_vec()
1589    };
1590    let edge_scale = if input.edge_scale.is_empty() {
1591        vec![1.0; vertex_count]
1592    } else {
1593        input.edge_scale.to_vec()
1594    };
1595    let face_count = input.indices.len() / 3;
1596    let (materials, material_groups) = build_pmx_parts_materials(&input.descriptor, face_count);
1597    let bones = build_pmx_parts_bones(&input.descriptor);
1598    let morphs = build_pmx_parts_morphs(&input.descriptor);
1599    let display_frames = build_pmx_parts_display_frames(&input.descriptor);
1600    let rigid_bodies = build_pmx_parts_rigid_bodies(&input.descriptor);
1601    let joints = build_pmx_parts_joints(&input.descriptor);
1602
1603    let model = PmxParsedModel {
1604        metadata: PmxParsedMetadata {
1605            format: "pmx".to_owned(),
1606            version: input.descriptor.version,
1607            encoding: input.descriptor.encoding,
1608            name: input.descriptor.name,
1609            english_name: input.descriptor.english_name,
1610            comment: input.descriptor.comment,
1611            english_comment: input.descriptor.english_comment,
1612            counts: PmxParsedCounts {
1613                vertices: vertex_count,
1614                faces: face_count,
1615                materials: materials.len(),
1616                bones: bones.len(),
1617                morphs: morphs.len(),
1618                display_frames: display_frames.len(),
1619                rigid_bodies: rigid_bodies.len(),
1620                joints: joints.len(),
1621                soft_bodies: 0,
1622            },
1623            index_sizes: PmxParsedIndexSizes {
1624                vertex: input.descriptor.index_sizes.vertex,
1625                texture: input.descriptor.index_sizes.texture,
1626                material: input.descriptor.index_sizes.material,
1627                bone: input.descriptor.index_sizes.bone,
1628                morph: input.descriptor.index_sizes.morph,
1629                rigid_body: input.descriptor.index_sizes.rigid_body,
1630            },
1631            additional_uv_count: 0,
1632        },
1633        geometry: PmxParsedGeometry {
1634            positions: input.positions_xyz.to_vec(),
1635            normals: input.normals_xyz.to_vec(),
1636            uvs: input.uvs_xy.to_vec(),
1637            additional_uvs: Vec::new(),
1638            indices: input.indices.to_vec(),
1639            skin_indices,
1640            skin_weights,
1641            edge_scale,
1642            material_groups,
1643            sdef: PmxParsedSdef::default(),
1644            qdef: PmxParsedQdef::default(),
1645        },
1646        materials,
1647        skeleton: PmxParsedSkeleton { bones },
1648        morphs,
1649        display_frames,
1650        rigid_bodies,
1651        joints,
1652        soft_bodies: Vec::new(),
1653        diagnostics: Vec::new(),
1654    };
1655    validate_pmx_export_model(&model)?;
1656    Ok(model)
1657}
1658
1659pub fn validate_pmx_export_model(model: &PmxParsedModel) -> Result<(), String> {
1660    let geometry = &model.geometry;
1661    if !geometry.positions.len().is_multiple_of(3) {
1662        return Err(format!(
1663            "PMX export positions length must be divisible by 3: {}",
1664            geometry.positions.len()
1665        ));
1666    }
1667    let vertex_count = geometry.positions.len() / 3;
1668    if geometry.normals.len() != vertex_count * 3 {
1669        return Err(format!(
1670            "PMX export normals length mismatch: expected {}, got {}",
1671            vertex_count * 3,
1672            geometry.normals.len()
1673        ));
1674    }
1675    if geometry.uvs.len() != vertex_count * 2 {
1676        return Err(format!(
1677            "PMX export uvs length mismatch: expected {}, got {}",
1678            vertex_count * 2,
1679            geometry.uvs.len()
1680        ));
1681    }
1682    if geometry.additional_uvs.len() != model.metadata.additional_uv_count as usize {
1683        return Err(format!(
1684            "PMX export additionalUvs set count mismatch: expected {}, got {}",
1685            model.metadata.additional_uv_count,
1686            geometry.additional_uvs.len()
1687        ));
1688    }
1689    for (index, values) in geometry.additional_uvs.iter().enumerate() {
1690        if values.len() != vertex_count * 4 {
1691            return Err(format!(
1692                "PMX export additionalUvs[{}] length mismatch: expected {}, got {}",
1693                index,
1694                vertex_count * 4,
1695                values.len()
1696            ));
1697        }
1698    }
1699    if !geometry.indices.len().is_multiple_of(3) {
1700        return Err(format!(
1701            "PMX export indices length must be divisible by 3: {}",
1702            geometry.indices.len()
1703        ));
1704    }
1705    let expected_skin_len = vertex_count * 4;
1706    if geometry.skin_indices.is_empty() != geometry.skin_weights.is_empty() {
1707        return Err(
1708            "PMX export skinIndices and skinWeights must be both empty or both provided".to_owned(),
1709        );
1710    }
1711    if !geometry.skin_indices.is_empty() && geometry.skin_indices.len() != expected_skin_len {
1712        return Err(format!(
1713            "PMX export skinIndices length mismatch: expected {}, got {}",
1714            expected_skin_len,
1715            geometry.skin_indices.len()
1716        ));
1717    }
1718    if !geometry.skin_weights.is_empty() && geometry.skin_weights.len() != expected_skin_len {
1719        return Err(format!(
1720            "PMX export skinWeights length mismatch: expected {}, got {}",
1721            expected_skin_len,
1722            geometry.skin_weights.len()
1723        ));
1724    }
1725    Ok(())
1726}
1727
1728fn validate_pmx_parts_geometry(
1729    positions_xyz: &[f32],
1730    normals_xyz: &[f32],
1731    uvs_xy: &[f32],
1732    indices: &[u32],
1733    skin_indices: &[u32],
1734    skin_weights: &[f32],
1735    edge_scale: &[f32],
1736) -> Result<usize, String> {
1737    if !positions_xyz.len().is_multiple_of(3) {
1738        return Err(format!(
1739            "positions_xyz must contain vertex_count * 3 values, got {}",
1740            positions_xyz.len()
1741        ));
1742    }
1743    let vertex_count = positions_xyz.len() / 3;
1744    if vertex_count == 0 {
1745        return Err("PMX parts export requires at least one vertex".to_owned());
1746    }
1747    if normals_xyz.len() != vertex_count * 3 {
1748        return Err(format!(
1749            "normals_xyz must contain vertex_count * 3 values, expected {}, got {}",
1750            vertex_count * 3,
1751            normals_xyz.len()
1752        ));
1753    }
1754    if uvs_xy.len() != vertex_count * 2 {
1755        return Err(format!(
1756            "uvs_xy must contain vertex_count * 2 values, expected {}, got {}",
1757            vertex_count * 2,
1758            uvs_xy.len()
1759        ));
1760    }
1761    if !indices.len().is_multiple_of(3) {
1762        return Err(format!(
1763            "indices must contain triangle indices and be divisible by 3, got {}",
1764            indices.len()
1765        ));
1766    }
1767    if let Some(&index) = indices
1768        .iter()
1769        .find(|&&index| index as usize >= vertex_count)
1770    {
1771        return Err(format!(
1772            "indices contains out-of-range vertex index {index} for vertex_count {vertex_count}"
1773        ));
1774    }
1775    if !skin_indices.is_empty() && skin_indices.len() != vertex_count * 4 {
1776        return Err(format!(
1777            "skin_indices must contain vertex_count * 4 values when provided, expected {}, got {}",
1778            vertex_count * 4,
1779            skin_indices.len()
1780        ));
1781    }
1782    if !skin_weights.is_empty() && skin_weights.len() != vertex_count * 4 {
1783        return Err(format!(
1784            "skin_weights must contain vertex_count * 4 values when provided, expected {}, got {}",
1785            vertex_count * 4,
1786            skin_weights.len()
1787        ));
1788    }
1789    if skin_indices.is_empty() != skin_weights.is_empty() {
1790        return Err(
1791            "skin_indices and skin_weights must either both be provided or both be empty"
1792                .to_owned(),
1793        );
1794    }
1795    if !edge_scale.is_empty() && edge_scale.len() != vertex_count {
1796        return Err(format!(
1797            "edge_scale must contain vertex_count values when provided, expected {}, got {}",
1798            vertex_count,
1799            edge_scale.len()
1800        ));
1801    }
1802    Ok(vertex_count)
1803}
1804
1805fn validate_pmx_parts_descriptor(
1806    descriptor: &PmxPartsDescriptor,
1807    vertex_count: usize,
1808    indices: &[u32],
1809    skin_indices: &[u32],
1810) -> Result<(), String> {
1811    if descriptor.version < 2.0 {
1812        return Err(format!(
1813            "PMX parts export supports PMX 2.0 or newer, got {}",
1814            descriptor.version
1815        ));
1816    }
1817    if descriptor.encoding != "utf-8" && descriptor.encoding != "utf-16-le" {
1818        return Err(format!(
1819            "PMX parts export encoding must be utf-8 or utf-16-le, got {}",
1820            descriptor.encoding
1821        ));
1822    }
1823    validate_pmx_index_size("vertex", descriptor.index_sizes.vertex)?;
1824    validate_pmx_index_size("texture", descriptor.index_sizes.texture)?;
1825    validate_pmx_index_size("material", descriptor.index_sizes.material)?;
1826    validate_pmx_index_size("bone", descriptor.index_sizes.bone)?;
1827    validate_pmx_index_size("morph", descriptor.index_sizes.morph)?;
1828    validate_pmx_index_size("rigidBody", descriptor.index_sizes.rigid_body)?;
1829    if indices.len() / 3 > i32::MAX as usize {
1830        return Err(format!(
1831            "face count {} exceeds PMX i32 material face count limit",
1832            indices.len() / 3
1833        ));
1834    }
1835    if !pmx_vertex_index_size_can_hold(descriptor.index_sizes.vertex, vertex_count) {
1836        return Err(format!(
1837            "vertex index size {} cannot hold vertex_count {}",
1838            descriptor.index_sizes.vertex, vertex_count
1839        ));
1840    }
1841    if !indices.is_empty()
1842        && !pmx_vertex_index_size_can_hold(
1843            descriptor.index_sizes.vertex,
1844            indices.iter().copied().max().unwrap_or(0) as usize + 1,
1845        )
1846    {
1847        return Err(format!(
1848            "vertex index size {} cannot hold max index {}",
1849            descriptor.index_sizes.vertex,
1850            indices.iter().copied().max().unwrap_or(0)
1851        ));
1852    }
1853    validate_pmx_parts_materials(descriptor, indices.len() / 3)?;
1854    validate_pmx_parts_bones(descriptor)?;
1855    validate_pmx_parts_morphs(descriptor, vertex_count)?;
1856    validate_pmx_parts_skin_indices(descriptor, skin_indices)?;
1857    validate_pmx_parts_display_frames(descriptor)?;
1858    validate_pmx_parts_physics(descriptor)?;
1859    Ok(())
1860}
1861
1862fn validate_pmx_parts_materials(
1863    descriptor: &PmxPartsDescriptor,
1864    face_count: usize,
1865) -> Result<(), String> {
1866    if descriptor.materials.is_empty() {
1867        return Ok(());
1868    }
1869    if !pmx_signed_index_size_can_hold_count(
1870        descriptor.index_sizes.material,
1871        descriptor.materials.len(),
1872    ) {
1873        return Err(format!(
1874            "material index size {} cannot hold material count {}",
1875            descriptor.index_sizes.material,
1876            descriptor.materials.len()
1877        ));
1878    }
1879    let mut total = 0usize;
1880    for (index, material) in descriptor.materials.iter().enumerate() {
1881        if material.face_count < 0 {
1882            return Err(format!(
1883                "materials[{index}].faceCount must be non-negative, got {}",
1884                material.face_count
1885            ));
1886        }
1887        total += material.face_count as usize;
1888    }
1889    if total != face_count {
1890        return Err(format!(
1891            "materials faceCount sum must match index face count, expected {}, got {}",
1892            face_count, total
1893        ));
1894    }
1895    Ok(())
1896}
1897
1898fn validate_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
1899    let bone_count = descriptor.bones.len().max(1);
1900    if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.bone, bone_count) {
1901        return Err(format!(
1902            "bone index size {} cannot hold bone count {}",
1903            descriptor.index_sizes.bone, bone_count
1904        ));
1905    }
1906    for (index, bone) in descriptor.bones.iter().enumerate() {
1907        if bone.parent_index < -1
1908            || (bone.parent_index != -1 && bone.parent_index as usize >= bone_count)
1909        {
1910            return Err(format!(
1911                "bones[{index}].parentIndex must be -1 or a valid bone index, got {}",
1912                bone.parent_index
1913            ));
1914        }
1915        if bone.tail_index < -1 || (bone.tail_index != -1 && bone.tail_index as usize >= bone_count)
1916        {
1917            return Err(format!(
1918                "bones[{index}].tailIndex must be -1 or a valid bone index, got {}",
1919                bone.tail_index
1920            ));
1921        }
1922    }
1923    Ok(())
1924}
1925
1926fn validate_pmx_parts_skin_indices(
1927    descriptor: &PmxPartsDescriptor,
1928    skin_indices: &[u32],
1929) -> Result<(), String> {
1930    if skin_indices.is_empty() {
1931        return Ok(());
1932    }
1933    let bone_count = descriptor.bones.len().max(1);
1934    if let Some(&index) = skin_indices
1935        .iter()
1936        .find(|&&index| index as usize >= bone_count)
1937    {
1938        return Err(format!(
1939            "skin_indices contains out-of-range bone index {index} for bone count {bone_count}"
1940        ));
1941    }
1942    Ok(())
1943}
1944
1945fn validate_pmx_parts_morphs(
1946    descriptor: &PmxPartsDescriptor,
1947    vertex_count: usize,
1948) -> Result<(), String> {
1949    if !pmx_signed_index_size_can_hold_count(descriptor.index_sizes.morph, descriptor.morphs.len())
1950    {
1951        return Err(format!(
1952            "morph index size {} cannot hold morph count {}",
1953            descriptor.index_sizes.morph,
1954            descriptor.morphs.len()
1955        ));
1956    }
1957    for (morph_index, morph) in descriptor.morphs.iter().enumerate() {
1958        match morph.kind.as_str() {
1959            "vertex" => {
1960                for (offset_index, offset) in morph.vertex_offsets.iter().enumerate() {
1961                    if offset.vertex_index as usize >= vertex_count {
1962                        return Err(format!(
1963                            "morphs[{morph_index}].vertexOffsets[{offset_index}].vertexIndex out of range: {} for vertex_count {vertex_count}",
1964                            offset.vertex_index
1965                        ));
1966                    }
1967                }
1968            }
1969            "group" => {
1970                for (offset_index, offset) in morph.group_offsets.iter().enumerate() {
1971                    if offset.morph_index < 0
1972                        || offset.morph_index as usize >= descriptor.morphs.len()
1973                    {
1974                        return Err(format!(
1975                            "morphs[{morph_index}].groupOffsets[{offset_index}].morphIndex out of range: {}",
1976                            offset.morph_index
1977                        ));
1978                    }
1979                }
1980            }
1981            other => {
1982                return Err(format!(
1983                    "morphs[{morph_index}].kind must be vertex or group, got {other}"
1984                ));
1985            }
1986        }
1987    }
1988    Ok(())
1989}
1990
1991fn validate_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
1992    let bone_count = descriptor.bones.len().max(1);
1993    let morph_count = descriptor.morphs.len();
1994    for (frame_index, frame) in descriptor.display_frames.iter().enumerate() {
1995        for (item_index, item) in frame.frames.iter().enumerate() {
1996            match item.kind.as_str() {
1997                "bone" => {
1998                    if item.index < 0 || item.index as usize >= bone_count {
1999                        return Err(format!(
2000                            "displayFrames[{frame_index}].frames[{item_index}].index must reference an existing bone, got {}",
2001                            item.index
2002                        ));
2003                    }
2004                }
2005                "morph" => {
2006                    if item.index < 0 || item.index as usize >= morph_count {
2007                        return Err(format!(
2008                            "displayFrames[{frame_index}].frames[{item_index}].index must reference an existing morph, got {}",
2009                            item.index
2010                        ));
2011                    }
2012                }
2013                other => {
2014                    return Err(format!(
2015                        "displayFrames[{frame_index}].frames[{item_index}].kind must be bone or morph, got {other}"
2016                    ));
2017                }
2018            }
2019        }
2020    }
2021    Ok(())
2022}
2023
2024fn validate_pmx_parts_physics(descriptor: &PmxPartsDescriptor) -> Result<(), String> {
2025    let bone_count = descriptor.bones.len().max(1);
2026    if !pmx_signed_index_size_can_hold_count(
2027        descriptor.index_sizes.rigid_body,
2028        descriptor.rigid_bodies.len(),
2029    ) {
2030        return Err(format!(
2031            "rigidBody index size {} cannot hold rigid body count {}",
2032            descriptor.index_sizes.rigid_body,
2033            descriptor.rigid_bodies.len()
2034        ));
2035    }
2036    for (index, body) in descriptor.rigid_bodies.iter().enumerate() {
2037        if body.bone_index < -1 || (body.bone_index != -1 && body.bone_index as usize >= bone_count)
2038        {
2039            return Err(format!(
2040                "rigidBodies[{index}].boneIndex must be -1 or a valid bone index, got {}",
2041                body.bone_index
2042            ));
2043        }
2044        if !matches!(body.shape.as_str(), "sphere" | "box" | "capsule") {
2045            return Err(format!(
2046                "rigidBodies[{index}].shape must be sphere, box, or capsule, got {}",
2047                body.shape
2048            ));
2049        }
2050        if !matches!(body.mode.as_str(), "static" | "dynamic" | "dynamicBone") {
2051            return Err(format!(
2052                "rigidBodies[{index}].mode must be static, dynamic, or dynamicBone, got {}",
2053                body.mode
2054            ));
2055        }
2056    }
2057    for (index, joint) in descriptor.joints.iter().enumerate() {
2058        validate_pmx_parts_rigid_body_ref(
2059            "rigidBodyIndexA",
2060            index,
2061            joint.rigid_body_index_a,
2062            descriptor.rigid_bodies.len(),
2063        )?;
2064        validate_pmx_parts_rigid_body_ref(
2065            "rigidBodyIndexB",
2066            index,
2067            joint.rigid_body_index_b,
2068            descriptor.rigid_bodies.len(),
2069        )?;
2070        if !matches!(
2071            joint.kind.as_str(),
2072            "generic6dofSpring" | "generic6dof" | "point2point" | "coneTwist" | "slider" | "hinge"
2073        ) {
2074            return Err(format!(
2075                "joints[{index}].kind has unsupported PMX joint type {}",
2076                joint.kind
2077            ));
2078        }
2079    }
2080    Ok(())
2081}
2082
2083fn validate_pmx_parts_rigid_body_ref(
2084    field: &str,
2085    joint_index: usize,
2086    value: i32,
2087    rigid_body_count: usize,
2088) -> Result<(), String> {
2089    if value < -1 || (value != -1 && value as usize >= rigid_body_count) {
2090        Err(format!(
2091            "joints[{joint_index}].{field} must be -1 or a valid rigid body index, got {value}"
2092        ))
2093    } else {
2094        Ok(())
2095    }
2096}
2097
2098fn validate_pmx_index_size(name: &str, value: u8) -> Result<(), String> {
2099    if matches!(value, 1 | 2 | 4) {
2100        Ok(())
2101    } else {
2102        Err(format!(
2103            "PMX {name} index size must be 1, 2, or 4, got {value}"
2104        ))
2105    }
2106}
2107
2108fn pmx_vertex_index_size_can_hold(size: u8, count: usize) -> bool {
2109    match size {
2110        1 => count <= u8::MAX as usize + 1,
2111        2 => count <= u16::MAX as usize + 1,
2112        4 => true,
2113        _ => false,
2114    }
2115}
2116
2117fn pmx_signed_index_size_can_hold_count(size: u8, count: usize) -> bool {
2118    match size {
2119        1 => count <= i8::MAX as usize + 1,
2120        2 => count <= i16::MAX as usize + 1,
2121        4 => true,
2122        _ => false,
2123    }
2124}
2125
2126fn default_pmx_skin_weights(vertex_count: usize) -> Vec<f32> {
2127    let mut weights = vec![0.0; vertex_count * 4];
2128    for vertex in 0..vertex_count {
2129        weights[vertex * 4] = 1.0;
2130    }
2131    weights
2132}
2133
2134fn build_pmx_parts_materials(
2135    descriptor: &PmxPartsDescriptor,
2136    face_count: usize,
2137) -> (Vec<PmxParsedMaterial>, Vec<PmxParsedMaterialGroup>) {
2138    if face_count == 0 {
2139        return (Vec::new(), Vec::new());
2140    }
2141    if descriptor.materials.is_empty() {
2142        return (
2143            vec![default_pmx_parts_material(
2144                &descriptor.material_name,
2145                &descriptor.english_material_name,
2146                face_count as i32,
2147            )],
2148            vec![PmxParsedMaterialGroup {
2149                start: 0,
2150                count: face_count * 3,
2151                material_index: 0,
2152            }],
2153        );
2154    }
2155
2156    let mut start = 0usize;
2157    let mut materials = Vec::with_capacity(descriptor.materials.len());
2158    let mut groups = Vec::with_capacity(descriptor.materials.len());
2159    for (index, material) in descriptor.materials.iter().enumerate() {
2160        let index_count = material.face_count as usize * 3;
2161        materials.push(pmx_parts_material_from_descriptor(material));
2162        groups.push(PmxParsedMaterialGroup {
2163            start,
2164            count: index_count,
2165            material_index: index,
2166        });
2167        start += index_count;
2168    }
2169    (materials, groups)
2170}
2171
2172fn pmx_parts_material_from_descriptor(material: &PmxPartsMaterialDescriptor) -> PmxParsedMaterial {
2173    PmxParsedMaterial {
2174        name: material.name.clone(),
2175        english_name: material.english_name.clone(),
2176        texture_path: material.texture_path.clone(),
2177        sphere_texture_path: material.sphere_texture_path.clone(),
2178        sphere_mode: if material.sphere_mode.is_empty() {
2179            "none".to_owned()
2180        } else {
2181            material.sphere_mode.clone()
2182        },
2183        toon_texture_path: material.toon_texture_path.clone(),
2184        shared_toon_index: material.shared_toon_index,
2185        diffuse: material.diffuse,
2186        specular: material.specular,
2187        specular_power: material.specular_power,
2188        ambient: material.ambient,
2189        edge_color: material.edge_color,
2190        edge_size: material.edge_size,
2191        flags: PmxParsedMaterialFlags {
2192            double_sided: material.flags.double_sided,
2193            ground_shadow: material.flags.ground_shadow,
2194            self_shadow_map: material.flags.self_shadow_map,
2195            self_shadow: material.flags.self_shadow,
2196            edge: material.flags.edge,
2197            vertex_color: material.flags.vertex_color,
2198            point_draw: material.flags.point_draw,
2199            line_draw: material.flags.line_draw,
2200        },
2201        face_count: material.face_count,
2202    }
2203}
2204
2205fn build_pmx_parts_bones(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedBone> {
2206    if descriptor.bones.is_empty() {
2207        return vec![default_pmx_parts_root_bone()];
2208    }
2209    descriptor
2210        .bones
2211        .iter()
2212        .map(pmx_parts_bone_from_descriptor)
2213        .collect()
2214}
2215
2216fn build_pmx_parts_display_frames(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedDisplayFrame> {
2217    descriptor
2218        .display_frames
2219        .iter()
2220        .map(|frame| PmxParsedDisplayFrame {
2221            name: frame.name.clone(),
2222            english_name: frame.english_name.clone(),
2223            special: frame.special,
2224            frames: frame
2225                .frames
2226                .iter()
2227                .map(|item| PmxParsedDisplayFrameElement {
2228                    kind: item.kind.clone(),
2229                    index: item.index,
2230                })
2231                .collect(),
2232        })
2233        .collect()
2234}
2235
2236fn build_pmx_parts_morphs(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedMorph> {
2237    descriptor
2238        .morphs
2239        .iter()
2240        .map(pmx_parts_morph_from_descriptor)
2241        .collect()
2242}
2243
2244fn build_pmx_parts_rigid_bodies(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedRigidBody> {
2245    descriptor
2246        .rigid_bodies
2247        .iter()
2248        .map(|body| PmxParsedRigidBody {
2249            name: body.name.clone(),
2250            english_name: body.english_name.clone(),
2251            bone_index: body.bone_index,
2252            group: body.group,
2253            mask: body.mask,
2254            shape: body.shape.clone(),
2255            size: body.size,
2256            position: body.position,
2257            rotation: body.rotation,
2258            mass: body.mass,
2259            linear_damping: body.linear_damping,
2260            angular_damping: body.angular_damping,
2261            restitution: body.restitution,
2262            friction: body.friction,
2263            mode: body.mode.clone(),
2264        })
2265        .collect()
2266}
2267
2268fn build_pmx_parts_joints(descriptor: &PmxPartsDescriptor) -> Vec<PmxParsedJoint> {
2269    descriptor
2270        .joints
2271        .iter()
2272        .map(|joint| PmxParsedJoint {
2273            name: joint.name.clone(),
2274            english_name: joint.english_name.clone(),
2275            kind: joint.kind.clone(),
2276            rigid_body_index_a: joint.rigid_body_index_a,
2277            rigid_body_index_b: joint.rigid_body_index_b,
2278            position: joint.position,
2279            rotation: joint.rotation,
2280            translation_lower_limit: joint.translation_lower_limit,
2281            translation_upper_limit: joint.translation_upper_limit,
2282            rotation_lower_limit: joint.rotation_lower_limit,
2283            rotation_upper_limit: joint.rotation_upper_limit,
2284            spring_translation_factor: joint.spring_translation_factor,
2285            spring_rotation_factor: joint.spring_rotation_factor,
2286        })
2287        .collect()
2288}
2289
2290fn pmx_parts_morph_from_descriptor(morph: &PmxPartsMorphDescriptor) -> PmxParsedMorph {
2291    let mut parsed = PmxParsedMorph {
2292        name: morph.name.clone(),
2293        english_name: morph.english_name.clone(),
2294        kind: morph.kind.clone(),
2295        vertex_offsets: Vec::new(),
2296        group_offsets: Vec::new(),
2297        bone_offsets: Vec::new(),
2298        uv_offsets: Vec::new(),
2299        additional_uv_offsets: Vec::new(),
2300        material_offsets: Vec::new(),
2301        flip_offsets: Vec::new(),
2302        impulse_offsets: Vec::new(),
2303    };
2304    match morph.kind.as_str() {
2305        "vertex" => {
2306            parsed.vertex_offsets = morph
2307                .vertex_offsets
2308                .iter()
2309                .map(|offset| PmxParsedVertexMorphOffset {
2310                    vertex_index: offset.vertex_index,
2311                    position: offset.position,
2312                })
2313                .collect();
2314        }
2315        "group" => {
2316            parsed.group_offsets = morph
2317                .group_offsets
2318                .iter()
2319                .map(|offset| PmxParsedGroupMorphOffset {
2320                    morph_index: offset.morph_index,
2321                    weight: offset.weight,
2322                })
2323                .collect();
2324        }
2325        _ => unreachable!("PmxPartsMorphDescriptor was validated before build"),
2326    }
2327    parsed
2328}
2329
2330fn pmx_parts_bone_from_descriptor(bone: &PmxPartsBoneDescriptor) -> PmxParsedBone {
2331    let tail_position = if bone.tail_index >= 0 {
2332        None
2333    } else {
2334        Some(bone.tail_position.unwrap_or([0.0, 1.0, 0.0]))
2335    };
2336    PmxParsedBone {
2337        name: bone.name.clone(),
2338        english_name: bone.english_name.clone(),
2339        parent_index: bone.parent_index,
2340        layer: bone.layer,
2341        position: bone.position,
2342        tail_index: bone.tail_index,
2343        tail_position,
2344        flags: PmxParsedBoneFlags {
2345            indexed_tail: bone.tail_index >= 0,
2346            rotatable: bone.rotatable,
2347            translatable: bone.translatable,
2348            visible: bone.visible,
2349            enabled: bone.enabled,
2350            ik: false,
2351            append_local: false,
2352            append_rotate: false,
2353            append_translate: false,
2354            fixed_axis: false,
2355            local_axis: false,
2356            external_parent_transform: false,
2357            transform_after_physics: false,
2358        },
2359        append_transform: None,
2360        fixed_axis: None,
2361        local_axis: None,
2362        external_parent_key: None,
2363        ik: None,
2364    }
2365}
2366
2367fn default_pmx_parts_material(
2368    name: &str,
2369    english_name: &str,
2370    face_count: i32,
2371) -> PmxParsedMaterial {
2372    PmxParsedMaterial {
2373        name: if name.is_empty() {
2374            "material".to_owned()
2375        } else {
2376            name.to_owned()
2377        },
2378        english_name: english_name.to_owned(),
2379        texture_path: String::new(),
2380        sphere_texture_path: String::new(),
2381        sphere_mode: "none".to_owned(),
2382        toon_texture_path: String::new(),
2383        shared_toon_index: Some(0),
2384        diffuse: [0.8, 0.8, 0.8, 1.0],
2385        specular: [0.0, 0.0, 0.0],
2386        specular_power: 1.0,
2387        ambient: [0.2, 0.2, 0.2],
2388        edge_color: [0.0, 0.0, 0.0, 1.0],
2389        edge_size: 1.0,
2390        flags: PmxParsedMaterialFlags {
2391            double_sided: true,
2392            ground_shadow: true,
2393            self_shadow_map: true,
2394            self_shadow: true,
2395            edge: false,
2396            vertex_color: false,
2397            point_draw: false,
2398            line_draw: false,
2399        },
2400        face_count,
2401    }
2402}
2403
2404fn default_pmx_parts_root_bone() -> PmxParsedBone {
2405    PmxParsedBone {
2406        name: "root".to_owned(),
2407        english_name: "root".to_owned(),
2408        parent_index: -1,
2409        layer: 0,
2410        position: [0.0, 0.0, 0.0],
2411        tail_index: -1,
2412        tail_position: Some([0.0, 1.0, 0.0]),
2413        flags: PmxParsedBoneFlags {
2414            indexed_tail: false,
2415            rotatable: true,
2416            translatable: true,
2417            visible: true,
2418            enabled: true,
2419            ik: false,
2420            append_local: false,
2421            append_rotate: false,
2422            append_translate: false,
2423            fixed_axis: false,
2424            local_axis: false,
2425            transform_after_physics: false,
2426            external_parent_transform: false,
2427        },
2428        append_transform: None,
2429        fixed_axis: None,
2430        local_axis: None,
2431        external_parent_key: None,
2432        ik: None,
2433    }
2434}
2435
2436pub fn parse_pmx_model(data: &[u8]) -> Result<PmxParsedModel, ImportError> {
2437    let (header, pos) = read_header(data)?;
2438    let mut r = Reader { data, pos };
2439    let name = r.read_string(header.encoding)?;
2440    let english_name = r.read_string(header.encoding)?;
2441    let comment = r.read_string(header.encoding)?;
2442    let english_comment = r.read_string(header.encoding)?;
2443
2444    let vertex_count = read_section_count_with_min_record(&mut r, 12 + 12 + 8 + 1 + 4)?;
2445    let geometry = read_parsed_geometry(&mut r, &header, vertex_count)?;
2446    let index_count = geometry.indices.len();
2447    let textures = read_parsed_textures(&mut r, header.encoding)?;
2448    let materials = read_parsed_materials(&mut r, &header, &textures)?;
2449    let material_groups = build_material_groups(&materials);
2450    let bone_count = read_section_count_with_min_record(&mut r, 4 + 4 + 12)?;
2451    let skeleton = PmxParsedSkeleton {
2452        bones: read_parsed_bones(&mut r, &header, bone_count)?,
2453    };
2454    let morphs = read_parsed_morphs(&mut r, &header)?;
2455    let display_frames = read_parsed_display_frames(&mut r, &header)?;
2456    let rigid_bodies = if r.remaining() >= 4 {
2457        read_parsed_rigid_bodies(&mut r, &header)?
2458    } else {
2459        Vec::new()
2460    };
2461    let joints = if r.remaining() >= 4 {
2462        read_parsed_joints(&mut r, &header)?
2463    } else {
2464        Vec::new()
2465    };
2466    let soft_bodies = if header.version >= 2.05 && r.remaining() >= 4 {
2467        read_parsed_soft_bodies(&mut r, &header)?
2468    } else {
2469        Vec::new()
2470    };
2471
2472    let mut geometry = geometry;
2473    geometry.material_groups = material_groups;
2474    let mut diagnostics = Vec::new();
2475    if !soft_bodies.is_empty() {
2476        diagnostics.push(PmxParserDiagnostic {
2477            level: "warning".to_owned(),
2478            code: "PMX_SOFT_BODY_UNSUPPORTED".to_owned(),
2479            message: format!(
2480                "{} PMX soft bodies are parsed only as header records by mmd-anim.",
2481                soft_bodies.len()
2482            ),
2483        });
2484    }
2485    if r.remaining() > 0 {
2486        diagnostics.push(PmxParserDiagnostic {
2487            level: "warning".to_owned(),
2488            code: "PMX_TRAILING_DATA_UNPARSED".to_owned(),
2489            message: format!("{} trailing PMX bytes were left unparsed.", r.remaining()),
2490        });
2491    }
2492
2493    Ok(PmxParsedModel {
2494        metadata: PmxParsedMetadata {
2495            format: "pmx".to_owned(),
2496            version: header.version,
2497            encoding: match header.encoding {
2498                TextEncoding::Utf8 => "utf-8".to_owned(),
2499                TextEncoding::Utf16Le => "utf-16-le".to_owned(),
2500            },
2501            name,
2502            english_name,
2503            comment,
2504            english_comment,
2505            counts: PmxParsedCounts {
2506                vertices: vertex_count,
2507                faces: index_count / 3,
2508                materials: materials.len(),
2509                bones: skeleton.bones.len(),
2510                morphs: morphs.len(),
2511                display_frames: display_frames.len(),
2512                rigid_bodies: rigid_bodies.len(),
2513                joints: joints.len(),
2514                soft_bodies: soft_bodies.len(),
2515            },
2516            index_sizes: PmxParsedIndexSizes {
2517                vertex: header.vertex_index_size,
2518                texture: header.texture_index_size,
2519                material: header.material_index_size,
2520                bone: header.bone_index_size,
2521                morph: header.morph_index_size,
2522                rigid_body: header.rigidbody_index_size,
2523            },
2524            additional_uv_count: header.extra_uv_count,
2525        },
2526        geometry,
2527        materials,
2528        skeleton,
2529        morphs,
2530        display_frames,
2531        rigid_bodies,
2532        joints,
2533        soft_bodies,
2534        diagnostics,
2535    })
2536}
2537
2538pub fn export_pmx_model(model: &PmxParsedModel) -> Vec<u8> {
2539    let encoding = pmx_text_encoding(&model.metadata);
2540    let mut out = Vec::new();
2541    out.extend_from_slice(&PMX_MAGIC);
2542    write_f32(&mut out, model.metadata.version);
2543    out.push(8);
2544    out.push(encoding as u8);
2545    out.push(model.metadata.additional_uv_count);
2546    out.push(model.metadata.index_sizes.vertex);
2547    out.push(model.metadata.index_sizes.texture);
2548    out.push(model.metadata.index_sizes.material);
2549    out.push(model.metadata.index_sizes.bone);
2550    out.push(model.metadata.index_sizes.morph);
2551    out.push(model.metadata.index_sizes.rigid_body);
2552
2553    write_pmx_string(&mut out, &model.metadata.name, encoding);
2554    write_pmx_string(&mut out, &model.metadata.english_name, encoding);
2555    write_pmx_string(&mut out, &model.metadata.comment, encoding);
2556    write_pmx_string(&mut out, &model.metadata.english_comment, encoding);
2557
2558    let vertex_count = model.geometry.positions.len() / 3;
2559    write_i32(&mut out, vertex_count as i32);
2560    for index in 0..vertex_count {
2561        write_f32_slice(
2562            &mut out,
2563            &model.geometry.positions[index * 3..index * 3 + 3],
2564        );
2565        write_f32_slice(&mut out, &model.geometry.normals[index * 3..index * 3 + 3]);
2566        write_f32_slice(&mut out, &model.geometry.uvs[index * 2..index * 2 + 2]);
2567        for uv in &model.geometry.additional_uvs {
2568            write_f32_slice(&mut out, &uv[index * 4..index * 4 + 4]);
2569        }
2570        out.push(2); // BDEF4 preserves the parsed skin index/weight arrays.
2571        for slot in 0..4 {
2572            write_sized_index(
2573                &mut out,
2574                model
2575                    .geometry
2576                    .skin_indices
2577                    .get(index * 4 + slot)
2578                    .copied()
2579                    .unwrap_or(0) as i32,
2580                model.metadata.index_sizes.bone,
2581            );
2582        }
2583        for slot in 0..4 {
2584            write_f32(
2585                &mut out,
2586                model
2587                    .geometry
2588                    .skin_weights
2589                    .get(index * 4 + slot)
2590                    .copied()
2591                    .unwrap_or(if slot == 0 { 1.0 } else { 0.0 }),
2592            );
2593        }
2594        write_f32(
2595            &mut out,
2596            model.geometry.edge_scale.get(index).copied().unwrap_or(1.0),
2597        );
2598    }
2599
2600    write_i32(&mut out, model.geometry.indices.len() as i32);
2601    for &index in &model.geometry.indices {
2602        write_vertex_index(&mut out, index, model.metadata.index_sizes.vertex);
2603    }
2604
2605    let textures = collect_pmx_textures(&model.materials);
2606    write_i32(&mut out, textures.len() as i32);
2607    for texture in &textures {
2608        write_pmx_string(&mut out, texture, encoding);
2609    }
2610
2611    write_i32(&mut out, model.materials.len() as i32);
2612    for material in &model.materials {
2613        write_pmx_string(&mut out, &material.name, encoding);
2614        write_pmx_string(&mut out, &material.english_name, encoding);
2615        write_f32_slice(&mut out, &material.diffuse);
2616        write_f32_slice(&mut out, &material.specular);
2617        write_f32(&mut out, material.specular_power);
2618        write_f32_slice(&mut out, &material.ambient);
2619        out.push(material_flag_bits(&material.flags));
2620        write_f32_slice(&mut out, &material.edge_color);
2621        write_f32(&mut out, material.edge_size);
2622        write_sized_index(
2623            &mut out,
2624            texture_index(&textures, &material.texture_path),
2625            model.metadata.index_sizes.texture,
2626        );
2627        write_sized_index(
2628            &mut out,
2629            texture_index(&textures, &material.sphere_texture_path),
2630            model.metadata.index_sizes.texture,
2631        );
2632        out.push(match material.sphere_mode.as_str() {
2633            "multiply" => 1,
2634            "add" => 2,
2635            "subTexture" => 3,
2636            _ => 0,
2637        });
2638        if let Some(shared) = material.shared_toon_index {
2639            out.push(1);
2640            out.push(shared);
2641        } else {
2642            out.push(0);
2643            write_sized_index(
2644                &mut out,
2645                texture_index(&textures, &material.toon_texture_path),
2646                model.metadata.index_sizes.texture,
2647            );
2648        }
2649        write_pmx_string(&mut out, "", encoding);
2650        write_i32(&mut out, material.face_count.saturating_mul(3));
2651    }
2652
2653    write_i32(&mut out, model.skeleton.bones.len() as i32);
2654    for bone in &model.skeleton.bones {
2655        write_pmx_string(&mut out, &bone.name, encoding);
2656        write_pmx_string(&mut out, &bone.english_name, encoding);
2657        write_f32_slice(&mut out, &bone.position);
2658        write_sized_index(&mut out, bone.parent_index, model.metadata.index_sizes.bone);
2659        write_i32(&mut out, bone.layer);
2660        let bits = bone_flag_bits(&bone.flags);
2661        write_u16(&mut out, bits);
2662        if bone.flags.indexed_tail {
2663            write_sized_index(&mut out, bone.tail_index, model.metadata.index_sizes.bone);
2664        } else {
2665            write_f32_slice(&mut out, &bone.tail_position.unwrap_or([0.0; 3]));
2666        }
2667        if bone.flags.append_rotate || bone.flags.append_translate {
2668            let append = bone.append_transform.as_ref();
2669            write_sized_index(
2670                &mut out,
2671                append.map(|value| value.parent_index).unwrap_or(-1),
2672                model.metadata.index_sizes.bone,
2673            );
2674            write_f32(&mut out, append.map(|value| value.weight).unwrap_or(0.0));
2675        }
2676        if bone.flags.fixed_axis {
2677            write_f32_slice(&mut out, &bone.fixed_axis.unwrap_or([0.0; 3]));
2678        }
2679        if bone.flags.local_axis {
2680            if let Some(axis) = &bone.local_axis {
2681                write_f32_slice(&mut out, &axis.x);
2682                write_f32_slice(&mut out, &axis.z);
2683            } else {
2684                write_f32_slice(&mut out, &[1.0, 0.0, 0.0]);
2685                write_f32_slice(&mut out, &[0.0, 0.0, 1.0]);
2686            }
2687        }
2688        if bone.flags.external_parent_transform {
2689            write_i32(&mut out, bone.external_parent_key.unwrap_or(0));
2690        }
2691        if bone.flags.ik {
2692            if let Some(ik) = &bone.ik {
2693                write_sized_index(&mut out, ik.target_index, model.metadata.index_sizes.bone);
2694                write_i32(&mut out, ik.loop_count);
2695                write_f32(&mut out, ik.limit_angle);
2696                write_i32(&mut out, ik.links.len() as i32);
2697                for link in &ik.links {
2698                    write_sized_index(&mut out, link.bone_index, model.metadata.index_sizes.bone);
2699                    out.push(u8::from(link.limits.is_some()));
2700                    if let Some(limits) = &link.limits {
2701                        write_f32_slice(&mut out, &limits.lower);
2702                        write_f32_slice(&mut out, &limits.upper);
2703                    }
2704                }
2705            } else {
2706                write_sized_index(&mut out, -1, model.metadata.index_sizes.bone);
2707                write_i32(&mut out, 0);
2708                write_f32(&mut out, 0.0);
2709                write_i32(&mut out, 0);
2710            }
2711        }
2712    }
2713
2714    write_i32(&mut out, model.morphs.len() as i32);
2715    for morph in &model.morphs {
2716        write_pmx_string(&mut out, &morph.name, encoding);
2717        write_pmx_string(&mut out, &morph.english_name, encoding);
2718        out.push(0);
2719        let morph_type = morph_type_byte(morph);
2720        out.push(morph_type);
2721        write_i32(&mut out, morph_offset_count(morph, morph_type) as i32);
2722        match morph_type {
2723            0 => write_group_morph_offsets(&mut out, &morph.group_offsets, model),
2724            1 => {
2725                for offset in &morph.vertex_offsets {
2726                    write_vertex_index(
2727                        &mut out,
2728                        offset.vertex_index,
2729                        model.metadata.index_sizes.vertex,
2730                    );
2731                    write_f32_slice(&mut out, &offset.position);
2732                }
2733            }
2734            2 => {
2735                for offset in &morph.bone_offsets {
2736                    write_sized_index(&mut out, offset.bone_index, model.metadata.index_sizes.bone);
2737                    write_f32_slice(&mut out, &offset.translation);
2738                    write_f32_slice(&mut out, &offset.rotation);
2739                }
2740            }
2741            3 => write_uv_morph_offsets(&mut out, &morph.uv_offsets, model),
2742            4..=7 => {
2743                for offset in &morph.additional_uv_offsets {
2744                    write_vertex_index(
2745                        &mut out,
2746                        offset.vertex_index,
2747                        model.metadata.index_sizes.vertex,
2748                    );
2749                    write_f32_slice(&mut out, &offset.uv);
2750                }
2751            }
2752            8 => {
2753                for offset in &morph.material_offsets {
2754                    write_sized_index(
2755                        &mut out,
2756                        offset.material_index,
2757                        model.metadata.index_sizes.material,
2758                    );
2759                    out.push(if offset.operation == "multiply" { 0 } else { 1 });
2760                    write_f32_slice(&mut out, &offset.diffuse);
2761                    write_f32_slice(&mut out, &offset.specular);
2762                    write_f32(&mut out, offset.specular_power);
2763                    write_f32_slice(&mut out, &offset.ambient);
2764                    write_f32_slice(&mut out, &offset.edge_color);
2765                    write_f32(&mut out, offset.edge_size);
2766                    write_f32_slice(&mut out, &offset.texture_factor);
2767                    write_f32_slice(&mut out, &offset.sphere_texture_factor);
2768                    write_f32_slice(&mut out, &offset.toon_texture_factor);
2769                }
2770            }
2771            9 => write_group_morph_offsets(&mut out, &morph.flip_offsets, model),
2772            10 => {
2773                for offset in &morph.impulse_offsets {
2774                    write_sized_index(
2775                        &mut out,
2776                        offset.rigid_body_index,
2777                        model.metadata.index_sizes.rigid_body,
2778                    );
2779                    out.push(u8::from(offset.local));
2780                    write_f32_slice(&mut out, &offset.velocity);
2781                    write_f32_slice(&mut out, &offset.torque);
2782                }
2783            }
2784            _ => {}
2785        }
2786    }
2787
2788    write_i32(&mut out, model.display_frames.len() as i32);
2789    for frame in &model.display_frames {
2790        write_pmx_string(&mut out, &frame.name, encoding);
2791        write_pmx_string(&mut out, &frame.english_name, encoding);
2792        out.push(u8::from(frame.special));
2793        write_i32(&mut out, frame.frames.len() as i32);
2794        for item in &frame.frames {
2795            if item.kind == "morph" {
2796                out.push(1);
2797                write_sized_index(&mut out, item.index, model.metadata.index_sizes.morph);
2798            } else {
2799                out.push(0);
2800                write_sized_index(&mut out, item.index, model.metadata.index_sizes.bone);
2801            }
2802        }
2803    }
2804
2805    write_i32(&mut out, model.rigid_bodies.len() as i32);
2806    for body in &model.rigid_bodies {
2807        write_pmx_string(&mut out, &body.name, encoding);
2808        write_pmx_string(&mut out, &body.english_name, encoding);
2809        write_sized_index(&mut out, body.bone_index, model.metadata.index_sizes.bone);
2810        out.push(body.group);
2811        write_u16(&mut out, body.mask);
2812        out.push(match body.shape.as_str() {
2813            "box" => 1,
2814            "capsule" => 2,
2815            _ => 0,
2816        });
2817        write_f32_slice(&mut out, &body.size);
2818        write_f32_slice(&mut out, &body.position);
2819        write_f32_slice(&mut out, &body.rotation);
2820        write_f32(&mut out, body.mass);
2821        write_f32(&mut out, body.linear_damping);
2822        write_f32(&mut out, body.angular_damping);
2823        write_f32(&mut out, body.restitution);
2824        write_f32(&mut out, body.friction);
2825        out.push(match body.mode.as_str() {
2826            "dynamic" => 1,
2827            "dynamicBone" => 2,
2828            _ => 0,
2829        });
2830    }
2831
2832    write_i32(&mut out, model.joints.len() as i32);
2833    for joint in &model.joints {
2834        write_pmx_string(&mut out, &joint.name, encoding);
2835        write_pmx_string(&mut out, &joint.english_name, encoding);
2836        out.push(match joint.kind.as_str() {
2837            "generic6dof" => 1,
2838            "point2point" => 2,
2839            "coneTwist" => 3,
2840            "slider" => 4,
2841            "hinge" => 5,
2842            _ => 0,
2843        });
2844        write_sized_index(
2845            &mut out,
2846            joint.rigid_body_index_a,
2847            model.metadata.index_sizes.rigid_body,
2848        );
2849        write_sized_index(
2850            &mut out,
2851            joint.rigid_body_index_b,
2852            model.metadata.index_sizes.rigid_body,
2853        );
2854        write_f32_slice(&mut out, &joint.position);
2855        write_f32_slice(&mut out, &joint.rotation);
2856        write_f32_slice(&mut out, &joint.translation_lower_limit);
2857        write_f32_slice(&mut out, &joint.translation_upper_limit);
2858        write_f32_slice(&mut out, &joint.rotation_lower_limit);
2859        write_f32_slice(&mut out, &joint.rotation_upper_limit);
2860        write_f32_slice(&mut out, &joint.spring_translation_factor);
2861        write_f32_slice(&mut out, &joint.spring_rotation_factor);
2862    }
2863
2864    if model.metadata.version >= 2.05 {
2865        write_i32(&mut out, model.soft_bodies.len() as i32);
2866        for soft_body in &model.soft_bodies {
2867            write_pmx_string(&mut out, &soft_body.name, encoding);
2868            write_pmx_string(&mut out, &soft_body.english_name, encoding);
2869            out.push(if soft_body.kind == "rope" { 1 } else { 0 });
2870            write_sized_index(
2871                &mut out,
2872                soft_body.material_index,
2873                model.metadata.index_sizes.material,
2874            );
2875            out.push(soft_body.collision_group);
2876            write_u16(&mut out, soft_body.collision_mask);
2877            out.push(soft_body.flags);
2878            write_i32(&mut out, soft_body.bending_constraints_distance);
2879            write_i32(&mut out, soft_body.cluster_count);
2880            write_f32(&mut out, soft_body.total_mass);
2881            write_f32(&mut out, soft_body.collision_margin);
2882            out.extend_from_slice(&[0u8; 4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4]);
2883            write_i32(&mut out, 0);
2884            write_i32(&mut out, 0);
2885        }
2886    }
2887
2888    out
2889}
2890
2891fn read_section_count(r: &mut Reader<'_>) -> Result<usize, ImportError> {
2892    let count = r.read_i32_le()?;
2893    if count < 0 {
2894        return Err(ImportError::SectionOverflow);
2895    }
2896    Ok(count as usize)
2897}
2898
2899fn read_section_count_with_min_record(
2900    r: &mut Reader<'_>,
2901    min_record_size: usize,
2902) -> Result<usize, ImportError> {
2903    let count = read_section_count(r)?;
2904    r.require_record_bytes(count, min_record_size)?;
2905    Ok(count)
2906}
2907
2908fn read_parsed_geometry(
2909    r: &mut Reader<'_>,
2910    header: &PmxHeader,
2911    vertex_count: usize,
2912) -> Result<PmxParsedGeometry, ImportError> {
2913    r.require_record_bytes(vertex_count, 12 + 12 + 8 + 1 + 4)?;
2914    let position_capacity = vertex_count
2915        .checked_mul(3)
2916        .ok_or(ImportError::SectionOverflow)?;
2917    let uv_capacity = vertex_count
2918        .checked_mul(2)
2919        .ok_or(ImportError::SectionOverflow)?;
2920    let skin_capacity = vertex_count
2921        .checked_mul(4)
2922        .ok_or(ImportError::SectionOverflow)?;
2923
2924    let mut positions = Vec::with_capacity(position_capacity);
2925    let mut normals = Vec::with_capacity(position_capacity);
2926    let mut uvs = Vec::with_capacity(uv_capacity);
2927    let mut additional_uvs =
2928        vec![Vec::with_capacity(skin_capacity); header.extra_uv_count as usize];
2929    let mut skin_indices = Vec::with_capacity(skin_capacity);
2930    let mut skin_weights = Vec::with_capacity(skin_capacity);
2931    let mut edge_scale = Vec::with_capacity(vertex_count);
2932    let mut sdef_enabled = vec![0.0f32; vertex_count];
2933    let mut sdef_c = vec![0.0f32; vertex_count * 3];
2934    let mut sdef_r0 = vec![0.0f32; vertex_count * 3];
2935    let mut sdef_r1 = vec![0.0f32; vertex_count * 3];
2936    let mut sdef_rw0 = vec![0.0f32; vertex_count * 3];
2937    let mut sdef_rw1 = vec![0.0f32; vertex_count * 3];
2938    let mut qdef_enabled = vec![0.0f32; vertex_count];
2939
2940    for vertex_i in 0..vertex_count {
2941        positions.extend_from_slice(&r.read_vec3_array()?);
2942        normals.extend_from_slice(&r.read_vec3_array()?);
2943        uvs.extend_from_slice(&r.read_vec2_array()?);
2944        for uv in additional_uvs.iter_mut() {
2945            uv.extend_from_slice(&r.read_vec4_array()?);
2946        }
2947        let weight_type = r.read_u8()?;
2948        let mut indices = [0u32; 4];
2949        let mut weights = [0.0f32; 4];
2950        match weight_type {
2951            0 => {
2952                indices[0] =
2953                    normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
2954                weights[0] = 1.0;
2955            }
2956            1 | 3 => {
2957                indices[0] =
2958                    normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
2959                indices[1] =
2960                    normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
2961                let w = r.read_f32_le()?;
2962                weights[0] = w;
2963                weights[1] = 1.0 - w;
2964                if weight_type == 3 {
2965                    let c = r.read_vec3_array()?;
2966                    let r0 = r.read_vec3_array()?;
2967                    let r1 = r.read_vec3_array()?;
2968                    let w1 = 1.0 - w;
2969                    let offset = vertex_i * 3;
2970                    sdef_enabled[vertex_i] = 1.0;
2971                    sdef_c[offset..offset + 3].copy_from_slice(&c);
2972                    sdef_r0[offset..offset + 3].copy_from_slice(&r0);
2973                    sdef_r1[offset..offset + 3].copy_from_slice(&r1);
2974                    for k in 0..3 {
2975                        let rw = r0[k] * w + r1[k] * w1;
2976                        sdef_rw0[offset + k] = (c[k] * 2.0 + r0[k] - rw) * 0.5;
2977                        sdef_rw1[offset + k] = (c[k] * 2.0 + r1[k] - rw) * 0.5;
2978                    }
2979                }
2980            }
2981            2 => {
2982                for index in &mut indices {
2983                    *index =
2984                        normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
2985                }
2986                for weight in &mut weights {
2987                    *weight = r.read_f32_le()?;
2988                }
2989            }
2990            4 => {
2991                for index in &mut indices {
2992                    *index =
2993                        normalize_nonnegative_index(r.read_sized_index(header.bone_index_size)?);
2994                }
2995                for weight in &mut weights {
2996                    *weight = r.read_f32_le()?;
2997                }
2998                qdef_enabled[vertex_i] = 1.0;
2999            }
3000            _ => return Err(ImportError::SectionOverflow),
3001        }
3002        skin_indices.extend_from_slice(&indices);
3003        skin_weights.extend_from_slice(&weights);
3004        edge_scale.push(r.read_f32_le()?);
3005    }
3006
3007    let index_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
3008    let mut indices = Vec::with_capacity(index_count);
3009    for _ in 0..index_count {
3010        indices.push(r.read_vertex_index(header.vertex_index_size)?);
3011    }
3012
3013    Ok(PmxParsedGeometry {
3014        positions,
3015        normals,
3016        uvs,
3017        additional_uvs,
3018        indices,
3019        skin_indices,
3020        skin_weights,
3021        edge_scale,
3022        material_groups: Vec::new(),
3023        sdef: PmxParsedSdef {
3024            enabled: sdef_enabled,
3025            c: sdef_c,
3026            r0: sdef_r0,
3027            r1: sdef_r1,
3028            rw0: sdef_rw0,
3029            rw1: sdef_rw1,
3030        },
3031        qdef: PmxParsedQdef {
3032            enabled: qdef_enabled,
3033        },
3034    })
3035}
3036
3037fn normalize_nonnegative_index(index: i32) -> u32 {
3038    if index < 0 { 0 } else { index as u32 }
3039}
3040
3041fn read_parsed_textures(
3042    r: &mut Reader<'_>,
3043    encoding: TextEncoding,
3044) -> Result<Vec<String>, ImportError> {
3045    let count = read_section_count_with_min_record(r, 4)?;
3046    let mut textures = Vec::with_capacity(count);
3047    for _ in 0..count {
3048        textures.push(r.read_string(encoding)?);
3049    }
3050    Ok(textures)
3051}
3052
3053fn read_parsed_materials(
3054    r: &mut Reader<'_>,
3055    header: &PmxHeader,
3056    textures: &[String],
3057) -> Result<Vec<PmxParsedMaterial>, ImportError> {
3058    let count = read_section_count_with_min_record(r, 4 + 4 + 16 + 16 + 12 + 1 + 16 + 4)?;
3059    let mut materials = Vec::with_capacity(count);
3060    for _ in 0..count {
3061        let name = r.read_string(header.encoding)?;
3062        let english_name = r.read_string(header.encoding)?;
3063        let diffuse = r.read_vec4_array()?;
3064        let specular = r.read_vec3_array()?;
3065        let specular_power = r.read_f32_le()?;
3066        let ambient = r.read_vec3_array()?;
3067        let flag_bits = r.read_u8()?;
3068        let edge_color = r.read_vec4_array()?;
3069        let edge_size = r.read_f32_le()?;
3070        let texture_index = r.read_sized_index(header.texture_index_size)?;
3071        let sphere_texture_index = r.read_sized_index(header.texture_index_size)?;
3072        let sphere_mode_raw = r.read_u8()?;
3073        let toon_flag = r.read_u8()?;
3074        let (toon_texture_path, shared_toon_index) = if toon_flag == 0 {
3075            let toon_texture_index = r.read_sized_index(header.texture_index_size)?;
3076            (texture_path(textures, toon_texture_index), None)
3077        } else {
3078            (String::new(), Some(r.read_u8()?))
3079        };
3080        let _memo = r.read_string(header.encoding)?;
3081        let face_count = r.read_i32_le()? / 3;
3082        materials.push(PmxParsedMaterial {
3083            name,
3084            english_name,
3085            texture_path: texture_path(textures, texture_index),
3086            sphere_texture_path: texture_path(textures, sphere_texture_index),
3087            sphere_mode: match sphere_mode_raw {
3088                1 => "multiply",
3089                2 => "add",
3090                3 => "subTexture",
3091                _ => "none",
3092            }
3093            .to_owned(),
3094            toon_texture_path,
3095            shared_toon_index,
3096            diffuse,
3097            specular,
3098            specular_power,
3099            ambient,
3100            edge_color,
3101            edge_size,
3102            flags: PmxParsedMaterialFlags {
3103                double_sided: flag_bits & 0x01 != 0,
3104                ground_shadow: flag_bits & 0x02 != 0,
3105                self_shadow_map: flag_bits & 0x04 != 0,
3106                self_shadow: flag_bits & 0x08 != 0,
3107                edge: flag_bits & 0x10 != 0,
3108                vertex_color: flag_bits & 0x20 != 0,
3109                point_draw: flag_bits & 0x40 != 0,
3110                line_draw: flag_bits & 0x80 != 0,
3111            },
3112            face_count,
3113        });
3114    }
3115    Ok(materials)
3116}
3117
3118fn texture_path(textures: &[String], index: i32) -> String {
3119    if index < 0 {
3120        String::new()
3121    } else {
3122        textures.get(index as usize).cloned().unwrap_or_default()
3123    }
3124}
3125
3126fn build_material_groups(materials: &[PmxParsedMaterial]) -> Vec<PmxParsedMaterialGroup> {
3127    let mut start = 0usize;
3128    materials
3129        .iter()
3130        .enumerate()
3131        .map(|(material_index, material)| {
3132            let count = (material.face_count.max(0) as usize) * 3;
3133            let group = PmxParsedMaterialGroup {
3134                start,
3135                count,
3136                material_index,
3137            };
3138            start += count;
3139            group
3140        })
3141        .collect()
3142}
3143
3144fn pmx_text_encoding(metadata: &PmxParsedMetadata) -> TextEncoding {
3145    match metadata.encoding.as_str() {
3146        "utf-16-le" => TextEncoding::Utf16Le,
3147        _ => TextEncoding::Utf8,
3148    }
3149}
3150
3151fn write_pmx_string(out: &mut Vec<u8>, text: &str, encoding: TextEncoding) {
3152    match encoding {
3153        TextEncoding::Utf8 => {
3154            write_i32(out, text.len() as i32);
3155            out.extend_from_slice(text.as_bytes());
3156        }
3157        TextEncoding::Utf16Le => {
3158            let bytes = text
3159                .encode_utf16()
3160                .flat_map(u16::to_le_bytes)
3161                .collect::<Vec<_>>();
3162            write_i32(out, bytes.len() as i32);
3163            out.extend_from_slice(&bytes);
3164        }
3165    }
3166}
3167
3168fn write_f32_slice(out: &mut Vec<u8>, values: &[f32]) {
3169    for &value in values {
3170        write_f32(out, value);
3171    }
3172}
3173
3174fn write_f32(out: &mut Vec<u8>, value: f32) {
3175    out.extend_from_slice(&value.to_le_bytes());
3176}
3177
3178fn write_i32(out: &mut Vec<u8>, value: i32) {
3179    out.extend_from_slice(&value.to_le_bytes());
3180}
3181
3182fn write_u16(out: &mut Vec<u8>, value: u16) {
3183    out.extend_from_slice(&value.to_le_bytes());
3184}
3185
3186fn write_sized_index(out: &mut Vec<u8>, value: i32, size: u8) {
3187    match size {
3188        1 => out.push(value as i8 as u8),
3189        2 => out.extend_from_slice(&(value as i16).to_le_bytes()),
3190        4 => out.extend_from_slice(&value.to_le_bytes()),
3191        _ => {}
3192    }
3193}
3194
3195fn write_vertex_index(out: &mut Vec<u8>, value: u32, size: u8) {
3196    match size {
3197        1 => out.push(value as u8),
3198        2 => out.extend_from_slice(&(value as u16).to_le_bytes()),
3199        4 => out.extend_from_slice(&(value as i32).to_le_bytes()),
3200        _ => {}
3201    }
3202}
3203
3204fn material_flag_bits(flags: &PmxParsedMaterialFlags) -> u8 {
3205    u8::from(flags.double_sided)
3206        | (u8::from(flags.ground_shadow) << 1)
3207        | (u8::from(flags.self_shadow_map) << 2)
3208        | (u8::from(flags.self_shadow) << 3)
3209        | (u8::from(flags.edge) << 4)
3210        | (u8::from(flags.vertex_color) << 5)
3211        | (u8::from(flags.point_draw) << 6)
3212        | (u8::from(flags.line_draw) << 7)
3213}
3214
3215fn bone_flag_bits(flags: &PmxParsedBoneFlags) -> u16 {
3216    u16::from(flags.indexed_tail)
3217        | (u16::from(flags.rotatable) << 1)
3218        | (u16::from(flags.translatable) << 2)
3219        | (u16::from(flags.visible) << 3)
3220        | (u16::from(flags.enabled) << 4)
3221        | if flags.ik { BONE_FLAG_IK } else { 0 }
3222        | if flags.append_local {
3223            BONE_FLAG_LOCAL_APPEND
3224        } else {
3225            0
3226        }
3227        | if flags.append_rotate {
3228            BONE_FLAG_APPEND_ROTATE
3229        } else {
3230            0
3231        }
3232        | if flags.append_translate {
3233            BONE_FLAG_APPEND_TRANSLATE
3234        } else {
3235            0
3236        }
3237        | if flags.fixed_axis {
3238            BONE_FLAG_FIXED_AXIS
3239        } else {
3240            0
3241        }
3242        | if flags.local_axis {
3243            BONE_FLAG_LOCAL_AXIS
3244        } else {
3245            0
3246        }
3247        | (u16::from(flags.transform_after_physics) << 12)
3248        | if flags.external_parent_transform {
3249            BONE_FLAG_EXTERNAL_PARENT
3250        } else {
3251            0
3252        }
3253}
3254
3255fn collect_pmx_textures(materials: &[PmxParsedMaterial]) -> Vec<String> {
3256    let mut textures = Vec::new();
3257    for texture in materials.iter().flat_map(|material| {
3258        [
3259            material.texture_path.as_str(),
3260            material.sphere_texture_path.as_str(),
3261            material.toon_texture_path.as_str(),
3262        ]
3263    }) {
3264        if !texture.is_empty() && !textures.iter().any(|existing| existing == texture) {
3265            textures.push(texture.to_owned());
3266        }
3267    }
3268    textures
3269}
3270
3271fn texture_index(textures: &[String], texture: &str) -> i32 {
3272    if texture.is_empty() {
3273        -1
3274    } else {
3275        textures
3276            .iter()
3277            .position(|candidate| candidate == texture)
3278            .map(|index| index as i32)
3279            .unwrap_or(-1)
3280    }
3281}
3282
3283fn morph_type_byte(morph: &PmxParsedMorph) -> u8 {
3284    match morph.kind.as_str() {
3285        "group" => 0,
3286        "vertex" => 1,
3287        "bone" => 2,
3288        "uv" => 3,
3289        "additionalUv" => morph
3290            .additional_uv_offsets
3291            .first()
3292            .map(|offset| offset.uv_index.saturating_add(4).min(7))
3293            .unwrap_or(4),
3294        "material" => 8,
3295        "flip" => 9,
3296        "impulse" => 10,
3297        _ => 0,
3298    }
3299}
3300
3301fn morph_offset_count(morph: &PmxParsedMorph, morph_type: u8) -> usize {
3302    match morph_type {
3303        0 => morph.group_offsets.len(),
3304        1 => morph.vertex_offsets.len(),
3305        2 => morph.bone_offsets.len(),
3306        3 => morph.uv_offsets.len(),
3307        4..=7 => morph.additional_uv_offsets.len(),
3308        8 => morph.material_offsets.len(),
3309        9 => morph.flip_offsets.len(),
3310        10 => morph.impulse_offsets.len(),
3311        _ => 0,
3312    }
3313}
3314
3315fn write_group_morph_offsets(
3316    out: &mut Vec<u8>,
3317    offsets: &[PmxParsedGroupMorphOffset],
3318    model: &PmxParsedModel,
3319) {
3320    for offset in offsets {
3321        write_sized_index(out, offset.morph_index, model.metadata.index_sizes.morph);
3322        write_f32(out, offset.weight);
3323    }
3324}
3325
3326fn write_uv_morph_offsets(
3327    out: &mut Vec<u8>,
3328    offsets: &[PmxParsedUvMorphOffset],
3329    model: &PmxParsedModel,
3330) {
3331    for offset in offsets {
3332        write_vertex_index(out, offset.vertex_index, model.metadata.index_sizes.vertex);
3333        write_f32_slice(out, &offset.uv);
3334    }
3335}
3336
3337fn read_parsed_bones(
3338    r: &mut Reader<'_>,
3339    header: &PmxHeader,
3340    count: usize,
3341) -> Result<Vec<PmxParsedBone>, ImportError> {
3342    r.require_record_bytes(count, 4 + 4 + 12)?;
3343    let mut bones = Vec::with_capacity(count);
3344    for _ in 0..count {
3345        let name = r.read_string(header.encoding)?;
3346        let english_name = r.read_string(header.encoding)?;
3347        let position = r.read_vec3_array()?;
3348        let parent_index = r.read_sized_index(header.bone_index_size)?;
3349        let layer = r.read_i32_le()?;
3350        let bits = r.read_u16_le()?;
3351        let flags = parsed_bone_flags(bits);
3352        let (tail_index, tail_position) = if flags.indexed_tail {
3353            (r.read_sized_index(header.bone_index_size)?, None)
3354        } else {
3355            (-1, Some(r.read_vec3_array()?))
3356        };
3357        let append_transform = if flags.append_rotate || flags.append_translate {
3358            Some(PmxParsedAppendTransform {
3359                parent_index: r.read_sized_index(header.bone_index_size)?,
3360                weight: r.read_f32_le()?,
3361            })
3362        } else {
3363            None
3364        };
3365        let fixed_axis = if flags.fixed_axis {
3366            Some(r.read_vec3_array()?)
3367        } else {
3368            None
3369        };
3370        let local_axis = if flags.local_axis {
3371            Some(PmxParsedLocalAxis {
3372                x: r.read_vec3_array()?,
3373                z: r.read_vec3_array()?,
3374            })
3375        } else {
3376            None
3377        };
3378        let external_parent_key = if flags.external_parent_transform {
3379            Some(r.read_i32_le()?)
3380        } else {
3381            None
3382        };
3383        let ik = if flags.ik {
3384            let target_index = r.read_sized_index(header.bone_index_size)?;
3385            let loop_count = r.read_i32_le()?;
3386            let limit_angle = r.read_f32_le()?;
3387            let link_count =
3388                read_section_count_with_min_record(r, header.bone_index_size as usize + 1)?;
3389            let mut links = Vec::with_capacity(link_count);
3390            for _ in 0..link_count {
3391                let bone_index = r.read_sized_index(header.bone_index_size)?;
3392                let has_limit = r.read_u8()? != 0;
3393                links.push(PmxParsedIkLink {
3394                    bone_index,
3395                    limits: if has_limit {
3396                        Some(PmxParsedIkLimit {
3397                            lower: r.read_vec3_array()?,
3398                            upper: r.read_vec3_array()?,
3399                        })
3400                    } else {
3401                        None
3402                    },
3403                });
3404            }
3405            Some(PmxParsedIk {
3406                target_index,
3407                loop_count,
3408                limit_angle,
3409                links,
3410            })
3411        } else {
3412            None
3413        };
3414        bones.push(PmxParsedBone {
3415            name,
3416            english_name,
3417            parent_index,
3418            layer,
3419            position,
3420            tail_index,
3421            tail_position,
3422            flags,
3423            append_transform,
3424            fixed_axis,
3425            local_axis,
3426            external_parent_key,
3427            ik,
3428        });
3429    }
3430    Ok(bones)
3431}
3432
3433fn parsed_bone_flags(bits: u16) -> PmxParsedBoneFlags {
3434    PmxParsedBoneFlags {
3435        indexed_tail: bits & BONE_FLAG_TAIL_INDEX != 0,
3436        rotatable: bits & 0x0002 != 0,
3437        translatable: bits & 0x0004 != 0,
3438        visible: bits & 0x0008 != 0,
3439        enabled: bits & 0x0010 != 0,
3440        ik: bits & BONE_FLAG_IK != 0,
3441        append_local: bits & BONE_FLAG_LOCAL_APPEND != 0,
3442        append_rotate: bits & BONE_FLAG_APPEND_ROTATE != 0,
3443        append_translate: bits & BONE_FLAG_APPEND_TRANSLATE != 0,
3444        fixed_axis: bits & BONE_FLAG_FIXED_AXIS != 0,
3445        local_axis: bits & BONE_FLAG_LOCAL_AXIS != 0,
3446        transform_after_physics: bits & 0x1000 != 0,
3447        external_parent_transform: bits & BONE_FLAG_EXTERNAL_PARENT != 0,
3448    }
3449}
3450
3451fn read_parsed_morphs(
3452    r: &mut Reader<'_>,
3453    header: &PmxHeader,
3454) -> Result<Vec<PmxParsedMorph>, ImportError> {
3455    let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 1 + 4)?;
3456    let mut morphs = Vec::with_capacity(count);
3457    for _ in 0..count {
3458        let name = r.read_string(header.encoding)?;
3459        let english_name = r.read_string(header.encoding)?;
3460        let _panel = r.read_u8()?;
3461        let morph_type = r.read_u8()?;
3462        let min_offset_size = match morph_type {
3463            0 => header.morph_index_size as usize + 4,
3464            1 => header.vertex_index_size as usize + 12,
3465            2 => header.bone_index_size as usize + 12 + 16,
3466            3..=7 => header.vertex_index_size as usize + 16,
3467            8 => header.material_index_size as usize + 1 + 16 + 12 + 4 + 12 + 16 + 4 + 16 + 16 + 16,
3468            9 => header.morph_index_size as usize + 4,
3469            10 => header.rigidbody_index_size as usize + 1 + 12 + 12,
3470            _ => return Err(ImportError::SectionOverflow),
3471        };
3472        let offset_count = read_section_count_with_min_record(r, min_offset_size)?;
3473        let mut morph = PmxParsedMorph {
3474            name,
3475            english_name,
3476            kind: match morph_type {
3477                0 => "group",
3478                1 => "vertex",
3479                2 => "bone",
3480                3 => "uv",
3481                4..=7 => "additionalUv",
3482                8 => "material",
3483                9 => "flip",
3484                10 => "impulse",
3485                _ => "unknown",
3486            }
3487            .to_owned(),
3488            vertex_offsets: Vec::new(),
3489            group_offsets: Vec::new(),
3490            bone_offsets: Vec::new(),
3491            uv_offsets: Vec::new(),
3492            additional_uv_offsets: Vec::new(),
3493            material_offsets: Vec::new(),
3494            flip_offsets: Vec::new(),
3495            impulse_offsets: Vec::new(),
3496        };
3497        for _ in 0..offset_count {
3498            match morph_type {
3499                0 => morph.group_offsets.push(PmxParsedGroupMorphOffset {
3500                    morph_index: r.read_sized_index(header.morph_index_size)?,
3501                    weight: r.read_f32_le()?,
3502                }),
3503                1 => morph.vertex_offsets.push(PmxParsedVertexMorphOffset {
3504                    vertex_index: r.read_vertex_index(header.vertex_index_size)?,
3505                    position: r.read_vec3_array()?,
3506                }),
3507                2 => morph.bone_offsets.push(PmxParsedBoneMorphOffset {
3508                    bone_index: r.read_sized_index(header.bone_index_size)?,
3509                    translation: r.read_vec3_array()?,
3510                    rotation: r.read_vec4_array()?,
3511                }),
3512                3 => morph.uv_offsets.push(PmxParsedUvMorphOffset {
3513                    vertex_index: r.read_vertex_index(header.vertex_index_size)?,
3514                    uv: r.read_vec4_array()?,
3515                }),
3516                4..=7 => morph
3517                    .additional_uv_offsets
3518                    .push(PmxParsedAdditionalUvMorphOffset {
3519                        vertex_index: r.read_vertex_index(header.vertex_index_size)?,
3520                        uv_index: morph_type - 4,
3521                        uv: r.read_vec4_array()?,
3522                    }),
3523                8 => morph.material_offsets.push(PmxParsedMaterialMorphOffset {
3524                    material_index: r.read_sized_index(header.material_index_size)?,
3525                    operation: if r.read_u8()? == 0 {
3526                        "multiply".to_owned()
3527                    } else {
3528                        "add".to_owned()
3529                    },
3530                    diffuse: r.read_vec4_array()?,
3531                    specular: r.read_vec3_array()?,
3532                    specular_power: r.read_f32_le()?,
3533                    ambient: r.read_vec3_array()?,
3534                    edge_color: r.read_vec4_array()?,
3535                    edge_size: r.read_f32_le()?,
3536                    texture_factor: r.read_vec4_array()?,
3537                    sphere_texture_factor: r.read_vec4_array()?,
3538                    toon_texture_factor: r.read_vec4_array()?,
3539                }),
3540                9 => morph.flip_offsets.push(PmxParsedGroupMorphOffset {
3541                    morph_index: r.read_sized_index(header.morph_index_size)?,
3542                    weight: r.read_f32_le()?,
3543                }),
3544                10 => morph.impulse_offsets.push(PmxParsedImpulseMorphOffset {
3545                    rigid_body_index: r.read_sized_index(header.rigidbody_index_size)?,
3546                    local: r.read_u8()? != 0,
3547                    velocity: r.read_vec3_array()?,
3548                    torque: r.read_vec3_array()?,
3549                }),
3550                _ => return Err(ImportError::SectionOverflow),
3551            }
3552        }
3553        morphs.push(morph);
3554    }
3555    Ok(morphs)
3556}
3557
3558fn read_parsed_display_frames(
3559    r: &mut Reader<'_>,
3560    header: &PmxHeader,
3561) -> Result<Vec<PmxParsedDisplayFrame>, ImportError> {
3562    let count = read_section_count_with_min_record(r, 4 + 4 + 1 + 4)?;
3563    let mut frames = Vec::with_capacity(count);
3564    for _ in 0..count {
3565        let name = r.read_string(header.encoding)?;
3566        let english_name = r.read_string(header.encoding)?;
3567        let special = r.read_u8()? == 1;
3568        let item_count = read_section_count_with_min_record(r, 1)?;
3569        let mut items = Vec::with_capacity(item_count);
3570        for _ in 0..item_count {
3571            let kind = r.read_u8()?;
3572            let index = match kind {
3573                0 => r.read_sized_index(header.bone_index_size)?,
3574                1 => r.read_sized_index(header.morph_index_size)?,
3575                _ => return Err(ImportError::SectionOverflow),
3576            };
3577            items.push(PmxParsedDisplayFrameElement {
3578                kind: if kind == 0 {
3579                    "bone".to_owned()
3580                } else {
3581                    "morph".to_owned()
3582                },
3583                index,
3584            });
3585        }
3586        frames.push(PmxParsedDisplayFrame {
3587            name,
3588            english_name,
3589            special,
3590            frames: items,
3591        });
3592    }
3593    Ok(frames)
3594}
3595
3596fn read_parsed_rigid_bodies(
3597    r: &mut Reader<'_>,
3598    header: &PmxHeader,
3599) -> Result<Vec<PmxParsedRigidBody>, ImportError> {
3600    let count = read_section_count_with_min_record(
3601        r,
3602        4 + 4 + header.bone_index_size as usize + 1 + 2 + 1 + 12 + 12 + 12 + 4 + 4 + 4 + 4 + 4 + 1,
3603    )?;
3604    let mut bodies = Vec::with_capacity(count);
3605    for _ in 0..count {
3606        bodies.push(PmxParsedRigidBody {
3607            name: r.read_string(header.encoding)?,
3608            english_name: r.read_string(header.encoding)?,
3609            bone_index: r.read_sized_index(header.bone_index_size)?,
3610            group: r.read_u8()?,
3611            mask: r.read_u16_le()?,
3612            shape: match r.read_u8()? {
3613                0 => "sphere",
3614                1 => "box",
3615                2 => "capsule",
3616                _ => "unknown",
3617            }
3618            .to_owned(),
3619            size: r.read_vec3_array()?,
3620            position: r.read_vec3_array()?,
3621            rotation: r.read_vec3_array()?,
3622            mass: r.read_f32_le()?,
3623            linear_damping: r.read_f32_le()?,
3624            angular_damping: r.read_f32_le()?,
3625            restitution: r.read_f32_le()?,
3626            friction: r.read_f32_le()?,
3627            mode: match r.read_u8()? {
3628                0 => "static",
3629                1 => "dynamic",
3630                2 => "dynamicBone",
3631                _ => "unknown",
3632            }
3633            .to_owned(),
3634        });
3635    }
3636    Ok(bodies)
3637}
3638
3639fn read_parsed_joints(
3640    r: &mut Reader<'_>,
3641    header: &PmxHeader,
3642) -> Result<Vec<PmxParsedJoint>, ImportError> {
3643    let count = read_section_count_with_min_record(
3644        r,
3645        4 + 4 + 1 + header.rigidbody_index_size as usize * 2 + 12 * 8,
3646    )?;
3647    let mut joints = Vec::with_capacity(count);
3648    for _ in 0..count {
3649        joints.push(PmxParsedJoint {
3650            name: r.read_string(header.encoding)?,
3651            english_name: r.read_string(header.encoding)?,
3652            kind: match r.read_u8()? {
3653                0 => "generic6dofSpring",
3654                1 => "generic6dof",
3655                2 => "point2point",
3656                3 => "coneTwist",
3657                4 => "slider",
3658                5 => "hinge",
3659                _ => "unknown",
3660            }
3661            .to_owned(),
3662            rigid_body_index_a: r.read_sized_index(header.rigidbody_index_size)?,
3663            rigid_body_index_b: r.read_sized_index(header.rigidbody_index_size)?,
3664            position: r.read_vec3_array()?,
3665            rotation: r.read_vec3_array()?,
3666            translation_lower_limit: r.read_vec3_array()?,
3667            translation_upper_limit: r.read_vec3_array()?,
3668            rotation_lower_limit: r.read_vec3_array()?,
3669            rotation_upper_limit: r.read_vec3_array()?,
3670            spring_translation_factor: r.read_vec3_array()?,
3671            spring_rotation_factor: r.read_vec3_array()?,
3672        });
3673    }
3674    Ok(joints)
3675}
3676
3677fn read_parsed_soft_bodies(
3678    r: &mut Reader<'_>,
3679    header: &PmxHeader,
3680) -> Result<Vec<PmxParsedSoftBody>, ImportError> {
3681    let count = read_section_count_with_min_record(
3682        r,
3683        4 + 4
3684            + 1
3685            + header.material_index_size as usize
3686            + 1
3687            + 2
3688            + 1
3689            + 4
3690            + 4
3691            + 4
3692            + 4
3693            + 4
3694            + 12 * 4
3695            + 6 * 4
3696            + 4 * 4
3697            + 3 * 4
3698            + 4
3699            + 4,
3700    )?;
3701    let mut soft_bodies = Vec::with_capacity(count);
3702    for _ in 0..count {
3703        let name = r.read_string(header.encoding)?;
3704        let english_name = r.read_string(header.encoding)?;
3705        let kind = match r.read_u8()? {
3706            0 => "triMesh",
3707            1 => "rope",
3708            _ => "unknown",
3709        }
3710        .to_owned();
3711        let material_index = r.read_sized_index(header.material_index_size)?;
3712        let collision_group = r.read_u8()?;
3713        let collision_mask = r.read_u16_le()?;
3714        let flags = r.read_u8()?;
3715        let bending_constraints_distance = r.read_i32_le()?;
3716        let cluster_count = r.read_i32_le()?;
3717        let total_mass = r.read_f32_le()?;
3718        let collision_margin = r.read_f32_le()?;
3719        r.skip(4 + 12 * 4 + 6 * 4 + 4 * 4 + 3 * 4)?;
3720        let anchor_count = read_section_count_with_min_record(
3721            r,
3722            header.rigidbody_index_size as usize + header.vertex_index_size as usize + 1,
3723        )?;
3724        for _ in 0..anchor_count {
3725            r.read_sized_index(header.rigidbody_index_size)?;
3726            r.read_vertex_index(header.vertex_index_size)?;
3727            r.skip(1)?;
3728        }
3729        let pin_count = read_section_count_with_min_record(r, header.vertex_index_size as usize)?;
3730        for _ in 0..pin_count {
3731            r.read_vertex_index(header.vertex_index_size)?;
3732        }
3733        soft_bodies.push(PmxParsedSoftBody {
3734            name,
3735            english_name,
3736            kind,
3737            material_index,
3738            collision_group,
3739            collision_mask,
3740            flags,
3741            bending_constraints_distance,
3742            cluster_count,
3743            total_mass,
3744            collision_margin,
3745        });
3746    }
3747    Ok(soft_bodies)
3748}
3749
3750#[derive(Debug)]
3751pub struct PmxRuntimeImport {
3752    pub model: ModelArena,
3753    pub bone_names: Vec<String>,
3754    pub bone_name_to_index: HashMap<Vec<u8>, BoneIndex>,
3755    pub morph_name_to_index: HashMap<Vec<u8>, MorphIndex>,
3756    pub ik_solver_bone_name_to_index: HashMap<Vec<u8>, usize>,
3757}
3758
3759pub fn import_pmx_runtime(data: &[u8]) -> Result<PmxRuntimeImport, ImportError> {
3760    let (header, pos) = read_header(data)?;
3761    let pos = skip_model_info(data, &header, pos)?;
3762    let pos = skip_vertices(data, &header, pos)?;
3763    let pos = skip_faces(data, header.vertex_index_size, pos)?;
3764    let pos = skip_textures(data, header.encoding, pos)?;
3765    let pos = skip_materials(data, &header, pos)?;
3766    let (bone_import, pos) = read_bones(data, &header, pos)?;
3767    let (morph_names, morph_init, _pos) = read_morph_offsets(data, &header, pos)?;
3768
3769    let mut bone_name_to_index = HashMap::with_capacity(bone_import.bone_name_bytes.len() * 2);
3770    for (i, bytes) in bone_import.bone_name_bytes.iter().enumerate() {
3771        let index = BoneIndex(i as u32);
3772        bone_name_to_index.insert(bytes.clone(), index);
3773        let decoded = bone_import.bone_names[i].as_bytes();
3774        if decoded != bytes.as_slice() && !decoded.is_empty() {
3775            bone_name_to_index.insert(decoded.to_vec(), index);
3776        }
3777    }
3778
3779    let mut morph_name_to_index = HashMap::with_capacity(morph_names.name_bytes.len() * 2);
3780    for (i, bytes) in morph_names.name_bytes.iter().enumerate() {
3781        let index = MorphIndex(i as u32);
3782        morph_name_to_index.insert(bytes.clone(), index);
3783        let decoded = morph_names.names[i].as_bytes();
3784        if decoded != bytes.as_slice() && !decoded.is_empty() {
3785            morph_name_to_index.insert(decoded.to_vec(), index);
3786        }
3787    }
3788
3789    let mut ik_solver_bone_name_to_index = HashMap::with_capacity(bone_import.ik_solvers.len() * 2);
3790    for (solver_idx, solver) in bone_import.ik_solvers.iter().enumerate() {
3791        let bone_idx = solver.ik_bone.as_usize();
3792        if bone_idx < bone_import.bone_name_bytes.len() {
3793            let bytes = &bone_import.bone_name_bytes[bone_idx];
3794            ik_solver_bone_name_to_index.insert(bytes.clone(), solver_idx);
3795            let decoded = bone_import.bone_names[bone_idx].as_bytes();
3796            if decoded != bytes.as_slice() && !decoded.is_empty() {
3797                ik_solver_bone_name_to_index.insert(decoded.to_vec(), solver_idx);
3798            }
3799        }
3800    }
3801
3802    let model = ModelArena::new_with_morphs(
3803        bone_import.bones,
3804        bone_import.ik_solvers,
3805        bone_import.append_transforms,
3806        morph_init,
3807    )
3808    .map_err(ImportError::ModelBuildFailed)?;
3809
3810    Ok(PmxRuntimeImport {
3811        model,
3812        bone_names: bone_import.bone_names,
3813        bone_name_to_index,
3814        morph_name_to_index,
3815        ik_solver_bone_name_to_index,
3816    })
3817}
3818
3819trait AppendTransformInitExt {
3820    fn with_rotation_if(self, on: bool) -> Self;
3821    fn with_translation_if(self, on: bool) -> Self;
3822    fn with_local_if(self, on: bool) -> Self;
3823}
3824
3825impl AppendTransformInitExt for AppendTransformInit {
3826    fn with_rotation_if(mut self, on: bool) -> Self {
3827        if on {
3828            self.affect_rotation = true;
3829        }
3830        self
3831    }
3832
3833    fn with_translation_if(mut self, on: bool) -> Self {
3834        if on {
3835            self.affect_translation = true;
3836        }
3837        self
3838    }
3839
3840    fn with_local_if(mut self, on: bool) -> Self {
3841        if on {
3842            self.local = true;
3843        }
3844        self
3845    }
3846}
3847
3848#[cfg(test)]
3849mod tests {
3850    use super::*;
3851    use glam::Vec3;
3852    use mmd_anim_runtime::RuntimeInstance;
3853    use std::sync::Arc;
3854
3855    fn build_small_pmx_header_bytes(bone_index_size: u8, encoding: TextEncoding) -> Vec<u8> {
3856        let mut buf = Vec::new();
3857        buf.extend_from_slice(b"PMX ");
3858        buf.extend_from_slice(&2.0f32.to_le_bytes());
3859        buf.push(8);
3860        buf.push(encoding as u8);
3861        buf.push(0);
3862        buf.push(4);
3863        buf.push(1);
3864        buf.push(1);
3865        buf.push(bone_index_size);
3866        buf.push(1);
3867        buf.push(1);
3868        buf
3869    }
3870
3871    fn build_empty_model_info(_encoding: TextEncoding) -> Vec<u8> {
3872        let mut buf = Vec::new();
3873        for _ in 0..4 {
3874            buf.extend_from_slice(&0i32.to_le_bytes());
3875        }
3876        buf
3877    }
3878
3879    fn build_empty_vertex_section() -> Vec<u8> {
3880        vec![0u8, 0, 0, 0]
3881    }
3882
3883    fn build_empty_face_section() -> Vec<u8> {
3884        vec![0u8, 0, 0, 0]
3885    }
3886
3887    fn build_empty_texture_section() -> Vec<u8> {
3888        vec![0u8, 0, 0, 0]
3889    }
3890
3891    fn build_empty_material_section() -> Vec<u8> {
3892        vec![0u8, 0, 0, 0]
3893    }
3894
3895    #[test]
3896    fn rejects_impossible_pmx_vertex_count_before_allocation() {
3897        let mut buf = build_small_pmx_header_bytes(1, TextEncoding::Utf8);
3898        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
3899        buf.extend_from_slice(&i32::MAX.to_le_bytes());
3900
3901        assert!(matches!(
3902            parse_pmx_model(&buf),
3903            Err(ImportError::UnexpectedEof(_))
3904        ));
3905        assert!(matches!(
3906            import_pmx_runtime(&buf),
3907            Err(ImportError::UnexpectedEof(_))
3908        ));
3909    }
3910
3911    #[test]
3912    fn rejects_invalid_pmx_index_sizes_in_header() {
3913        let buf = build_small_pmx_header_bytes(3, TextEncoding::Utf8);
3914
3915        assert!(matches!(
3916            read_header(&buf),
3917            Err(ImportError::InvalidIndexSize(3))
3918        ));
3919    }
3920
3921    fn build_bone_name_bytes(name: &str) -> Vec<u8> {
3922        let mut buf = Vec::new();
3923        let name_bytes = name.as_bytes();
3924        buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
3925        buf.extend_from_slice(name_bytes);
3926        buf
3927    }
3928
3929    fn build_bone_section_header(count: u32) -> Vec<u8> {
3930        let mut buf = Vec::new();
3931        buf.extend_from_slice(&count.to_le_bytes());
3932        buf
3933    }
3934
3935    fn build_morph_name_bytes(name: &str) -> Vec<u8> {
3936        let mut buf = Vec::new();
3937        let name_bytes = name.as_bytes();
3938        buf.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
3939        buf.extend_from_slice(name_bytes);
3940        buf
3941    }
3942
3943    fn empty_pmx_flags() -> PmxParsedMaterialFlags {
3944        PmxParsedMaterialFlags {
3945            double_sided: false,
3946            ground_shadow: false,
3947            self_shadow_map: false,
3948            self_shadow: false,
3949            edge: false,
3950            vertex_color: false,
3951            point_draw: false,
3952            line_draw: false,
3953        }
3954    }
3955
3956    fn basic_bone_flags(indexed_tail: bool) -> PmxParsedBoneFlags {
3957        PmxParsedBoneFlags {
3958            indexed_tail,
3959            rotatable: true,
3960            translatable: false,
3961            visible: true,
3962            enabled: true,
3963            ik: false,
3964            append_local: false,
3965            append_rotate: false,
3966            append_translate: false,
3967            fixed_axis: false,
3968            local_axis: false,
3969            transform_after_physics: false,
3970            external_parent_transform: false,
3971        }
3972    }
3973
3974    fn parsed_pmx_fixture() -> PmxParsedModel {
3975        PmxParsedModel {
3976            metadata: PmxParsedMetadata {
3977                format: "pmx".to_owned(),
3978                version: 2.0,
3979                encoding: "utf-8".to_owned(),
3980                name: "model".to_owned(),
3981                english_name: "model-en".to_owned(),
3982                comment: "comment".to_owned(),
3983                english_comment: "comment-en".to_owned(),
3984                counts: PmxParsedCounts {
3985                    vertices: 1,
3986                    faces: 1,
3987                    materials: 1,
3988                    bones: 1,
3989                    morphs: 1,
3990                    display_frames: 1,
3991                    rigid_bodies: 1,
3992                    joints: 1,
3993                    soft_bodies: 0,
3994                },
3995                index_sizes: PmxParsedIndexSizes {
3996                    vertex: 4,
3997                    texture: 1,
3998                    material: 1,
3999                    bone: 2,
4000                    morph: 1,
4001                    rigid_body: 1,
4002                },
4003                additional_uv_count: 1,
4004            },
4005            geometry: PmxParsedGeometry {
4006                positions: vec![1.0, 2.0, 3.0],
4007                normals: vec![0.0, 1.0, 0.0],
4008                uvs: vec![0.25, 0.75],
4009                additional_uvs: vec![vec![1.0, 2.0, 3.0, 4.0]],
4010                indices: vec![0, 0, 0],
4011                skin_indices: vec![0, 0, 0, 0],
4012                skin_weights: vec![1.0, 0.0, 0.0, 0.0],
4013                edge_scale: vec![1.25],
4014                material_groups: vec![PmxParsedMaterialGroup {
4015                    start: 0,
4016                    count: 3,
4017                    material_index: 0,
4018                }],
4019                sdef: PmxParsedSdef {
4020                    enabled: vec![0.0],
4021                    c: vec![0.0, 0.0, 0.0],
4022                    r0: vec![0.0, 0.0, 0.0],
4023                    r1: vec![0.0, 0.0, 0.0],
4024                    rw0: vec![0.0, 0.0, 0.0],
4025                    rw1: vec![0.0, 0.0, 0.0],
4026                },
4027                qdef: PmxParsedQdef { enabled: vec![0.0] },
4028            },
4029            materials: vec![PmxParsedMaterial {
4030                name: "mat".to_owned(),
4031                english_name: "mat-en".to_owned(),
4032                texture_path: "tex.png".to_owned(),
4033                sphere_texture_path: "sphere.spa".to_owned(),
4034                sphere_mode: "add".to_owned(),
4035                toon_texture_path: "toon.bmp".to_owned(),
4036                shared_toon_index: None,
4037                diffuse: [0.1, 0.2, 0.3, 0.4],
4038                specular: [0.5, 0.6, 0.7],
4039                specular_power: 8.0,
4040                ambient: [0.8, 0.9, 1.0],
4041                edge_color: [0.0, 0.1, 0.2, 0.3],
4042                edge_size: 1.5,
4043                flags: PmxParsedMaterialFlags {
4044                    double_sided: true,
4045                    edge: true,
4046                    ..empty_pmx_flags()
4047                },
4048                face_count: 1,
4049            }],
4050            skeleton: PmxParsedSkeleton {
4051                bones: vec![PmxParsedBone {
4052                    name: "Root".to_owned(),
4053                    english_name: "RootEn".to_owned(),
4054                    parent_index: -1,
4055                    layer: 0,
4056                    position: [0.0, 1.0, 2.0],
4057                    tail_index: -1,
4058                    tail_position: Some([0.0, 2.0, 0.0]),
4059                    flags: basic_bone_flags(false),
4060                    append_transform: None,
4061                    fixed_axis: None,
4062                    local_axis: None,
4063                    external_parent_key: None,
4064                    ik: None,
4065                }],
4066            },
4067            morphs: vec![PmxParsedMorph {
4068                name: "Smile".to_owned(),
4069                english_name: "SmileEn".to_owned(),
4070                kind: "vertex".to_owned(),
4071                vertex_offsets: vec![PmxParsedVertexMorphOffset {
4072                    vertex_index: 0,
4073                    position: [0.1, 0.2, 0.3],
4074                }],
4075                group_offsets: Vec::new(),
4076                bone_offsets: Vec::new(),
4077                uv_offsets: Vec::new(),
4078                additional_uv_offsets: Vec::new(),
4079                material_offsets: Vec::new(),
4080                flip_offsets: Vec::new(),
4081                impulse_offsets: Vec::new(),
4082            }],
4083            display_frames: vec![PmxParsedDisplayFrame {
4084                name: "RootFrame".to_owned(),
4085                english_name: "RootFrameEn".to_owned(),
4086                special: true,
4087                frames: vec![
4088                    PmxParsedDisplayFrameElement {
4089                        kind: "bone".to_owned(),
4090                        index: 0,
4091                    },
4092                    PmxParsedDisplayFrameElement {
4093                        kind: "morph".to_owned(),
4094                        index: 0,
4095                    },
4096                ],
4097            }],
4098            rigid_bodies: vec![PmxParsedRigidBody {
4099                name: "body".to_owned(),
4100                english_name: "body-en".to_owned(),
4101                bone_index: 0,
4102                group: 1,
4103                mask: 2,
4104                shape: "box".to_owned(),
4105                size: [1.0, 2.0, 3.0],
4106                position: [4.0, 5.0, 6.0],
4107                rotation: [0.1, 0.2, 0.3],
4108                mass: 10.0,
4109                linear_damping: 0.4,
4110                angular_damping: 0.5,
4111                restitution: 0.6,
4112                friction: 0.7,
4113                mode: "dynamicBone".to_owned(),
4114            }],
4115            joints: vec![PmxParsedJoint {
4116                name: "joint".to_owned(),
4117                english_name: "joint-en".to_owned(),
4118                kind: "generic6dofSpring".to_owned(),
4119                rigid_body_index_a: 0,
4120                rigid_body_index_b: -1,
4121                position: [1.0, 1.1, 1.2],
4122                rotation: [2.0, 2.1, 2.2],
4123                translation_lower_limit: [-1.0, -1.1, -1.2],
4124                translation_upper_limit: [1.0, 1.1, 1.2],
4125                rotation_lower_limit: [-0.1, -0.2, -0.3],
4126                rotation_upper_limit: [0.1, 0.2, 0.3],
4127                spring_translation_factor: [3.0, 3.1, 3.2],
4128                spring_rotation_factor: [4.0, 4.1, 4.2],
4129            }],
4130            soft_bodies: Vec::new(),
4131            diagnostics: Vec::new(),
4132        }
4133    }
4134
4135    fn assert_pmx_roundtrip_eq(left: &PmxParsedModel, right: &PmxParsedModel) {
4136        assert_eq!(
4137            serde_json::to_value(left).unwrap(),
4138            serde_json::to_value(right).unwrap()
4139        );
4140    }
4141
4142    fn json_keys(value: &serde_json::Value) -> Vec<String> {
4143        let mut keys = value
4144            .as_object()
4145            .unwrap()
4146            .keys()
4147            .map(ToOwned::to_owned)
4148            .collect::<Vec<_>>();
4149        keys.sort();
4150        keys
4151    }
4152
4153    #[test]
4154    fn pmx_model_json_top_level_schema_is_stable() {
4155        let parsed = parsed_pmx_fixture();
4156        let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
4157
4158        assert_eq!(
4159            keys,
4160            vec![
4161                "diagnostics",
4162                "displayFrames",
4163                "geometry",
4164                "joints",
4165                "materials",
4166                "metadata",
4167                "morphs",
4168                "rigidBodies",
4169                "skeleton",
4170                "softBodies",
4171            ]
4172        );
4173    }
4174
4175    #[test]
4176    fn parses_pmx_header() {
4177        let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
4178        let (header, pos) = read_header(&header_bytes).unwrap();
4179
4180        assert_eq!(header.version, 2.0);
4181        assert_eq!(header.encoding, TextEncoding::Utf8);
4182        assert_eq!(header.bone_index_size, 4);
4183        assert_eq!(header.vertex_index_size, 4);
4184        assert!(pos > 0);
4185    }
4186
4187    #[test]
4188    fn pmx_bone_flag_values_match_reference_layout() {
4189        assert_eq!(BONE_FLAG_TAIL_INDEX, 0x0001);
4190        assert_eq!(BONE_FLAG_IK, 0x0020);
4191        assert_eq!(BONE_FLAG_LOCAL_APPEND, 0x0080);
4192        assert_eq!(BONE_FLAG_APPEND_ROTATE, 0x0100);
4193        assert_eq!(BONE_FLAG_APPEND_TRANSLATE, 0x0200);
4194        assert_eq!(BONE_FLAG_FIXED_AXIS, 0x0400);
4195        assert_eq!(BONE_FLAG_LOCAL_AXIS, 0x0800);
4196        assert_eq!(BONE_FLAG_EXTERNAL_PARENT, 0x2000);
4197    }
4198
4199    #[test]
4200    fn rejects_bad_magic() {
4201        let mut header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf8);
4202        header_bytes[0] = 0xFF;
4203        assert_eq!(
4204            read_header(&header_bytes).unwrap_err(),
4205            ImportError::InvalidPmxMagic
4206        );
4207    }
4208
4209    #[test]
4210    fn reads_single_bone_no_parent() {
4211        let mut buf = Vec::new();
4212        buf.extend_from_slice(&build_small_pmx_header_bytes(4, TextEncoding::Utf8));
4213        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4214        buf.extend_from_slice(&build_empty_vertex_section());
4215        buf.extend_from_slice(&build_empty_face_section());
4216        buf.extend_from_slice(&build_empty_texture_section());
4217        buf.extend_from_slice(&build_empty_material_section());
4218        buf.extend_from_slice(&build_bone_section_header(1));
4219
4220        buf.extend_from_slice(&build_bone_name_bytes("Root"));
4221        buf.extend_from_slice(&build_bone_name_bytes(""));
4222        buf.extend_from_slice(&0.0f32.to_le_bytes());
4223        buf.extend_from_slice(&1.0f32.to_le_bytes());
4224        buf.extend_from_slice(&2.0f32.to_le_bytes());
4225        buf.extend_from_slice(&(-1i32).to_le_bytes());
4226        buf.extend_from_slice(&0i32.to_le_bytes());
4227        buf.extend_from_slice(&0u16.to_le_bytes());
4228        buf.extend_from_slice(&0.0f32.to_le_bytes());
4229        buf.extend_from_slice(&0.0f32.to_le_bytes());
4230        buf.extend_from_slice(&0.0f32.to_le_bytes());
4231
4232        let result = import_pmx_model(&buf).unwrap();
4233        assert_eq!(result.bones.len(), 1);
4234        assert_eq!(result.bone_names.len(), 1);
4235        assert_eq!(result.bone_names[0], "Root");
4236        assert_eq!(&result.bone_name_bytes[0][..], b"Root");
4237        assert_eq!(result.bones[0].parent, None);
4238        assert!((result.bones[0].rest_position.x - 0.0).abs() < 0.001);
4239        assert!((result.bones[0].rest_position.y - 1.0).abs() < 0.001);
4240        assert!((result.bones[0].rest_position.z - 2.0).abs() < 0.001);
4241        assert!(result.ik_solvers.is_empty());
4242        assert!(result.append_transforms.is_empty());
4243    }
4244
4245    #[test]
4246    fn reads_bone_with_parent() {
4247        let mut buf = Vec::new();
4248        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
4249        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4250        buf.extend_from_slice(&build_empty_vertex_section());
4251        buf.extend_from_slice(&build_empty_face_section());
4252        buf.extend_from_slice(&build_empty_texture_section());
4253        buf.extend_from_slice(&build_empty_material_section());
4254        buf.extend_from_slice(&build_bone_section_header(2));
4255
4256        buf.extend_from_slice(&build_bone_name_bytes("Root"));
4257        buf.extend_from_slice(&build_bone_name_bytes(""));
4258        buf.extend_from_slice(&0.0f32.to_le_bytes());
4259        buf.extend_from_slice(&0.0f32.to_le_bytes());
4260        buf.extend_from_slice(&0.0f32.to_le_bytes());
4261        buf.extend_from_slice(&(-1i16).to_le_bytes());
4262        buf.extend_from_slice(&0i32.to_le_bytes());
4263        buf.extend_from_slice(&0u16.to_le_bytes());
4264        buf.extend_from_slice(&0.0f32.to_le_bytes());
4265        buf.extend_from_slice(&0.0f32.to_le_bytes());
4266        buf.extend_from_slice(&0.0f32.to_le_bytes());
4267
4268        buf.extend_from_slice(&build_bone_name_bytes("Child"));
4269        buf.extend_from_slice(&build_bone_name_bytes(""));
4270        buf.extend_from_slice(&1.0f32.to_le_bytes());
4271        buf.extend_from_slice(&0.0f32.to_le_bytes());
4272        buf.extend_from_slice(&0.0f32.to_le_bytes());
4273        buf.extend_from_slice(&0i16.to_le_bytes());
4274        buf.extend_from_slice(&0i32.to_le_bytes());
4275        buf.extend_from_slice(&0u16.to_le_bytes());
4276        buf.extend_from_slice(&0.0f32.to_le_bytes());
4277        buf.extend_from_slice(&0.0f32.to_le_bytes());
4278        buf.extend_from_slice(&0.0f32.to_le_bytes());
4279
4280        let result = import_pmx_model(&buf).unwrap();
4281        assert_eq!(result.bones.len(), 2);
4282        assert_eq!(result.bone_names[0], "Root");
4283        assert_eq!(result.bone_names[1], "Child");
4284        assert_eq!(result.bones[0].parent, None);
4285        assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
4286    }
4287
4288    #[test]
4289    fn reads_bone_with_append_transform() {
4290        let mut buf = Vec::new();
4291        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
4292        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4293        buf.extend_from_slice(&build_empty_vertex_section());
4294        buf.extend_from_slice(&build_empty_face_section());
4295        buf.extend_from_slice(&build_empty_texture_section());
4296        buf.extend_from_slice(&build_empty_material_section());
4297        buf.extend_from_slice(&build_bone_section_header(2));
4298
4299        buf.extend_from_slice(&build_bone_name_bytes("Root"));
4300        buf.extend_from_slice(&build_bone_name_bytes(""));
4301        buf.extend_from_slice(&0.0f32.to_le_bytes());
4302        buf.extend_from_slice(&0.0f32.to_le_bytes());
4303        buf.extend_from_slice(&0.0f32.to_le_bytes());
4304        buf.extend_from_slice(&(-1i16).to_le_bytes());
4305        buf.extend_from_slice(&0i32.to_le_bytes());
4306        buf.extend_from_slice(&0u16.to_le_bytes());
4307        buf.extend_from_slice(&0.0f32.to_le_bytes());
4308        buf.extend_from_slice(&0.0f32.to_le_bytes());
4309        buf.extend_from_slice(&0.0f32.to_le_bytes());
4310
4311        buf.extend_from_slice(&build_bone_name_bytes("AppendBone"));
4312        buf.extend_from_slice(&build_bone_name_bytes(""));
4313        buf.extend_from_slice(&1.0f32.to_le_bytes());
4314        buf.extend_from_slice(&0.0f32.to_le_bytes());
4315        buf.extend_from_slice(&0.0f32.to_le_bytes());
4316        buf.extend_from_slice(&0i16.to_le_bytes());
4317        buf.extend_from_slice(&0i32.to_le_bytes());
4318        buf.extend_from_slice(
4319            &(BONE_FLAG_LOCAL_APPEND | BONE_FLAG_APPEND_ROTATE | BONE_FLAG_APPEND_TRANSLATE)
4320                .to_le_bytes(),
4321        );
4322        buf.extend_from_slice(&0.0f32.to_le_bytes());
4323        buf.extend_from_slice(&0.0f32.to_le_bytes());
4324        buf.extend_from_slice(&0.0f32.to_le_bytes());
4325        buf.extend_from_slice(&0i16.to_le_bytes());
4326        buf.extend_from_slice(&0.5f32.to_le_bytes());
4327
4328        let result = import_pmx_model(&buf).unwrap();
4329        assert_eq!(result.bones.len(), 2);
4330        assert_eq!(result.append_transforms.len(), 1);
4331        assert_eq!(result.append_transforms[0].target_bone, BoneIndex(1));
4332        assert_eq!(result.append_transforms[0].source_bone, BoneIndex(0));
4333        assert!((result.append_transforms[0].ratio - 0.5).abs() < 0.001);
4334        assert!(result.append_transforms[0].affect_rotation);
4335        assert!(result.append_transforms[0].affect_translation);
4336        assert!(result.append_transforms[0].local);
4337    }
4338
4339    #[test]
4340    fn reads_ik_bone_with_links_and_angle_limits() {
4341        let mut buf = Vec::new();
4342        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
4343        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4344        buf.extend_from_slice(&build_empty_vertex_section());
4345        buf.extend_from_slice(&build_empty_face_section());
4346        buf.extend_from_slice(&build_empty_texture_section());
4347        buf.extend_from_slice(&build_empty_material_section());
4348        buf.extend_from_slice(&build_bone_section_header(3));
4349
4350        buf.extend_from_slice(&build_bone_name_bytes("Root"));
4351        buf.extend_from_slice(&build_bone_name_bytes(""));
4352        buf.extend_from_slice(&0.0f32.to_le_bytes());
4353        buf.extend_from_slice(&0.0f32.to_le_bytes());
4354        buf.extend_from_slice(&0.0f32.to_le_bytes());
4355        buf.extend_from_slice(&(-1i16).to_le_bytes());
4356        buf.extend_from_slice(&0i32.to_le_bytes());
4357        buf.extend_from_slice(&0u16.to_le_bytes());
4358        buf.extend_from_slice(&0.0f32.to_le_bytes());
4359        buf.extend_from_slice(&0.0f32.to_le_bytes());
4360        buf.extend_from_slice(&0.0f32.to_le_bytes());
4361
4362        buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
4363        buf.extend_from_slice(&build_bone_name_bytes(""));
4364        buf.extend_from_slice(&2.0f32.to_le_bytes());
4365        buf.extend_from_slice(&0.0f32.to_le_bytes());
4366        buf.extend_from_slice(&0.0f32.to_le_bytes());
4367        buf.extend_from_slice(&0i16.to_le_bytes());
4368        buf.extend_from_slice(&0i32.to_le_bytes());
4369        buf.extend_from_slice(&0u16.to_le_bytes());
4370        buf.extend_from_slice(&0.0f32.to_le_bytes());
4371        buf.extend_from_slice(&0.0f32.to_le_bytes());
4372        buf.extend_from_slice(&0.0f32.to_le_bytes());
4373
4374        buf.extend_from_slice(&build_bone_name_bytes("IKBone"));
4375        buf.extend_from_slice(&build_bone_name_bytes(""));
4376        buf.extend_from_slice(&4.0f32.to_le_bytes());
4377        buf.extend_from_slice(&0.0f32.to_le_bytes());
4378        buf.extend_from_slice(&0.0f32.to_le_bytes());
4379        buf.extend_from_slice(&0i16.to_le_bytes());
4380        buf.extend_from_slice(&0i32.to_le_bytes());
4381        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
4382        buf.extend_from_slice(&1i16.to_le_bytes());
4383        buf.extend_from_slice(&1i16.to_le_bytes());
4384        buf.extend_from_slice(&10i32.to_le_bytes());
4385        buf.extend_from_slice(&0.5f32.to_le_bytes());
4386        buf.extend_from_slice(&1i32.to_le_bytes());
4387        buf.extend_from_slice(&1i16.to_le_bytes());
4388        buf.push(1u8);
4389        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
4390        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
4391        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
4392        buf.extend_from_slice(&1.0f32.to_le_bytes());
4393        buf.extend_from_slice(&1.0f32.to_le_bytes());
4394        buf.extend_from_slice(&1.0f32.to_le_bytes());
4395
4396        let result = import_pmx_model(&buf).unwrap();
4397        assert_eq!(result.bones.len(), 3);
4398        assert_eq!(result.ik_solvers.len(), 1);
4399        assert_eq!(result.ik_solvers[0].ik_bone, BoneIndex(2));
4400        assert_eq!(result.ik_solvers[0].target_bone, BoneIndex(1));
4401        assert_eq!(result.ik_solvers[0].links.len(), 1);
4402        assert_eq!(result.ik_solvers[0].links[0].bone, BoneIndex(1));
4403        assert!(result.ik_solvers[0].links[0].angle_limit.is_some());
4404        assert_eq!(result.ik_solvers[0].iteration_count, 10);
4405        assert!((result.ik_solvers[0].limit_angle - 0.5).abs() < 0.001);
4406    }
4407
4408    #[test]
4409    fn handles_utf16le_encoding() {
4410        let header_bytes = build_small_pmx_header_bytes(4, TextEncoding::Utf16Le);
4411        let (header, _) = read_header(&header_bytes).unwrap();
4412        assert_eq!(header.encoding, TextEncoding::Utf16Le);
4413    }
4414
4415    #[test]
4416    fn decodes_utf16le_surrogate_pairs() {
4417        let bytes = [0x3D, 0xD8, 0x00, 0xDE, 0x00, 0x00];
4418        assert_eq!(decode_utf16le_lossy(&bytes), "\u{1F600}");
4419    }
4420
4421    #[test]
4422    fn reports_eof_on_truncated_data() {
4423        let buf = vec![0x50, 0x4D, 0x58]; // incomplete magic
4424        assert!(matches!(
4425            read_header(&buf).unwrap_err(),
4426            ImportError::UnexpectedEof(_)
4427        ));
4428    }
4429
4430    #[test]
4431    fn bone_names_retained_and_mapped_to_indices() {
4432        let mut buf = Vec::new();
4433        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
4434        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4435        buf.extend_from_slice(&build_empty_vertex_section());
4436        buf.extend_from_slice(&build_empty_face_section());
4437        buf.extend_from_slice(&build_empty_texture_section());
4438        buf.extend_from_slice(&build_empty_material_section());
4439        buf.extend_from_slice(&build_bone_section_header(2));
4440
4441        buf.extend_from_slice(&build_bone_name_bytes("Hips"));
4442        buf.extend_from_slice(&build_bone_name_bytes(""));
4443        buf.extend_from_slice(&0.0f32.to_le_bytes());
4444        buf.extend_from_slice(&0.0f32.to_le_bytes());
4445        buf.extend_from_slice(&0.0f32.to_le_bytes());
4446        buf.extend_from_slice(&(-1i16).to_le_bytes());
4447        buf.extend_from_slice(&0i32.to_le_bytes());
4448        buf.extend_from_slice(&0u16.to_le_bytes());
4449        buf.extend_from_slice(&0.0f32.to_le_bytes());
4450        buf.extend_from_slice(&0.0f32.to_le_bytes());
4451        buf.extend_from_slice(&0.0f32.to_le_bytes());
4452
4453        buf.extend_from_slice(&build_bone_name_bytes("Spine"));
4454        buf.extend_from_slice(&build_bone_name_bytes(""));
4455        buf.extend_from_slice(&1.0f32.to_le_bytes());
4456        buf.extend_from_slice(&0.0f32.to_le_bytes());
4457        buf.extend_from_slice(&0.0f32.to_le_bytes());
4458        buf.extend_from_slice(&0i16.to_le_bytes());
4459        buf.extend_from_slice(&0i32.to_le_bytes());
4460        buf.extend_from_slice(&0u16.to_le_bytes());
4461        buf.extend_from_slice(&0.0f32.to_le_bytes());
4462        buf.extend_from_slice(&0.0f32.to_le_bytes());
4463        buf.extend_from_slice(&0.0f32.to_le_bytes());
4464
4465        let result = import_pmx_model(&buf).unwrap();
4466        assert_eq!(result.bone_name_bytes.len(), 2);
4467        assert_eq!(&result.bone_name_bytes[0][..], b"Hips");
4468        assert_eq!(&result.bone_name_bytes[1][..], b"Spine");
4469        assert_eq!(result.bone_names[0], "Hips");
4470        assert_eq!(result.bone_names[1], "Spine");
4471
4472        let bone_map: HashMap<Vec<u8>, BoneIndex> = result
4473            .bone_name_bytes
4474            .iter()
4475            .enumerate()
4476            .map(|(i, b)| (b.clone(), BoneIndex(i as u32)))
4477            .collect();
4478
4479        assert_eq!(bone_map.get(b"Hips".as_slice()), Some(&BoneIndex(0)));
4480        assert_eq!(bone_map.get(b"Spine".as_slice()), Some(&BoneIndex(1)));
4481        assert_eq!(bone_map.get(b"Unknown".as_slice()), None);
4482    }
4483
4484    #[test]
4485    fn ik_solver_name_mapped_to_solver_index() {
4486        let mut buf = Vec::new();
4487        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
4488        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4489        buf.extend_from_slice(&build_empty_vertex_section());
4490        buf.extend_from_slice(&build_empty_face_section());
4491        buf.extend_from_slice(&build_empty_texture_section());
4492        buf.extend_from_slice(&build_empty_material_section());
4493        buf.extend_from_slice(&build_bone_section_header(3));
4494
4495        buf.extend_from_slice(&build_bone_name_bytes("Root"));
4496        buf.extend_from_slice(&build_bone_name_bytes(""));
4497        buf.extend_from_slice(&0.0f32.to_le_bytes());
4498        buf.extend_from_slice(&0.0f32.to_le_bytes());
4499        buf.extend_from_slice(&0.0f32.to_le_bytes());
4500        buf.extend_from_slice(&(-1i16).to_le_bytes());
4501        buf.extend_from_slice(&0i32.to_le_bytes());
4502        buf.extend_from_slice(&0u16.to_le_bytes());
4503        buf.extend_from_slice(&0.0f32.to_le_bytes());
4504        buf.extend_from_slice(&0.0f32.to_le_bytes());
4505        buf.extend_from_slice(&0.0f32.to_le_bytes());
4506
4507        buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
4508        buf.extend_from_slice(&build_bone_name_bytes(""));
4509        buf.extend_from_slice(&2.0f32.to_le_bytes());
4510        buf.extend_from_slice(&0.0f32.to_le_bytes());
4511        buf.extend_from_slice(&0.0f32.to_le_bytes());
4512        buf.extend_from_slice(&0i16.to_le_bytes());
4513        buf.extend_from_slice(&0i32.to_le_bytes());
4514        buf.extend_from_slice(&0u16.to_le_bytes());
4515        buf.extend_from_slice(&0.0f32.to_le_bytes());
4516        buf.extend_from_slice(&0.0f32.to_le_bytes());
4517        buf.extend_from_slice(&0.0f32.to_le_bytes());
4518
4519        buf.extend_from_slice(&build_bone_name_bytes("LeftLegIK"));
4520        buf.extend_from_slice(&build_bone_name_bytes(""));
4521        buf.extend_from_slice(&4.0f32.to_le_bytes());
4522        buf.extend_from_slice(&0.0f32.to_le_bytes());
4523        buf.extend_from_slice(&0.0f32.to_le_bytes());
4524        buf.extend_from_slice(&0i16.to_le_bytes());
4525        buf.extend_from_slice(&0i32.to_le_bytes());
4526        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
4527        buf.extend_from_slice(&1i16.to_le_bytes());
4528        buf.extend_from_slice(&1i16.to_le_bytes());
4529        buf.extend_from_slice(&10i32.to_le_bytes());
4530        buf.extend_from_slice(&0.5f32.to_le_bytes());
4531        buf.extend_from_slice(&1i32.to_le_bytes());
4532        buf.extend_from_slice(&1i16.to_le_bytes());
4533        buf.push(1u8);
4534        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
4535        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
4536        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
4537        buf.extend_from_slice(&1.0f32.to_le_bytes());
4538        buf.extend_from_slice(&1.0f32.to_le_bytes());
4539        buf.extend_from_slice(&1.0f32.to_le_bytes());
4540
4541        let bone_import = import_pmx_model(&buf).unwrap();
4542        assert_eq!(bone_import.ik_solvers.len(), 1);
4543        assert_eq!(bone_import.ik_solvers[0].ik_bone, BoneIndex(2));
4544        assert_eq!(
4545            bone_import.bone_names[bone_import.ik_solvers[0].ik_bone.as_usize()],
4546            "LeftLegIK"
4547        );
4548
4549        let ik_name_map: HashMap<Vec<u8>, usize> = bone_import
4550            .ik_solvers
4551            .iter()
4552            .enumerate()
4553            .filter_map(|(solver_idx, solver)| {
4554                let bone_idx = solver.ik_bone.as_usize();
4555                if bone_idx < bone_import.bone_name_bytes.len() {
4556                    Some((bone_import.bone_name_bytes[bone_idx].clone(), solver_idx))
4557                } else {
4558                    None
4559                }
4560            })
4561            .collect();
4562
4563        assert_eq!(ik_name_map.get(b"LeftLegIK".as_slice()), Some(&0));
4564    }
4565
4566    #[test]
4567    fn reads_morph_names_without_payloads() {
4568        let (header, pos) =
4569            read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
4570
4571        let mut buf = vec![0u8; pos];
4572        buf.extend_from_slice(&1i32.to_le_bytes());
4573        buf.extend_from_slice(&build_morph_name_bytes("Smile"));
4574        buf.extend_from_slice(&build_morph_name_bytes(""));
4575        buf.push(4u8);
4576        buf.push(0u8);
4577        buf.extend_from_slice(&0i32.to_le_bytes());
4578
4579        let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
4580        assert_eq!(morphs.name_bytes.len(), 1);
4581        assert_eq!(&morphs.name_bytes[0][..], b"Smile");
4582        assert_eq!(morphs.names[0], "Smile");
4583        assert!(new_pos > pos);
4584    }
4585
4586    #[test]
4587    fn reads_morph_type_zero_with_vertex_group_offsets() {
4588        let (header, pos) =
4589            read_header(&build_small_pmx_header_bytes(4, TextEncoding::Utf8)).unwrap();
4590        assert_eq!(header.vertex_index_size, 4);
4591        assert_eq!(header.morph_index_size, 1);
4592
4593        let mut buf = vec![0u8; pos];
4594        buf.extend_from_slice(&1i32.to_le_bytes());
4595        buf.extend_from_slice(&build_morph_name_bytes("Blink"));
4596        buf.extend_from_slice(&build_morph_name_bytes(""));
4597        buf.push(0u8);
4598        buf.push(0u8);
4599        buf.extend_from_slice(&2i32.to_le_bytes());
4600        buf.push(0u8);
4601        buf.extend_from_slice(&[0u8; 4]);
4602        buf.push(1u8);
4603        buf.extend_from_slice(&[0u8; 4]);
4604
4605        let (morphs, new_pos) = read_morph_names(&buf[..], &header, pos).unwrap();
4606        assert_eq!(morphs.name_bytes.len(), 1);
4607        assert_eq!(&morphs.name_bytes[0][..], b"Blink");
4608        assert_eq!(morphs.names[0], "Blink");
4609        assert_eq!(new_pos, buf.len());
4610    }
4611
4612    #[test]
4613    fn runtime_import_builds_model_arena_with_name_lookup() {
4614        let mut buf = Vec::new();
4615        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
4616        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4617        buf.extend_from_slice(&build_empty_vertex_section());
4618        buf.extend_from_slice(&build_empty_face_section());
4619        buf.extend_from_slice(&build_empty_texture_section());
4620        buf.extend_from_slice(&build_empty_material_section());
4621        buf.extend_from_slice(&build_bone_section_header(2));
4622
4623        buf.extend_from_slice(&build_bone_name_bytes("Hips"));
4624        buf.extend_from_slice(&build_bone_name_bytes(""));
4625        buf.extend_from_slice(&0.0f32.to_le_bytes());
4626        buf.extend_from_slice(&0.0f32.to_le_bytes());
4627        buf.extend_from_slice(&0.0f32.to_le_bytes());
4628        buf.extend_from_slice(&(-1i16).to_le_bytes());
4629        buf.extend_from_slice(&0i32.to_le_bytes());
4630        buf.extend_from_slice(&0u16.to_le_bytes());
4631        buf.extend_from_slice(&0.0f32.to_le_bytes());
4632        buf.extend_from_slice(&0.0f32.to_le_bytes());
4633        buf.extend_from_slice(&0.0f32.to_le_bytes());
4634
4635        buf.extend_from_slice(&build_bone_name_bytes("Spine"));
4636        buf.extend_from_slice(&build_bone_name_bytes(""));
4637        buf.extend_from_slice(&0.0f32.to_le_bytes());
4638        buf.extend_from_slice(&1.0f32.to_le_bytes());
4639        buf.extend_from_slice(&0.0f32.to_le_bytes());
4640        buf.extend_from_slice(&0i16.to_le_bytes());
4641        buf.extend_from_slice(&0i32.to_le_bytes());
4642        buf.extend_from_slice(&0u16.to_le_bytes());
4643        buf.extend_from_slice(&0.0f32.to_le_bytes());
4644        buf.extend_from_slice(&0.0f32.to_le_bytes());
4645        buf.extend_from_slice(&0.0f32.to_le_bytes());
4646        buf.extend_from_slice(&0i32.to_le_bytes());
4647
4648        let rt = import_pmx_runtime(&buf).unwrap();
4649        assert_eq!(rt.model.bone_count(), 2);
4650        assert_eq!(rt.model.parent_index(BoneIndex(1)), Some(BoneIndex(0)));
4651        assert_eq!(
4652            rt.bone_name_to_index.get(b"Hips".as_slice()),
4653            Some(&BoneIndex(0))
4654        );
4655        assert_eq!(
4656            rt.bone_name_to_index.get(b"Spine".as_slice()),
4657            Some(&BoneIndex(1))
4658        );
4659        assert!(rt.morph_name_to_index.is_empty());
4660        assert!(rt.ik_solver_bone_name_to_index.is_empty());
4661    }
4662
4663    #[test]
4664    fn parse_pmx_model_exposes_three_loader_shaped_sections() {
4665        let mut buf = Vec::new();
4666        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
4667        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
4668        buf.extend_from_slice(&build_empty_vertex_section());
4669        buf.extend_from_slice(&build_empty_face_section());
4670        buf.extend_from_slice(&build_empty_texture_section());
4671        buf.extend_from_slice(&build_empty_material_section());
4672        buf.extend_from_slice(&build_bone_section_header(2));
4673
4674        buf.extend_from_slice(&build_bone_name_bytes("Root"));
4675        buf.extend_from_slice(&build_bone_name_bytes(""));
4676        buf.extend_from_slice(&0.0f32.to_le_bytes());
4677        buf.extend_from_slice(&0.0f32.to_le_bytes());
4678        buf.extend_from_slice(&0.0f32.to_le_bytes());
4679        buf.extend_from_slice(&(-1i16).to_le_bytes());
4680        buf.extend_from_slice(&0i32.to_le_bytes());
4681        buf.extend_from_slice(&0u16.to_le_bytes());
4682        buf.extend_from_slice(&0.0f32.to_le_bytes());
4683        buf.extend_from_slice(&0.0f32.to_le_bytes());
4684        buf.extend_from_slice(&0.0f32.to_le_bytes());
4685
4686        buf.extend_from_slice(&build_bone_name_bytes("Child"));
4687        buf.extend_from_slice(&build_bone_name_bytes(""));
4688        buf.extend_from_slice(&0.0f32.to_le_bytes());
4689        buf.extend_from_slice(&1.0f32.to_le_bytes());
4690        buf.extend_from_slice(&0.0f32.to_le_bytes());
4691        buf.extend_from_slice(&0i16.to_le_bytes());
4692        buf.extend_from_slice(&0i32.to_le_bytes());
4693        buf.extend_from_slice(&0u16.to_le_bytes());
4694        buf.extend_from_slice(&0.0f32.to_le_bytes());
4695        buf.extend_from_slice(&0.0f32.to_le_bytes());
4696        buf.extend_from_slice(&0.0f32.to_le_bytes());
4697
4698        buf.extend_from_slice(&0i32.to_le_bytes()); // morphs
4699        buf.extend_from_slice(&1i32.to_le_bytes()); // display frames
4700        buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
4701        buf.extend_from_slice(&build_bone_name_bytes(""));
4702        buf.push(1);
4703        buf.extend_from_slice(&1i32.to_le_bytes());
4704        buf.push(0);
4705        buf.extend_from_slice(&1i16.to_le_bytes());
4706        buf.extend_from_slice(&0i32.to_le_bytes()); // rigid bodies
4707        buf.extend_from_slice(&0i32.to_le_bytes()); // joints
4708
4709        let parsed = parse_pmx_model(&buf).unwrap();
4710        assert_eq!(parsed.metadata.format, "pmx");
4711        assert_eq!(parsed.metadata.counts.bones, 2);
4712        assert_eq!(parsed.skeleton.bones[0].name, "Root");
4713        assert_eq!(parsed.skeleton.bones[1].parent_index, 0);
4714        assert_eq!(parsed.display_frames.len(), 1);
4715        assert_eq!(parsed.display_frames[0].frames[0].kind, "bone");
4716        assert_eq!(parsed.display_frames[0].frames[0].index, 1);
4717        assert!(parsed.diagnostics.is_empty());
4718    }
4719
4720    #[test]
4721    fn exports_pmx_semantic_roundtrip() {
4722        let parsed = parsed_pmx_fixture();
4723        let exported = export_pmx_model(&parsed);
4724        let reparsed = parse_pmx_model(&exported).unwrap();
4725
4726        assert_pmx_roundtrip_eq(&parsed, &reparsed);
4727    }
4728
4729    #[test]
4730    fn exports_pmx_json_dto_semantic_roundtrip() {
4731        let parsed = parsed_pmx_fixture();
4732        let json = serde_json::to_string(&parsed).unwrap();
4733        let from_json: PmxParsedModel = serde_json::from_str(&json).unwrap();
4734        let exported = export_pmx_model(&from_json);
4735        let reparsed = parse_pmx_model(&exported).unwrap();
4736
4737        assert_pmx_roundtrip_eq(&parsed, &reparsed);
4738    }
4739
4740    #[test]
4741    fn builds_pmx_parts_with_material_descriptors() {
4742        let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
4743            "name": "material-parts",
4744            "materials": [
4745                {
4746                    "name": "red",
4747                    "englishName": "red-en",
4748                    "diffuse": [1.0, 0.0, 0.0, 1.0],
4749                    "ambient": [0.5, 0.0, 0.0],
4750                    "flags": { "edge": true },
4751                    "faceCount": 1
4752                },
4753                {
4754                    "name": "blue",
4755                    "diffuse": [0.0, 0.0, 1.0, 1.0],
4756                    "faceCount": 1
4757                }
4758            ],
4759            "bones": [
4760                { "name": "root", "englishName": "root-en", "tailIndex": 1 },
4761                { "name": "child", "parentIndex": 0, "position": [0.0, 1.0, 0.0], "rotatable": true }
4762            ],
4763            "morphs": [
4764                {
4765                    "name": "raise",
4766                    "englishName": "raise-en",
4767                    "kind": "vertex",
4768                    "vertexOffsets": [{ "vertexIndex": 0, "position": [0.0, 0.1, 0.0] }]
4769                },
4770                {
4771                    "name": "combo",
4772                    "kind": "group",
4773                    "groupOffsets": [{ "morphIndex": 0, "weight": 0.5 }]
4774                }
4775            ],
4776            "displayFrames": [
4777                {
4778                    "name": "Root",
4779                    "englishName": "Root-en",
4780                    "special": true,
4781                    "frames": [
4782                        { "kind": "bone", "index": 0 },
4783                        { "kind": "bone", "index": 1 },
4784                        { "kind": "morph", "index": 0 }
4785                    ]
4786                }
4787            ],
4788            "rigidBodies": [
4789                {
4790                    "name": "body",
4791                    "englishName": "body-en",
4792                    "boneIndex": 1,
4793                    "group": 2,
4794                    "mask": 3,
4795                    "shape": "box",
4796                    "size": [1.0, 2.0, 3.0],
4797                    "position": [0.0, 1.0, 0.0],
4798                    "rotation": [0.1, 0.2, 0.3],
4799                    "mass": 2.0,
4800                    "linearDamping": 0.4,
4801                    "angularDamping": 0.5,
4802                    "restitution": 0.6,
4803                    "friction": 0.7,
4804                    "mode": "dynamicBone"
4805                }
4806            ],
4807            "joints": [
4808                {
4809                    "name": "joint",
4810                    "type": "generic6dofSpring",
4811                    "rigidBodyIndexA": 0,
4812                    "rigidBodyIndexB": -1,
4813                    "position": [0.0, 1.0, 0.0],
4814                    "translationLowerLimit": [-1.0, -1.0, -1.0],
4815                    "translationUpperLimit": [1.0, 1.0, 1.0],
4816                    "springTranslationFactor": [0.1, 0.2, 0.3]
4817                }
4818            ],
4819            "indexSizes": {
4820                "vertex": 1,
4821                "material": 1,
4822                "texture": 1,
4823                "bone": 1,
4824                "morph": 1,
4825                "rigidBody": 1
4826            }
4827        }))
4828        .unwrap();
4829        let model = build_pmx_model_from_parts(PmxPartsInput {
4830            descriptor,
4831            positions_xyz: &[
4832                0.0, 0.0, 0.0, //
4833                1.0, 0.0, 0.0, //
4834                1.0, 1.0, 0.0, //
4835                0.0, 1.0, 0.0,
4836            ],
4837            normals_xyz: &[
4838                0.0, 0.0, 1.0, //
4839                0.0, 0.0, 1.0, //
4840                0.0, 0.0, 1.0, //
4841                0.0, 0.0, 1.0,
4842            ],
4843            uvs_xy: &[0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],
4844            indices: &[0, 1, 2, 0, 2, 3],
4845            skin_indices: &[0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0],
4846            skin_weights: &[
4847                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,
4848            ],
4849            edge_scale: &[],
4850        })
4851        .unwrap();
4852        let exported = export_pmx_model(&model);
4853        let reparsed = parse_pmx_model(&exported).unwrap();
4854
4855        assert_eq!(reparsed.metadata.name, "material-parts");
4856        assert_eq!(reparsed.metadata.counts.materials, 2);
4857        assert_eq!(reparsed.materials[0].name, "red");
4858        assert_eq!(reparsed.materials[0].english_name, "red-en");
4859        assert_eq!(reparsed.materials[0].face_count, 1);
4860        assert!(reparsed.materials[0].flags.edge);
4861        assert_eq!(reparsed.materials[1].name, "blue");
4862        assert_eq!(reparsed.materials[1].face_count, 1);
4863        assert_eq!(reparsed.geometry.material_groups.len(), 2);
4864        assert_eq!(reparsed.geometry.material_groups[0].start, 0);
4865        assert_eq!(reparsed.geometry.material_groups[0].count, 3);
4866        assert_eq!(reparsed.geometry.material_groups[1].start, 3);
4867        assert_eq!(reparsed.geometry.material_groups[1].count, 3);
4868        assert_eq!(reparsed.metadata.counts.bones, 2);
4869        assert_eq!(reparsed.skeleton.bones[0].name, "root");
4870        assert_eq!(reparsed.skeleton.bones[0].english_name, "root-en");
4871        assert_eq!(reparsed.skeleton.bones[0].tail_index, 1);
4872        assert!(reparsed.skeleton.bones[0].flags.indexed_tail);
4873        assert_eq!(reparsed.skeleton.bones[1].name, "child");
4874        assert_eq!(reparsed.skeleton.bones[1].parent_index, 0);
4875        assert!(reparsed.skeleton.bones[1].flags.rotatable);
4876        assert_eq!(reparsed.geometry.skin_indices[1], 1);
4877        assert_eq!(reparsed.geometry.skin_weights[1], 0.25);
4878        assert_eq!(reparsed.metadata.counts.morphs, 2);
4879        assert_eq!(reparsed.morphs[0].name, "raise");
4880        assert_eq!(reparsed.morphs[0].english_name, "raise-en");
4881        assert_eq!(reparsed.morphs[0].kind, "vertex");
4882        assert_eq!(reparsed.morphs[0].vertex_offsets[0].vertex_index, 0);
4883        assert_eq!(
4884            reparsed.morphs[0].vertex_offsets[0].position,
4885            [0.0, 0.1, 0.0]
4886        );
4887        assert_eq!(reparsed.morphs[1].name, "combo");
4888        assert_eq!(reparsed.morphs[1].kind, "group");
4889        assert_eq!(reparsed.morphs[1].group_offsets[0].morph_index, 0);
4890        assert_eq!(reparsed.morphs[1].group_offsets[0].weight, 0.5);
4891        assert_eq!(reparsed.display_frames.len(), 1);
4892        assert_eq!(reparsed.display_frames[0].name, "Root");
4893        assert_eq!(reparsed.display_frames[0].english_name, "Root-en");
4894        assert!(reparsed.display_frames[0].special);
4895        assert_eq!(reparsed.display_frames[0].frames.len(), 3);
4896        assert_eq!(reparsed.display_frames[0].frames[1].kind, "bone");
4897        assert_eq!(reparsed.display_frames[0].frames[1].index, 1);
4898        assert_eq!(reparsed.display_frames[0].frames[2].kind, "morph");
4899        assert_eq!(reparsed.display_frames[0].frames[2].index, 0);
4900        assert_eq!(reparsed.rigid_bodies.len(), 1);
4901        assert_eq!(reparsed.rigid_bodies[0].name, "body");
4902        assert_eq!(reparsed.rigid_bodies[0].english_name, "body-en");
4903        assert_eq!(reparsed.rigid_bodies[0].bone_index, 1);
4904        assert_eq!(reparsed.rigid_bodies[0].group, 2);
4905        assert_eq!(reparsed.rigid_bodies[0].mask, 3);
4906        assert_eq!(reparsed.rigid_bodies[0].shape, "box");
4907        assert_eq!(reparsed.rigid_bodies[0].mode, "dynamicBone");
4908        assert_eq!(reparsed.joints.len(), 1);
4909        assert_eq!(reparsed.joints[0].name, "joint");
4910        assert_eq!(reparsed.joints[0].kind, "generic6dofSpring");
4911        assert_eq!(reparsed.joints[0].rigid_body_index_a, 0);
4912        assert_eq!(reparsed.joints[0].rigid_body_index_b, -1);
4913    }
4914
4915    #[test]
4916    fn rejects_pmx_parts_material_face_count_mismatch() {
4917        let descriptor: PmxPartsDescriptor = serde_json::from_value(serde_json::json!({
4918            "materials": [{ "name": "bad", "faceCount": 2 }]
4919        }))
4920        .unwrap();
4921        let error = build_pmx_model_from_parts(PmxPartsInput {
4922            descriptor,
4923            positions_xyz: &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
4924            normals_xyz: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
4925            uvs_xy: &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
4926            indices: &[0, 1, 2],
4927            skin_indices: &[],
4928            skin_weights: &[],
4929            edge_scale: &[],
4930        })
4931        .unwrap_err();
4932
4933        assert!(error.contains("materials faceCount sum"));
4934    }
4935
4936    #[test]
4937    fn rejects_pmx_export_model_partial_skin_arrays() {
4938        let mut parsed = parsed_pmx_fixture();
4939        parsed.geometry.skin_weights.clear();
4940
4941        let error = validate_pmx_export_model(&parsed).unwrap_err();
4942
4943        assert!(error.contains("skinIndices and skinWeights"));
4944    }
4945
4946    // Synthetic PMX 2.2 gate — no real PMX 2.2 fixture exists in the scan set yet.
4947    // Verifies that version=2.2 survives export_pmx_model -> parse_pmx_model unchanged.
4948    #[test]
4949    fn exports_pmx_22_synthetic_roundtrip() {
4950        let mut parsed = parsed_pmx_fixture();
4951        parsed.metadata.version = 2.2;
4952
4953        let exported = export_pmx_model(&parsed);
4954        let reparsed = parse_pmx_model(&exported).unwrap();
4955
4956        assert_eq!(
4957            reparsed.metadata.version, 2.2,
4958            "PMX 2.2 version must survive the export-parse cycle"
4959        );
4960        assert_pmx_roundtrip_eq(&parsed, &reparsed);
4961    }
4962
4963    #[test]
4964    fn exports_pmx_soft_body_header_fields_with_diagnostic_for_pmx_21_and_22() {
4965        for version in [2.1, 2.2] {
4966            let mut parsed = parsed_pmx_fixture();
4967            parsed.metadata.version = version;
4968            parsed.soft_bodies = vec![PmxParsedSoftBody {
4969                name: "soft".to_owned(),
4970                english_name: "soft-en".to_owned(),
4971                kind: "rope".to_owned(),
4972                material_index: 0,
4973                collision_group: 1,
4974                collision_mask: 2,
4975                flags: 3,
4976                bending_constraints_distance: 4,
4977                cluster_count: 5,
4978                total_mass: 6.0,
4979                collision_margin: 0.7,
4980            }];
4981            let exported = export_pmx_model(&parsed);
4982            let reparsed = parse_pmx_model(&exported).unwrap();
4983
4984            assert_eq!(reparsed.metadata.version, version);
4985            assert_eq!(reparsed.metadata.counts.soft_bodies, 1);
4986            assert_eq!(reparsed.soft_bodies.len(), 1);
4987            assert_eq!(reparsed.soft_bodies[0].name, "soft");
4988            assert_eq!(reparsed.soft_bodies[0].english_name, "soft-en");
4989            assert_eq!(reparsed.soft_bodies[0].kind, "rope");
4990            assert_eq!(reparsed.soft_bodies[0].material_index, 0);
4991            assert_eq!(reparsed.soft_bodies[0].collision_group, 1);
4992            assert_eq!(reparsed.soft_bodies[0].collision_mask, 2);
4993            assert_eq!(reparsed.soft_bodies[0].flags, 3);
4994            assert_eq!(reparsed.soft_bodies[0].bending_constraints_distance, 4);
4995            assert_eq!(reparsed.soft_bodies[0].cluster_count, 5);
4996            assert_eq!(reparsed.soft_bodies[0].total_mass, 6.0);
4997            assert_eq!(reparsed.soft_bodies[0].collision_margin, 0.7);
4998            assert_eq!(reparsed.diagnostics.len(), 1);
4999            assert_eq!(reparsed.diagnostics[0].code, "PMX_SOFT_BODY_UNSUPPORTED");
5000        }
5001    }
5002
5003    #[test]
5004    fn exports_parsed_pmx_skeleton_display_roundtrip() {
5005        let mut buf = Vec::new();
5006        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5007        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5008        buf.extend_from_slice(&build_empty_vertex_section());
5009        buf.extend_from_slice(&build_empty_face_section());
5010        buf.extend_from_slice(&build_empty_texture_section());
5011        buf.extend_from_slice(&build_empty_material_section());
5012        buf.extend_from_slice(&build_bone_section_header(2));
5013
5014        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5015        buf.extend_from_slice(&build_bone_name_bytes(""));
5016        buf.extend_from_slice(&0.0f32.to_le_bytes());
5017        buf.extend_from_slice(&0.0f32.to_le_bytes());
5018        buf.extend_from_slice(&0.0f32.to_le_bytes());
5019        buf.extend_from_slice(&(-1i16).to_le_bytes());
5020        buf.extend_from_slice(&0i32.to_le_bytes());
5021        buf.extend_from_slice(&0u16.to_le_bytes());
5022        buf.extend_from_slice(&0.0f32.to_le_bytes());
5023        buf.extend_from_slice(&0.0f32.to_le_bytes());
5024        buf.extend_from_slice(&0.0f32.to_le_bytes());
5025
5026        buf.extend_from_slice(&build_bone_name_bytes("Child"));
5027        buf.extend_from_slice(&build_bone_name_bytes(""));
5028        buf.extend_from_slice(&0.0f32.to_le_bytes());
5029        buf.extend_from_slice(&1.0f32.to_le_bytes());
5030        buf.extend_from_slice(&0.0f32.to_le_bytes());
5031        buf.extend_from_slice(&0i16.to_le_bytes());
5032        buf.extend_from_slice(&0i32.to_le_bytes());
5033        buf.extend_from_slice(&0u16.to_le_bytes());
5034        buf.extend_from_slice(&0.0f32.to_le_bytes());
5035        buf.extend_from_slice(&0.0f32.to_le_bytes());
5036        buf.extend_from_slice(&0.0f32.to_le_bytes());
5037
5038        buf.extend_from_slice(&0i32.to_le_bytes()); // morphs
5039        buf.extend_from_slice(&1i32.to_le_bytes()); // display frames
5040        buf.extend_from_slice(&build_bone_name_bytes("RootFrame"));
5041        buf.extend_from_slice(&build_bone_name_bytes(""));
5042        buf.push(1);
5043        buf.extend_from_slice(&1i32.to_le_bytes());
5044        buf.push(0);
5045        buf.extend_from_slice(&1i16.to_le_bytes());
5046        buf.extend_from_slice(&0i32.to_le_bytes()); // rigid bodies
5047        buf.extend_from_slice(&0i32.to_le_bytes()); // joints
5048
5049        let parsed = parse_pmx_model(&buf).unwrap();
5050        let exported = export_pmx_model(&parsed);
5051        let reparsed = parse_pmx_model(&exported).unwrap();
5052
5053        assert_pmx_roundtrip_eq(&parsed, &reparsed);
5054    }
5055
5056    #[test]
5057    fn runtime_import_ik_solver_name_lookup() {
5058        let mut buf = Vec::new();
5059        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5060        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5061        buf.extend_from_slice(&build_empty_vertex_section());
5062        buf.extend_from_slice(&build_empty_face_section());
5063        buf.extend_from_slice(&build_empty_texture_section());
5064        buf.extend_from_slice(&build_empty_material_section());
5065        buf.extend_from_slice(&build_bone_section_header(3));
5066
5067        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5068        buf.extend_from_slice(&build_bone_name_bytes(""));
5069        buf.extend_from_slice(&0.0f32.to_le_bytes());
5070        buf.extend_from_slice(&0.0f32.to_le_bytes());
5071        buf.extend_from_slice(&0.0f32.to_le_bytes());
5072        buf.extend_from_slice(&(-1i16).to_le_bytes());
5073        buf.extend_from_slice(&0i32.to_le_bytes());
5074        buf.extend_from_slice(&0u16.to_le_bytes());
5075        buf.extend_from_slice(&0.0f32.to_le_bytes());
5076        buf.extend_from_slice(&0.0f32.to_le_bytes());
5077        buf.extend_from_slice(&0.0f32.to_le_bytes());
5078
5079        buf.extend_from_slice(&build_bone_name_bytes("IKLink"));
5080        buf.extend_from_slice(&build_bone_name_bytes(""));
5081        buf.extend_from_slice(&2.0f32.to_le_bytes());
5082        buf.extend_from_slice(&0.0f32.to_le_bytes());
5083        buf.extend_from_slice(&0.0f32.to_le_bytes());
5084        buf.extend_from_slice(&0i16.to_le_bytes());
5085        buf.extend_from_slice(&0i32.to_le_bytes());
5086        buf.extend_from_slice(&0u16.to_le_bytes());
5087        buf.extend_from_slice(&0.0f32.to_le_bytes());
5088        buf.extend_from_slice(&0.0f32.to_le_bytes());
5089        buf.extend_from_slice(&0.0f32.to_le_bytes());
5090
5091        buf.extend_from_slice(&build_bone_name_bytes("LeftLegIK"));
5092        buf.extend_from_slice(&build_bone_name_bytes(""));
5093        buf.extend_from_slice(&4.0f32.to_le_bytes());
5094        buf.extend_from_slice(&0.0f32.to_le_bytes());
5095        buf.extend_from_slice(&0.0f32.to_le_bytes());
5096        buf.extend_from_slice(&0i16.to_le_bytes());
5097        buf.extend_from_slice(&0i32.to_le_bytes());
5098        buf.extend_from_slice(&(BONE_FLAG_TAIL_INDEX | BONE_FLAG_IK).to_le_bytes());
5099        buf.extend_from_slice(&1i16.to_le_bytes());
5100        buf.extend_from_slice(&1i16.to_le_bytes());
5101        buf.extend_from_slice(&10i32.to_le_bytes());
5102        buf.extend_from_slice(&0.5f32.to_le_bytes());
5103        buf.extend_from_slice(&1i32.to_le_bytes());
5104        buf.extend_from_slice(&1i16.to_le_bytes());
5105        buf.push(1u8);
5106        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5107        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5108        buf.extend_from_slice(&(-1.0f32).to_le_bytes());
5109        buf.extend_from_slice(&1.0f32.to_le_bytes());
5110        buf.extend_from_slice(&1.0f32.to_le_bytes());
5111        buf.extend_from_slice(&1.0f32.to_le_bytes());
5112        buf.extend_from_slice(&0i32.to_le_bytes());
5113
5114        let rt = import_pmx_runtime(&buf).unwrap();
5115        assert_eq!(rt.model.bone_count(), 3);
5116        assert_eq!(rt.model.ik_count(), 1);
5117        assert_eq!(
5118            rt.ik_solver_bone_name_to_index.get(b"LeftLegIK".as_slice()),
5119            Some(&0)
5120        );
5121        assert_eq!(
5122            rt.bone_name_to_index.get(b"LeftLegIK".as_slice()),
5123            Some(&BoneIndex(2))
5124        );
5125        assert_eq!(
5126            rt.ik_solver_bone_name_to_index.get(b"NoSuchIK".as_slice()),
5127            None
5128        );
5129    }
5130
5131    #[test]
5132    fn converts_absolute_pmx_positions_to_local_rest_positions() {
5133        let mut buf = Vec::new();
5134        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5135        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5136        buf.extend_from_slice(&build_empty_vertex_section());
5137        buf.extend_from_slice(&build_empty_face_section());
5138        buf.extend_from_slice(&build_empty_texture_section());
5139        buf.extend_from_slice(&build_empty_material_section());
5140        buf.extend_from_slice(&build_bone_section_header(2));
5141
5142        buf.extend_from_slice(&build_bone_name_bytes("Parent"));
5143        buf.extend_from_slice(&build_bone_name_bytes(""));
5144        buf.extend_from_slice(&1.0f32.to_le_bytes());
5145        buf.extend_from_slice(&2.0f32.to_le_bytes());
5146        buf.extend_from_slice(&0.0f32.to_le_bytes());
5147        buf.extend_from_slice(&(-1i16).to_le_bytes());
5148        buf.extend_from_slice(&0i32.to_le_bytes());
5149        buf.extend_from_slice(&0u16.to_le_bytes());
5150        buf.extend_from_slice(&0.0f32.to_le_bytes());
5151        buf.extend_from_slice(&0.0f32.to_le_bytes());
5152        buf.extend_from_slice(&0.0f32.to_le_bytes());
5153
5154        buf.extend_from_slice(&build_bone_name_bytes("Child"));
5155        buf.extend_from_slice(&build_bone_name_bytes(""));
5156        buf.extend_from_slice(&1.0f32.to_le_bytes());
5157        buf.extend_from_slice(&3.0f32.to_le_bytes());
5158        buf.extend_from_slice(&0.0f32.to_le_bytes());
5159        buf.extend_from_slice(&0i16.to_le_bytes());
5160        buf.extend_from_slice(&0i32.to_le_bytes());
5161        buf.extend_from_slice(&0u16.to_le_bytes());
5162        buf.extend_from_slice(&0.0f32.to_le_bytes());
5163        buf.extend_from_slice(&0.0f32.to_le_bytes());
5164        buf.extend_from_slice(&0.0f32.to_le_bytes());
5165
5166        let result = import_pmx_model(&buf).unwrap();
5167        assert_eq!(result.bones.len(), 2);
5168        assert_eq!(result.bones[0].parent, None);
5169        assert!((result.bones[0].rest_position.x - 1.0).abs() < 0.001);
5170        assert!((result.bones[0].rest_position.y - 2.0).abs() < 0.001);
5171        assert!((result.bones[0].rest_position.z - 0.0).abs() < 0.001);
5172        assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
5173        assert!((result.bones[1].rest_position.x - 0.0).abs() < 0.001);
5174        assert!((result.bones[1].rest_position.y - 1.0).abs() < 0.001);
5175        assert!((result.bones[1].rest_position.z - 0.0).abs() < 0.001);
5176    }
5177
5178    #[test]
5179    fn root_inverse_bind_cancels_rest_world() {
5180        let mut buf = Vec::new();
5181        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5182        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5183        buf.extend_from_slice(&build_empty_vertex_section());
5184        buf.extend_from_slice(&build_empty_face_section());
5185        buf.extend_from_slice(&build_empty_texture_section());
5186        buf.extend_from_slice(&build_empty_material_section());
5187        buf.extend_from_slice(&build_bone_section_header(1));
5188
5189        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5190        buf.extend_from_slice(&build_bone_name_bytes(""));
5191        buf.extend_from_slice(&1.0f32.to_le_bytes());
5192        buf.extend_from_slice(&2.0f32.to_le_bytes());
5193        buf.extend_from_slice(&3.0f32.to_le_bytes());
5194        buf.extend_from_slice(&(-1i16).to_le_bytes());
5195        buf.extend_from_slice(&0i32.to_le_bytes());
5196        buf.extend_from_slice(&0u16.to_le_bytes());
5197        buf.extend_from_slice(&0.0f32.to_le_bytes());
5198        buf.extend_from_slice(&0.0f32.to_le_bytes());
5199        buf.extend_from_slice(&0.0f32.to_le_bytes());
5200
5201        let result = import_pmx_model(&buf).unwrap();
5202        let ibm = result.bones[0].inverse_bind_matrix;
5203        let rest_world = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
5204        assert_eq!(rest_world * ibm, Mat4::IDENTITY);
5205    }
5206
5207    #[test]
5208    fn reads_pmx_fixed_axis_bone_descriptor() {
5209        let mut buf = Vec::new();
5210        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5211        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5212        buf.extend_from_slice(&build_empty_vertex_section());
5213        buf.extend_from_slice(&build_empty_face_section());
5214        buf.extend_from_slice(&build_empty_texture_section());
5215        buf.extend_from_slice(&build_empty_material_section());
5216        buf.extend_from_slice(&build_bone_section_header(1));
5217
5218        buf.extend_from_slice(&build_bone_name_bytes("Fixed"));
5219        buf.extend_from_slice(&build_bone_name_bytes(""));
5220        buf.extend_from_slice(&0.0f32.to_le_bytes());
5221        buf.extend_from_slice(&0.0f32.to_le_bytes());
5222        buf.extend_from_slice(&0.0f32.to_le_bytes());
5223        buf.extend_from_slice(&(-1i16).to_le_bytes());
5224        buf.extend_from_slice(&0i32.to_le_bytes());
5225        buf.extend_from_slice(&BONE_FLAG_FIXED_AXIS.to_le_bytes());
5226        buf.extend_from_slice(&0.0f32.to_le_bytes());
5227        buf.extend_from_slice(&0.0f32.to_le_bytes());
5228        buf.extend_from_slice(&0.0f32.to_le_bytes());
5229        buf.extend_from_slice(&0.0f32.to_le_bytes());
5230        buf.extend_from_slice(&1.0f32.to_le_bytes());
5231        buf.extend_from_slice(&0.0f32.to_le_bytes());
5232
5233        let result = import_pmx_model(&buf).unwrap();
5234        assert_eq!(result.bones[0].fixed_axis, Some(Vec3A::Y));
5235    }
5236
5237    #[test]
5238    fn child_inverse_bind_uses_absolute_not_local_position() {
5239        let mut buf = Vec::new();
5240        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5241        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5242        buf.extend_from_slice(&build_empty_vertex_section());
5243        buf.extend_from_slice(&build_empty_face_section());
5244        buf.extend_from_slice(&build_empty_texture_section());
5245        buf.extend_from_slice(&build_empty_material_section());
5246        buf.extend_from_slice(&build_bone_section_header(2));
5247
5248        buf.extend_from_slice(&build_bone_name_bytes("Root"));
5249        buf.extend_from_slice(&build_bone_name_bytes(""));
5250        buf.extend_from_slice(&1.0f32.to_le_bytes());
5251        buf.extend_from_slice(&0.0f32.to_le_bytes());
5252        buf.extend_from_slice(&0.0f32.to_le_bytes());
5253        buf.extend_from_slice(&(-1i16).to_le_bytes());
5254        buf.extend_from_slice(&0i32.to_le_bytes());
5255        buf.extend_from_slice(&0u16.to_le_bytes());
5256        buf.extend_from_slice(&0.0f32.to_le_bytes());
5257        buf.extend_from_slice(&0.0f32.to_le_bytes());
5258        buf.extend_from_slice(&0.0f32.to_le_bytes());
5259
5260        buf.extend_from_slice(&build_bone_name_bytes("Child"));
5261        buf.extend_from_slice(&build_bone_name_bytes(""));
5262        buf.extend_from_slice(&5.0f32.to_le_bytes());
5263        buf.extend_from_slice(&0.0f32.to_le_bytes());
5264        buf.extend_from_slice(&0.0f32.to_le_bytes());
5265        buf.extend_from_slice(&0i16.to_le_bytes());
5266        buf.extend_from_slice(&0i32.to_le_bytes());
5267        buf.extend_from_slice(&0u16.to_le_bytes());
5268        buf.extend_from_slice(&0.0f32.to_le_bytes());
5269        buf.extend_from_slice(&0.0f32.to_le_bytes());
5270        buf.extend_from_slice(&0.0f32.to_le_bytes());
5271
5272        let result = import_pmx_model(&buf).unwrap();
5273        assert_eq!(result.bones[1].parent, Some(BoneIndex(0)));
5274        assert_eq!(result.bones[1].rest_position, Vec3A::new(4.0, 0.0, 0.0));
5275        let ibm = result.bones[1].inverse_bind_matrix;
5276        let rest_world = Mat4::from_translation(Vec3::new(5.0, 0.0, 0.0));
5277        assert_eq!(rest_world * ibm, Mat4::IDENTITY);
5278    }
5279
5280    #[test]
5281    fn runtime_rest_pose_skinning_matrices_are_identity() {
5282        let mut buf = Vec::new();
5283        buf.extend_from_slice(&build_small_pmx_header_bytes(2, TextEncoding::Utf8));
5284        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5285        buf.extend_from_slice(&build_empty_vertex_section());
5286        buf.extend_from_slice(&build_empty_face_section());
5287        buf.extend_from_slice(&build_empty_texture_section());
5288        buf.extend_from_slice(&build_empty_material_section());
5289        buf.extend_from_slice(&build_bone_section_header(2));
5290
5291        buf.extend_from_slice(&build_bone_name_bytes("Parent"));
5292        buf.extend_from_slice(&build_bone_name_bytes(""));
5293        buf.extend_from_slice(&1.0f32.to_le_bytes());
5294        buf.extend_from_slice(&2.0f32.to_le_bytes());
5295        buf.extend_from_slice(&0.0f32.to_le_bytes());
5296        buf.extend_from_slice(&(-1i16).to_le_bytes());
5297        buf.extend_from_slice(&0i32.to_le_bytes());
5298        buf.extend_from_slice(&0u16.to_le_bytes());
5299        buf.extend_from_slice(&0.0f32.to_le_bytes());
5300        buf.extend_from_slice(&0.0f32.to_le_bytes());
5301        buf.extend_from_slice(&0.0f32.to_le_bytes());
5302
5303        buf.extend_from_slice(&build_bone_name_bytes("Child"));
5304        buf.extend_from_slice(&build_bone_name_bytes(""));
5305        buf.extend_from_slice(&1.0f32.to_le_bytes());
5306        buf.extend_from_slice(&3.0f32.to_le_bytes());
5307        buf.extend_from_slice(&0.0f32.to_le_bytes());
5308        buf.extend_from_slice(&0i16.to_le_bytes());
5309        buf.extend_from_slice(&0i32.to_le_bytes());
5310        buf.extend_from_slice(&0u16.to_le_bytes());
5311        buf.extend_from_slice(&0.0f32.to_le_bytes());
5312        buf.extend_from_slice(&0.0f32.to_le_bytes());
5313        buf.extend_from_slice(&0.0f32.to_le_bytes());
5314
5315        buf.extend_from_slice(&0i32.to_le_bytes());
5316
5317        let rt = import_pmx_runtime(&buf).unwrap();
5318        let model = Arc::new(rt.model);
5319        let mut instance = RuntimeInstance::new(model);
5320        instance.evaluate_current_pose();
5321        let matrices = instance.skinning_matrices();
5322        assert_eq!(matrices.len(), 2);
5323        assert_eq!(matrices[0], Mat4::IDENTITY);
5324        assert_eq!(matrices[1], Mat4::IDENTITY);
5325    }
5326
5327    fn build_sdef_vertex_pmx() -> Vec<u8> {
5328        // Header: PMX 2.0, UTF-8, extra_uv=0, vertex_idx=1, tex_idx=1, mat_idx=1,
5329        //         bone_idx=1, morph_idx=1, rigidBody_idx=1
5330        let mut buf = Vec::new();
5331        buf.extend_from_slice(b"PMX ");
5332        buf.extend_from_slice(&2.0f32.to_le_bytes());
5333        buf.push(8); // data count
5334        buf.push(1); // UTF-8
5335        buf.push(0); // extra_uv_count
5336        buf.push(1); // vertex_index_size
5337        buf.push(1); // texture_index_size
5338        buf.push(1); // material_index_size
5339        buf.push(1); // bone_index_size
5340        buf.push(1); // morph_index_size
5341        buf.push(1); // rigidbody_index_size
5342        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5343        // 1 vertex, SDEF (type 3): bone indices 0 and 0 (i8 size=1), weight 0.25
5344        // SDEF params: c=[1,2,3], r0=[4,5,6], r1=[7,8,9]
5345        buf.extend_from_slice(&1i32.to_le_bytes());
5346        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position x
5347        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position y
5348        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position z
5349        buf.extend_from_slice(&0.0f32.to_le_bytes()); // normal x
5350        buf.extend_from_slice(&1.0f32.to_le_bytes()); // normal y
5351        buf.extend_from_slice(&0.0f32.to_le_bytes()); // normal z
5352        buf.extend_from_slice(&0.0f32.to_le_bytes()); // uv u
5353        buf.extend_from_slice(&0.0f32.to_le_bytes()); // uv v
5354        buf.push(3u8); // weight type = 3 (SDEF)
5355        buf.push(0u8); // bone_index_0 (1 byte)
5356        buf.push(0u8); // bone_index_1 (1 byte)
5357        buf.extend_from_slice(&0.25f32.to_le_bytes()); // weight
5358        buf.extend_from_slice(&1.0f32.to_le_bytes()); // c.x
5359        buf.extend_from_slice(&2.0f32.to_le_bytes()); // c.y
5360        buf.extend_from_slice(&3.0f32.to_le_bytes()); // c.z
5361        buf.extend_from_slice(&4.0f32.to_le_bytes()); // r0.x
5362        buf.extend_from_slice(&5.0f32.to_le_bytes()); // r0.y
5363        buf.extend_from_slice(&6.0f32.to_le_bytes()); // r0.z
5364        buf.extend_from_slice(&7.0f32.to_le_bytes()); // r1.x
5365        buf.extend_from_slice(&8.0f32.to_le_bytes()); // r1.y
5366        buf.extend_from_slice(&9.0f32.to_le_bytes()); // r1.z
5367        buf.extend_from_slice(&1.0f32.to_le_bytes()); // edge_scale
5368        for _ in 0..8 {
5369            buf.extend_from_slice(&0i32.to_le_bytes()); // 0 faces/textures/materials/bones/morphs/displayFrames/rigidBodies/joints
5370        }
5371        buf
5372    }
5373
5374    fn build_qdef_vertex_pmx() -> Vec<u8> {
5375        // Header: PMX 2.0, UTF-8, all index sizes = 1
5376        let mut buf = Vec::new();
5377        buf.extend_from_slice(b"PMX ");
5378        buf.extend_from_slice(&2.0f32.to_le_bytes());
5379        buf.push(8);
5380        buf.push(1); // UTF-8
5381        buf.push(0); // extra_uv_count
5382        buf.push(1); // vertex_index_size
5383        buf.push(1); // texture_index_size
5384        buf.push(1); // material_index_size
5385        buf.push(1); // bone_index_size
5386        buf.push(1); // morph_index_size
5387        buf.push(1); // rigidbody_index_size
5388        buf.extend_from_slice(&build_empty_model_info(TextEncoding::Utf8));
5389        // 1 vertex, QDEF (type 4): 4 bone indices (i8 each), 4 weights
5390        buf.extend_from_slice(&1i32.to_le_bytes());
5391        buf.extend_from_slice(&0.0f32.to_le_bytes()); // position
5392        buf.extend_from_slice(&0.0f32.to_le_bytes());
5393        buf.extend_from_slice(&0.0f32.to_le_bytes());
5394        buf.extend_from_slice(&0.0f32.to_le_bytes()); // normal
5395        buf.extend_from_slice(&1.0f32.to_le_bytes());
5396        buf.extend_from_slice(&0.0f32.to_le_bytes());
5397        buf.extend_from_slice(&0.0f32.to_le_bytes()); // uv
5398        buf.extend_from_slice(&0.0f32.to_le_bytes());
5399        buf.push(4u8); // weight type = 4 (QDEF)
5400        buf.push(0u8);
5401        buf.push(0u8);
5402        buf.push(0u8);
5403        buf.push(0u8); // 4 bone indices
5404        buf.extend_from_slice(&1.0f32.to_le_bytes()); // w0
5405        buf.extend_from_slice(&0.0f32.to_le_bytes()); // w1
5406        buf.extend_from_slice(&0.0f32.to_le_bytes()); // w2
5407        buf.extend_from_slice(&0.0f32.to_le_bytes()); // w3
5408        buf.extend_from_slice(&1.0f32.to_le_bytes()); // edge_scale
5409        for _ in 0..8 {
5410            buf.extend_from_slice(&0i32.to_le_bytes());
5411        }
5412        buf
5413    }
5414
5415    #[test]
5416    fn pmx_sdef_geometry_json_includes_sdef_fields() {
5417        let buf = build_sdef_vertex_pmx();
5418        let model = parse_pmx_model(&buf).unwrap();
5419        assert_eq!(model.geometry.sdef.enabled.len(), 1);
5420        assert!((model.geometry.sdef.enabled[0] - 1.0).abs() < 1e-5);
5421        assert_eq!(model.geometry.sdef.c, vec![1.0, 2.0, 3.0]);
5422        assert_eq!(model.geometry.sdef.r0, vec![4.0, 5.0, 6.0]);
5423        assert_eq!(model.geometry.sdef.r1, vec![7.0, 8.0, 9.0]);
5424        // rw values: w=0.25, w1=0.75
5425        // rw[k] = r0[k]*0.25 + r1[k]*0.75
5426        // rw0[k] = (c[k]*2 + r0[k] - rw[k]) * 0.5
5427        // rw1[k] = (c[k]*2 + r1[k] - rw[k]) * 0.5
5428        let w = 0.25f32;
5429        let w1 = 0.75f32;
5430        let c = [1.0f32, 2.0, 3.0];
5431        let r0 = [4.0f32, 5.0, 6.0];
5432        let r1 = [7.0f32, 8.0, 9.0];
5433        for k in 0..3 {
5434            let rw = r0[k] * w + r1[k] * w1;
5435            let expected_rw0 = (c[k] * 2.0 + r0[k] - rw) * 0.5;
5436            let expected_rw1 = (c[k] * 2.0 + r1[k] - rw) * 0.5;
5437            assert!(
5438                (model.geometry.sdef.rw0[k] - expected_rw0).abs() < 1e-4,
5439                "rw0[{k}]: expected {expected_rw0}, got {}",
5440                model.geometry.sdef.rw0[k]
5441            );
5442            assert!(
5443                (model.geometry.sdef.rw1[k] - expected_rw1).abs() < 1e-4,
5444                "rw1[{k}]: expected {expected_rw1}, got {}",
5445                model.geometry.sdef.rw1[k]
5446            );
5447        }
5448        assert_eq!(model.geometry.qdef.enabled[0], 0.0);
5449        // Verify roundtrip through JSON preserves the sdef key
5450        let json = serde_json::to_string(&model).unwrap();
5451        assert!(json.contains("\"sdef\""), "JSON must contain sdef key");
5452        assert!(json.contains("\"qdef\""), "JSON must contain qdef key");
5453        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
5454        assert_eq!(parsed["geometry"]["sdef"]["enabled"][0], 1.0);
5455    }
5456
5457    #[test]
5458    fn pmx_qdef_vertex_marks_enabled() {
5459        let buf = build_qdef_vertex_pmx();
5460        let model = parse_pmx_model(&buf).unwrap();
5461        assert_eq!(model.geometry.qdef.enabled.len(), 1);
5462        assert!((model.geometry.qdef.enabled[0] - 1.0).abs() < 1e-5);
5463        assert_eq!(model.geometry.sdef.enabled[0], 0.0);
5464    }
5465}