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();
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);
}