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