Skip to main content

animsmith_core/
static_bake.rs

1//! Canonical baking of static mesh node transforms into vertex data.
2//!
3//! The operation is deliberately narrow: it produces a static-only scene
4//! with identity node transforms. Animations, skins, reflections, malformed
5//! primitive data, and ill-conditioned transforms are rejected rather than
6//! being silently rewritten with different semantics.
7
8use crate::model::{
9    Bone, Document, MeshAsset, MeshInstance, Primitive, SceneAsset, SceneAssets, Skeleton,
10    SourceSkeletonAssets, Transform, WorldMatrixError, world_rest_matrices,
11};
12use glam::{Mat3, Mat4};
13use serde::Serialize;
14use std::collections::HashSet;
15
16/// Relative determinant threshold below which a linear transform is too
17/// ill-conditioned to invert reliably for normal transformation.
18const MIN_RELATIVE_DETERMINANT: f32 = 1.0e-6;
19
20/// Result of [`bake_static_mesh_transforms`].
21#[derive(Debug, Clone)]
22#[non_exhaustive]
23pub struct StaticMeshBake {
24    /// Canonical static document with one identity root and one identity mesh
25    /// child per input mesh instance.
26    pub document: Document,
27    /// Deterministic evidence describing every baked input instance.
28    pub evidence: StaticMeshBakeEvidence,
29}
30
31/// Machine-readable evidence emitted by a static mesh bake.
32#[derive(Debug, Clone, Serialize)]
33#[non_exhaustive]
34pub struct StaticMeshBakeEvidence {
35    /// Baked instances in ascending source-node order.
36    pub entries: Vec<StaticMeshBakeInstanceEvidence>,
37}
38
39/// Evidence for one input mesh-node attachment.
40#[derive(Debug, Clone, Serialize)]
41#[non_exhaustive]
42pub struct StaticMeshBakeInstanceEvidence {
43    /// Source-format node index of the mesh attachment.
44    pub source_node_index: usize,
45    /// Source node name from the input skeleton.
46    pub source_node_name: String,
47    /// Ordinal of the input mesh definition in [`SceneAssets::meshes`].
48    pub source_mesh_ordinal: usize,
49    /// Stable mesh-definition identity supplied by the source loader.
50    pub source_mesh_index: usize,
51    /// Source mesh-definition name.
52    pub source_mesh_name: String,
53    /// Node ordinal in the canonical output document.
54    pub output_node_index: usize,
55    /// Mesh ordinal in the canonical output document.
56    pub output_mesh_index: usize,
57    /// Accumulated rest/world matrix in column-major order.
58    pub world_transform: [f32; 16],
59    /// Determinant of the baked linear transform.
60    pub linear_determinant: f32,
61    /// Number of primitives copied into the output mesh.
62    pub primitive_count: usize,
63    /// Number of positions copied across all output primitives.
64    pub position_count: usize,
65    /// Number of normals copied across all output primitives.
66    pub normal_count: usize,
67}
68
69/// Rejection reason returned by [`bake_static_mesh_transforms`].
70#[derive(Debug, thiserror::Error)]
71#[non_exhaustive]
72pub enum StaticMeshBakeError {
73    /// The input carries no mesh-node attachments to bake.
74    #[error("cannot bake static transforms: document has no mesh instances")]
75    NoInstances,
76    /// An input mesh definition is not attached to exactly one mesh node.
77    #[error(
78        "cannot bake static transforms: mesh {mesh} (source mesh {source_mesh_index}) has {instances} instances; each mesh definition must have exactly one"
79    )]
80    MeshInstanceCount {
81        /// Ordinal in [`SceneAssets::meshes`].
82        mesh: usize,
83        /// Source mesh identity.
84        source_mesh_index: usize,
85        /// Number of input instances that reference the definition.
86        instances: usize,
87    },
88    /// A mesh instance references a mesh definition that does not exist.
89    #[error(
90        "cannot bake static transforms: source node {source_node_index} references missing mesh {mesh}"
91    )]
92    MissingMesh {
93        /// Source node identity from the instance.
94        source_node_index: usize,
95        /// Missing mesh ordinal.
96        mesh: usize,
97    },
98    /// A mesh instance references a skeleton node that does not exist.
99    #[error(
100        "cannot bake static transforms: source node {source_node_index} maps to missing skeleton node {node}"
101    )]
102    MissingNode {
103        /// Source node identity from the instance.
104        source_node_index: usize,
105        /// Missing skeleton node ordinal.
106        node: usize,
107    },
108    /// More than one model instance claims one source mesh node.
109    #[error(
110        "cannot bake static transforms: source node {source_node_index} has duplicate mesh attachments"
111    )]
112    DuplicateNodeAttachment {
113        /// Source node identity claimed more than once.
114        source_node_index: usize,
115    },
116    /// More than one model instance attaches a mesh to one internal skeleton node.
117    #[error("cannot bake static transforms: skeleton node {node} has duplicate mesh attachments")]
118    DuplicateSkeletonNodeAttachment {
119        /// Internal skeleton node claimed more than once.
120        node: usize,
121    },
122    /// The input contains animation data, which a static-only bake cannot retain.
123    #[error(
124        "cannot bake static transforms: clip {clip:?} animates skeleton node {node} ({property})"
125    )]
126    AnimationTrack {
127        /// Clip name.
128        clip: String,
129        /// Target skeleton node.
130        node: usize,
131        /// Animated property name.
132        property: &'static str,
133    },
134    /// A skeleton carries inverse-bind data, indicating a skin domain.
135    #[error("cannot bake static transforms: skeleton node {node} carries an inverse bind matrix")]
136    SkeletonSkin {
137        /// Skeleton node carrying inverse-bind data.
138        node: usize,
139    },
140    /// A mesh instance carries skin joints or inverse bind matrices.
141    #[error("cannot bake static transforms: source node {source_node_index} is skinned")]
142    SkinnedInstance {
143        /// Source node identity from the instance.
144        source_node_index: usize,
145    },
146    /// A primitive carries skin vertex attributes.
147    #[error(
148        "cannot bake static transforms: mesh {mesh} primitive {primitive} carries skin attributes"
149    )]
150    SkinnedPrimitive {
151        /// Input mesh ordinal.
152        mesh: usize,
153        /// Primitive ordinal within the mesh.
154        primitive: usize,
155    },
156    /// A skeleton parent is absent or is not earlier than its child.
157    #[error("cannot bake static transforms: skeleton node {node} has invalid parent {parent}")]
158    InvalidParent {
159        /// Skeleton node ordinal.
160        node: usize,
161        /// Invalid parent ordinal.
162        parent: usize,
163    },
164    /// A local or accumulated matrix has a non-finite component.
165    #[error("cannot bake static transforms: skeleton node {node} has a non-finite transform")]
166    NonFiniteTransform {
167        /// Skeleton node ordinal.
168        node: usize,
169    },
170    /// A mesh primitive has malformed attributes, indices, or material data.
171    #[error(
172        "cannot bake static transforms: mesh {mesh} primitive {primitive} is invalid: {reason}"
173    )]
174    InvalidPrimitive {
175        /// Input mesh ordinal.
176        mesh: usize,
177        /// Primitive ordinal within the mesh.
178        primitive: usize,
179        /// Stable machine-readable reason.
180        reason: &'static str,
181    },
182    /// A material factor contains a non-finite value.
183    #[error("cannot bake static transforms: material {material} has non-finite {factor}")]
184    NonFiniteMaterial {
185        /// Material ordinal.
186        material: usize,
187        /// Material factor name.
188        factor: &'static str,
189    },
190    /// A position, normal, or UV contains a non-finite component.
191    #[error(
192        "cannot bake static transforms: mesh {mesh} primitive {primitive} has non-finite {attribute} at vertex {vertex}"
193    )]
194    NonFiniteAttribute {
195        /// Input mesh ordinal.
196        mesh: usize,
197        /// Primitive ordinal within the mesh.
198        primitive: usize,
199        /// Attribute name.
200        attribute: &'static str,
201        /// Vertex ordinal.
202        vertex: usize,
203    },
204    /// A normal cannot be normalized after applying the normal matrix.
205    #[error(
206        "cannot bake static transforms: mesh {mesh} primitive {primitive} has zero normal at vertex {vertex}"
207    )]
208    ZeroNormal {
209        /// Input mesh ordinal.
210        mesh: usize,
211        /// Primitive ordinal within the mesh.
212        primitive: usize,
213        /// Vertex ordinal.
214        vertex: usize,
215    },
216    /// A linear transform is singular or too close to singular for a stable inverse transpose.
217    #[error(
218        "cannot bake static transforms: source node {source_node_index} has singular or near-singular transform (determinant {determinant})"
219    )]
220    SingularTransform {
221        /// Source node identity from the instance.
222        source_node_index: usize,
223        /// Linear determinant.
224        determinant: f32,
225    },
226    /// A linear transform reverses winding, which would require changing preserved indices.
227    #[error(
228        "cannot bake static transforms: source node {source_node_index} has reflection transform (determinant {determinant})"
229    )]
230    ReflectionTransform {
231        /// Source node identity from the instance.
232        source_node_index: usize,
233        /// Linear determinant.
234        determinant: f32,
235    },
236}
237
238#[derive(Debug, Clone, Copy)]
239struct BakePlan {
240    instance: usize,
241    mesh: usize,
242    node: usize,
243    world: Mat4,
244    normal: Mat3,
245    determinant: f32,
246}
247
248/// Bake accumulated rest/world transforms into static mesh positions and normals.
249///
250/// The returned document is canonical: it contains one identity root, one
251/// identity child per input mesh instance, one private mesh definition per
252/// child, no clips, no skins, and one scene. Positions are transformed by the
253/// accumulated rest/world matrix; normals use its inverse transpose and are
254/// normalized. Indices, UVs, material assignment, material values, and
255/// embedded base-color and normal-texture state is copied unchanged.
256///
257/// # Errors
258///
259/// Returns [`StaticMeshBakeError`] when there are no mesh instances, any
260/// animation track or skin signal is present, or a mesh definition is not
261/// attached to exactly one distinct source node. Missing references, invalid
262/// hierarchy or primitive layouts, non-finite transforms, attributes, or
263/// material factors, zero or unstable normals, near-singular transforms, and
264/// reflections are also rejected. Reflections would require rewriting the
265/// preserved triangle winding. The consumed fields, including transformed
266/// positions and normals, are validated before output is constructed.
267pub fn bake_static_mesh_transforms(doc: &Document) -> Result<StaticMeshBake, StaticMeshBakeError> {
268    let plans = validate(doc)?;
269    let mut meshes = Vec::with_capacity(plans.len());
270    let mut instances = Vec::with_capacity(plans.len());
271    let mut nodes = Vec::with_capacity(plans.len() + 1);
272    let mut evidence = Vec::with_capacity(plans.len());
273
274    nodes.push(Bone {
275        name: "animsmith-static-root".to_string(),
276        parent: None,
277        rest: Transform::IDENTITY,
278        inverse_bind: None,
279    });
280
281    for (ordinal, plan) in plans.iter().enumerate() {
282        let input_mesh = &doc.assets.meshes[plan.mesh];
283        let input_instance = &doc.assets.instances[plan.instance];
284        let output_node = ordinal + 1;
285        let output_mesh = ordinal;
286        let baked_mesh = bake_mesh(input_mesh, plan, plan.mesh)?;
287        let position_count = baked_mesh
288            .primitives
289            .iter()
290            .map(|primitive| primitive.positions.len())
291            .sum();
292        let normal_count = baked_mesh
293            .primitives
294            .iter()
295            .map(|primitive| primitive.normals.len())
296            .sum();
297
298        nodes.push(Bone {
299            name: format!("animsmith-static-mesh-{ordinal}"),
300            parent: Some(0),
301            rest: Transform::IDENTITY,
302            inverse_bind: None,
303        });
304        meshes.push(baked_mesh);
305        instances.push(MeshInstance {
306            source_node_index: input_instance.source_node_index,
307            node: output_node,
308            mesh: output_mesh,
309            skin_joints: Vec::new(),
310            skin_ibms: Vec::new(),
311        });
312        evidence.push(StaticMeshBakeInstanceEvidence {
313            source_node_index: input_instance.source_node_index,
314            source_node_name: doc.skeleton.bones[plan.node].name.clone(),
315            source_mesh_ordinal: plan.mesh,
316            source_mesh_index: input_mesh.source_mesh_index,
317            source_mesh_name: input_mesh.name.clone(),
318            output_node_index: output_node,
319            output_mesh_index: output_mesh,
320            world_transform: plan.world.to_cols_array(),
321            linear_determinant: plan.determinant,
322            primitive_count: input_mesh.primitives.len(),
323            position_count,
324            normal_count,
325        });
326    }
327
328    Ok(StaticMeshBake {
329        document: Document {
330            skeleton: Skeleton { bones: nodes },
331            clips: Vec::new(),
332            assets: SceneAssets {
333                meshes,
334                instances,
335                materials: doc.assets.materials.clone(),
336                material_resources: doc.assets.material_resources.clone(),
337                scenes: vec![SceneAsset {
338                    source_scene_index: 0,
339                    name: None,
340                    roots: vec![0],
341                }],
342                default_scene: Some(0),
343                source_skeleton: SourceSkeletonAssets::default(),
344            },
345            source: doc.source.clone(),
346        },
347        evidence: StaticMeshBakeEvidence { entries: evidence },
348    })
349}
350
351fn validate(doc: &Document) -> Result<Vec<BakePlan>, StaticMeshBakeError> {
352    if doc.assets.instances.is_empty() {
353        return Err(StaticMeshBakeError::NoInstances);
354    }
355    for clip in &doc.clips {
356        if let Some(track) = clip.tracks.first() {
357            return Err(StaticMeshBakeError::AnimationTrack {
358                clip: clip.name.clone(),
359                node: track.bone,
360                property: track.property.as_str(),
361            });
362        }
363    }
364    for (node, bone) in doc.skeleton.bones.iter().enumerate() {
365        if bone.inverse_bind.is_some() {
366            return Err(StaticMeshBakeError::SkeletonSkin { node });
367        }
368    }
369
370    let worlds = world_matrices(&doc.skeleton)?;
371    validate_materials(&doc.assets)?;
372    let mut mesh_counts = vec![0usize; doc.assets.meshes.len()];
373    let mut source_nodes = HashSet::with_capacity(doc.assets.instances.len());
374    let mut skeleton_nodes = vec![false; doc.skeleton.bones.len()];
375    for instance in &doc.assets.instances {
376        let Some(count) = mesh_counts.get_mut(instance.mesh) else {
377            return Err(StaticMeshBakeError::MissingMesh {
378                source_node_index: instance.source_node_index,
379                mesh: instance.mesh,
380            });
381        };
382        *count += 1;
383        if instance.node >= doc.skeleton.bones.len() {
384            return Err(StaticMeshBakeError::MissingNode {
385                source_node_index: instance.source_node_index,
386                node: instance.node,
387            });
388        }
389        if !source_nodes.insert(instance.source_node_index) {
390            return Err(StaticMeshBakeError::DuplicateNodeAttachment {
391                source_node_index: instance.source_node_index,
392            });
393        }
394        if skeleton_nodes[instance.node] {
395            return Err(StaticMeshBakeError::DuplicateSkeletonNodeAttachment {
396                node: instance.node,
397            });
398        }
399        skeleton_nodes[instance.node] = true;
400        if !instance.skin_joints.is_empty() || !instance.skin_ibms.is_empty() {
401            return Err(StaticMeshBakeError::SkinnedInstance {
402                source_node_index: instance.source_node_index,
403            });
404        }
405    }
406    for (mesh, &instances) in mesh_counts.iter().enumerate() {
407        if instances != 1 {
408            return Err(StaticMeshBakeError::MeshInstanceCount {
409                mesh,
410                source_mesh_index: doc.assets.meshes[mesh].source_mesh_index,
411                instances,
412            });
413        }
414    }
415
416    let mut plans = Vec::with_capacity(doc.assets.instances.len());
417    for (instance_ordinal, instance) in doc.assets.instances.iter().enumerate() {
418        let mesh = &doc.assets.meshes[instance.mesh];
419        validate_mesh(mesh, instance.mesh, doc.assets.materials.len())?;
420        let world = worlds[instance.node];
421        let linear = Mat3::from_mat4(world);
422        let determinant = linear.determinant();
423        let scale = linear
424            .to_cols_array()
425            .into_iter()
426            .fold(0.0_f32, |largest, component| largest.max(component.abs()));
427        let threshold = MIN_RELATIVE_DETERMINANT * scale * scale * scale;
428        if !determinant.is_finite() || scale == 0.0 || determinant.abs() <= threshold {
429            return Err(StaticMeshBakeError::SingularTransform {
430                source_node_index: instance.source_node_index,
431                determinant,
432            });
433        }
434        if determinant < 0.0 {
435            return Err(StaticMeshBakeError::ReflectionTransform {
436                source_node_index: instance.source_node_index,
437                determinant,
438            });
439        }
440        let normal = linear.inverse().transpose();
441        if !matrix3_is_finite(normal) {
442            return Err(StaticMeshBakeError::SingularTransform {
443                source_node_index: instance.source_node_index,
444                determinant,
445            });
446        }
447        validate_baked_mesh(mesh, instance.mesh, world, normal)?;
448        plans.push(BakePlan {
449            instance: instance_ordinal,
450            mesh: instance.mesh,
451            node: instance.node,
452            world,
453            normal,
454            determinant,
455        });
456    }
457    plans.sort_unstable_by_key(|plan| doc.assets.instances[plan.instance].source_node_index);
458    Ok(plans)
459}
460
461fn validate_materials(assets: &SceneAssets) -> Result<(), StaticMeshBakeError> {
462    for (material, value) in assets.materials.iter().enumerate() {
463        if !value
464            .base_color
465            .iter()
466            .all(|component| component.is_finite())
467        {
468            return Err(StaticMeshBakeError::NonFiniteMaterial {
469                material,
470                factor: "base_color",
471            });
472        }
473        if !value.metallic.is_finite() {
474            return Err(StaticMeshBakeError::NonFiniteMaterial {
475                material,
476                factor: "metallic",
477            });
478        }
479        if !value.roughness.is_finite() {
480            return Err(StaticMeshBakeError::NonFiniteMaterial {
481                material,
482                factor: "roughness",
483            });
484        }
485        if value
486            .normal_texture
487            .as_ref()
488            .is_some_and(|normal| !normal.scale.is_finite())
489        {
490            return Err(StaticMeshBakeError::NonFiniteMaterial {
491                material,
492                factor: "normal_texture_scale",
493            });
494        }
495    }
496    Ok(())
497}
498
499fn world_matrices(skeleton: &Skeleton) -> Result<Vec<Mat4>, StaticMeshBakeError> {
500    world_rest_matrices(skeleton).map_err(|error| match error {
501        WorldMatrixError::NonFiniteTransform { node } => {
502            StaticMeshBakeError::NonFiniteTransform { node }
503        }
504        WorldMatrixError::InvalidParent { node, parent } => {
505            StaticMeshBakeError::InvalidParent { node, parent }
506        }
507    })
508}
509
510fn validate_mesh(
511    mesh: &MeshAsset,
512    mesh_ordinal: usize,
513    materials: usize,
514) -> Result<(), StaticMeshBakeError> {
515    if mesh.primitives.is_empty() {
516        return Err(StaticMeshBakeError::InvalidPrimitive {
517            mesh: mesh_ordinal,
518            primitive: 0,
519            reason: "empty_primitives",
520        });
521    }
522    for (primitive_ordinal, primitive) in mesh.primitives.iter().enumerate() {
523        validate_primitive(primitive, mesh_ordinal, primitive_ordinal, materials)?;
524    }
525    Ok(())
526}
527
528fn validate_primitive(
529    primitive: &Primitive,
530    mesh: usize,
531    primitive_ordinal: usize,
532    materials: usize,
533) -> Result<(), StaticMeshBakeError> {
534    let invalid = |reason| StaticMeshBakeError::InvalidPrimitive {
535        mesh,
536        primitive: primitive_ordinal,
537        reason,
538    };
539    if primitive
540        .material
541        .is_some_and(|material| material >= materials)
542    {
543        return Err(invalid("material_index"));
544    }
545    if primitive.positions.is_empty() {
546        return Err(invalid("empty_positions"));
547    }
548    if !primitive.normals.is_empty() && primitive.normals.len() != primitive.positions.len() {
549        return Err(invalid("normal_count"));
550    }
551    if !primitive.uvs.is_empty() && primitive.uvs.len() != primitive.positions.len() {
552        return Err(invalid("uv_count"));
553    }
554    if !primitive.joints.is_empty() || !primitive.weights.is_empty() {
555        return Err(StaticMeshBakeError::SkinnedPrimitive {
556            mesh,
557            primitive: primitive_ordinal,
558        });
559    }
560    if primitive.indices.is_empty() {
561        if !primitive.positions.len().is_multiple_of(3) {
562            return Err(invalid("unindexed_triangle_count"));
563        }
564    } else {
565        if !primitive.indices.len().is_multiple_of(3) {
566            return Err(invalid("indexed_triangle_count"));
567        }
568        if primitive.indices.iter().any(|&index| {
569            usize::try_from(index).map_or(true, |index| index >= primitive.positions.len())
570        }) {
571            return Err(invalid("triangle_index"));
572        }
573    }
574    for (vertex, position) in primitive.positions.iter().enumerate() {
575        if !position.is_finite() {
576            return Err(StaticMeshBakeError::NonFiniteAttribute {
577                mesh,
578                primitive: primitive_ordinal,
579                attribute: "position",
580                vertex,
581            });
582        }
583    }
584    for (vertex, normal) in primitive.normals.iter().enumerate() {
585        if !normal.is_finite() {
586            return Err(StaticMeshBakeError::NonFiniteAttribute {
587                mesh,
588                primitive: primitive_ordinal,
589                attribute: "normal",
590                vertex,
591            });
592        }
593        if normal.length_squared() == 0.0 {
594            return Err(StaticMeshBakeError::ZeroNormal {
595                mesh,
596                primitive: primitive_ordinal,
597                vertex,
598            });
599        }
600    }
601    for (vertex, uv) in primitive.uvs.iter().enumerate() {
602        if !uv.iter().all(|component| component.is_finite()) {
603            return Err(StaticMeshBakeError::NonFiniteAttribute {
604                mesh,
605                primitive: primitive_ordinal,
606                attribute: "uv",
607                vertex,
608            });
609        }
610    }
611    Ok(())
612}
613
614fn validate_baked_mesh(
615    mesh: &MeshAsset,
616    mesh_ordinal: usize,
617    world: Mat4,
618    normal_matrix: Mat3,
619) -> Result<(), StaticMeshBakeError> {
620    for (primitive_ordinal, primitive) in mesh.primitives.iter().enumerate() {
621        for (vertex, position) in primitive.positions.iter().enumerate() {
622            if !world.transform_point3(*position).is_finite() {
623                return Err(StaticMeshBakeError::NonFiniteAttribute {
624                    mesh: mesh_ordinal,
625                    primitive: primitive_ordinal,
626                    attribute: "baked_position",
627                    vertex,
628                });
629            }
630        }
631        for (vertex, normal) in primitive.normals.iter().enumerate() {
632            let transformed = normal_matrix * *normal;
633            if !transformed.is_finite() {
634                return Err(StaticMeshBakeError::NonFiniteAttribute {
635                    mesh: mesh_ordinal,
636                    primitive: primitive_ordinal,
637                    attribute: "baked_normal",
638                    vertex,
639                });
640            }
641            if transformed.try_normalize().is_none() {
642                return Err(StaticMeshBakeError::ZeroNormal {
643                    mesh: mesh_ordinal,
644                    primitive: primitive_ordinal,
645                    vertex,
646                });
647            }
648        }
649    }
650    Ok(())
651}
652
653fn bake_mesh(
654    mesh: &MeshAsset,
655    plan: &BakePlan,
656    mesh_ordinal: usize,
657) -> Result<MeshAsset, StaticMeshBakeError> {
658    let mut baked = mesh.clone();
659    for (primitive_ordinal, primitive) in baked.primitives.iter_mut().enumerate() {
660        for position in &mut primitive.positions {
661            *position = plan.world.transform_point3(*position);
662        }
663        for (vertex, normal) in primitive.normals.iter_mut().enumerate() {
664            // Leave canonical unit normals under an exact identity matrix
665            // untouched so baking an already baked artifact stays byte-stable.
666            if plan.world == Mat4::IDENTITY && normal.is_normalized() {
667                continue;
668            }
669            let transformed = plan.normal * *normal;
670            let Some(normalized) = transformed.try_normalize() else {
671                return Err(StaticMeshBakeError::ZeroNormal {
672                    mesh: mesh_ordinal,
673                    primitive: primitive_ordinal,
674                    vertex,
675                });
676            };
677            *normal = normalized;
678        }
679    }
680    Ok(baked)
681}
682
683fn matrix3_is_finite(matrix: Mat3) -> bool {
684    matrix
685        .to_cols_array()
686        .into_iter()
687        .all(|component| component.is_finite())
688}