Skip to main content

mmd_anim_format/pmx/
mod.rs

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