Skip to main content

mmd_anim_runtime/
model.rs

1use glam::{Mat3, Mat4, Quat, Vec3, Vec3A};
2use thiserror::Error;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub struct BoneIndex(pub u32);
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct MorphIndex(pub u32);
9
10impl MorphIndex {
11    #[inline]
12    pub fn as_usize(self) -> usize {
13        self.0 as usize
14    }
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct BoneMorphOffset {
19    pub target_bone: BoneIndex,
20    pub position_offset: Vec3A,
21    pub rotation_offset: Quat,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct VertexMorphOffset {
26    pub vertex_index: u32,
27    pub position_offset: Vec3A,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct GroupMorphOffset {
32    pub child_morph: MorphIndex,
33    pub ratio: f32,
34}
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct MorphOffsetSpan {
38    pub start: u32,
39    pub count: u32,
40}
41
42#[derive(Clone, Debug, Default)]
43pub struct MorphInit {
44    pub morph_count: u32,
45    pub vertex_offsets: Vec<VertexMorphOffset>,
46    pub vertex_spans: Vec<MorphOffsetSpan>,
47    pub bone_offsets: Vec<BoneMorphOffset>,
48    pub bone_spans: Vec<MorphOffsetSpan>,
49    pub group_offsets: Vec<GroupMorphOffset>,
50    pub group_spans: Vec<MorphOffsetSpan>,
51}
52
53/// Build the canonical grouped morph tables used by every runtime input
54/// adapter.  Inputs retain their source order within each morph bucket while
55/// this function owns span construction, index checking, and cycle traversal.
56pub fn build_morph_init_from_offsets(
57    morph_count: u32,
58    bone_offsets: Vec<(MorphIndex, BoneMorphOffset)>,
59    group_offsets: Vec<(MorphIndex, GroupMorphOffset)>,
60) -> Result<MorphInit, ModelBuildError> {
61    let bone_offsets = index_morph_offsets(bone_offsets)?;
62    let group_offsets = index_morph_offsets(group_offsets)?;
63    build_morph_init_from_indexed_offsets(morph_count, bone_offsets, group_offsets).map_err(
64        |error| match error {
65            ModelBuildError::GroupMorphCycleAt { morph, .. } => {
66                ModelBuildError::GroupMorphCycle { morph }
67            }
68            other => other,
69        },
70    )
71}
72
73pub(crate) fn build_morph_init_from_indexed_offsets(
74    morph_count: u32,
75    bone_offsets: Vec<(usize, MorphIndex, BoneMorphOffset)>,
76    group_offsets: Vec<(usize, MorphIndex, GroupMorphOffset)>,
77) -> Result<MorphInit, ModelBuildError> {
78    let count = morph_count as usize;
79    if count == 0 && (!bone_offsets.is_empty() || !group_offsets.is_empty()) {
80        return Err(ModelBuildError::MorphCountZeroWithData);
81    }
82
83    let mut bone_counts = try_zeroed_vec(count)?;
84    for (_, index, _) in &bone_offsets {
85        let count = bone_counts
86            .get_mut(index.as_usize())
87            .ok_or(ModelBuildError::InvalidBoneMorphMorph { offset: index.0 })?;
88        *count = count
89            .checked_add(1)
90            .ok_or(ModelBuildError::MorphStorageAllocation)?;
91    }
92    let mut bone_buckets = try_empty_buckets(count)?;
93    reserve_bucket_storage(&mut bone_buckets, &bone_counts)?;
94    for (_, index, offset) in bone_offsets {
95        let bucket = bone_buckets
96            .get_mut(index.as_usize())
97            .ok_or(ModelBuildError::InvalidBoneMorphMorph { offset: index.0 })?;
98        bucket.push(offset);
99    }
100    let mut group_counts = try_zeroed_vec(count)?;
101    for (_, index, offset) in &group_offsets {
102        if offset.child_morph.as_usize() >= count {
103            return Err(ModelBuildError::InvalidGroupMorphChild {
104                morph: index.as_usize(),
105                child: offset.child_morph.0,
106            });
107        }
108        let count = group_counts
109            .get_mut(index.as_usize())
110            .ok_or(ModelBuildError::InvalidGroupMorph { morph: index.0 })?;
111        *count = count
112            .checked_add(1)
113            .ok_or(ModelBuildError::MorphStorageAllocation)?;
114    }
115    let mut group_buckets = try_empty_buckets(count)?;
116    let mut group_sources = try_empty_buckets(count)?;
117    reserve_bucket_storage(&mut group_buckets, &group_counts)?;
118    reserve_bucket_storage(&mut group_sources, &group_counts)?;
119    for (source, index, offset) in group_offsets {
120        let bucket = group_buckets
121            .get_mut(index.as_usize())
122            .ok_or(ModelBuildError::InvalidGroupMorph { morph: index.0 })?;
123        bucket.push(offset);
124        group_sources[index.as_usize()].push(source);
125    }
126
127    let mut bone_flat = try_vec_with_capacity(bone_counts.iter().sum())?;
128    let mut bone_spans = try_vec_with_capacity(count)?;
129    for bucket in bone_buckets {
130        let start =
131            u32::try_from(bone_flat.len()).map_err(|_| ModelBuildError::MorphStorageOverflow)?;
132        bone_flat.extend(bucket);
133        let count = u32::try_from(bone_flat.len() - start as usize)
134            .map_err(|_| ModelBuildError::MorphStorageOverflow)?;
135        bone_spans.push(MorphOffsetSpan { start, count });
136    }
137
138    let mut group_flat = try_vec_with_capacity(group_counts.iter().sum())?;
139    let mut group_source_flat = try_vec_with_capacity(group_counts.iter().sum())?;
140    let mut group_spans = try_vec_with_capacity(count)?;
141    for (bucket, sources) in group_buckets.into_iter().zip(group_sources) {
142        let start =
143            u32::try_from(group_flat.len()).map_err(|_| ModelBuildError::MorphStorageOverflow)?;
144        group_flat.extend(bucket);
145        group_source_flat.extend(sources);
146        let count = u32::try_from(group_flat.len() - start as usize)
147            .map_err(|_| ModelBuildError::MorphStorageOverflow)?;
148        group_spans.push(MorphOffsetSpan { start, count });
149    }
150    let morph = MorphInit {
151        morph_count,
152        bone_offsets: bone_flat,
153        bone_spans,
154        group_offsets: group_flat,
155        group_spans,
156        ..MorphInit::default()
157    };
158    validate_group_morph_cycles_with_sources(&morph, &group_source_flat)?;
159    Ok(morph)
160}
161
162fn index_morph_offsets<T>(
163    offsets: Vec<(MorphIndex, T)>,
164) -> Result<Vec<(usize, MorphIndex, T)>, ModelBuildError> {
165    let mut indexed = try_vec_with_capacity(offsets.len())?;
166    indexed.extend(
167        offsets
168            .into_iter()
169            .enumerate()
170            .map(|(source, (morph, offset))| (source, morph, offset)),
171    );
172    Ok(indexed)
173}
174
175fn try_vec_with_capacity<T>(capacity: usize) -> Result<Vec<T>, ModelBuildError> {
176    let mut values = Vec::new();
177    values
178        .try_reserve_exact(capacity)
179        .map_err(|_| ModelBuildError::MorphStorageAllocation)?;
180    Ok(values)
181}
182
183fn try_zeroed_vec(length: usize) -> Result<Vec<usize>, ModelBuildError> {
184    let mut values = try_vec_with_capacity(length)?;
185    values.resize(length, 0);
186    Ok(values)
187}
188
189fn try_empty_buckets<T>(length: usize) -> Result<Vec<Vec<T>>, ModelBuildError> {
190    let mut buckets = try_vec_with_capacity(length)?;
191    buckets.resize_with(length, Vec::new);
192    Ok(buckets)
193}
194
195fn reserve_bucket_storage<T>(
196    buckets: &mut [Vec<T>],
197    counts: &[usize],
198) -> Result<(), ModelBuildError> {
199    for (bucket, &count) in buckets.iter_mut().zip(counts) {
200        bucket
201            .try_reserve_exact(count)
202            .map_err(|_| ModelBuildError::MorphStorageAllocation)?;
203    }
204    Ok(())
205}
206
207impl BoneIndex {
208    #[inline]
209    pub fn as_usize(self) -> usize {
210        self.0 as usize
211    }
212}
213
214/// PMX bone local-axis descriptor (bone-local X/Z directions from the file).
215///
216/// The runtime builds a defensive right-handed orthonormal frame from these
217/// vectors for IK angle-limit evaluation only. Ordinary pose evaluation does
218/// not reorient bones by this frame.
219#[derive(Clone, Copy, Debug, PartialEq)]
220pub struct LocalAxis {
221    pub x: Vec3A,
222    pub z: Vec3A,
223}
224
225impl LocalAxis {
226    pub fn new(x: Vec3A, z: Vec3A) -> Self {
227        Self { x, z }
228    }
229
230    /// Build a right-handed orthonormal basis quaternion whose columns are
231    /// `(x, y, z)` in bone-local space: `y = normalize(z × x)`, `z = x × y`.
232    ///
233    /// Returns `None` when inputs are non-finite or degenerate (near-zero /
234    /// nearly parallel axes). Callers must treat `None` as "no local axis".
235    pub fn basis_quat(self) -> Option<Quat> {
236        build_local_axis_basis_quat(self.x, self.z)
237    }
238}
239
240#[derive(Clone, Debug)]
241pub struct BoneInit {
242    pub parent: Option<BoneIndex>,
243    pub rest_position: Vec3A,
244    pub inverse_bind_matrix: Mat4,
245    pub transform_order: i32,
246    pub transform_after_physics: bool,
247    pub fixed_axis: Option<Vec3A>,
248    /// When true, ordinary world-pose evaluation projects local rotation onto
249    /// `fixed_axis`. PMX import keeps this false so VMD/local rotations stay
250    /// unprojected; fixed-axis still constrains CCD during IK steps via the
251    /// stored `fixed_axis` descriptor.
252    pub enforce_fixed_axis: bool,
253}
254
255impl BoneInit {
256    pub fn new(parent: Option<BoneIndex>, rest_position: Vec3A) -> Self {
257        Self {
258            parent,
259            rest_position,
260            inverse_bind_matrix: Mat4::IDENTITY,
261            transform_order: 0,
262            transform_after_physics: false,
263            fixed_axis: None,
264            enforce_fixed_axis: false,
265        }
266    }
267
268    pub fn with_fixed_axis(mut self, axis: Vec3A) -> Self {
269        self.fixed_axis = Some(axis);
270        self.enforce_fixed_axis = true;
271        self
272    }
273}
274
275/// Build a defensive right-handed local-axis basis quaternion.
276///
277/// Frame construction matches nanoem / Dayo style:
278/// `x = normalize(local_x)`, `y = normalize(local_z × x)`, `z = x × y`.
279fn build_local_axis_basis_quat(x: Vec3A, z: Vec3A) -> Option<Quat> {
280    if !x.is_finite() || !z.is_finite() {
281        return None;
282    }
283    let x_len_sq = x.length_squared();
284    if x_len_sq <= f32::EPSILON {
285        return None;
286    }
287    let x_n = x * x_len_sq.sqrt().recip();
288    let y = z.cross(x_n);
289    let y_len_sq = y.length_squared();
290    if y_len_sq <= f32::EPSILON {
291        return None;
292    }
293    let y_n = y * y_len_sq.sqrt().recip();
294    let z_n = x_n.cross(y_n);
295    let z_len_sq = z_n.length_squared();
296    if z_len_sq <= f32::EPSILON {
297        return None;
298    }
299    let z_n = z_n * z_len_sq.sqrt().recip();
300    if !x_n.is_finite() || !y_n.is_finite() || !z_n.is_finite() {
301        return None;
302    }
303    let mat = Mat3::from_cols(Vec3::from(x_n), Vec3::from(y_n), Vec3::from(z_n));
304    let quat = Quat::from_mat3(&mat);
305    if !quat.is_finite() || quat.length_squared() <= f32::EPSILON {
306        return None;
307    }
308    Some(quat.normalize())
309}
310
311#[derive(Clone, Copy, Debug, PartialEq)]
312pub struct IkAngleLimit {
313    pub min: Vec3A,
314    pub max: Vec3A,
315}
316
317impl IkAngleLimit {
318    pub fn new(min: Vec3A, max: Vec3A) -> Self {
319        Self { min, max }
320    }
321}
322
323#[derive(Clone, Debug, PartialEq)]
324pub struct IkLinkInit {
325    pub bone: BoneIndex,
326    pub angle_limit: Option<IkAngleLimit>,
327}
328
329impl IkLinkInit {
330    pub fn new(bone: BoneIndex) -> Self {
331        Self {
332            bone,
333            angle_limit: None,
334        }
335    }
336
337    pub fn with_angle_limit(mut self, angle_limit: IkAngleLimit) -> Self {
338        self.angle_limit = Some(angle_limit);
339        self
340    }
341}
342
343#[derive(Clone, Debug, PartialEq)]
344pub struct IkSolverInit {
345    pub ik_bone: BoneIndex,
346    pub target_bone: BoneIndex,
347    pub links: Vec<IkLinkInit>,
348    pub iteration_count: u32,
349    pub limit_angle: f32,
350}
351
352#[derive(Clone, Debug, PartialEq)]
353pub struct AppendTransformInit {
354    pub target_bone: BoneIndex,
355    pub source_bone: BoneIndex,
356    pub ratio: f32,
357    pub affect_rotation: bool,
358    pub affect_translation: bool,
359    pub local: bool,
360}
361
362impl AppendTransformInit {
363    pub fn new(target_bone: BoneIndex, source_bone: BoneIndex, ratio: f32) -> Self {
364        Self {
365            target_bone,
366            source_bone,
367            ratio,
368            affect_rotation: false,
369            affect_translation: false,
370            local: false,
371        }
372    }
373
374    pub fn with_rotation(mut self) -> Self {
375        self.affect_rotation = true;
376        self
377    }
378
379    pub fn with_translation(mut self) -> Self {
380        self.affect_translation = true;
381        self
382    }
383
384    pub fn with_local(mut self) -> Self {
385        self.local = true;
386        self
387    }
388}
389
390impl IkSolverInit {
391    pub fn new(ik_bone: BoneIndex, target_bone: BoneIndex, links: Vec<IkLinkInit>) -> Self {
392        Self {
393            ik_bone,
394            target_bone,
395            links,
396            iteration_count: 1,
397            limit_angle: 0.0,
398        }
399    }
400}
401
402#[derive(Debug, Error, PartialEq, Eq)]
403pub enum ModelBuildError {
404    #[error("model must contain at least one bone")]
405    EmptyModel,
406    #[error("runtime descriptor validation failed at {path}: {reason}")]
407    InvalidRuntimeDescriptor { path: String, reason: String },
408    #[error("bone {bone} references invalid parent {parent}")]
409    InvalidParent { bone: usize, parent: u32 },
410    #[error("bone hierarchy contains a cycle involving bone {bone}")]
411    ParentCycle { bone: usize },
412    #[error("ik solver {solver} references invalid {role} bone {bone}")]
413    InvalidIkBone {
414        solver: usize,
415        role: &'static str,
416        bone: u32,
417    },
418    #[error("append transform {append} references invalid {role} bone {bone}")]
419    InvalidAppendBone {
420        append: usize,
421        role: &'static str,
422        bone: u32,
423    },
424    #[error("bone {bone} has more than one append transform")]
425    DuplicateAppendTransform { bone: u32 },
426    #[error("bone morph offset {offset} references invalid target bone {bone}")]
427    InvalidBoneMorphBone { offset: usize, bone: u32 },
428    #[error("morph span list has length {actual}, expected {expected}")]
429    InvalidMorphSpanCount { actual: usize, expected: usize },
430    #[error("morph {morph} has invalid {kind} offset span")]
431    InvalidMorphSpan { morph: usize, kind: &'static str },
432    #[error("group morph {morph} references invalid child morph {child}")]
433    InvalidGroupMorphChild { morph: usize, child: u32 },
434    #[error("group morph cycle detected at morph {morph}")]
435    GroupMorphCycle { morph: usize },
436    #[error("group morph cycle detected at morph {morph} (offset {offset})")]
437    GroupMorphCycleAt { morph: usize, offset: usize },
438    #[error("morph count is zero but morph offsets are present")]
439    MorphCountZeroWithData,
440    #[error("bone morph references invalid morph {offset}")]
441    InvalidBoneMorphMorph { offset: u32 },
442    #[error("group morph references invalid morph {morph}")]
443    InvalidGroupMorph { morph: u32 },
444    #[error("morph storage exceeds u32 span capacity")]
445    MorphStorageOverflow,
446    #[error("morph storage allocation failed")]
447    MorphStorageAllocation,
448}
449
450#[derive(Debug)]
451pub struct ModelArena {
452    parent_indices: Box<[i32]>,
453    rest_positions: Box<[Vec3A]>,
454    inverse_bind_matrices: Box<[Mat4]>,
455    transform_orders: Box<[i32]>,
456    fixed_axis_flags: Box<[u8]>,
457    fixed_axis_constraint_flags: Box<[u8]>,
458    fixed_axes: Box<[Vec3A]>,
459    local_axis_flags: Box<[u8]>,
460    local_axis_x: Box<[Vec3A]>,
461    local_axis_z: Box<[Vec3A]>,
462    local_axis_basis: Box<[Quat]>,
463    transform_after_physics_flags: Box<[u8]>,
464    ik_link_flags: Box<[u8]>,
465    eval_order: Box<[BoneIndex]>,
466    eval_order_before_physics: Box<[BoneIndex]>,
467    eval_order_after_physics: Box<[BoneIndex]>,
468    eval_order_positions: Box<[usize]>,
469    ik_bone_solver_spans: Box<[MorphOffsetSpan]>,
470    ik_bone_solver_indices: Box<[u32]>,
471    ik_solvers: Box<[IkSolver]>,
472    append_transforms: Box<[AppendTransform]>,
473    append_transform_indices: Box<[i32]>,
474    morph_count: u32,
475    vertex_morph_offsets: Box<[VertexMorphOffset]>,
476    vertex_morph_spans: Box<[MorphOffsetSpan]>,
477    bone_morph_offsets: Box<[BoneMorphOffset]>,
478    bone_morph_spans: Box<[MorphOffsetSpan]>,
479    group_morph_offsets: Box<[GroupMorphOffset]>,
480    group_morph_spans: Box<[MorphOffsetSpan]>,
481}
482
483impl ModelArena {
484    pub fn new(bones: Vec<BoneInit>) -> Result<Self, ModelBuildError> {
485        Self::new_full(bones, Vec::new(), Vec::new())
486    }
487
488    pub fn new_with_ik(
489        bones: Vec<BoneInit>,
490        ik_solvers: Vec<IkSolverInit>,
491    ) -> Result<Self, ModelBuildError> {
492        Self::new_full(bones, ik_solvers, Vec::new())
493    }
494
495    pub fn new_full(
496        bones: Vec<BoneInit>,
497        ik_solvers: Vec<IkSolverInit>,
498        append_transforms: Vec<AppendTransformInit>,
499    ) -> Result<Self, ModelBuildError> {
500        Self::new_with_morphs(bones, ik_solvers, append_transforms, MorphInit::default())
501    }
502
503    pub fn new_with_morphs(
504        bones: Vec<BoneInit>,
505        ik_solvers: Vec<IkSolverInit>,
506        append_transforms: Vec<AppendTransformInit>,
507        morph: MorphInit,
508    ) -> Result<Self, ModelBuildError> {
509        if bones.is_empty() {
510            return Err(ModelBuildError::EmptyModel);
511        }
512
513        let bone_count = bones.len();
514        let mut parent_indices = Vec::with_capacity(bone_count);
515        let mut rest_positions = Vec::with_capacity(bone_count);
516        let mut inverse_bind_matrices = Vec::with_capacity(bone_count);
517        let mut transform_orders = Vec::with_capacity(bone_count);
518        let mut transform_after_physics_flags = Vec::with_capacity(bone_count);
519        let mut fixed_axis_flags = Vec::with_capacity(bone_count);
520        let mut fixed_axis_constraint_flags = Vec::with_capacity(bone_count);
521        let mut fixed_axes = Vec::with_capacity(bone_count);
522        let mut local_axis_flags = Vec::with_capacity(bone_count);
523        let mut local_axis_x = Vec::with_capacity(bone_count);
524        let mut local_axis_z = Vec::with_capacity(bone_count);
525        let mut local_axis_basis = Vec::with_capacity(bone_count);
526
527        for (bone_index, bone) in bones.iter().enumerate() {
528            let parent = match bone.parent {
529                Some(parent) if parent.as_usize() < bone_count => parent.0 as i32,
530                Some(parent) => {
531                    return Err(ModelBuildError::InvalidParent {
532                        bone: bone_index,
533                        parent: parent.0,
534                    });
535                }
536                None => -1,
537            };
538
539            parent_indices.push(parent);
540            rest_positions.push(bone.rest_position);
541            inverse_bind_matrices.push(bone.inverse_bind_matrix);
542            transform_orders.push(bone.transform_order);
543            transform_after_physics_flags.push(u8::from(bone.transform_after_physics));
544            match bone.fixed_axis {
545                Some(axis) if axis.length_squared() > f32::EPSILON && axis.is_finite() => {
546                    fixed_axis_flags.push(1);
547                    fixed_axis_constraint_flags.push(u8::from(bone.enforce_fixed_axis));
548                    fixed_axes.push(axis.normalize());
549                }
550                _ => {
551                    fixed_axis_flags.push(0);
552                    fixed_axis_constraint_flags.push(0);
553                    fixed_axes.push(Vec3A::X);
554                }
555            }
556            // Local axes are applied via `with_local_axes` so existing BoneInit
557            // struct literals stay source-compatible (no new required fields).
558            local_axis_flags.push(0);
559            local_axis_x.push(Vec3A::X);
560            local_axis_z.push(Vec3A::Z);
561            local_axis_basis.push(Quat::IDENTITY);
562        }
563
564        let eval_order = build_eval_order(&parent_indices, &transform_orders)?;
565        let eval_order_before_physics =
566            build_eval_order_for_phase(&eval_order, &transform_after_physics_flags, false);
567        let eval_order_after_physics =
568            build_eval_order_for_phase(&eval_order, &transform_after_physics_flags, true);
569        let eval_order_positions = build_eval_order_positions(&eval_order, bone_count);
570        let (ik_solvers, ik_link_flags) = build_ik_solvers(ik_solvers, bone_count)?;
571        let (ik_bone_solver_spans, ik_bone_solver_indices) =
572            build_ik_bone_solver_lookup(&ik_solvers, bone_count);
573        let (append_transforms, append_transform_indices) =
574            build_append_transforms(append_transforms, bone_count)?;
575        validate_morph_init(&morph, bone_count)?;
576
577        Ok(Self {
578            parent_indices: parent_indices.into_boxed_slice(),
579            rest_positions: rest_positions.into_boxed_slice(),
580            inverse_bind_matrices: inverse_bind_matrices.into_boxed_slice(),
581            transform_orders: transform_orders.into_boxed_slice(),
582            fixed_axis_flags: fixed_axis_flags.into_boxed_slice(),
583            fixed_axis_constraint_flags: fixed_axis_constraint_flags.into_boxed_slice(),
584            fixed_axes: fixed_axes.into_boxed_slice(),
585            local_axis_flags: local_axis_flags.into_boxed_slice(),
586            local_axis_x: local_axis_x.into_boxed_slice(),
587            local_axis_z: local_axis_z.into_boxed_slice(),
588            local_axis_basis: local_axis_basis.into_boxed_slice(),
589            transform_after_physics_flags: transform_after_physics_flags.into_boxed_slice(),
590            ik_link_flags,
591            eval_order,
592            eval_order_before_physics,
593            eval_order_after_physics,
594            eval_order_positions,
595            ik_bone_solver_spans,
596            ik_bone_solver_indices,
597            ik_solvers,
598            append_transforms,
599            append_transform_indices,
600            morph_count: morph.morph_count,
601            vertex_morph_offsets: morph.vertex_offsets.into_boxed_slice(),
602            vertex_morph_spans: morph.vertex_spans.into_boxed_slice(),
603            bone_morph_offsets: morph.bone_offsets.into_boxed_slice(),
604            bone_morph_spans: morph.bone_spans.into_boxed_slice(),
605            group_morph_offsets: morph.group_offsets.into_boxed_slice(),
606            group_morph_spans: morph.group_spans.into_boxed_slice(),
607        })
608    }
609
610    #[inline]
611    pub fn bone_count(&self) -> usize {
612        self.parent_indices.len()
613    }
614
615    #[inline]
616    pub fn parent_index(&self, bone: BoneIndex) -> Option<BoneIndex> {
617        let parent = self.parent_indices[bone.as_usize()];
618        if parent < 0 {
619            None
620        } else {
621            Some(BoneIndex(parent as u32))
622        }
623    }
624
625    #[inline]
626    pub fn rest_position(&self, bone: BoneIndex) -> Vec3A {
627        self.rest_positions[bone.as_usize()]
628    }
629
630    #[inline]
631    pub fn inverse_bind_matrix(&self, bone: BoneIndex) -> Mat4 {
632        self.inverse_bind_matrices[bone.as_usize()]
633    }
634
635    #[inline]
636    pub fn transform_order(&self, bone: BoneIndex) -> i32 {
637        self.transform_orders[bone.as_usize()]
638    }
639
640    #[inline]
641    pub fn fixed_axis(&self, bone: BoneIndex) -> Option<Vec3A> {
642        if self.fixed_axis_flags[bone.as_usize()] != 0 {
643            Some(self.fixed_axes[bone.as_usize()])
644        } else {
645            None
646        }
647    }
648
649    #[inline]
650    pub(crate) fn fixed_axis_constraint(&self, bone: BoneIndex) -> Option<Vec3A> {
651        if self.fixed_axis_constraint_flags[bone.as_usize()] != 0 {
652            Some(self.fixed_axes[bone.as_usize()])
653        } else {
654            None
655        }
656    }
657
658    #[inline]
659    pub fn fixed_axis_count(&self) -> usize {
660        self.fixed_axis_flags
661            .iter()
662            .filter(|&&flag| flag != 0)
663            .count()
664    }
665
666    /// Overlay per-bone PMX local-axis descriptors without changing existing
667    /// constructors. Entries beyond `bone_count` are ignored; shorter lists
668    /// leave trailing bones without a local axis. Degenerate axes are dropped.
669    pub fn with_local_axes(
670        mut self,
671        local_axes: impl IntoIterator<Item = Option<LocalAxis>>,
672    ) -> Self {
673        for (bone_index, axis) in local_axes.into_iter().enumerate() {
674            if bone_index >= self.bone_count() {
675                break;
676            }
677            match axis.and_then(|axis| {
678                let basis = axis.basis_quat()?;
679                Some((axis.x, axis.z, basis))
680            }) {
681                Some((x, z, basis)) => {
682                    self.local_axis_flags[bone_index] = 1;
683                    self.local_axis_x[bone_index] = x;
684                    self.local_axis_z[bone_index] = z;
685                    self.local_axis_basis[bone_index] = basis;
686                }
687                None => {
688                    self.local_axis_flags[bone_index] = 0;
689                    self.local_axis_x[bone_index] = Vec3A::X;
690                    self.local_axis_z[bone_index] = Vec3A::Z;
691                    self.local_axis_basis[bone_index] = Quat::IDENTITY;
692                }
693            }
694        }
695        self
696    }
697
698    #[inline]
699    pub fn local_axis(&self, bone: BoneIndex) -> Option<LocalAxis> {
700        if self.local_axis_flags[bone.as_usize()] != 0 {
701            Some(LocalAxis {
702                x: self.local_axis_x[bone.as_usize()],
703                z: self.local_axis_z[bone.as_usize()],
704            })
705        } else {
706            None
707        }
708    }
709
710    /// Orthonormal local-axis basis as a quaternion (columns of the bone-local
711    /// frame). Used only as the IK angle-limit evaluation frame.
712    #[inline]
713    pub fn local_axis_basis(&self, bone: BoneIndex) -> Option<Quat> {
714        if self.local_axis_flags[bone.as_usize()] != 0 {
715            Some(self.local_axis_basis[bone.as_usize()])
716        } else {
717            None
718        }
719    }
720
721    #[inline]
722    pub fn local_axis_count(&self) -> usize {
723        self.local_axis_flags
724            .iter()
725            .filter(|&&flag| flag != 0)
726            .count()
727    }
728
729    #[inline]
730    pub fn transform_after_physics(&self, bone: BoneIndex) -> bool {
731        self.transform_after_physics_flags[bone.as_usize()] != 0
732    }
733
734    #[inline]
735    pub(crate) fn is_ik_link_bone(&self, bone: BoneIndex) -> bool {
736        self.ik_link_flags[bone.as_usize()] != 0
737    }
738
739    #[inline]
740    pub fn eval_order(&self) -> &[BoneIndex] {
741        &self.eval_order
742    }
743
744    #[inline]
745    pub(crate) fn eval_order_for_phase(&self, after_physics: bool) -> &[BoneIndex] {
746        if after_physics {
747            &self.eval_order_after_physics
748        } else {
749            &self.eval_order_before_physics
750        }
751    }
752
753    #[inline]
754    pub(crate) fn eval_order_position(&self, bone: BoneIndex) -> usize {
755        self.eval_order_positions[bone.as_usize()]
756    }
757
758    #[inline]
759    pub fn ik_count(&self) -> usize {
760        self.ik_solvers.len()
761    }
762
763    #[inline]
764    pub fn ik_solvers(&self) -> &[IkSolver] {
765        &self.ik_solvers
766    }
767
768    #[inline]
769    pub(crate) fn ik_solver_count_for_bone(&self, bone: BoneIndex) -> usize {
770        self.ik_bone_solver_spans[bone.as_usize()].count as usize
771    }
772
773    #[inline]
774    pub(crate) fn ik_solver_index_for_bone(&self, bone: BoneIndex, local_index: usize) -> usize {
775        let span = &self.ik_bone_solver_spans[bone.as_usize()];
776        self.ik_bone_solver_indices[span.start as usize + local_index] as usize
777    }
778
779    #[inline]
780    pub fn append_transform_index(&self, bone: BoneIndex) -> Option<usize> {
781        let index = self.append_transform_indices[bone.as_usize()];
782        if index < 0 {
783            None
784        } else {
785            Some(index as usize)
786        }
787    }
788
789    #[inline]
790    pub fn append_transform(&self, append_index: usize) -> &AppendTransform {
791        &self.append_transforms[append_index]
792    }
793
794    #[inline]
795    pub fn append_transforms(&self) -> &[AppendTransform] {
796        &self.append_transforms
797    }
798
799    #[inline]
800    pub fn morph_count(&self) -> u32 {
801        self.morph_count
802    }
803
804    #[inline]
805    pub fn vertex_morph_offsets(&self) -> &[VertexMorphOffset] {
806        &self.vertex_morph_offsets
807    }
808
809    #[inline]
810    pub fn vertex_morph_spans(&self) -> &[MorphOffsetSpan] {
811        &self.vertex_morph_spans
812    }
813
814    #[inline]
815    pub fn bone_morph_offsets(&self) -> &[BoneMorphOffset] {
816        &self.bone_morph_offsets
817    }
818
819    #[inline]
820    pub fn bone_morph_spans(&self) -> &[MorphOffsetSpan] {
821        &self.bone_morph_spans
822    }
823
824    #[inline]
825    pub fn group_morph_offsets(&self) -> &[GroupMorphOffset] {
826        &self.group_morph_offsets
827    }
828
829    #[inline]
830    pub fn group_morph_spans(&self) -> &[MorphOffsetSpan] {
831        &self.group_morph_spans
832    }
833}
834
835#[derive(Clone, Debug, PartialEq)]
836pub struct IkSolver {
837    pub ik_bone: BoneIndex,
838    pub target_bone: BoneIndex,
839    pub links: Box<[IkLink]>,
840    pub iteration_count: u32,
841    pub limit_angle: f32,
842}
843
844#[derive(Clone, Debug, PartialEq)]
845pub struct IkLink {
846    pub bone: BoneIndex,
847    pub angle_limit: Option<IkAngleLimit>,
848}
849
850#[derive(Clone, Copy, Debug, PartialEq)]
851pub struct AppendTransform {
852    pub target_bone: BoneIndex,
853    pub source_bone: BoneIndex,
854    pub ratio: f32,
855    pub affect_rotation: bool,
856    pub affect_translation: bool,
857    pub local: bool,
858}
859
860type AppendTransformBuildOutput = (Box<[AppendTransform]>, Box<[i32]>);
861type IkSolverBuildOutput = (Box<[IkSolver]>, Box<[u8]>);
862
863fn build_ik_solvers(
864    ik_solvers: Vec<IkSolverInit>,
865    bone_count: usize,
866) -> Result<IkSolverBuildOutput, ModelBuildError> {
867    let mut solvers = Vec::with_capacity(ik_solvers.len());
868    let mut ik_link_flags = vec![0; bone_count];
869
870    for (solver_index, solver) in ik_solvers.into_iter().enumerate() {
871        validate_ik_bone(solver_index, "ik", solver.ik_bone, bone_count)?;
872        validate_ik_bone(solver_index, "target", solver.target_bone, bone_count)?;
873
874        let mut links = Vec::with_capacity(solver.links.len());
875        for link in solver.links {
876            validate_ik_bone(solver_index, "link", link.bone, bone_count)?;
877            ik_link_flags[link.bone.as_usize()] = 1;
878            links.push(IkLink {
879                bone: link.bone,
880                angle_limit: link.angle_limit,
881            });
882        }
883
884        solvers.push(IkSolver {
885            ik_bone: solver.ik_bone,
886            target_bone: solver.target_bone,
887            links: links.into_boxed_slice(),
888            iteration_count: solver.iteration_count,
889            limit_angle: solver.limit_angle,
890        });
891    }
892
893    Ok((solvers.into_boxed_slice(), ik_link_flags.into_boxed_slice()))
894}
895
896type IkBoneSolverLookup = (Box<[MorphOffsetSpan]>, Box<[u32]>);
897
898fn build_ik_bone_solver_lookup(ik_solvers: &[IkSolver], bone_count: usize) -> IkBoneSolverLookup {
899    let mut counts = vec![0u32; bone_count];
900    for solver in ik_solvers {
901        counts[solver.ik_bone.as_usize()] += 1;
902    }
903
904    let mut spans = Vec::with_capacity(bone_count);
905    let mut next_offset = 0u32;
906    for count in counts {
907        spans.push(MorphOffsetSpan {
908            start: next_offset,
909            count,
910        });
911        next_offset += count;
912    }
913
914    let mut indices = vec![0u32; ik_solvers.len()];
915    let mut write_positions: Vec<u32> = spans.iter().map(|span| span.start).collect();
916    for (solver_index, solver) in ik_solvers.iter().enumerate() {
917        let bone = solver.ik_bone.as_usize();
918        let write_index = write_positions[bone] as usize;
919        indices[write_index] = solver_index as u32;
920        write_positions[bone] += 1;
921    }
922
923    (spans.into_boxed_slice(), indices.into_boxed_slice())
924}
925
926fn validate_ik_bone(
927    solver: usize,
928    role: &'static str,
929    bone: BoneIndex,
930    bone_count: usize,
931) -> Result<(), ModelBuildError> {
932    if bone.as_usize() < bone_count {
933        Ok(())
934    } else {
935        Err(ModelBuildError::InvalidIkBone {
936            solver,
937            role,
938            bone: bone.0,
939        })
940    }
941}
942
943fn build_append_transforms(
944    append_transforms: Vec<AppendTransformInit>,
945    bone_count: usize,
946) -> Result<AppendTransformBuildOutput, ModelBuildError> {
947    let mut transforms = Vec::with_capacity(append_transforms.len());
948    let mut indices = vec![-1; bone_count];
949
950    for (append_index, append) in append_transforms.into_iter().enumerate() {
951        validate_append_bone(append_index, "target", append.target_bone, bone_count)?;
952        validate_append_bone(append_index, "source", append.source_bone, bone_count)?;
953
954        let target = append.target_bone.as_usize();
955        if indices[target] >= 0 {
956            return Err(ModelBuildError::DuplicateAppendTransform {
957                bone: append.target_bone.0,
958            });
959        }
960        indices[target] = append_index as i32;
961        transforms.push(AppendTransform {
962            target_bone: append.target_bone,
963            source_bone: append.source_bone,
964            ratio: append.ratio,
965            affect_rotation: append.affect_rotation,
966            affect_translation: append.affect_translation,
967            local: append.local,
968        });
969    }
970
971    Ok((transforms.into_boxed_slice(), indices.into_boxed_slice()))
972}
973
974fn validate_append_bone(
975    append: usize,
976    role: &'static str,
977    bone: BoneIndex,
978    bone_count: usize,
979) -> Result<(), ModelBuildError> {
980    if bone.as_usize() < bone_count {
981        Ok(())
982    } else {
983        Err(ModelBuildError::InvalidAppendBone {
984            append,
985            role,
986            bone: bone.0,
987        })
988    }
989}
990
991fn build_eval_order(
992    parent_indices: &[i32],
993    transform_orders: &[i32],
994) -> Result<Box<[BoneIndex]>, ModelBuildError> {
995    let mut state = vec![VisitState::Unvisited; parent_indices.len()];
996    let mut order = Vec::with_capacity(parent_indices.len());
997    let mut start_order = Vec::with_capacity(parent_indices.len());
998    for bone in 0..parent_indices.len() {
999        start_order.push(bone);
1000    }
1001    start_order.sort_by_key(|bone| (transform_orders[*bone], *bone));
1002
1003    for bone in start_order {
1004        visit_bone(bone, parent_indices, &mut state, &mut order)?;
1005    }
1006
1007    Ok(order.into_boxed_slice())
1008}
1009
1010fn build_eval_order_for_phase(
1011    eval_order: &[BoneIndex],
1012    transform_after_physics_flags: &[u8],
1013    after_physics: bool,
1014) -> Box<[BoneIndex]> {
1015    eval_order
1016        .iter()
1017        .copied()
1018        .filter(|bone| {
1019            let bone_after_physics = transform_after_physics_flags[bone.as_usize()] != 0;
1020            bone_after_physics == after_physics
1021        })
1022        .collect()
1023}
1024
1025fn build_eval_order_positions(eval_order: &[BoneIndex], bone_count: usize) -> Box<[usize]> {
1026    let mut positions = vec![0; bone_count];
1027    for (position, bone) in eval_order.iter().enumerate() {
1028        positions[bone.as_usize()] = position;
1029    }
1030    positions.into_boxed_slice()
1031}
1032
1033fn visit_bone(
1034    bone: usize,
1035    parent_indices: &[i32],
1036    state: &mut [VisitState],
1037    order: &mut Vec<BoneIndex>,
1038) -> Result<(), ModelBuildError> {
1039    if state[bone] == VisitState::Visited {
1040        return Ok(());
1041    }
1042
1043    // Explicit post-order stack keeps malformed/deep host descriptors from
1044    // consuming the native call stack.  `expanded` is the return edge from a
1045    // child to its parent in the former recursive implementation.
1046    let mut stack = vec![(bone, false)];
1047    while let Some((current, expanded)) = stack.pop() {
1048        if expanded {
1049            state[current] = VisitState::Visited;
1050            order.push(BoneIndex(current as u32));
1051            continue;
1052        }
1053
1054        match state[current] {
1055            VisitState::Visited => continue,
1056            VisitState::Visiting => {
1057                return Err(ModelBuildError::ParentCycle { bone: current });
1058            }
1059            VisitState::Unvisited => {}
1060        }
1061
1062        state[current] = VisitState::Visiting;
1063        stack.push((current, true));
1064        let parent = parent_indices[current];
1065        if parent >= 0 {
1066            stack.push((parent as usize, false));
1067        }
1068    }
1069
1070    Ok(())
1071}
1072
1073#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1074enum VisitState {
1075    Unvisited,
1076    Visiting,
1077    Visited,
1078}
1079
1080fn validate_morph_init(morph: &MorphInit, bone_count: usize) -> Result<(), ModelBuildError> {
1081    let morph_count = morph.morph_count as usize;
1082    if !morph.vertex_spans.is_empty() || !morph.vertex_offsets.is_empty() {
1083        validate_morph_spans(
1084            "vertex",
1085            &morph.vertex_spans,
1086            morph_count,
1087            morph.vertex_offsets.len(),
1088        )?;
1089    }
1090    validate_morph_spans(
1091        "bone",
1092        &morph.bone_spans,
1093        morph_count,
1094        morph.bone_offsets.len(),
1095    )?;
1096    validate_morph_spans(
1097        "group",
1098        &morph.group_spans,
1099        morph_count,
1100        morph.group_offsets.len(),
1101    )?;
1102
1103    for (offset_index, offset) in morph.bone_offsets.iter().enumerate() {
1104        if offset.target_bone.as_usize() >= bone_count {
1105            return Err(ModelBuildError::InvalidBoneMorphBone {
1106                offset: offset_index,
1107                bone: offset.target_bone.0,
1108            });
1109        }
1110    }
1111
1112    for (morph_index, span) in morph.group_spans.iter().enumerate() {
1113        for offset_index in span.start..span.start + span.count {
1114            let child = morph.group_offsets[offset_index as usize].child_morph;
1115            if child.as_usize() >= morph_count {
1116                return Err(ModelBuildError::InvalidGroupMorphChild {
1117                    morph: morph_index,
1118                    child: child.0,
1119                });
1120            }
1121        }
1122    }
1123    validate_group_morph_cycles(morph)?;
1124
1125    Ok(())
1126}
1127
1128fn validate_group_morph_cycles(morph: &MorphInit) -> Result<(), ModelBuildError> {
1129    let mut sources = try_vec_with_capacity(morph.group_offsets.len())?;
1130    sources.extend(0..morph.group_offsets.len());
1131    validate_group_morph_cycles_with_sources(morph, &sources).map_err(|error| match error {
1132        ModelBuildError::GroupMorphCycleAt { morph, .. } => {
1133            ModelBuildError::GroupMorphCycle { morph }
1134        }
1135        other => other,
1136    })
1137}
1138
1139fn validate_group_morph_cycles_with_sources(
1140    morph: &MorphInit,
1141    sources: &[usize],
1142) -> Result<(), ModelBuildError> {
1143    let mut state = try_vec_with_capacity(morph.morph_count as usize)?;
1144    state.resize(morph.morph_count as usize, VisitState::Unvisited);
1145    let mut stack = try_vec_with_capacity(morph.morph_count as usize)?;
1146    for morph_index in 0..morph.morph_count as usize {
1147        visit_group_morph(morph_index, morph, &mut state, sources, &mut stack)?;
1148    }
1149    Ok(())
1150}
1151
1152fn visit_group_morph(
1153    morph_index: usize,
1154    morph: &MorphInit,
1155    state: &mut [VisitState],
1156    sources: &[usize],
1157    stack: &mut Vec<(usize, usize)>,
1158) -> Result<(), ModelBuildError> {
1159    if state[morph_index] == VisitState::Visited {
1160        return Ok(());
1161    }
1162
1163    // Iterative DFS equivalent to the previous recursive walk.  The second
1164    // tuple member is the next offset in this morph's span to inspect.
1165    stack.clear();
1166    stack.push((morph_index, 0usize));
1167    state[morph_index] = VisitState::Visiting;
1168    while let Some((current, next_offset)) = stack.last_mut() {
1169        let span = morph.group_spans[*current];
1170        if *next_offset >= span.count as usize {
1171            state[*current] = VisitState::Visited;
1172            stack.pop();
1173            continue;
1174        }
1175
1176        let offset_index = span.start as usize + *next_offset;
1177        *next_offset += 1;
1178        let child = morph.group_offsets[offset_index].child_morph.as_usize();
1179        if morph.group_spans[child].count == 0 {
1180            continue;
1181        }
1182        match state[child] {
1183            VisitState::Visited => {}
1184            VisitState::Visiting => {
1185                return Err(ModelBuildError::GroupMorphCycleAt {
1186                    morph: child,
1187                    offset: sources[offset_index],
1188                });
1189            }
1190            VisitState::Unvisited => {
1191                state[child] = VisitState::Visiting;
1192                stack.push((child, 0));
1193            }
1194        }
1195    }
1196
1197    Ok(())
1198}
1199
1200fn validate_morph_spans(
1201    kind: &'static str,
1202    spans: &[MorphOffsetSpan],
1203    morph_count: usize,
1204    offset_count: usize,
1205) -> Result<(), ModelBuildError> {
1206    if spans.len() != morph_count {
1207        return Err(ModelBuildError::InvalidMorphSpanCount {
1208            actual: spans.len(),
1209            expected: morph_count,
1210        });
1211    }
1212
1213    for (morph_index, span) in spans.iter().enumerate() {
1214        let start = span.start as usize;
1215        let count = span.count as usize;
1216        if start
1217            .checked_add(count)
1218            .is_none_or(|end| end > offset_count)
1219        {
1220            return Err(ModelBuildError::InvalidMorphSpan {
1221                morph: morph_index,
1222                kind,
1223            });
1224        }
1225    }
1226
1227    Ok(())
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232    use super::*;
1233
1234    #[test]
1235    fn reports_capacity_overflow_as_morph_storage_allocation_error() {
1236        assert_eq!(
1237            try_vec_with_capacity::<u8>(usize::MAX).unwrap_err(),
1238            ModelBuildError::MorphStorageAllocation
1239        );
1240    }
1241
1242    #[test]
1243    fn rejects_invalid_parent() {
1244        let error =
1245            ModelArena::new(vec![BoneInit::new(Some(BoneIndex(10)), Vec3A::ZERO)]).unwrap_err();
1246
1247        assert_eq!(
1248            error,
1249            ModelBuildError::InvalidParent {
1250                bone: 0,
1251                parent: 10
1252            }
1253        );
1254    }
1255
1256    #[test]
1257    fn eval_order_for_phase_preserves_relative_order_within_each_phase() {
1258        let mut root = BoneInit::new(None, Vec3A::ZERO);
1259        root.transform_order = 0;
1260        let mut pre_child = BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO);
1261        pre_child.transform_order = 2;
1262        let mut after_parent = BoneInit::new(None, Vec3A::ZERO);
1263        after_parent.transform_order = 1;
1264        after_parent.transform_after_physics = true;
1265        let mut after_child = BoneInit::new(Some(BoneIndex(2)), Vec3A::ZERO);
1266        after_child.transform_order = 3;
1267        after_child.transform_after_physics = true;
1268        let mut pre_sibling = BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO);
1269        pre_sibling.transform_order = 4;
1270
1271        let model = ModelArena::new(vec![
1272            root,
1273            pre_child,
1274            after_parent,
1275            after_child,
1276            pre_sibling,
1277        ])
1278        .unwrap();
1279
1280        let expected_before_physics: Vec<_> = model
1281            .eval_order()
1282            .iter()
1283            .copied()
1284            .filter(|bone| !model.transform_after_physics(*bone))
1285            .collect();
1286        let expected_after_physics: Vec<_> = model
1287            .eval_order()
1288            .iter()
1289            .copied()
1290            .filter(|bone| model.transform_after_physics(*bone))
1291            .collect();
1292
1293        assert_eq!(
1294            model.eval_order_for_phase(false),
1295            expected_before_physics.as_slice()
1296        );
1297        assert_eq!(
1298            model.eval_order_for_phase(true),
1299            expected_after_physics.as_slice()
1300        );
1301        assert_eq!(
1302            model.eval_order_for_phase(false),
1303            &[BoneIndex(0), BoneIndex(1), BoneIndex(4)]
1304        );
1305        assert_eq!(
1306            model.eval_order_for_phase(true),
1307            &[BoneIndex(2), BoneIndex(3)]
1308        );
1309    }
1310
1311    #[test]
1312    fn parent_is_ordered_before_child_even_if_input_order_is_not_transform_order() {
1313        let mut root = BoneInit::new(None, Vec3A::ZERO);
1314        root.transform_order = 10;
1315        let child = BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO);
1316
1317        let model = ModelArena::new(vec![root, child]).unwrap();
1318
1319        assert_eq!(model.eval_order(), &[BoneIndex(0), BoneIndex(1)]);
1320    }
1321
1322    #[test]
1323    fn stores_ik_solver_descriptors() {
1324        let solver = IkSolverInit {
1325            ik_bone: BoneIndex(2),
1326            target_bone: BoneIndex(1),
1327            links: vec![
1328                IkLinkInit::new(BoneIndex(0))
1329                    .with_angle_limit(IkAngleLimit::new(Vec3A::splat(-1.0), Vec3A::splat(1.0))),
1330            ],
1331            iteration_count: 4,
1332            limit_angle: 0.5,
1333        };
1334
1335        let model = ModelArena::new_with_ik(
1336            vec![
1337                BoneInit::new(None, Vec3A::ZERO),
1338                BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1339                BoneInit::new(Some(BoneIndex(1)), Vec3A::ZERO),
1340            ],
1341            vec![solver],
1342        )
1343        .unwrap();
1344
1345        assert_eq!(model.ik_count(), 1);
1346        assert_eq!(model.ik_solvers()[0].ik_bone, BoneIndex(2));
1347        assert_eq!(model.ik_solvers()[0].target_bone, BoneIndex(1));
1348        assert_eq!(model.ik_solvers()[0].links[0].bone, BoneIndex(0));
1349        assert_eq!(model.ik_solvers()[0].iteration_count, 4);
1350        assert_eq!(model.ik_solvers()[0].limit_angle, 0.5);
1351    }
1352
1353    #[test]
1354    fn ik_bone_solver_lookup_maps_multiple_bones_and_preserves_registration_order() {
1355        let model = ModelArena::new_with_ik(
1356            vec![
1357                BoneInit::new(None, Vec3A::ZERO),
1358                BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1359                BoneInit::new(Some(BoneIndex(1)), Vec3A::ZERO),
1360                BoneInit::new(None, Vec3A::ZERO),
1361                BoneInit::new(None, Vec3A::ZERO),
1362            ],
1363            vec![
1364                IkSolverInit::new(
1365                    BoneIndex(2),
1366                    BoneIndex(1),
1367                    vec![IkLinkInit::new(BoneIndex(0))],
1368                ),
1369                IkSolverInit::new(
1370                    BoneIndex(4),
1371                    BoneIndex(3),
1372                    vec![IkLinkInit::new(BoneIndex(0))],
1373                ),
1374                IkSolverInit::new(
1375                    BoneIndex(2),
1376                    BoneIndex(1),
1377                    vec![IkLinkInit::new(BoneIndex(0))],
1378                ),
1379            ],
1380        )
1381        .unwrap();
1382
1383        assert_eq!(model.ik_solver_count_for_bone(BoneIndex(0)), 0);
1384        assert_eq!(model.ik_solver_count_for_bone(BoneIndex(1)), 0);
1385        assert_eq!(model.ik_solver_count_for_bone(BoneIndex(2)), 2);
1386        assert_eq!(model.ik_solver_count_for_bone(BoneIndex(3)), 0);
1387        assert_eq!(model.ik_solver_count_for_bone(BoneIndex(4)), 1);
1388        assert_eq!(model.ik_solver_index_for_bone(BoneIndex(2), 0), 0);
1389        assert_eq!(model.ik_solver_index_for_bone(BoneIndex(2), 1), 2);
1390        assert_eq!(model.ik_solver_index_for_bone(BoneIndex(4), 0), 1);
1391    }
1392
1393    #[test]
1394    fn stores_normalized_fixed_axis_descriptors() {
1395        let model = ModelArena::new(vec![
1396            BoneInit::new(None, Vec3A::ZERO).with_fixed_axis(Vec3A::new(0.0, 2.0, 0.0)),
1397            BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1398        ])
1399        .unwrap();
1400
1401        assert_eq!(model.fixed_axis(BoneIndex(0)), Some(Vec3A::Y));
1402        assert_eq!(model.fixed_axis(BoneIndex(1)), None);
1403    }
1404
1405    #[test]
1406    fn stores_local_axis_descriptors_and_basis() {
1407        let model = ModelArena::new(vec![
1408            BoneInit::new(None, Vec3A::ZERO),
1409            BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1410            BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1411        ])
1412        .unwrap()
1413        .with_local_axes([
1414            Some(LocalAxis::new(
1415                Vec3A::new(0.0, 1.0, 0.0),
1416                Vec3A::new(0.0, 0.0, 1.0),
1417            )),
1418            Some(LocalAxis::new(Vec3A::ZERO, Vec3A::Z)),
1419            None,
1420        ]);
1421
1422        let axis = model.local_axis(BoneIndex(0)).expect("local axis retained");
1423        assert_eq!(axis.x, Vec3A::new(0.0, 1.0, 0.0));
1424        assert_eq!(axis.z, Vec3A::new(0.0, 0.0, 1.0));
1425        let basis = model
1426            .local_axis_basis(BoneIndex(0))
1427            .expect("local axis basis");
1428        // Right-handed rebuild: x=(0,1,0), y=z×x=(0,0,1)×(0,1,0)=(-1,0,0), z=x×y=(0,0,1).
1429        let x_dir = basis * Vec3A::X;
1430        let y_dir = basis * Vec3A::Y;
1431        let z_dir = basis * Vec3A::Z;
1432        assert!((x_dir - Vec3A::Y).length() < 1.0e-5);
1433        assert!((y_dir - (-Vec3A::X)).length() < 1.0e-5);
1434        assert!((z_dir - Vec3A::Z).length() < 1.0e-5);
1435        assert!(model.local_axis(BoneIndex(1)).is_none());
1436        assert!(model.local_axis_basis(BoneIndex(1)).is_none());
1437        assert!(model.local_axis(BoneIndex(2)).is_none());
1438        assert_eq!(model.local_axis_count(), 1);
1439    }
1440
1441    #[test]
1442    fn existing_bone_init_constructors_have_no_local_axes() {
1443        // Source-compatible construction: BoneInit::new and full struct literal
1444        // without any local-axis field retain empty local-axis storage.
1445        let via_new = ModelArena::new(vec![BoneInit::new(None, Vec3A::ZERO)]).unwrap();
1446        assert_eq!(via_new.local_axis_count(), 0);
1447        assert!(via_new.local_axis(BoneIndex(0)).is_none());
1448
1449        let via_literal = ModelArena::new(vec![BoneInit {
1450            parent: None,
1451            rest_position: Vec3A::ZERO,
1452            inverse_bind_matrix: Mat4::IDENTITY,
1453            transform_order: 0,
1454            transform_after_physics: false,
1455            fixed_axis: None,
1456            enforce_fixed_axis: false,
1457        }])
1458        .unwrap();
1459        assert_eq!(via_literal.local_axis_count(), 0);
1460    }
1461
1462    #[test]
1463    fn rejects_invalid_ik_link_bone() {
1464        let error = ModelArena::new_with_ik(
1465            vec![BoneInit::new(None, Vec3A::ZERO)],
1466            vec![IkSolverInit::new(
1467                BoneIndex(0),
1468                BoneIndex(0),
1469                vec![IkLinkInit::new(BoneIndex(10))],
1470            )],
1471        )
1472        .unwrap_err();
1473
1474        assert_eq!(
1475            error,
1476            ModelBuildError::InvalidIkBone {
1477                solver: 0,
1478                role: "link",
1479                bone: 10,
1480            }
1481        );
1482    }
1483
1484    #[test]
1485    fn stores_append_transform_descriptors() {
1486        let model = ModelArena::new_full(
1487            vec![
1488                BoneInit::new(None, Vec3A::ZERO),
1489                BoneInit::new(None, Vec3A::ZERO),
1490            ],
1491            Vec::new(),
1492            vec![
1493                AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 0.5)
1494                    .with_rotation()
1495                    .with_translation(),
1496            ],
1497        )
1498        .unwrap();
1499
1500        let append_index = model.append_transform_index(BoneIndex(1)).unwrap();
1501        let append = model.append_transform(append_index);
1502        assert_eq!(append.source_bone, BoneIndex(0));
1503        assert_eq!(append.ratio, 0.5);
1504        assert!(append.affect_rotation);
1505        assert!(append.affect_translation);
1506    }
1507
1508    #[test]
1509    fn rejects_duplicate_append_transform_target() {
1510        let error = ModelArena::new_full(
1511            vec![
1512                BoneInit::new(None, Vec3A::ZERO),
1513                BoneInit::new(None, Vec3A::ZERO),
1514            ],
1515            Vec::new(),
1516            vec![
1517                AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 1.0),
1518                AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 1.0),
1519            ],
1520        )
1521        .unwrap_err();
1522
1523        assert_eq!(error, ModelBuildError::DuplicateAppendTransform { bone: 1 });
1524    }
1525
1526    #[test]
1527    fn rejects_invalid_bone_morph_target_bone() {
1528        let error = ModelArena::new_with_morphs(
1529            vec![BoneInit::new(None, Vec3A::ZERO)],
1530            Vec::new(),
1531            Vec::new(),
1532            MorphInit {
1533                morph_count: 1,
1534                bone_offsets: vec![BoneMorphOffset {
1535                    target_bone: BoneIndex(10),
1536                    position_offset: Vec3A::ZERO,
1537                    rotation_offset: Quat::IDENTITY,
1538                }],
1539                bone_spans: vec![MorphOffsetSpan { start: 0, count: 1 }],
1540                group_offsets: Vec::new(),
1541                group_spans: vec![MorphOffsetSpan::default()],
1542                ..MorphInit::default()
1543            },
1544        )
1545        .unwrap_err();
1546
1547        assert_eq!(
1548            error,
1549            ModelBuildError::InvalidBoneMorphBone {
1550                offset: 0,
1551                bone: 10
1552            }
1553        );
1554    }
1555
1556    #[test]
1557    fn accepts_group_morph_child_that_is_later() {
1558        let model = ModelArena::new_with_morphs(
1559            vec![BoneInit::new(None, Vec3A::ZERO)],
1560            Vec::new(),
1561            Vec::new(),
1562            MorphInit {
1563                morph_count: 2,
1564                bone_offsets: Vec::new(),
1565                bone_spans: vec![MorphOffsetSpan::default(), MorphOffsetSpan::default()],
1566                group_offsets: vec![GroupMorphOffset {
1567                    child_morph: MorphIndex(1),
1568                    ratio: 1.0,
1569                }],
1570                group_spans: vec![
1571                    MorphOffsetSpan { start: 0, count: 1 },
1572                    MorphOffsetSpan::default(),
1573                ],
1574                ..MorphInit::default()
1575            },
1576        )
1577        .unwrap();
1578
1579        assert_eq!(model.morph_count(), 2);
1580    }
1581
1582    #[test]
1583    fn rejects_group_morph_child_out_of_range() {
1584        let error = ModelArena::new_with_morphs(
1585            vec![BoneInit::new(None, Vec3A::ZERO)],
1586            Vec::new(),
1587            Vec::new(),
1588            MorphInit {
1589                morph_count: 2,
1590                bone_offsets: Vec::new(),
1591                bone_spans: vec![MorphOffsetSpan::default(), MorphOffsetSpan::default()],
1592                group_offsets: vec![GroupMorphOffset {
1593                    child_morph: MorphIndex(2),
1594                    ratio: 1.0,
1595                }],
1596                group_spans: vec![
1597                    MorphOffsetSpan { start: 0, count: 1 },
1598                    MorphOffsetSpan::default(),
1599                ],
1600                ..MorphInit::default()
1601            },
1602        )
1603        .unwrap_err();
1604
1605        assert_eq!(
1606            error,
1607            ModelBuildError::InvalidGroupMorphChild { morph: 0, child: 2 }
1608        );
1609    }
1610
1611    #[test]
1612    fn rejects_group_morph_cycle() {
1613        let error = ModelArena::new_with_morphs(
1614            vec![BoneInit::new(None, Vec3A::ZERO)],
1615            Vec::new(),
1616            Vec::new(),
1617            MorphInit {
1618                morph_count: 2,
1619                bone_offsets: Vec::new(),
1620                bone_spans: vec![MorphOffsetSpan::default(), MorphOffsetSpan::default()],
1621                group_offsets: vec![
1622                    GroupMorphOffset {
1623                        child_morph: MorphIndex(1),
1624                        ratio: 1.0,
1625                    },
1626                    GroupMorphOffset {
1627                        child_morph: MorphIndex(0),
1628                        ratio: 1.0,
1629                    },
1630                ],
1631                group_spans: vec![
1632                    MorphOffsetSpan { start: 0, count: 1 },
1633                    MorphOffsetSpan { start: 1, count: 1 },
1634                ],
1635                ..MorphInit::default()
1636            },
1637        )
1638        .unwrap_err();
1639
1640        assert_eq!(error, ModelBuildError::GroupMorphCycle { morph: 0 });
1641    }
1642
1643    #[test]
1644    fn stores_vertex_morph_offsets() {
1645        let model = ModelArena::new_with_morphs(
1646            vec![BoneInit::new(None, Vec3A::ZERO)],
1647            Vec::new(),
1648            Vec::new(),
1649            MorphInit {
1650                morph_count: 1,
1651                vertex_offsets: vec![VertexMorphOffset {
1652                    vertex_index: 7,
1653                    position_offset: Vec3A::new(1.0, 2.0, 3.0),
1654                }],
1655                vertex_spans: vec![MorphOffsetSpan { start: 0, count: 1 }],
1656                bone_spans: vec![MorphOffsetSpan::default()],
1657                group_spans: vec![MorphOffsetSpan::default()],
1658                ..MorphInit::default()
1659            },
1660        )
1661        .unwrap();
1662
1663        assert_eq!(
1664            model.vertex_morph_offsets(),
1665            &[VertexMorphOffset {
1666                vertex_index: 7,
1667                position_offset: Vec3A::new(1.0, 2.0, 3.0),
1668            }]
1669        );
1670        assert_eq!(
1671            model.vertex_morph_spans(),
1672            &[MorphOffsetSpan { start: 0, count: 1 }]
1673        );
1674    }
1675}