BREP_RANSAC 0.1.2

Topology-aware analytic surface recognition for CAD triangle meshes
Documentation
use brep_ransac::{
    reconstruct_surface_from_vertices, synthetic, AnalyticSurface, ConstraintMask, FitPath,
    MetadataTrust, RecognitionError, RecognitionOptions, SamplingMode, SphereSurface,
    SurfaceConstraints, SurfaceHint, SurfaceType, Vec3,
};

fn options() -> RecognitionOptions {
    RecognitionOptions {
        distance_tolerance: 3.0e-2,
        relative_tolerance: 1.0e-9,
        normal_tolerance: 0.35,
        minimum_support: 1,
        sampling: SamplingMode::TriangleCentroids,
        ..Default::default()
    }
}

#[test]
fn vertex_selection_rejects_empty_out_of_range_and_duplicate_indices() {
    let (_, mesh) = synthetic::canonical(SurfaceType::Sphere, 12);
    let hint = SurfaceHint::KnownType {
        surface_type: SurfaceType::Sphere,
    };

    for (selection, expected) in [
        (Vec::new(), "vertex selection is empty"),
        (vec![mesh.vertices.len()], "vertex index out of range"),
        (vec![0, 1, 1], "vertex selection contains duplicate indices"),
    ] {
        let error = reconstruct_surface_from_vertices(&mesh, &selection, &hint, &options())
            .expect_err("invalid vertex selection must be rejected");
        assert_eq!(error, RecognitionError::InvalidSelection(expected.into()));
    }
}

#[test]
fn known_type_fits_exactly_the_requested_vertex_subset() {
    let (truth, mut mesh) = synthetic::canonical(SurfaceType::Sphere, 16);
    let vertices: Vec<_> = (0..mesh.vertices.len()).step_by(2).collect();
    // An omitted vertex is deliberately moved off the carrier. A triangle-
    // expanded implementation would include it and fail this tight check.
    assert!(!vertices.contains(&1));
    mesh.vertices[1].x += 100.0;
    let fit = reconstruct_surface_from_vertices(
        &mesh,
        &vertices,
        &SurfaceHint::KnownType {
            surface_type: SurfaceType::Sphere,
        },
        &options(),
    )
    .unwrap();

    let (AnalyticSurface::Sphere(expected), AnalyticSurface::Sphere(actual)) = (truth, fit.surface)
    else {
        panic!("known sphere vertex subset selected the wrong primitive")
    };
    assert!(actual.center.distance(expected.center) < 1.0e-5);
    assert!((actual.radius - expected.radius).abs() < 1.0e-5);
    assert_eq!(fit.diagnostics.path, FitPath::KnownTypeFit);
    assert!(fit.metrics.max_error < 1.0e-8);
}

#[test]
fn exact_candidate_uses_requested_vertices_independent_of_sampling_mode() {
    let (truth, mesh) = synthetic::canonical(SurfaceType::Cylinder, 16);
    let vertices: Vec<_> = (0..mesh.vertices.len()).step_by(3).collect();
    let fit = reconstruct_surface_from_vertices(
        &mesh,
        &vertices,
        &SurfaceHint::ExactCandidate { surface: truth },
        &options(),
    )
    .unwrap();

    assert_eq!(fit.surface, truth);
    assert_eq!(fit.diagnostics.path, FitPath::ExactCandidateReused);
    assert!(fit.diagnostics.exact_parameters_reused);
}

