#![allow(clippy::unwrap_used, clippy::expect_used)]
use brepkit_math::traits::ParametricSurface;
use brepkit_operations::blend_ops::{chamfer_distance_angle, chamfer_v2, fillet_v2};
use brepkit_operations::measure::solid_volume;
use brepkit_operations::primitives::{make_box, make_cone, make_cylinder};
use brepkit_topology::Topology;
use brepkit_topology::edge::EdgeCurve;
use brepkit_topology::explorer::{solid_edges, solid_faces};
use brepkit_topology::face::FaceSurface;
const BOX_VOLUME: f64 = 1000.0;
#[test]
fn fillet_box_single_edge() {
let mut topo = Topology::new();
let solid = make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, solid).unwrap();
assert!(!edges.is_empty(), "box must have edges");
let result = fillet_v2(&mut topo, solid, &edges[..1], 1.0).unwrap();
let faces = solid_faces(&topo, result.solid).unwrap();
assert!(
faces.len() > 6,
"filleted box should have more than 6 faces"
);
assert!(
!result.succeeded.is_empty(),
"at least one edge should succeed"
);
let vol = solid_volume(&topo, result.solid, 0.01).unwrap();
assert!(
(vol - BOX_VOLUME).abs() > 0.01,
"filleted volume {vol} should differ from original {BOX_VOLUME}"
);
}
#[test]
fn fillet_box_multiple_edges() {
let mut topo = Topology::new();
let solid = make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, solid).unwrap();
let n = edges.len().min(4);
let target = &edges[..n];
let result = fillet_v2(&mut topo, solid, target, 0.5).unwrap();
assert!(
!result.succeeded.is_empty(),
"at least some edges should succeed"
);
let vol = solid_volume(&topo, result.solid, 0.01).unwrap();
assert!(
(vol - BOX_VOLUME).abs() > 0.01,
"filleted volume {vol} should differ from original {BOX_VOLUME}"
);
}
#[test]
fn chamfer_box_symmetric() {
let mut topo = Topology::new();
let solid = make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, solid).unwrap();
let result = chamfer_v2(&mut topo, solid, &edges[..1], 1.0, 1.0).unwrap();
let faces = solid_faces(&topo, result.solid).unwrap();
assert!(
faces.len() > 6,
"chamfered box should have more than 6 faces"
);
let vol = solid_volume(&topo, result.solid, 0.01).unwrap();
assert!(
(vol - BOX_VOLUME).abs() > 0.01,
"chamfered volume {vol} should differ from original {BOX_VOLUME}"
);
}
#[test]
fn chamfer_box_distance_angle() {
let mut topo = Topology::new();
let solid = make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, solid).unwrap();
let result = chamfer_distance_angle(
&mut topo,
solid,
&edges[..1],
1.0,
std::f64::consts::FRAC_PI_4,
)
.unwrap();
assert!(
!result.succeeded.is_empty(),
"distance-angle chamfer should succeed on at least one edge"
);
}
#[test]
fn fillet_zero_radius_error() {
let mut topo = Topology::new();
let solid = make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, solid).unwrap();
let err = fillet_v2(&mut topo, solid, &edges[..1], 0.0);
assert!(err.is_err(), "zero radius should return an error");
}
#[test]
fn chamfer_zero_distance_error() {
let mut topo = Topology::new();
let solid = make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, solid).unwrap();
let err = chamfer_v2(&mut topo, solid, &edges[..1], 0.0, 1.0);
assert!(err.is_err(), "zero distance should return an error");
}
#[test]
fn fillet_empty_edges_error() {
let mut topo = Topology::new();
let solid = make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let err = fillet_v2(&mut topo, solid, &[], 1.0);
assert!(err.is_err(), "empty edges should return an error");
}
#[test]
fn fillet_cylinder_base_circle_produces_torus() {
let mut topo = Topology::new();
let solid = make_cylinder(&mut topo, 2.0, 4.0).unwrap();
let base_vol = solid_volume(&topo, solid, 0.005).unwrap();
let bottom_rim = solid_edges(&topo, solid)
.unwrap()
.into_iter()
.find(|&eid| {
let e = topo.edge(eid).unwrap();
matches!(e.curve(), EdgeCurve::Circle(_))
&& topo.vertex(e.start()).unwrap().point().z().abs() < 1e-9
})
.expect("cylinder must have a bottom rim circle edge");
let result = fillet_v2(&mut topo, solid, &[bottom_rim], 0.3).unwrap();
assert!(
!result.succeeded.is_empty(),
"cylinder rim fillet must produce at least one stripe; failed = {:?}",
result.failed
);
let new_faces: Vec<_> = solid_faces(&topo, result.solid).unwrap();
let torus = new_faces.iter().find_map(|&fid| {
if let FaceSurface::Torus(t) = topo.face(fid).unwrap().surface() {
Some(t.clone())
} else {
None
}
});
let torus = torus.expect("analytic fast path should produce a Torus face");
let r_c = 2.0;
let r_fillet = 0.3;
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-9,
"torus minor radius should equal fillet radius {r_fillet}, got {}",
torus.minor_radius()
);
assert!(
(torus.major_radius() - (r_c - r_fillet)).abs() < 1e-9,
"torus major radius should equal r_c − r_fillet ({}), got {}",
r_c - r_fillet,
torus.major_radius()
);
let want_plate = brepkit_math::vec::Point3::new(0.0, r_c - r_fillet, 0.0);
let want_cyl = brepkit_math::vec::Point3::new(0.0, r_c, r_fillet);
let mut closest_plate = f64::INFINITY;
let mut closest_cyl = f64::INFINITY;
for i in 0..1440 {
let v = (f64::from(i) / 1440.0) * std::f64::consts::TAU;
let p = ParametricSurface::evaluate(&torus, 0.0, v);
closest_plate = closest_plate.min((p - want_plate).length());
closest_cyl = closest_cyl.min((p - want_cyl).length());
}
assert!(
closest_plate < 1e-6,
"torus should touch plate at {want_plate:?}; closest sample was {closest_plate:.6}"
);
assert!(
closest_cyl < 1e-6,
"torus should touch cylinder at {want_cyl:?}; closest sample was {closest_cyl:.6}"
);
let sd = topo.solid(result.solid).unwrap();
let sh = topo.shell(sd.outer_shell()).unwrap();
brepkit_topology::validation::validate_shell_closed(sh, &topo)
.expect("rim fillet result must be watertight");
brepkit_topology::validation::validate_shell_manifold(sh, &topo)
.expect("rim fillet result must be manifold");
let vol = solid_volume(&topo, result.solid, 0.005).unwrap();
assert!(
vol < base_vol && vol > base_vol * 0.99,
"rim fillet should remove only a thin sliver: base={base_vol:.3}, filleted={vol:.3}"
);
}
#[test]
fn fillet_cone_bottom_rim_produces_torus() {
let mut topo = Topology::new();
let solid = make_cone(&mut topo, 3.0, 1.0, 4.0).unwrap();
let fillet_r = 0.3;
let bottom_rim = solid_edges(&topo, solid)
.unwrap()
.into_iter()
.find(|&eid| {
if let EdgeCurve::Circle(c) = topo.edge(eid).unwrap().curve() {
(c.radius() - 3.0).abs() < 1e-6
} else {
false
}
})
.expect("frustum bottom rim should exist with radius 3");
let result = fillet_v2(&mut topo, solid, &[bottom_rim], fillet_r).unwrap();
assert!(
!result.succeeded.is_empty(),
"cone bottom-rim fillet must produce at least one stripe; failed = {:?}",
result.failed
);
let new_faces = solid_faces(&topo, result.solid).unwrap();
let torus = new_faces
.iter()
.find_map(|&fid| {
if let FaceSurface::Torus(t) = topo.face(fid).unwrap().surface() {
Some(t.clone())
} else {
None
}
})
.expect("plane-cone fillet should produce a Torus face");
let alpha = 6.0_f64.atan2(3.0);
let r_p = 3.0;
let expected_major = r_p + fillet_r * (alpha * 0.5).tan().recip();
assert!(
(torus.minor_radius() - fillet_r).abs() < 1e-9,
"torus minor should equal fillet radius {fillet_r}, got {}",
torus.minor_radius()
);
assert!(
(torus.major_radius() - expected_major).abs() < 1e-6,
"torus major should be r_p + r·cot(α/2) = {expected_major:.6}, got {}",
torus.major_radius()
);
let want_plate = brepkit_math::vec::Point3::new(0.0, -expected_major, 0.0);
let cone_contact_axial = -fillet_r * (1.0 + alpha.cos());
let cone_contact_radial = expected_major - fillet_r * alpha.sin();
let want_cone = brepkit_math::vec::Point3::new(0.0, -cone_contact_radial, cone_contact_axial);
let v_plate = 3.0 * std::f64::consts::FRAC_PI_2;
let p_plate = ParametricSurface::evaluate(&torus, 0.0, v_plate);
let v_cone = alpha.cos().atan2(-alpha.sin());
let p_cone = ParametricSurface::evaluate(&torus, 0.0, v_cone);
assert!(
(p_plate - want_plate).length() < 1e-9,
"torus at v=3π/2 should touch plate at {want_plate:?}; got {p_plate:?}"
);
assert!(
(p_cone - want_cone).length() < 1e-9,
"torus at v=atan2(cos α, -sin α) should touch cone at {want_cone:?}; got {p_cone:?}"
);
}
#[test]
fn chamfer_cylinder_base_circle_produces_cone() {
let mut topo = Topology::new();
let r_c = 2.0;
let height = 4.0;
let solid = make_cylinder(&mut topo, r_c, height).unwrap();
let d = 0.4;
let bottom_rim = solid_edges(&topo, solid)
.unwrap()
.into_iter()
.find(|&eid| {
let edge = topo.edge(eid).unwrap();
matches!(edge.curve(), EdgeCurve::Circle(_))
&& topo.vertex(edge.start()).unwrap().point().z().abs() < 1e-9
})
.expect("cylinder bottom rim must exist");
let result = chamfer_v2(&mut topo, solid, &[bottom_rim], d, d).unwrap();
assert!(
!result.succeeded.is_empty(),
"cylinder rim chamfer must produce at least one stripe; failed = {:?}",
result.failed
);
let new_faces = solid_faces(&topo, result.solid).unwrap();
let cone = new_faces
.iter()
.find_map(|&fid| {
if let FaceSurface::Cone(c) = topo.face(fid).unwrap().surface() {
Some(c.clone())
} else {
None
}
})
.expect("plane-cylinder chamfer should produce a Cone face via the analytic fast path");
assert!(
(cone.half_angle() - std::f64::consts::FRAC_PI_4).abs() < 1e-12,
"cone half-angle should be π/4 for symmetric chamfer; got {}",
cone.half_angle()
);
let want_plate = brepkit_math::vec::Point3::new(0.0, r_c - d, 0.0);
let want_cyl = brepkit_math::vec::Point3::new(0.0, r_c, d);
let alpha = std::f64::consts::FRAC_PI_4;
let v_plate = (r_c - d) / alpha.cos();
let p_plate = ParametricSurface::evaluate(&cone, 0.0, v_plate);
assert!(
(p_plate - want_plate).length() < 1e-9,
"cone at v={v_plate:.6} should touch plate at {want_plate:?}; got {p_plate:?}"
);
let v_cyl = r_c / alpha.cos();
let p_cyl = ParametricSurface::evaluate(&cone, 0.0, v_cyl);
assert!(
(p_cyl - want_cyl).length() < 1e-9,
"cone at v={v_cyl:.6} should touch cylinder at {want_cyl:?}; got {p_cyl:?}"
);
let cone_face = new_faces
.iter()
.copied()
.find(|&fid| matches!(topo.face(fid).unwrap().surface(), FaceSurface::Cone(_)))
.unwrap();
let cone_wire = topo.face(cone_face).unwrap().outer_wire();
let has_misplaced = topo.wire(cone_wire).unwrap().edges().iter().any(|oe| {
if let EdgeCurve::Circle(c) = topo.edge(oe.edge()).unwrap().curve() {
(c.center().z() + d).abs() < 1e-6 && (c.radius() - r_c).abs() < 1e-6
} else {
false
}
});
assert!(
!has_misplaced,
"no contact circle should sit at z = -d (cylinder contact misplaced)"
);
}
#[test]
fn chamfer_cone_bottom_rim_produces_cone() {
let mut topo = Topology::new();
let solid = make_cone(&mut topo, 3.0, 1.0, 4.0).unwrap();
let d = 0.4;
let bottom_rim = solid_edges(&topo, solid)
.unwrap()
.into_iter()
.find(|&eid| {
if let EdgeCurve::Circle(c) = topo.edge(eid).unwrap().curve() {
(c.radius() - 3.0).abs() < 1e-6
} else {
false
}
})
.expect("frustum bottom rim should exist with radius 3");
let result = chamfer_v2(&mut topo, solid, &[bottom_rim], d, d).unwrap();
assert!(
!result.succeeded.is_empty(),
"cone bottom-rim chamfer must produce at least one stripe; failed = {:?}",
result.failed
);
let new_faces = solid_faces(&topo, result.solid).unwrap();
let alpha = 6.0_f64.atan2(3.0);
let r_p = 3.0;
let expected_half_angle = std::f64::consts::FRAC_PI_2 - alpha * 0.5;
let chamfer_cone = new_faces
.iter()
.find_map(|&fid| {
if let FaceSurface::Cone(c) = topo.face(fid).unwrap().surface() {
if (c.half_angle() - expected_half_angle).abs() < 1e-6 {
return Some(c.clone());
}
}
None
})
.expect("chamfer cone face with half-angle = π/2 - α/2 should exist");
assert!(
(chamfer_cone.half_angle() - expected_half_angle).abs() < 1e-9,
"chamfer cone half-angle should be π/2 - α/2 = {expected_half_angle:.6}, got {}",
chamfer_cone.half_angle()
);
let want_plate = brepkit_math::vec::Point3::new(0.0, -(r_p - d), 0.0);
let cone_contact_axial = d * alpha.sin();
let cone_contact_radial = r_p - d * alpha.cos();
let want_cone = brepkit_math::vec::Point3::new(0.0, -cone_contact_radial, cone_contact_axial);
let v_plate = (r_p - d) / expected_half_angle.cos();
let p_plate = ParametricSurface::evaluate(&chamfer_cone, 0.0, v_plate);
assert!(
(p_plate - want_plate).length() < 1e-9,
"chamfer cone at v={v_plate:.6} should touch plate at {want_plate:?}; got {p_plate:?}"
);
let v_cone = (r_p - d * alpha.cos()) / expected_half_angle.cos();
let p_cone = ParametricSurface::evaluate(&chamfer_cone, 0.0, v_cone);
assert!(
(p_cone - want_cone).length() < 1e-9,
"chamfer cone at v={v_cone:.6} should touch cone at {want_cone:?}; got {p_cone:?}"
);
}