1use crate::model::{
8 AffineDomainViolation, Bone, Document, MeshAsset, MeshInstance, PositiveUniformAffineTolerance,
9 SceneAsset, SceneAssets, Skeleton, SourceSkeletonAssets, Transform, WorldMatrixError,
10 affine_axis_lengths, classify_positive_uniform_affine, world_rest_matrices,
11};
12use glam::{Mat3, Mat4, Vec3};
13use std::collections::BTreeSet;
14
15const MATRIX_EPSILON: f32 = 1.0e-4;
16const MIN_RELATIVE_DETERMINANT: f32 = 1.0e-6;
17const COORDINATE_AFFINE_TOLERANCE: PositiveUniformAffineTolerance =
18 PositiveUniformAffineTolerance {
19 equal_axis: MATRIX_EPSILON as f64,
20 relative_orthogonality: MATRIX_EPSILON as f64,
21 singular_determinant_relative: 0.0,
22 };
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum SkinnedBindPosePlacement {
27 #[default]
29 Preserve,
30 GroundAndCenter,
32}
33
34#[derive(Debug, Clone, Copy)]
36pub struct SkinnedBindPoseCanonicalizationOptions {
37 pub source_to_meters_y_up: Mat4,
41 pub placement: SkinnedBindPosePlacement,
43}
44
45impl Default for SkinnedBindPoseCanonicalizationOptions {
46 fn default() -> Self {
47 Self {
48 source_to_meters_y_up: Mat4::IDENTITY,
49 placement: SkinnedBindPosePlacement::Preserve,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56#[non_exhaustive]
57pub struct SkinnedBindPoseCanonicalization {
58 pub document: Document,
60 pub source_world_to_canonical: Mat4,
62 pub converted_bounds_min: Vec3,
64 pub converted_bounds_max: Vec3,
66}
67
68#[derive(Debug, thiserror::Error)]
70#[non_exhaustive]
71pub enum SkinnedBindPoseCanonicalizationError {
72 #[error("cannot canonicalize skinned bind pose: document has no mesh instances")]
74 NoInstances,
75 #[error(
77 "cannot canonicalize skinned bind pose: clip {clip:?} animates node {node} ({property})"
78 )]
79 AnimationTrack {
80 clip: String,
82 node: usize,
84 property: &'static str,
86 },
87 #[error("cannot canonicalize skinned bind pose: source_to_meters_y_up is invalid ({reason})")]
89 InvalidCoordinateTransform {
90 reason: &'static str,
92 },
93 #[error(
95 "cannot canonicalize skinned bind pose: skeleton node {node} has invalid parent {parent}"
96 )]
97 InvalidParent {
98 node: usize,
100 parent: usize,
102 },
103 #[error(
105 "cannot canonicalize skinned bind pose: skeleton node {node} has a non-finite transform"
106 )]
107 NonFiniteTransform {
108 node: usize,
110 },
111 #[error(
113 "cannot canonicalize skinned bind pose: skeleton root {node} cannot be represented as TRS"
114 )]
115 NonTrsRoot {
116 node: usize,
118 },
119 #[error(
121 "cannot canonicalize skinned bind pose: source node {source_node_index} references missing {kind} {index}"
122 )]
123 MissingReference {
124 source_node_index: usize,
126 kind: &'static str,
128 index: usize,
130 },
131 #[error(
133 "cannot canonicalize skinned bind pose: source node {source_node_index} is not skinned"
134 )]
135 UnskinnedInstance {
136 source_node_index: usize,
138 },
139 #[error(
141 "cannot canonicalize skinned bind pose: source node {source_node_index} has {ibms} inverse bind matrices for {joints} joints"
142 )]
143 InverseBindCount {
144 source_node_index: usize,
146 ibms: usize,
148 joints: usize,
150 },
151 #[error(
153 "cannot canonicalize skinned bind pose: source node {source_node_index} joint {joint} has no inverse bind matrix"
154 )]
155 MissingInverseBind {
156 source_node_index: usize,
158 joint: usize,
160 },
161 #[error(
163 "cannot canonicalize skinned bind pose: source node {source_node_index} joint {joint} has an inconsistent inverse bind matrix"
164 )]
165 InconsistentInverseBind {
166 source_node_index: usize,
168 joint: usize,
170 },
171 #[error(
173 "cannot canonicalize skinned bind pose: joint {joint} has a singular or near-singular rest transform"
174 )]
175 SingularJoint {
176 joint: usize,
178 },
179 #[error(
182 "cannot canonicalize skinned bind pose: source node {source_node_index} has {reason} geometry transform"
183 )]
184 InvalidGeometryTransform {
185 source_node_index: usize,
187 reason: &'static str,
189 },
190 #[error(
192 "cannot canonicalize skinned bind pose: mesh {mesh} primitive {primitive} is invalid ({reason})"
193 )]
194 InvalidPrimitive {
195 mesh: usize,
197 primitive: usize,
199 reason: &'static str,
201 },
202 #[error(
204 "cannot canonicalize skinned bind pose: mesh {mesh} primitive {primitive} has non-finite {attribute} at vertex {vertex}"
205 )]
206 NonFiniteAttribute {
207 mesh: usize,
209 primitive: usize,
211 attribute: &'static str,
213 vertex: usize,
215 },
216 #[error(
218 "cannot canonicalize skinned bind pose: mesh {mesh} primitive {primitive} has zero normal at vertex {vertex}"
219 )]
220 ZeroNormal {
221 mesh: usize,
223 primitive: usize,
225 vertex: usize,
227 },
228}
229
230#[derive(Debug, Clone, Copy)]
231struct Plan {
232 instance: usize,
233 geometry_world: Mat4,
234}
235
236pub fn canonicalize_skinned_bind_pose(
252 doc: &Document,
253 options: SkinnedBindPoseCanonicalizationOptions,
254) -> Result<SkinnedBindPoseCanonicalization, SkinnedBindPoseCanonicalizationError> {
255 validate_coordinate_transform(options.source_to_meters_y_up)?;
256 let source_worlds = world_matrices(&doc.skeleton)?;
257 let plans = validate(doc, &source_worlds)?;
258 let (bounds_min, bounds_max) = converted_bounds(doc, &plans, options.source_to_meters_y_up)?;
259 let placement = placement_transform(options.placement, bounds_min, bounds_max);
260 let source_world_to_canonical = placement * options.source_to_meters_y_up;
261 let mut skeleton = canonical_skeleton(&doc.skeleton, source_world_to_canonical)?;
262 let canonical_worlds = world_matrices(&skeleton)?;
263 let mut meshes = Vec::with_capacity(plans.len());
264 let mut instances = Vec::with_capacity(plans.len());
265 let mut joints_with_skins = BTreeSet::new();
266
267 for plan in plans {
268 let input = &doc.assets.instances[plan.instance];
269 let output_mesh = meshes.len();
270 let geometry_transform = source_world_to_canonical * plan.geometry_world;
271 meshes.push(transform_mesh(
272 &doc.assets.meshes[input.mesh],
273 input.mesh,
274 geometry_transform,
275 )?);
276 let skin_joints: Vec<usize> = input.skin_joints.iter().map(|joint| joint + 1).collect();
277 let skin_ibms = skin_joints
278 .iter()
279 .map(|&joint| {
280 let inverse = inverse_joint_matrix(canonical_worlds[joint], joint)?;
281 joints_with_skins.insert(joint);
282 Ok(inverse)
283 })
284 .collect::<Result<Vec<_>, _>>()?;
285 instances.push(MeshInstance {
286 source_node_index: input.source_node_index,
287 node: 0,
288 mesh: output_mesh,
289 skin_joints,
290 skin_ibms,
291 });
292 }
293 for joint in joints_with_skins {
294 skeleton.bones[joint].inverse_bind =
295 Some(inverse_joint_matrix(canonical_worlds[joint], joint)?);
296 }
297 Ok(SkinnedBindPoseCanonicalization {
298 document: Document {
299 skeleton,
300 clips: doc.clips.clone(),
301 assets: SceneAssets {
302 meshes,
303 instances,
304 materials: doc.assets.materials.clone(),
305 material_resources: doc.assets.material_resources.clone(),
306 scenes: vec![SceneAsset {
307 source_scene_index: 0,
308 name: None,
309 roots: vec![0],
310 }],
311 default_scene: Some(0),
312 source_skeleton: SourceSkeletonAssets::default(),
313 },
314 source: doc.source.clone(),
315 },
316 source_world_to_canonical,
317 converted_bounds_min: bounds_min,
318 converted_bounds_max: bounds_max,
319 })
320}
321
322fn validate_coordinate_transform(
323 transform: Mat4,
324) -> Result<(), SkinnedBindPoseCanonicalizationError> {
325 if !matrix4_is_finite(transform) {
326 return Err(
327 SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
328 reason: "non_finite",
329 },
330 );
331 }
332 if !transform.w_axis.abs_diff_eq(glam::Vec4::W, MATRIX_EPSILON) {
333 return Err(
334 SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
335 reason: "non_affine",
336 },
337 );
338 }
339 let linear = Mat3::from_mat4(transform);
340 let scale = affine_axis_lengths(linear)[0];
341 if !scale.is_finite() || scale <= f64::from(MATRIX_EPSILON) {
342 return Err(
343 SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
344 reason: "zero_scale",
345 },
346 );
347 }
348 classify_positive_uniform_affine(linear, COORDINATE_AFFINE_TOLERANCE)
349 .map(|_| ())
350 .map_err(
351 |violation| SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
352 reason: coordinate_transform_violation_reason(violation),
353 },
354 )
355}
356
357fn coordinate_transform_violation_reason(violation: AffineDomainViolation) -> &'static str {
360 match violation {
361 AffineDomainViolation::NonFinite => "non_finite",
362 AffineDomainViolation::NonUniformScale | AffineDomainViolation::Sheared => {
363 "non_uniform_or_sheared"
364 }
365 AffineDomainViolation::Reflected | AffineDomainViolation::Singular => {
366 "reflection_or_singular"
367 }
368 }
369}
370
371fn validate(
372 doc: &Document,
373 worlds: &[Mat4],
374) -> Result<Vec<Plan>, SkinnedBindPoseCanonicalizationError> {
375 if doc.assets.instances.is_empty() {
376 return Err(SkinnedBindPoseCanonicalizationError::NoInstances);
377 }
378 for clip in &doc.clips {
379 if let Some(track) = clip.tracks.first() {
380 return Err(SkinnedBindPoseCanonicalizationError::AnimationTrack {
381 clip: clip.name.clone(),
382 node: track.bone,
383 property: track.property.as_str(),
384 });
385 }
386 }
387 let mut plans = Vec::with_capacity(doc.assets.instances.len());
388 for (instance_ordinal, instance) in doc.assets.instances.iter().enumerate() {
389 let mesh = doc.assets.meshes.get(instance.mesh).ok_or(
390 SkinnedBindPoseCanonicalizationError::MissingReference {
391 source_node_index: instance.source_node_index,
392 kind: "mesh",
393 index: instance.mesh,
394 },
395 )?;
396 worlds.get(instance.node).ok_or(
402 SkinnedBindPoseCanonicalizationError::MissingReference {
403 source_node_index: instance.source_node_index,
404 kind: "node",
405 index: instance.node,
406 },
407 )?;
408 if instance.skin_joints.is_empty() {
409 return Err(SkinnedBindPoseCanonicalizationError::UnskinnedInstance {
410 source_node_index: instance.source_node_index,
411 });
412 }
413 if !instance.skin_ibms.is_empty() && instance.skin_ibms.len() != instance.skin_joints.len()
414 {
415 return Err(SkinnedBindPoseCanonicalizationError::InverseBindCount {
416 source_node_index: instance.source_node_index,
417 ibms: instance.skin_ibms.len(),
418 joints: instance.skin_joints.len(),
419 });
420 }
421 let mut geometry_world = None;
422 for (slot, &joint) in instance.skin_joints.iter().enumerate() {
423 let joint_world = *worlds.get(joint).ok_or(
424 SkinnedBindPoseCanonicalizationError::MissingReference {
425 source_node_index: instance.source_node_index,
426 kind: "joint",
427 index: joint,
428 },
429 )?;
430 let actual = if instance.skin_ibms.is_empty() {
431 doc.skeleton.bones[joint].inverse_bind.ok_or(
432 SkinnedBindPoseCanonicalizationError::MissingInverseBind {
433 source_node_index: instance.source_node_index,
434 joint,
435 },
436 )?
437 } else {
438 instance.skin_ibms[slot]
439 };
440 let candidate = joint_world * actual;
441 if !matrix4_is_finite(actual)
442 || !matrix4_is_finite(candidate)
443 || geometry_world
444 .is_some_and(|geometry| !matrix_approximately_equal(candidate, geometry))
445 {
446 return Err(
447 SkinnedBindPoseCanonicalizationError::InconsistentInverseBind {
448 source_node_index: instance.source_node_index,
449 joint,
450 },
451 );
452 }
453 geometry_world.get_or_insert(candidate);
454 }
455 let Some(geometry_world) = geometry_world else {
456 return Err(SkinnedBindPoseCanonicalizationError::UnskinnedInstance {
457 source_node_index: instance.source_node_index,
458 });
459 };
460 validate_geometry_transform(geometry_world, instance.source_node_index)?;
461 validate_mesh(mesh, instance.mesh, instance)?;
462 plans.push(Plan {
463 instance: instance_ordinal,
464 geometry_world,
465 });
466 }
467 plans.sort_unstable_by_key(|plan| {
468 let instance = &doc.assets.instances[plan.instance];
469 (instance.source_node_index, plan.instance)
470 });
471 Ok(plans)
472}
473
474fn world_matrices(skeleton: &Skeleton) -> Result<Vec<Mat4>, SkinnedBindPoseCanonicalizationError> {
475 world_rest_matrices(skeleton).map_err(|error| match error {
476 WorldMatrixError::NonFiniteTransform { node } => {
477 SkinnedBindPoseCanonicalizationError::NonFiniteTransform { node }
478 }
479 WorldMatrixError::InvalidParent { node, parent } => {
480 SkinnedBindPoseCanonicalizationError::InvalidParent { node, parent }
481 }
482 })
483}
484
485fn inverse_joint_matrix(
486 world: Mat4,
487 joint: usize,
488) -> Result<Mat4, SkinnedBindPoseCanonicalizationError> {
489 let linear = Mat3::from_mat4(world);
490 let scale = linear
491 .to_cols_array()
492 .into_iter()
493 .fold(0.0_f32, |largest, component| largest.max(component.abs()));
494 let determinant = linear.determinant();
495 if !determinant.is_finite()
496 || scale == 0.0
497 || determinant.abs() <= MIN_RELATIVE_DETERMINANT * scale * scale * scale
498 {
499 return Err(SkinnedBindPoseCanonicalizationError::SingularJoint { joint });
500 }
501 let inverse = world.inverse();
502 if !matrix4_is_finite(inverse) {
503 return Err(SkinnedBindPoseCanonicalizationError::SingularJoint { joint });
504 }
505 Ok(inverse)
506}
507
508fn validate_geometry_transform(
509 world: Mat4,
510 source_node_index: usize,
511) -> Result<(), SkinnedBindPoseCanonicalizationError> {
512 let linear = Mat3::from_mat4(world);
513 let scale = linear
514 .to_cols_array()
515 .into_iter()
516 .fold(0.0_f32, |largest, component| largest.max(component.abs()));
517 let determinant = linear.determinant();
518 if !determinant.is_finite()
519 || scale == 0.0
520 || determinant.abs() <= MIN_RELATIVE_DETERMINANT * scale * scale * scale
521 {
522 return Err(
523 SkinnedBindPoseCanonicalizationError::InvalidGeometryTransform {
524 source_node_index,
525 reason: "singular_or_near_singular",
526 },
527 );
528 }
529 if determinant < 0.0 {
530 return Err(
531 SkinnedBindPoseCanonicalizationError::InvalidGeometryTransform {
532 source_node_index,
533 reason: "reflection",
534 },
535 );
536 }
537 Ok(())
538}
539
540fn validate_mesh(
541 mesh: &MeshAsset,
542 mesh_ordinal: usize,
543 instance: &MeshInstance,
544) -> Result<(), SkinnedBindPoseCanonicalizationError> {
545 if mesh.primitives.is_empty() {
546 return Err(invalid_primitive(mesh_ordinal, 0, "empty_primitives"));
547 }
548 for (primitive_ordinal, primitive) in mesh.primitives.iter().enumerate() {
549 let invalid = |reason| invalid_primitive(mesh_ordinal, primitive_ordinal, reason);
550 if primitive.positions.is_empty() {
551 return Err(invalid("empty_positions"));
552 }
553 if !primitive.normals.is_empty() && primitive.normals.len() != primitive.positions.len() {
554 return Err(invalid("normal_count"));
555 }
556 if primitive.joints.len() != primitive.positions.len()
557 || primitive.weights.len() != primitive.positions.len()
558 {
559 return Err(invalid("skin_attribute_count"));
560 }
561 if primitive.indices.is_empty() {
562 if !primitive.positions.len().is_multiple_of(3) {
563 return Err(invalid("unindexed_triangle_count"));
564 }
565 } else if !primitive.indices.len().is_multiple_of(3)
566 || primitive
567 .indices
568 .iter()
569 .any(|&index| index as usize >= primitive.positions.len())
570 {
571 return Err(invalid("triangle_indices"));
572 }
573 for (vertex, position) in primitive.positions.iter().enumerate() {
574 if !position.is_finite() {
575 return Err(non_finite(
576 mesh_ordinal,
577 primitive_ordinal,
578 "position",
579 vertex,
580 ));
581 }
582 }
583 for (vertex, normal) in primitive.normals.iter().enumerate() {
584 if !normal.is_finite() {
585 return Err(non_finite(
586 mesh_ordinal,
587 primitive_ordinal,
588 "normal",
589 vertex,
590 ));
591 }
592 if normal.length_squared() == 0.0 {
593 return Err(SkinnedBindPoseCanonicalizationError::ZeroNormal {
594 mesh: mesh_ordinal,
595 primitive: primitive_ordinal,
596 vertex,
597 });
598 }
599 }
600 for (vertex, weights) in primitive.weights.iter().enumerate() {
601 if weights.iter().any(|weight| !weight.is_finite()) {
602 return Err(non_finite(
603 mesh_ordinal,
604 primitive_ordinal,
605 "weight",
606 vertex,
607 ));
608 }
609 if weights.iter().any(|weight| *weight < 0.0)
610 || (weights.iter().sum::<f32>() - 1.0).abs() > MATRIX_EPSILON
611 {
612 return Err(invalid("skin_weights"));
613 }
614 if primitive.joints[vertex]
615 .iter()
616 .zip(weights)
617 .any(|(&joint_slot, &weight)| {
618 weight != 0.0 && joint_slot as usize >= instance.skin_joints.len()
619 })
620 {
621 return Err(invalid("joint_index"));
622 }
623 }
624 }
625 Ok(())
626}
627
628fn converted_bounds(
629 doc: &Document,
630 plans: &[Plan],
631 source_to_meters_y_up: Mat4,
632) -> Result<(Vec3, Vec3), SkinnedBindPoseCanonicalizationError> {
633 let mut min = Vec3::splat(f32::INFINITY);
634 let mut max = Vec3::splat(f32::NEG_INFINITY);
635 for plan in plans {
636 let instance = &doc.assets.instances[plan.instance];
637 let transform = source_to_meters_y_up * plan.geometry_world;
638 for (primitive_ordinal, primitive) in doc.assets.meshes[instance.mesh]
639 .primitives
640 .iter()
641 .enumerate()
642 {
643 for (vertex, position) in primitive.positions.iter().enumerate() {
644 let converted = transform.transform_point3(*position);
645 if !converted.is_finite() {
646 return Err(non_finite(
647 instance.mesh,
648 primitive_ordinal,
649 "converted_position",
650 vertex,
651 ));
652 }
653 min = min.min(converted);
654 max = max.max(converted);
655 }
656 }
657 }
658 Ok((min, max))
659}
660
661fn placement_transform(placement: SkinnedBindPosePlacement, min: Vec3, max: Vec3) -> Mat4 {
662 match placement {
663 SkinnedBindPosePlacement::Preserve => Mat4::IDENTITY,
664 SkinnedBindPosePlacement::GroundAndCenter => Mat4::from_translation(Vec3::new(
665 -(min.x + max.x) * 0.5,
666 -min.y,
667 -(min.z + max.z) * 0.5,
668 )),
669 }
670}
671
672fn canonical_skeleton(
673 source: &Skeleton,
674 transform: Mat4,
675) -> Result<Skeleton, SkinnedBindPoseCanonicalizationError> {
676 let mut bones = Vec::with_capacity(source.bones.len() + 1);
677 bones.push(Bone {
678 name: "animsmith-canonical-root".to_string(),
679 parent: None,
680 rest: Transform::IDENTITY,
681 inverse_bind: None,
682 });
683 for (node, source_bone) in source.bones.iter().enumerate() {
684 let (parent, rest) = match source_bone.parent {
685 Some(parent) => (Some(parent + 1), source_bone.rest),
686 None => (
687 Some(0),
688 transform_to_trs(transform * source_bone.rest.to_mat4())
689 .ok_or(SkinnedBindPoseCanonicalizationError::NonTrsRoot { node })?,
690 ),
691 };
692 bones.push(Bone {
693 name: source_bone.name.clone(),
694 parent,
695 rest,
696 inverse_bind: None,
697 });
698 }
699 Ok(Skeleton { bones })
700}
701
702fn transform_mesh(
703 mesh: &MeshAsset,
704 mesh_ordinal: usize,
705 transform: Mat4,
706) -> Result<MeshAsset, SkinnedBindPoseCanonicalizationError> {
707 let normal_transform = Mat3::from_mat4(transform).inverse().transpose();
708 if !matrix3_is_finite(normal_transform) {
709 return Err(
710 SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
711 reason: "normal_matrix",
712 },
713 );
714 }
715 let mut output = mesh.clone();
716 for (primitive_ordinal, primitive) in output.primitives.iter_mut().enumerate() {
717 for (vertex, position) in primitive.positions.iter_mut().enumerate() {
718 *position = transform.transform_point3(*position);
719 if !position.is_finite() {
720 return Err(non_finite(
721 mesh_ordinal,
722 primitive_ordinal,
723 "canonical_position",
724 vertex,
725 ));
726 }
727 }
728 for (vertex, normal) in primitive.normals.iter_mut().enumerate() {
729 let Some(normalized) = (normal_transform * *normal).try_normalize() else {
730 return Err(SkinnedBindPoseCanonicalizationError::ZeroNormal {
731 mesh: mesh_ordinal,
732 primitive: primitive_ordinal,
733 vertex,
734 });
735 };
736 *normal = normalized;
737 }
738 }
739 Ok(output)
740}
741
742fn transform_to_trs(matrix: Mat4) -> Option<Transform> {
743 if !matrix4_is_finite(matrix) {
744 return None;
745 }
746 let (scale, rotation, translation) = matrix.to_scale_rotation_translation();
747 if !scale.is_finite() || !rotation.is_finite() || !translation.is_finite() {
748 return None;
749 }
750 let transform = Transform {
751 translation,
752 rotation,
753 scale,
754 };
755 matrix_approximately_equal(matrix, transform.to_mat4()).then_some(transform)
756}
757
758fn invalid_primitive(
759 mesh: usize,
760 primitive: usize,
761 reason: &'static str,
762) -> SkinnedBindPoseCanonicalizationError {
763 SkinnedBindPoseCanonicalizationError::InvalidPrimitive {
764 mesh,
765 primitive,
766 reason,
767 }
768}
769
770fn non_finite(
771 mesh: usize,
772 primitive: usize,
773 attribute: &'static str,
774 vertex: usize,
775) -> SkinnedBindPoseCanonicalizationError {
776 SkinnedBindPoseCanonicalizationError::NonFiniteAttribute {
777 mesh,
778 primitive,
779 attribute,
780 vertex,
781 }
782}
783
784fn matrix_approximately_equal(left: Mat4, right: Mat4) -> bool {
785 left.to_cols_array()
786 .into_iter()
787 .zip(right.to_cols_array())
788 .all(|(left, right)| {
789 (left - right).abs() <= MATRIX_EPSILON * left.abs().max(right.abs()).max(1.0)
790 })
791}
792
793fn matrix4_is_finite(matrix: Mat4) -> bool {
794 matrix
795 .to_cols_array()
796 .into_iter()
797 .all(|component| component.is_finite())
798}
799
800fn matrix3_is_finite(matrix: Mat3) -> bool {
801 matrix
802 .to_cols_array()
803 .into_iter()
804 .all(|component| component.is_finite())
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810 use crate::model::affine_test_fixtures;
811
812 #[test]
813 fn coordinate_transform_accepts_the_shared_fixtures_inside_its_1e4_bands() {
814 assert_eq!(
815 COORDINATE_AFFINE_TOLERANCE,
816 PositiveUniformAffineTolerance {
817 equal_axis: f64::from(1.0e-4_f32),
818 relative_orthogonality: f64::from(1.0e-4_f32),
819 singular_determinant_relative: 0.0,
820 },
821 "the canonicalization policy tuple is an explicit contract"
822 );
823 for basis in [
824 affine_test_fixtures::tolerance_divergence_basis(),
825 affine_test_fixtures::orthogonality_tolerance_divergence_basis(),
826 ] {
827 validate_coordinate_transform(Mat4::from_mat3(basis))
828 .expect("skinned canonicalization keeps its declared 1e-4 policy");
829 }
830 }
831}