Skip to main content

mmd_anim_format/pmx/
mod.rs

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