runmat-meshing-surface 0.6.0

Surface meshing stage for RunMat meshing
Documentation
use super::*;
use crate::{discretize_topology_surfaces, SurfaceDiscretizationOptions};
use runmat_meshing_cad::{SourceTopologyFace, SourceTopologyModel, SourceTopologyVertex};

#[test]
fn validates_closed_surface_recovery() {
    let topology = cube_topology();
    let surface = discretize_topology_surfaces(&topology, SurfaceDiscretizationOptions::default())
        .expect("surface should discretize");

    let report = validate_surface_recovery(&topology, &surface, SurfaceRecoveryOptions::default())
        .expect("surface recovery should validate");

    assert_eq!(report.surface_element_count, 12);
    assert_eq!(report.open_edge_count, 0);
    assert_eq!(report.nonmanifold_edge_count, 0);
    assert_eq!(report.source_face_coverage_ratio, 1.0);
    assert!(report.min_normal_alignment >= 1.0 - 1.0e-8);
}

#[test]
fn rejects_surface_with_open_edge() {
    let topology = cube_topology();
    let mut surface =
        discretize_topology_surfaces(&topology, SurfaceDiscretizationOptions::default())
            .expect("surface should discretize");
    surface.elements.pop();

    let err = validate_surface_recovery(
        &topology,
        &surface,
        SurfaceRecoveryOptions {
            require_closed: true,
            ..SurfaceRecoveryOptions::default()
        },
    )
    .expect_err("open surface should fail recovery");

    assert!(matches!(
        err,
        SurfaceRecoveryError::UncoveredSourceFace { .. } | SurfaceRecoveryError::OpenEdge { .. }
    ));
}

#[test]
fn rejects_surface_area_mismatch() {
    let topology = cube_topology();
    let mut surface =
        discretize_topology_surfaces(&topology, SurfaceDiscretizationOptions::default())
            .expect("surface should discretize");
    surface.nodes[1].coordinates_m = [2.0, 0.0, 0.0];

    let err = validate_surface_recovery(
        &topology,
        &surface,
        SurfaceRecoveryOptions {
            max_area_relative_error: 1.0e-12,
            ..SurfaceRecoveryOptions::default()
        },
    )
    .expect_err("area mismatch should fail recovery");

    assert!(matches!(err, SurfaceRecoveryError::AreaMismatch { .. }));
}

fn cube_topology() -> SourceTopologyModel {
    let vertices = [
        [0.0, 0.0, 0.0],
        [1.0, 0.0, 0.0],
        [1.0, 1.0, 0.0],
        [0.0, 1.0, 0.0],
        [0.0, 0.0, 1.0],
        [1.0, 0.0, 1.0],
        [1.0, 1.0, 1.0],
        [0.0, 1.0, 1.0],
    ]
    .into_iter()
    .enumerate()
    .map(|(vertex_id, coordinates_m)| SourceTopologyVertex {
        vertex_id: vertex_id as u32,
        coordinates_m,
    })
    .collect::<Vec<_>>();
    let faces = [
        ([0, 2, 1], [0.0, 0.0, -1.0]),
        ([0, 3, 2], [0.0, 0.0, -1.0]),
        ([4, 5, 6], [0.0, 0.0, 1.0]),
        ([4, 6, 7], [0.0, 0.0, 1.0]),
        ([0, 1, 5], [0.0, -1.0, 0.0]),
        ([0, 5, 4], [0.0, -1.0, 0.0]),
        ([1, 2, 6], [1.0, 0.0, 0.0]),
        ([1, 6, 5], [1.0, 0.0, 0.0]),
        ([2, 3, 7], [0.0, 1.0, 0.0]),
        ([2, 7, 6], [0.0, 1.0, 0.0]),
        ([3, 0, 4], [-1.0, 0.0, 0.0]),
        ([3, 4, 7], [-1.0, 0.0, 0.0]),
    ]
    .into_iter()
    .enumerate()
    .map(|(face_id, (node_ids, unit_normal))| SourceTopologyFace {
        face_id: face_id as u32,
        source_triangle_id: face_id as u32,
        node_ids,
        edge_ids: [
            face_id as u32 * 3,
            face_id as u32 * 3 + 1,
            face_id as u32 * 3 + 2,
        ],
        region_ids: Vec::new(),
        material_region_ids: Vec::new(),
        area_m2: 0.5,
        unit_normal,
    })
    .collect::<Vec<_>>();

    SourceTopologyModel {
        mesh_id: "surface_recovery_cube".to_string(),
        source_geometry_id: "geo_surface_recovery_cube".to_string(),
        source_geometry_revision: 1,
        source_geometry_sha256: None,
        vertices,
        edges: Vec::new(),
        faces,
        bounds_min_m: [0.0, 0.0, 0.0],
        bounds_max_m: [1.0, 1.0, 1.0],
        region_ids: Vec::new(),
        material_region_ids: Vec::new(),
    }
}