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