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