#[test]
fn unknown_initial_guess_and_constrained_hints_fit_vertex_subsets() {
    let (truth, mesh) = synthetic::canonical(SurfaceType::Sphere, 16);
    let AnalyticSurface::Sphere(exact) = truth else {
        panic!()
    };
    let vertices: Vec<_> = (0..mesh.vertices.len()).step_by(2).collect();
    let approximate = AnalyticSurface::Sphere(SphereSurface {
        center: exact.center + Vec3::new(0.02, -0.01, 0.015),
        radius: exact.radius * 1.005,
    });
    let cases = [
        (SurfaceHint::Unknown, FitPath::GenericRecognition),
        (
            SurfaceHint::InitialGuess {
                surface: approximate,
                trust: MetadataTrust::InitialGuess,
            },
            FitPath::UnconstrainedRefinement,
        ),
        (
            SurfaceHint::Constrained {
                surface_type: SurfaceType::Sphere,
                constraints: SurfaceConstraints {
                    initial: Some(approximate),
                    fixed: ConstraintMask {
                        radius: true,
                        ..Default::default()
                    },
                },
                trust: MetadataTrust::StrongHint,
            },
            FitPath::ConstrainedRefinement,
        ),
    ];

    for (hint, expected_path) in cases {
        let fit = reconstruct_surface_from_vertices(&mesh, &vertices, &hint, &options())
            .unwrap_or_else(|error| panic!("{expected_path:?}: {error}"));
        assert_eq!(fit.surface.surface_type(), SurfaceType::Sphere);
        assert_eq!(fit.diagnostics.path, expected_path);
        assert!(fit.metrics.max_error <= options().distance_tolerance);
    }
}

#[test]
fn vertex_support_metrics_match_incident_triangle_area_accounting() {
    let (truth, mesh) = synthetic::canonical(SurfaceType::Cylinder, 12);
    let vertices: Vec<_> = (0..mesh.vertices.len()).step_by(3).collect();
    let analyzed = mesh.analyze(&Default::default()).unwrap();
    let selected: std::collections::BTreeSet<_> = vertices.iter().copied().collect();
    let expected_triangles = analyzed
        .triangles
        .iter()
        .filter(|triangle| {
            triangle.area > 0.0
                && triangle
                    .vertices
                    .iter()
                    .any(|vertex| selected.contains(vertex))
        })
        .count();
    let expected_area: f64 = vertices
        .iter()
        .map(|&vertex| analyzed.vertex_area_weights[vertex])
        .sum();
    let fit = reconstruct_surface_from_vertices(
        &mesh,
        &vertices,
        &SurfaceHint::ExactCandidate { surface: truth },
        &options(),
    )
    .unwrap();

    assert_eq!(fit.metrics.support_triangles, expected_triangles);
    assert!((fit.metrics.supported_area - expected_area).abs() < 1.0e-12);
}

#[test]
fn incident_normals_are_used_when_vertex_normals_are_absent() {
    let (truth, mut mesh) = synthetic::canonical(SurfaceType::Cylinder, 16);
    mesh.vertex_normals = None;
    let vertices: Vec<_> = (0..mesh.vertices.len()).collect();
    let fit = reconstruct_surface_from_vertices(
        &mesh,
        &vertices,
        &SurfaceHint::ExactCandidate { surface: truth },
        &options(),
    )
    .unwrap();

    assert_eq!(fit.surface, truth);
    assert_eq!(fit.orientation, 1);
    assert!(fit.metrics.max_normal_error <= options().normal_tolerance);
}

#[test]
fn globally_reversed_vertex_normals_are_aligned_to_mesh_winding() {
    let (truth, mut mesh) = synthetic::canonical(SurfaceType::Cylinder, 16);
    let AnalyticSurface::Cylinder(cylinder) = truth else {
        panic!()
    };
    mesh.vertex_normals = Some(
        mesh.vertices
            .iter()
            .map(|point| {
                let offset = *point - cylinder.axis_origin;
                (offset - cylinder.axis * offset.dot(cylinder.axis))
                    .normalized()
                    .unwrap()
            })
            .collect(),
    );
    let vertices: Vec<_> = (0..mesh.vertices.len()).collect();
    let baseline = reconstruct_surface_from_vertices(
        &mesh,
        &vertices,
        &SurfaceHint::ExactCandidate { surface: truth },
        &options(),
    )
    .unwrap();
    let mut reversed = mesh.clone();
    for normal in reversed.vertex_normals.as_mut().unwrap() {
        *normal = -*normal;
    }
    let fit = reconstruct_surface_from_vertices(
        &reversed,
        &vertices,
        &SurfaceHint::ExactCandidate { surface: truth },
        &options(),
    )
    .unwrap();

    assert_eq!(fit.orientation, baseline.orientation);
    assert!((fit.metrics.rms_normal_error - baseline.metrics.rms_normal_error).abs() < 1.0e-12);
    assert!((fit.metrics.max_normal_error - baseline.metrics.max_normal_error).abs() < 1.0e-12);
}