Skip to main content

mmd_anim_format/pmx/
mod.rs

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