use brep_ransac::{
recognize_surfaces_with_unresolved, reconstruct_surface, synthetic, AnalyticSurface,
ConeSurface, CylinderSurface, RecognitionOptions, SamplingMode, SphereSurface, SurfaceHint,
SurfaceType, Vec3,
};
use std::f64::consts::{FRAC_PI_2, PI, TAU};
fn options() -> RecognitionOptions {
RecognitionOptions {
distance_tolerance: 1.0e-6,
relative_tolerance: 1.0e-8,
normal_tolerance: 0.05,
minimum_support: 4,
sampling: SamplingMode::Vertices,
deterministic_seed: Some(0x434f_4e44_4954_494f),
..Default::default()
}
}
fn cylinder_patch(angular: [f64; 2], axial: [f64; 2]) -> brep_ransac::Mesh {
let surface = AnalyticSurface::Cylinder(CylinderSurface {
axis_origin: Vec3::new(2.0, -3.0, 1.0),
axis: Vec3::Z,
radius: 7.0,
});
synthetic::tessellate_with_normals(
surface,
synthetic::Patch {
u: angular,
v: axial,
u_segments: 32,
v_segments: 8,
..Default::default()
},
)
}
#[test]
fn explicit_partial_and_extreme_aspect_cylinders_are_recognized() {
for (name, angular, axial) in [
("half", [0.0, PI], [-2.0, 3.0]),
("quarter", [0.0, FRAC_PI_2], [-2.0, 3.0]),
("very long", [0.0, TAU], [-1.0e5, 1.0e5]),
("very short", [0.0, TAU], [2.0, 2.0001]),
] {
let mesh = cylinder_patch(angular, axial);
let result = recognize_surfaces_with_unresolved(&mesh, &options())
.unwrap_or_else(|error| panic!("{name} cylinder: {error}"));
assert_eq!(result.regions.len(), 1, "{name}: {result:?}");
assert_eq!(
result.regions[0].surface.surface_type(),
SurfaceType::Cylinder,
"{name}: {:?}",
result.regions[0]
);
assert!(result.unresolved_triangles.is_empty(), "{name}");
}
}
#[test]
fn steep_truncated_cone_is_recovered_by_unknown_and_known_type_paths() {
let truth = ConeSurface {
apex: Vec3::new(-2.0, 3.0, 0.5),
axis: Vec3::new(0.2, -0.3, 0.9).normalized().unwrap(),
half_angle: 1.2,
};
let mesh = synthetic::tessellate_with_normals(
AnalyticSurface::Cone(truth),
synthetic::Patch {
u: [0.0, TAU],
v: [2.0, 5.0],
u_segments: 32,
v_segments: 12,
..Default::default()
},
);
let unknown = recognize_surfaces_with_unresolved(&mesh, &options()).unwrap();
assert!(unknown.unresolved_triangles.is_empty(), "{unknown:?}");
assert_eq!(unknown.regions.len(), 1, "{unknown:?}");
let AnalyticSurface::Cone(unknown_cone) = unknown.regions[0].surface else {
panic!(
"unknown recognition selected {:?}",
unknown.regions[0].surface
);
};
let ids = (0..mesh.triangles.len()).collect::<Vec<_>>();
let known = reconstruct_surface(
&mesh,
&ids,
&SurfaceHint::KnownType {
surface_type: SurfaceType::Cone,
},
&options(),
)
.unwrap();
let AnalyticSurface::Cone(known_cone) = known.surface else {
panic!("known-type fit selected {:?}", known.surface);
};
for recovered in [unknown_cone, known_cone] {
assert!(
recovered.apex.distance(truth.apex) < 1.0e-8,
"{recovered:?}"
);
assert!(
recovered.axis.dot(truth.axis) > 1.0 - 1.0e-10,
"{recovered:?}"
);
assert!(
(recovered.half_angle - truth.half_angle).abs() < 1.0e-10,
"{recovered:?}"
);
}
}
#[test]
fn explicit_scale_origin_and_aspect_fixtures_reconstruct_their_carriers() {
for (name, (truth, mesh)) in [
("very small", synthetic::very_small_patch()),
("large scale", synthetic::large_scale_patch()),
("far origin", synthetic::far_origin_patch()),
("high aspect", synthetic::high_aspect_patch()),
] {
let ids = (0..mesh.triangles.len()).collect::<Vec<_>>();
let fit = reconstruct_surface(
&mesh,
&ids,
&SurfaceHint::KnownType {
surface_type: truth.surface_type(),
},
&options(),
)
.unwrap_or_else(|error| panic!("{name}: {error}"));
assert_eq!(fit.surface.surface_type(), truth.surface_type(), "{name}");
let tolerance = options().distance_tolerance
+ options().relative_tolerance * mesh.analyze(&Default::default()).unwrap().diagonal;
assert!(
fit.metrics.max_error <= tolerance.max(1.0e-6),
"{name}: {fit:?}"
);
}
}
#[test]
fn tiny_radius_sphere_is_recognized_end_to_end() {
let truth = AnalyticSurface::Sphere(SphereSurface {
center: Vec3::new(4.0e-7, -3.0e-7, 2.0e-7),
radius: 1.0e-7,
});
let mesh = synthetic::tessellate_with_normals(
truth,
synthetic::Patch {
u: [0.0, TAU],
v: [-1.1, 1.1],
u_segments: 18,
v_segments: 12,
..Default::default()
},
);
let ids = (0..mesh.triangles.len()).collect::<Vec<_>>();
let tiny_options = RecognitionOptions {
distance_tolerance: 1.0e-13,
relative_tolerance: 0.0,
normal_tolerance: 0.05,
minimum_support: 4,
sampling: SamplingMode::Vertices,
..options()
};
for hint in [
SurfaceHint::Unknown,
SurfaceHint::KnownType {
surface_type: SurfaceType::Sphere,
},
] {
let fit = reconstruct_surface(&mesh, &ids, &hint, &tiny_options).unwrap_or_else(|error| {
let probes = [
SurfaceType::Plane,
SurfaceType::Sphere,
SurfaceType::Cylinder,
SurfaceType::Cone,
SurfaceType::Torus,
]
.map(|surface_type| {
(
surface_type,
reconstruct_surface(
&mesh,
&ids,
&SurfaceHint::KnownType { surface_type },
&tiny_options,
),
)
});
panic!("{hint:?}: {error}; probes={probes:?}")
});
assert_eq!(fit.surface.surface_type(), SurfaceType::Sphere);
assert!(fit.metrics.max_error <= 1.0e-13, "{hint:?}: {fit:?}");
}
}
#[test]
fn one_mesh_with_five_orders_of_carrier_scale_recovers_both_regions() {
let fixture = synthetic::mixed_scale_model();
let mixed_options = RecognitionOptions {
distance_tolerance: 1.0e-8,
relative_tolerance: 0.0,
normal_tolerance: 0.05,
minimum_support: 4,
sampling: SamplingMode::Vertices,
..options()
};
let result = recognize_surfaces_with_unresolved(&fixture.mesh, &mixed_options).unwrap();
assert!(result.unresolved_triangles.is_empty(), "{result:?}");
assert_eq!(result.regions.len(), 2, "{result:?}");
let recovered = result
.regions
.iter()
.map(|region| region.surface.surface_type())
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
recovered,
[SurfaceType::Sphere, SurfaceType::Cylinder]
.into_iter()
.collect()
);
}
#[test]
fn noisy_jittered_cylinder_is_recognized_deterministically() {
let truth = CylinderSurface {
axis_origin: Vec3::new(2.0, -3.0, 1.0),
axis: Vec3::new(0.2, -0.3, 0.9).normalized().unwrap(),
radius: 7.0,
};
let mesh = synthetic::tessellate_with_normals(
AnalyticSurface::Cylinder(truth),
synthetic::Patch {
u: [0.0, TAU],
v: [-2.0, 3.0],
u_segments: 32,
v_segments: 12,
jitter: 5.0e-8,
position_noise: 2.0e-5,
normal_noise: 5.0e-5,
irregular_triangulation: true,
density_power: [1.3, 0.8],
seed: 0x4e4f_4953_5943_594c,
},
);
let noisy_options = RecognitionOptions {
distance_tolerance: 3.0e-5,
relative_tolerance: 0.0,
normal_tolerance: 1.0e-3,
minimum_support: 16,
sampling: SamplingMode::Vertices,
deterministic_seed: Some(0x4e4f_4953_5943_594c),
..options()
};
let first = recognize_surfaces_with_unresolved(&mesh, &noisy_options).unwrap();
let second = recognize_surfaces_with_unresolved(&mesh, &noisy_options).unwrap();
assert_eq!(first, second);
assert!(first.unresolved_triangles.is_empty(), "{first:?}");
assert_eq!(first.regions.len(), 1, "{first:?}");
let AnalyticSurface::Cylinder(recovered) = first.regions[0].surface else {
panic!("selected {:?}", first.regions[0].surface);
};
assert!(recovered.axis.dot(truth.axis).abs() > 1.0 - 1.0e-8);
assert!((recovered.radius - truth.radius).abs() < 5.0e-6);
assert!(first.regions[0].metrics.max_error <= 3.0e-5);
}
#[test]
fn noisy_jittered_sphere_known_type_recovers_ground_truth() {
let truth = SphereSurface {
center: Vec3::new(-4.0, 1.5, 2.0),
radius: 3.25,
};
let mesh = synthetic::tessellate_with_normals(
AnalyticSurface::Sphere(truth),
synthetic::Patch {
u: [0.15, 5.9],
v: [-1.2, 1.15],
u_segments: 28,
v_segments: 16,
jitter: 2.0e-8,
position_noise: 1.0e-5,
normal_noise: 5.0e-5,
irregular_triangulation: true,
density_power: [0.8, 1.4],
seed: 0x4e4f_4953_5953_5048,
},
);
let ids = (0..mesh.triangles.len()).collect::<Vec<_>>();
let noisy_options = RecognitionOptions {
distance_tolerance: 1.5e-5,
relative_tolerance: 0.0,
normal_tolerance: 1.0e-3,
sampling: SamplingMode::Vertices,
deterministic_seed: Some(0x4e4f_4953_5953_5048),
..options()
};
let hint = SurfaceHint::KnownType {
surface_type: SurfaceType::Sphere,
};
let first = reconstruct_surface(&mesh, &ids, &hint, &noisy_options).unwrap();
let second = reconstruct_surface(&mesh, &ids, &hint, &noisy_options).unwrap();
assert_eq!(first, second);
let AnalyticSurface::Sphere(recovered) = first.surface else {
panic!("selected {:?}", first.surface);
};
assert!(recovered.center.distance(truth.center) < 3.0e-6);
assert!((recovered.radius - truth.radius).abs() < 2.0e-6);
assert!(first.metrics.max_error <= 1.5e-5);
}
#[test]
fn nearly_collinear_planar_strip_remains_recognizable() {
let truth = AnalyticSurface::Plane(brep_ransac::PlaneSurface {
origin: Vec3::new(1.0, -2.0, 3.0),
normal: Vec3::new(0.2, -0.1, 0.97).normalized().unwrap(),
});
let mesh = synthetic::tessellate(
truth,
synthetic::Patch {
u: [-1.0, 1.0],
v: [-5.0e-9, 5.0e-9],
u_segments: 16,
v_segments: 2,
..Default::default()
},
);
let strip_options = RecognitionOptions {
distance_tolerance: 1.0e-12,
relative_tolerance: 0.0,
normal_tolerance: 1.0e-6,
minimum_support: 4,
sampling: SamplingMode::TriangleCentroids,
..options()
};
let result = recognize_surfaces_with_unresolved(&mesh, &strip_options).unwrap();
assert!(result.unresolved_triangles.is_empty(), "{result:?}");
assert_eq!(result.regions.len(), 1, "{result:?}");
assert_eq!(result.regions[0].surface.surface_type(), SurfaceType::Plane);
assert!(result.regions[0].metrics.max_error <= 1.0e-12);
}