#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
fn make_spine(topo: &mut Topology, a: Point3, b: Point3) -> (Spine, FaceId, FaceId) {
let v0 = topo.add_vertex(Vertex::new(a, 1e-7));
let v1 = topo.add_vertex(Vertex::new(b, 1e-7));
let eid = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let oe = OrientedEdge::new(eid, true);
let w1 = topo.add_wire(Wire::new(vec![oe], false).unwrap());
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], false).unwrap());
let f1 = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let f2 = topo.add_face(Face::new(
w2,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 1.0, 0.0),
d: 0.0,
},
));
let spine = Spine::from_single_edge(topo, eid).unwrap();
(spine, f1, f2)
}
#[test]
fn plane_plane_90_degree_fillet() {
let mut topo = Topology::new();
let n1 = Vec3::new(0.0, 0.0, 1.0); let n2 = Vec3::new(0.0, 1.0, 0.0); let (spine, f1, f2) = make_spine(
&mut topo,
Point3::new(0.0, 0.0, 0.0),
Point3::new(10.0, 0.0, 0.0),
);
let radius = 2.0;
let result = plane_plane_fillet(&spine, &topo, n1, n2, radius, f1, f2).unwrap();
match &result.stripe.surface {
FaceSurface::Cylinder(cyl) => {
assert!(
(cyl.radius() - radius).abs() < 1e-10,
"Expected radius {radius}, got {}",
cyl.radius()
);
let axis = cyl.axis();
assert!(
axis.dot(Vec3::new(1.0, 0.0, 0.0)).abs() > 0.99,
"Cylinder axis should be along X, got {axis:?}"
);
}
other => panic!("Expected Cylinder surface, got {other:?}"),
}
let c1_start = result.stripe.contact1.evaluate(0.0);
let c1_end = result.stripe.contact1.evaluate(1.0);
let c1_dir = (c1_end - c1_start).normalize().unwrap();
assert!(
c1_dir.dot(Vec3::new(1.0, 0.0, 0.0)).abs() > 0.99,
"Contact 1 should be along X"
);
let half_angle = std::f64::consts::FRAC_PI_4;
let expected_offset = radius / half_angle.sin();
let sections = &result.stripe.sections;
assert_eq!(sections.len(), 2);
assert!((sections[0].radius - radius).abs() < 1e-10);
let bisector = (n1 + n2).normalize().unwrap();
let expected_center = Point3::new(0.0, 0.0, 0.0) + bisector * expected_offset;
let actual_center = sections[0].center;
assert!(
(actual_center - expected_center).length() < 1e-10,
"Expected center at {expected_center:?}, got {actual_center:?}"
);
}
#[test]
fn plane_plane_60_degree_fillet() {
let mut topo = Topology::new();
let n1 = Vec3::new(0.0, 0.0, 1.0);
let angle = std::f64::consts::FRAC_PI_3;
let n2 = Vec3::new(0.0, angle.sin(), angle.cos());
let n2 = n2.normalize().unwrap();
let (spine, f1, f2) = make_spine(
&mut topo,
Point3::new(0.0, 0.0, 0.0),
Point3::new(5.0, 0.0, 0.0),
);
let radius = 1.5;
let result = plane_plane_fillet(&spine, &topo, n1, n2, radius, f1, f2).unwrap();
match &result.stripe.surface {
FaceSurface::Cylinder(cyl) => {
assert!(
(cyl.radius() - radius).abs() < 1e-10,
"Expected radius {radius}, got {}",
cyl.radius()
);
}
other => panic!("Expected Cylinder surface, got {other:?}"),
}
let cos_angle = n1.dot(n2);
let half = (std::f64::consts::PI - cos_angle.acos()) / 2.0;
let expected_offset = radius / half.sin();
let center = result.stripe.sections[0].center;
let origin = Point3::new(0.0, 0.0, 0.0);
let actual_offset = (center - origin).length();
assert!(
(actual_offset - expected_offset).abs() < 1e-10,
"Expected offset {expected_offset}, got {actual_offset}"
);
}
#[test]
fn plane_plane_chamfer_is_flat() {
let mut topo = Topology::new();
let n1 = Vec3::new(0.0, 0.0, 1.0);
let n2 = Vec3::new(0.0, 1.0, 0.0);
let (spine, f1, f2) = make_spine(
&mut topo,
Point3::new(0.0, 0.0, 0.0),
Point3::new(10.0, 0.0, 0.0),
);
let d1 = 3.0;
let d2 = 2.0;
let result = plane_plane_chamfer(&spine, &topo, n1, n2, d1, d2, f1, f2).unwrap();
match &result.stripe.surface {
FaceSurface::Plane { normal, d } => {
let spine_dir = Vec3::new(1.0, 0.0, 0.0);
assert!(
normal.dot(spine_dir).abs() < 1e-10,
"Chamfer normal should be perpendicular to spine, dot={:.6}",
normal.dot(spine_dir)
);
assert!(
(normal.length() - 1.0).abs() < 1e-10,
"Normal should be unit length"
);
assert!(d.is_finite(), "d should be finite");
}
other => panic!("Expected Plane surface for chamfer, got {other:?}"),
}
let c1_start = result.stripe.contact1.evaluate(0.0);
let c1_end = result.stripe.contact1.evaluate(1.0);
let c1_dir = (c1_end - c1_start).normalize().unwrap();
assert!(
c1_dir.dot(Vec3::new(1.0, 0.0, 0.0)).abs() > 0.99,
"Contact 1 should be along X"
);
}
#[test]
fn non_analytic_returns_none() {
let mut topo = Topology::new();
let nurbs_surf = FaceSurface::Nurbs(
brepkit_math::nurbs::surface::NurbsSurface::new(
1,
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0],
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
vec![Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
],
vec![vec![1.0, 1.0], vec![1.0, 1.0]],
)
.unwrap(),
);
let plane_surf = FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
};
let (spine, f1, f2) = make_spine(
&mut topo,
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
);
let result = try_analytic_fillet(&nurbs_surf, &plane_surf, &spine, &topo, 1.0, f1, f2).unwrap();
assert!(result.is_none(), "NURBS-Plane pair should return None");
let result =
try_analytic_chamfer(&nurbs_surf, &plane_surf, &spine, &topo, 1.0, 1.0, f1, f2).unwrap();
assert!(result.is_none(), "NURBS-Plane chamfer should return None");
}
#[test]
fn plane_cylinder_fillet_concave_emits_torus_with_smaller_major() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let r_c: f64 = 2.0;
let r_fillet = 0.3;
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let cyl_surface =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl_surface.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, -1.0);
let result = plane_cylinder_fillet(
n_p_inward,
0.0,
&cyl_surface,
&spine,
&topo,
r_fillet,
face_plate,
face_cyl,
)
.unwrap()
.expect("concave plane-cylinder fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-9,
"torus minor should equal fillet radius {r_fillet}, got {}",
torus.minor_radius()
);
assert!(
(torus.major_radius() - (r_c - r_fillet)).abs() < 1e-9,
"torus major should be r_c − r_fillet = {} for concave, got {}",
r_c - r_fillet,
torus.major_radius()
);
let center = torus.center();
assert!(
(center.x()).abs() < 1e-9 && (center.y()).abs() < 1e-9,
"torus center should be on the cylinder axis"
);
assert!(
(center.z() - r_fillet).abs() < 1e-9,
"concave torus center should sit at z = +r ({r_fillet}), got {}",
center.z()
);
}
#[test]
fn plane_cylinder_fillet_rim_emits_torus_with_smaller_major() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let r_c: f64 = 2.0;
let r_fillet = 0.3;
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, -1.0),
d: 0.0,
},
));
let cyl_surface =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new(
w2,
vec![],
FaceSurface::Cylinder(cyl_surface.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, 1.0);
let result = plane_cylinder_fillet(
n_p_inward,
0.0,
&cyl_surface,
&spine,
&topo,
r_fillet,
face_plate,
face_cyl,
)
.unwrap()
.expect("rim plane-cylinder fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
assert!(
(torus.major_radius() - (r_c - r_fillet)).abs() < 1e-9,
"torus major should be r_c − r_fillet = {} for an inward rim, got {}",
r_c - r_fillet,
torus.major_radius()
);
let center = torus.center();
assert!(
(center.z() - r_fillet).abs() < 1e-9,
"rim torus centre should sit at z = +r ({r_fillet}), got {}",
center.z()
);
}
#[test]
fn plane_cylinder_fillet_concave_rejects_spindle_radius() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let r_c: f64 = 2.0;
let setup = |topo: &mut Topology, reversed: bool| {
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, 0.0), 1e-7));
let circle =
Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, -1.0),
d: 0.0,
},
));
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c)
.unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = if reversed {
topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl.clone()),
))
} else {
topo.add_face(Face::new(w2, vec![], FaceSurface::Cylinder(cyl.clone())))
};
(spine, cyl, face_plate, face_cyl)
};
let mut topo_concave = Topology::new();
let (spine_concave, cyl_concave, fp_concave, fc_concave) = setup(&mut topo_concave, true);
let n_p_inward = Vec3::new(0.0, 0.0, -1.0);
let result = plane_cylinder_fillet(
n_p_inward,
0.0,
&cyl_concave,
&spine_concave,
&topo_concave,
1.1,
fp_concave,
fc_concave,
)
.unwrap();
assert!(
result.is_none(),
"concave fillet must reject r > r_c/2 (spindle-torus regime)"
);
let result_eq = plane_cylinder_fillet(
n_p_inward,
0.0,
&cyl_concave,
&spine_concave,
&topo_concave,
1.0,
fp_concave,
fc_concave,
)
.unwrap();
assert!(
result_eq.is_none(),
"concave fillet must reject r = r_c/2 (degenerate major = minor)"
);
let mut topo_rim = Topology::new();
let (spine_rim, cyl_rim, fp_rim, fc_rim) = setup(&mut topo_rim, false);
let n_p_inward_rim = Vec3::new(0.0, 0.0, 1.0);
let result_rim_spindle = plane_cylinder_fillet(
n_p_inward_rim,
0.0,
&cyl_rim,
&spine_rim,
&topo_rim,
1.5,
fp_rim,
fc_rim,
)
.unwrap();
assert!(
result_rim_spindle.is_none(),
"rim fillet must reject r > r_c/2 (spindle-torus regime)"
);
let result_rim_ok = plane_cylinder_fillet(
n_p_inward_rim,
0.0,
&cyl_rim,
&spine_rim,
&topo_rim,
0.5,
fp_rim,
fc_rim,
)
.unwrap();
assert!(
result_rim_ok.is_some(),
"rim fillet should accept r < r_c/2"
);
}
#[test]
fn plane_cone_fillet_concave_emits_torus_with_smaller_major() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let alpha = 6.0_f64.atan2(3.0);
let r_p = 3.0;
let r_fillet = 0.3;
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let cone_surface =
ConicalSurface::new(Point3::new(0.0, 0.0, 6.0), Vec3::new(0.0, 0.0, -1.0), alpha).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone_surface.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, -1.0);
let result = plane_cone_fillet(
n_p_inward,
0.0,
&cone_surface,
&spine,
&topo,
r_fillet,
face_plate,
face_cone,
)
.unwrap()
.expect("concave plane-cone fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let expected_major = r_p - r_fillet * (alpha * 0.5).tan().recip();
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-9,
"torus minor should equal fillet radius {r_fillet}, got {}",
torus.minor_radius()
);
assert!(
(torus.major_radius() - expected_major).abs() < 1e-9,
"concave torus major should be r_p − r·cot(α/2) ≈ {expected_major:.6}, got {}",
torus.major_radius()
);
let center = torus.center();
assert!(
(center.z() - r_fillet).abs() < 1e-9,
"concave torus center should sit at z = +r ({r_fillet}), got {}",
center.z()
);
}
#[test]
fn plane_cone_fillet_concave_rejects_spindle_radius() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let alpha = 6.0_f64.atan2(3.0);
let r_p = 3.0;
let cot_half = (alpha * 0.5).tan().recip();
let r_max = r_p / (cot_half + 1.0);
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let cone_surface =
ConicalSurface::new(Point3::new(0.0, 0.0, 6.0), Vec3::new(0.0, 0.0, -1.0), alpha).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone_surface.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, -1.0);
let result_spindle = plane_cone_fillet(
n_p_inward,
0.0,
&cone_surface,
&spine,
&topo,
r_max * 1.01,
face_plate,
face_cone,
)
.unwrap();
assert!(
result_spindle.is_none(),
"concave fillet must reject r > r_p / (cot(α/2)+1) (spindle-torus regime)"
);
let result_horn = plane_cone_fillet(
n_p_inward,
0.0,
&cone_surface,
&spine,
&topo,
r_max,
face_plate,
face_cone,
)
.unwrap();
assert!(
result_horn.is_none(),
"concave fillet must reject r = r_p / (cot(α/2)+1) (horn-torus boundary)"
);
let result_ok = plane_cone_fillet(
n_p_inward,
0.0,
&cone_surface,
&spine,
&topo,
r_max * 0.5,
face_plate,
face_cone,
)
.unwrap();
assert!(
result_ok.is_some(),
"concave fillet should accept r below the spindle threshold"
);
}
#[test]
fn plane_cylinder_chamfer_concave_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let r_c: f64 = 2.0;
let d = 0.4;
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let cyl_surface =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl_surface.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, 1.0);
let result = plane_cylinder_chamfer(
n_p_inward,
0.0,
&cyl_surface,
&spine,
&topo,
d,
d,
face_plate,
face_cyl,
)
.unwrap()
.expect("concave plane-cylinder chamfer should produce a stripe");
let cone_surf = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
assert!(
(cone_surf.half_angle() - std::f64::consts::FRAC_PI_4).abs() < 1e-12,
"chamfer cone half-angle should be π/4 for symmetric d, got {}",
cone_surf.half_angle()
);
let want_plate = Point3::new(0.0, r_c + d, 0.0);
let want_cyl = Point3::new(0.0, r_c, -d);
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(&cone_surf, 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-3,
"concave chamfer cone should pass near plate contact at {want_plate:?}; closest = {closest_plate:.6}"
);
assert!(
closest_cyl < 1e-3,
"concave chamfer cone should pass near cyl contact at {want_cyl:?}; closest = {closest_cyl:.6}"
);
}
#[test]
fn plane_sphere_fillet_convex_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r: f64 = 2.0;
let h_real: f64 = 1.0;
let r_fillet: f64 = 0.3;
let r_p_sq = big_r * big_r - h_real * h_real;
let r_p = r_p_sq.sqrt();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, h_real), big_r).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w2, vec![], FaceSurface::Sphere(sphere.clone())));
let n_p_inward = Vec3::new(0.0, 0.0, -1.0);
let result = plane_sphere_fillet(
n_p_inward,
0.0,
&sphere,
&spine,
&topo,
r_fillet,
face_plate,
face_sphere,
)
.unwrap()
.expect("convex plane-sphere fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let expected_major_sq = r_p_sq + 2.0 * r_fillet * (big_r + h_real);
let expected_major = expected_major_sq.sqrt();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-12,
"torus major should be √(r_p² + 2r(R+h)) = {expected_major}, got {}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"torus minor should equal fillet radius {r_fillet}, got {}",
torus.minor_radius()
);
let center = torus.center();
assert!(
(center.x()).abs() < 1e-12 && (center.y()).abs() < 1e-12,
"torus center should be on z-axis, got {center:?}"
);
assert!(
(center.z() - r_fillet).abs() < 1e-12,
"torus center z should be +r_fillet = {r_fillet}, got {}",
center.z()
);
let want_plate = Point3::new(expected_major, 0.0, 0.0);
let r_plus_r = big_r + r_fillet;
let want_sphere = Point3::new(
expected_major * big_r / r_plus_r,
0.0,
r_fillet * (big_r + h_real) / r_plus_r,
);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_plate);
let on_torus_plate = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_s, v_s) = ParametricSurface::project_point(&torus, want_sphere);
let on_torus_sphere = ParametricSurface::evaluate(&torus, u_s, v_s);
assert!(
(on_torus_plate - want_plate).length() < 1e-9,
"plate contact must lie on torus: project→eval gave {on_torus_plate:?}, want {want_plate:?}"
);
assert!(
(on_torus_sphere - want_sphere).length() < 1e-9,
"sphere contact must lie on torus: project→eval gave {on_torus_sphere:?}, want {want_sphere:?}"
);
let sphere_dist = (want_sphere - Point3::new(0.0, 0.0, h_real)).length();
assert!(
(sphere_dist - big_r).abs() < 1e-9,
"sphere contact must lie on sphere: distance from center = {sphere_dist}, want {big_r}"
);
}
#[test]
fn plane_sphere_fillet_concave_emits_torus_with_smaller_major() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r: f64 = 2.0;
let h_real: f64 = 1.0;
let r_fillet: f64 = 0.3;
let r_p_sq = big_r * big_r - h_real * h_real;
let r_p = r_p_sq.sqrt();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, -h_real), big_r).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_sphere = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(sphere.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, -1.0);
let result = plane_sphere_fillet(
n_p_inward,
0.0,
&sphere,
&spine,
&topo,
r_fillet,
face_plate,
face_sphere,
)
.unwrap()
.expect("concave plane-sphere fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let expected_major_sq = r_p_sq - 2.0 * r_fillet * (big_r - h_real);
let expected_major = expected_major_sq.sqrt();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-12,
"concave torus major should be √(r_p² − 2r(R−h)) = {expected_major}, got {}",
torus.major_radius()
);
assert!(
torus.major_radius() < r_p,
"concave torus major must be smaller than spine radius (plate contact moves INWARD), got {} vs r_p={r_p}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"torus minor should equal fillet radius {r_fillet}, got {}",
torus.minor_radius()
);
let center = torus.center();
assert!(
(center.z() - (-r_fillet)).abs() < 1e-12,
"concave torus center z should be −r_fillet = {}, got {}",
-r_fillet,
center.z()
);
let denom = big_r - r_fillet;
let want_sphere = Point3::new(
expected_major * big_r / denom,
0.0,
-r_fillet * (big_r - h_real) / denom,
);
assert!(
want_sphere.z() < 0.0,
"concave sphere contact must be on lower hemisphere (z<0), got z={}",
want_sphere.z()
);
let sphere_dist = (want_sphere - Point3::new(0.0, 0.0, -h_real)).length();
assert!(
(sphere_dist - big_r).abs() < 1e-9,
"sphere contact must lie on sphere: distance from center = {sphere_dist}, want {big_r}"
);
let want_plate = Point3::new(expected_major, 0.0, 0.0);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_plate);
let on_torus_plate = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_s, v_s) = ParametricSurface::project_point(&torus, want_sphere);
let on_torus_sphere = ParametricSurface::evaluate(&torus, u_s, v_s);
assert!(
(on_torus_plate - want_plate).length() < 1e-9,
"plate contact must lie on torus: project→eval gave {on_torus_plate:?}, want {want_plate:?}"
);
assert!(
(on_torus_sphere - want_sphere).length() < 1e-9,
"sphere contact must lie on torus: project→eval gave {on_torus_sphere:?}, want {want_sphere:?}"
);
}
#[test]
fn plane_sphere_fillet_concave_rejects_spindle_radius() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r: f64 = 2.0;
let h_real: f64 = 1.0;
let r_p_sq = big_r * big_r - h_real * h_real;
let r_p = r_p_sq.sqrt();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, -h_real), big_r).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_sphere = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(sphere.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, -1.0);
let too_big = 1.5;
let result = plane_sphere_fillet(
n_p_inward,
0.0,
&sphere,
&spine,
&topo,
too_big,
face_plate,
face_sphere,
)
.unwrap();
assert!(
result.is_none(),
"concave fillet at r={too_big} should reject (spindle / R_t < minor)"
);
let mut topo2 = Topology::new();
let v2 = topo2.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle2 = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid2 = topo2.add_edge(Edge::new(v2, v2, EdgeCurve::Circle(circle2)));
let spine2 = Spine::from_single_edge(&topo2, eid2).unwrap();
let w1b = topo2.add_wire(Wire::new(vec![OrientedEdge::new(eid2, true)], true).unwrap());
let face_plate2 = topo2.add_face(Face::new(
w1b,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let sphere2 = SphericalSurface::new(Point3::new(0.0, 0.0, h_real), big_r).unwrap();
let w2b = topo2.add_wire(Wire::new(vec![OrientedEdge::new(eid2, false)], true).unwrap());
let face_sphere2 = topo2.add_face(Face::new(w2b, vec![], FaceSurface::Sphere(sphere2.clone())));
let result_convex = plane_sphere_fillet(
n_p_inward,
0.0,
&sphere2,
&spine2,
&topo2,
too_big,
face_plate2,
face_sphere2,
)
.unwrap();
assert!(
result_convex.is_some(),
"convex fillet at the same r={too_big} should still succeed"
);
}
#[test]
fn plane_sphere_chamfer_convex_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r: f64 = 2.0;
let h_real: f64 = 1.0;
let d: f64 = 0.3;
let r_p_sq = big_r * big_r - h_real * h_real;
let r_p = r_p_sq.sqrt();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, h_real), big_r).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w2, vec![], FaceSurface::Sphere(sphere.clone())));
let n_p_inward = Vec3::new(0.0, 0.0, 1.0);
let result = plane_sphere_chamfer(
n_p_inward,
0.0,
&sphere,
&spine,
&topo,
d,
d,
face_plate,
face_sphere,
)
.unwrap()
.expect("convex plane-sphere chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta = d / big_r;
let (sin_d, cos_d) = delta.sin_cos();
let sphere_radial_pred = r_p * cos_d + h_real * sin_d;
let sphere_axial_pred = h_real * (1.0 - cos_d) + r_p * sin_d;
let delta_r = sphere_radial_pred - (r_p + d);
let delta_z = sphere_axial_pred;
assert!(delta_r < 0.0, "Δr should be negative (cone narrows up)");
let expected_beta = (-delta_z / delta_r).atan();
assert!(
(chamfer_cone.half_angle() - expected_beta).abs() < 1e-12,
"chamfer cone half-angle should be atan(−Δz/Δr) = {expected_beta}, got {}",
chamfer_cone.half_angle()
);
let expected_apex_z = (r_p + d) * expected_beta.tan();
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"apex z = {}, expected {expected_apex_z}",
apex.z()
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) < -1.0 + 1e-12,
"chamfer cone axis should be -z, got {axis:?}"
);
let want_plate = Point3::new(r_p + d, 0.0, 0.0);
let want_sphere = Point3::new(sphere_radial_pred, 0.0, sphere_axial_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_plate);
let on_cone_plate = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_s, v_s) = ParametricSurface::project_point(&chamfer_cone, want_sphere);
let on_cone_sphere = ParametricSurface::evaluate(&chamfer_cone, u_s, v_s);
assert!(
(on_cone_plate - want_plate).length() < 1e-9,
"plate contact must lie on chamfer cone: project→eval gave {on_cone_plate:?}, want {want_plate:?}"
);
assert!(
(on_cone_sphere - want_sphere).length() < 1e-9,
"sphere contact must lie on chamfer cone: project→eval gave {on_cone_sphere:?}, want {want_sphere:?}"
);
let sphere_dist = (want_sphere - Point3::new(0.0, 0.0, h_real)).length();
assert!(
(sphere_dist - big_r).abs() < 1e-9,
"sphere contact must lie on sphere: distance = {sphere_dist}, want {big_r}"
);
}
#[test]
fn plane_sphere_chamfer_concave_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r: f64 = 2.0;
let h_real: f64 = 1.0;
let d: f64 = 0.3;
let r_p_sq = big_r * big_r - h_real * h_real;
let r_p = r_p_sq.sqrt();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, -h_real), big_r).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_sphere = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(sphere.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, 1.0);
let result = plane_sphere_chamfer(
n_p_inward,
0.0,
&sphere,
&spine,
&topo,
d,
d,
face_plate,
face_sphere,
)
.unwrap()
.expect("concave plane-sphere chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let h_signed = -h_real;
let signed_offset = -1.0_f64;
let delta = d / big_r;
let (sin_d, cos_d) = delta.sin_cos();
let sphere_radial_pred = r_p * cos_d + signed_offset * h_signed * sin_d;
let sphere_axial_pred = h_signed * (1.0 - cos_d) + signed_offset * r_p * sin_d;
let delta_r = sphere_radial_pred - (r_p + d);
let delta_z = sphere_axial_pred;
let expected_z_apex = -(r_p + d) * delta_z / delta_r;
let expected_beta = (expected_z_apex.abs() / (r_p + d)).atan();
assert!(
expected_z_apex < 0.0,
"concave: z_apex should be negative (apex below plate), got {expected_z_apex}"
);
assert!(
sphere_axial_pred < 0.0,
"concave: sphere contact must be below plate (z<0), got {sphere_axial_pred}"
);
assert!(
(chamfer_cone.half_angle() - expected_beta).abs() < 1e-12,
"chamfer cone half-angle should be {expected_beta}, got {}",
chamfer_cone.half_angle()
);
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_z_apex).abs() < 1e-9,
"apex z = {}, expected {expected_z_apex}",
apex.z()
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"concave chamfer cone axis should be +z, got {axis:?}"
);
let want_plate = Point3::new(r_p + d, 0.0, 0.0);
let want_sphere = Point3::new(sphere_radial_pred, 0.0, sphere_axial_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_plate);
let on_cone_plate = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_s, v_s) = ParametricSurface::project_point(&chamfer_cone, want_sphere);
let on_cone_sphere = ParametricSurface::evaluate(&chamfer_cone, u_s, v_s);
assert!(
(on_cone_plate - want_plate).length() < 1e-9,
"plate contact must lie on chamfer cone: gave {on_cone_plate:?}, want {want_plate:?}"
);
assert!(
(on_cone_sphere - want_sphere).length() < 1e-9,
"sphere contact must lie on chamfer cone: gave {on_cone_sphere:?}, want {want_sphere:?}"
);
let sphere_dist = (want_sphere - Point3::new(0.0, 0.0, -h_real)).length();
assert!(
(sphere_dist - big_r).abs() < 1e-9,
"sphere contact must lie on sphere: distance = {sphere_dist}, want {big_r}"
);
}
#[test]
fn sphere_sphere_fillet_convex_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r1: f64 = 2.0;
let big_r2: f64 = 2.5;
let big_d: f64 = 3.0;
let r_fillet: f64 = 0.4;
let a0 = (big_r1 * big_r1 - big_r2 * big_r2 + big_d * big_d) / (2.0 * big_d);
let r_p_sq = big_r1 * big_r1 - a0 * a0;
let r_p = r_p_sq.sqrt();
let s1 = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r1).unwrap();
let s2 = SphericalSurface::new(Point3::new(0.0, 0.0, big_d), big_r2).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(s1.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new(w2, vec![], FaceSurface::Sphere(s2.clone())));
let result = sphere_sphere_fillet(&s1, &s2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("convex sphere-sphere fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let big_delta = (big_r1 - big_r2) / big_d;
let a_ball = a0 + r_fillet * big_delta;
let expected_major = ((big_r1 + r_fillet) * (big_r1 + r_fillet) - a_ball * a_ball).sqrt();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-12,
"major should be √((R1+r)²−a_ball²)={expected_major}, got {}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"minor should equal fillet radius {r_fillet}, got {}",
torus.minor_radius()
);
let center = torus.center();
assert!(
center.x().abs() < 1e-12 && center.y().abs() < 1e-12,
"torus center should be on +z axis, got {center:?}"
);
assert!(
(center.z() - a_ball).abs() < 1e-12,
"torus center z should be a_ball={a_ball}, got {}",
center.z()
);
let s1_axial = big_r1 * a_ball / (big_r1 + r_fillet);
let s1_radial = big_r1 * expected_major / (big_r1 + r_fillet);
let want_s1 = Point3::new(s1_radial, 0.0, s1_axial);
let s2_axial_from_c2 = big_r2 * (a_ball - big_d) / (big_r2 + r_fillet);
let s2_radial = big_r2 * expected_major / (big_r2 + r_fillet);
let want_s2 = Point3::new(s2_radial, 0.0, big_d + s2_axial_from_c2);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_s1);
let on_torus_s1 = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_s2);
let on_torus_s2 = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_s1 - want_s1).length() < 1e-9,
"sphere1 contact must lie on torus: gave {on_torus_s1:?}, want {want_s1:?}"
);
assert!(
(on_torus_s2 - want_s2).length() < 1e-9,
"sphere2 contact must lie on torus: gave {on_torus_s2:?}, want {want_s2:?}"
);
let dist_s1 = (want_s1 - Point3::new(0.0, 0.0, 0.0)).length();
let dist_s2 = (want_s2 - Point3::new(0.0, 0.0, big_d)).length();
assert!(
(dist_s1 - big_r1).abs() < 1e-9,
"sphere1 contact must lie on sphere1: distance={dist_s1}, want R1={big_r1}"
);
assert!(
(dist_s2 - big_r2).abs() < 1e-9,
"sphere2 contact must lie on sphere2: distance={dist_s2}, want R2={big_r2}"
);
}
#[test]
fn sphere_sphere_fillet_both_concave_emits_smaller_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r1: f64 = 2.0;
let big_r2: f64 = 2.5;
let big_d: f64 = 3.0;
let r_fillet: f64 = 0.4;
let a0 = (big_r1 * big_r1 - big_r2 * big_r2 + big_d * big_d) / (2.0 * big_d);
let r_p_sq = big_r1 * big_r1 - a0 * a0;
let r_p = r_p_sq.sqrt();
let s1 = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r1).unwrap();
let s2 = SphericalSurface::new(Point3::new(0.0, 0.0, big_d), big_r2).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(s1.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(s2.clone()),
));
let result = sphere_sphere_fillet(&s1, &s2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("both-concave sphere-sphere fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let q1 = big_r1 - r_fillet;
let q2 = big_r2 - r_fillet;
let a_ball = (q1 * q1 - q2 * q2 + big_d * big_d) / (2.0 * big_d);
let expected_major = (q1 * q1 - a_ball * a_ball).sqrt();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-12,
"major should be √(Q1²−a_ball²)={expected_major}, got {}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"minor should equal fillet radius {r_fillet}, got {}",
torus.minor_radius()
);
let q1_conv = big_r1 + r_fillet;
let q2_conv = big_r2 + r_fillet;
let a_ball_conv = (q1_conv * q1_conv - q2_conv * q2_conv + big_d * big_d) / (2.0 * big_d);
let convex_major = (q1_conv * q1_conv - a_ball_conv * a_ball_conv).sqrt();
assert!(
torus.major_radius() < convex_major,
"concave major ({}) must be smaller than convex major ({convex_major}) at same r",
torus.major_radius()
);
let s1_axial = big_r1 * a_ball / q1;
let s1_radial = big_r1 * expected_major / q1;
let want_s1 = Point3::new(s1_radial, 0.0, s1_axial);
let s2_axial_from_c2 = big_r2 * (a_ball - big_d) / q2;
let s2_radial = big_r2 * expected_major / q2;
let want_s2 = Point3::new(s2_radial, 0.0, big_d + s2_axial_from_c2);
let dist_s1 = (want_s1 - Point3::new(0.0, 0.0, 0.0)).length();
let dist_s2 = (want_s2 - Point3::new(0.0, 0.0, big_d)).length();
assert!(
(dist_s1 - big_r1).abs() < 1e-9,
"sphere1 contact must lie on sphere1: distance={dist_s1}, want R1={big_r1}"
);
assert!(
(dist_s2 - big_r2).abs() < 1e-9,
"sphere2 contact must lie on sphere2: distance={dist_s2}, want R2={big_r2}"
);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_s1);
let on_torus_s1 = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_s2);
let on_torus_s2 = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_s1 - want_s1).length() < 1e-9,
"sphere1 contact must lie on torus: {on_torus_s1:?} vs {want_s1:?}"
);
assert!(
(on_torus_s2 - want_s2).length() < 1e-9,
"sphere2 contact must lie on torus: {on_torus_s2:?} vs {want_s2:?}"
);
}
#[test]
fn sphere_sphere_fillet_concave_rejects_collapsing_q() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r: f64 = 2.0;
let big_d: f64 = 3.0;
let r_too_big = 2.1;
let a0 = (big_r * big_r - big_r * big_r + big_d * big_d) / (2.0 * big_d);
let r_p_sq = big_r * big_r - a0 * a0;
let r_p = r_p_sq.sqrt();
let s1 = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r).unwrap();
let s2 = SphericalSurface::new(Point3::new(0.0, 0.0, big_d), big_r).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(s1.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(s2.clone()),
));
let result = sphere_sphere_fillet(&s1, &s2, &spine, &topo, r_too_big, face1, face2).unwrap();
assert!(
result.is_none(),
"concave fillet at r≥R should reject (Qi collapses to ≤ 0)"
);
let mut topo2 = Topology::new();
let v2 = topo2.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let circle2 = Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid2 = topo2.add_edge(Edge::new(v2, v2, EdgeCurve::Circle(circle2)));
let spine2 = Spine::from_single_edge(&topo2, eid2).unwrap();
let w1b = topo2.add_wire(Wire::new(vec![OrientedEdge::new(eid2, true)], true).unwrap());
let face1b = topo2.add_face(Face::new(w1b, vec![], FaceSurface::Sphere(s1.clone())));
let w2b = topo2.add_wire(Wire::new(vec![OrientedEdge::new(eid2, false)], true).unwrap());
let face2b = topo2.add_face(Face::new(w2b, vec![], FaceSurface::Sphere(s2.clone())));
let result_convex =
sphere_sphere_fillet(&s1, &s2, &spine2, &topo2, r_too_big, face1b, face2b).unwrap();
assert!(
result_convex.is_some(),
"convex fillet at the same r={r_too_big} should still succeed"
);
}
#[test]
fn sphere_sphere_fillet_mixed_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r1: f64 = 2.0;
let big_r2: f64 = 2.5;
let big_d: f64 = 3.0;
let r_fillet: f64 = 0.4;
let a0 = (big_r1 * big_r1 - big_r2 * big_r2 + big_d * big_d) / (2.0 * big_d);
let r_p_sq = big_r1 * big_r1 - a0 * a0;
let r_p = r_p_sq.sqrt();
let s1 = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r1).unwrap();
let s2 = SphericalSurface::new(Point3::new(0.0, 0.0, big_d), big_r2).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(s1.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(s2.clone()),
));
let result = sphere_sphere_fillet(&s1, &s2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("mixed sphere-sphere fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let q1 = big_r1 + r_fillet; let q2 = big_r2 - r_fillet; let a_ball = (q1 * q1 - q2 * q2 + big_d * big_d) / (2.0 * big_d);
let expected_major = (q1 * q1 - a_ball * a_ball).sqrt();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-12,
"mixed major should be √(Q1²−a_ball²)={expected_major}, got {}",
torus.major_radius()
);
let q1_cc = big_r1 + r_fillet;
let q2_cc = big_r2 + r_fillet;
let a_ball_cc = (q1_cc * q1_cc - q2_cc * q2_cc + big_d * big_d) / (2.0 * big_d);
let convex_convex_major = (q1_cc * q1_cc - a_ball_cc * a_ball_cc).sqrt();
let q1_kk = big_r1 - r_fillet;
let q2_kk = big_r2 - r_fillet;
let a_ball_kk = (q1_kk * q1_kk - q2_kk * q2_kk + big_d * big_d) / (2.0 * big_d);
let concave_concave_major = (q1_kk * q1_kk - a_ball_kk * a_ball_kk).sqrt();
assert!(
torus.major_radius() < convex_convex_major && torus.major_radius() > concave_concave_major,
"mixed major ({}) should sit between concave-concave ({concave_concave_major}) and convex-convex ({convex_convex_major})",
torus.major_radius()
);
let s1_axial = big_r1 * a_ball / q1;
let s1_radial = big_r1 * expected_major / q1;
let want_s1 = Point3::new(s1_radial, 0.0, s1_axial);
let s2_axial_from_c2 = big_r2 * (a_ball - big_d) / q2;
let s2_radial = big_r2 * expected_major / q2;
let want_s2 = Point3::new(s2_radial, 0.0, big_d + s2_axial_from_c2);
let dist_s1 = (want_s1 - Point3::new(0.0, 0.0, 0.0)).length();
let dist_s2 = (want_s2 - Point3::new(0.0, 0.0, big_d)).length();
assert!(
(dist_s1 - big_r1).abs() < 1e-9,
"sphere1 contact must lie on sphere1: distance={dist_s1}, want R1={big_r1}"
);
assert!(
(dist_s2 - big_r2).abs() < 1e-9,
"sphere2 contact must lie on sphere2: distance={dist_s2}, want R2={big_r2}"
);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_s1);
let on_torus_s1 = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_s2);
let on_torus_s2 = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_s1 - want_s1).length() < 1e-9,
"sphere1 contact must lie on torus: {on_torus_s1:?} vs {want_s1:?}"
);
assert!(
(on_torus_s2 - want_s2).length() < 1e-9,
"sphere2 contact must lie on torus: {on_torus_s2:?} vs {want_s2:?}"
);
}
#[test]
fn sphere_sphere_chamfer_convex_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r1: f64 = 2.0;
let big_r2: f64 = 2.5;
let big_d: f64 = 3.0;
let d: f64 = 0.4;
let a0 = (big_r1 * big_r1 - big_r2 * big_r2 + big_d * big_d) / (2.0 * big_d);
let r_p_sq = big_r1 * big_r1 - a0 * a0;
let r_p = r_p_sq.sqrt();
let s1 = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r1).unwrap();
let s2 = SphericalSurface::new(Point3::new(0.0, 0.0, big_d), big_r2).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(s1.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new(w2, vec![], FaceSurface::Sphere(s2.clone())));
let result = sphere_sphere_chamfer(&s1, &s2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("convex sphere-sphere chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta1 = d / big_r1;
let delta2 = d / big_r2;
let (sin1, cos1) = delta1.sin_cos();
let (sin2, cos2) = delta2.sin_cos();
let p1_r = r_p * cos1 + a0 * sin1;
let p1_z = a0 * cos1 - r_p * sin1;
let p2_r = r_p * cos2 + (big_d - a0) * sin2;
let p2_z = big_d - (big_d - a0) * cos2 + r_p * sin2;
assert!(p1_z < a0, "convex contact1 should be below spine z=a0");
assert!(p2_z > a0, "convex contact2 should be above spine z=a0");
let dr = p2_r - p1_r;
let dz = p2_z - p1_z;
let expected_apex_z = p1_z - p1_r * dz / dr;
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"apex z = {}, expected {expected_apex_z}",
apex.z()
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"convex chamfer cone axis should be +z, got {axis:?}"
);
let want_p1 = Point3::new(p1_r, 0.0, p1_z);
let want_p2 = Point3::new(p2_r, 0.0, p2_z);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_p1);
let on_cone_p1 = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_p2);
let on_cone_p2 = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_p1 - want_p1).length() < 1e-9,
"contact1 must lie on chamfer cone: {on_cone_p1:?} vs {want_p1:?}"
);
assert!(
(on_cone_p2 - want_p2).length() < 1e-9,
"contact2 must lie on chamfer cone: {on_cone_p2:?} vs {want_p2:?}"
);
let dist_s1 = (want_p1 - Point3::new(0.0, 0.0, 0.0)).length();
let dist_s2 = (want_p2 - Point3::new(0.0, 0.0, big_d)).length();
assert!(
(dist_s1 - big_r1).abs() < 1e-9,
"contact1 must lie on sphere1: distance={dist_s1}, want R1={big_r1}"
);
assert!(
(dist_s2 - big_r2).abs() < 1e-9,
"contact2 must lie on sphere2: distance={dist_s2}, want R2={big_r2}"
);
}
#[test]
fn sphere_cylinder_fillet_convex_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{CylindricalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let r_c: f64 = 2.0;
let r_fillet: f64 = 0.4;
let h_s = (big_r_s * big_r_s - r_c * r_c).sqrt();
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, h_s), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, h_s), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(sph.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new(w2, vec![], FaceSurface::Cylinder(cyl.clone())));
let result = sphere_cylinder_fillet(&sph, &cyl, &spine, &topo, r_fillet, face_sphere, face_cyl)
.unwrap()
.expect("convex sphere-cylinder fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let q_s = big_r_s + r_fillet;
let q_c = r_c + r_fillet;
let expected_major = q_c;
let expected_a_ball = (q_s * q_s - q_c * q_c).sqrt();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-12,
"major should be Q_c = {expected_major}, got {}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"minor should be r = {r_fillet}, got {}",
torus.minor_radius()
);
let center = torus.center();
assert!(
center.x().abs() < 1e-12 && center.y().abs() < 1e-12,
"torus center should be on z-axis, got {center:?}"
);
assert!(
(center.z() - expected_a_ball).abs() < 1e-12,
"torus center z should be a_ball = {expected_a_ball}, got {}",
center.z()
);
let sph_axial = big_r_s * expected_a_ball / q_s;
let sph_radial = big_r_s * q_c / q_s;
let want_sph = Point3::new(sph_radial, 0.0, sph_axial);
let want_cyl = Point3::new(r_c, 0.0, expected_a_ball);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_sph);
let on_torus_sph = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_cyl);
let on_torus_cyl = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_sph - want_sph).length() < 1e-9,
"sphere contact must lie on torus: {on_torus_sph:?} vs {want_sph:?}"
);
assert!(
(on_torus_cyl - want_cyl).length() < 1e-9,
"cylinder contact must lie on torus: {on_torus_cyl:?} vs {want_cyl:?}"
);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: distance = {dist_sph}, want R_s = {big_r_s}"
);
let dist_cyl_radial = (want_cyl.x().powi(2) + want_cyl.y().powi(2)).sqrt();
assert!(
(dist_cyl_radial - r_c).abs() < 1e-9,
"cylinder contact must lie on cylinder: radial = {dist_cyl_radial}, want r_c = {r_c}"
);
}
#[test]
fn sphere_cylinder_chamfer_convex_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{CylindricalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let r_c: f64 = 2.0;
let d: f64 = 0.4;
let h_s = (big_r_s * big_r_s - r_c * r_c).sqrt();
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, h_s), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, h_s), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(sph.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new(w2, vec![], FaceSurface::Cylinder(cyl.clone())));
let result = sphere_cylinder_chamfer(&sph, &cyl, &spine, &topo, d, d, face_sphere, face_cyl)
.unwrap()
.expect("convex sphere-cylinder chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta = d / big_r_s;
let (sin_d, cos_d) = delta.sin_cos();
let r_sph_pred = r_c * cos_d - h_s * sin_d;
let z_sph_pred = h_s * cos_d + r_c * sin_d;
let r_cyl_pred = r_c;
let z_cyl_pred = h_s - d;
let dr = r_cyl_pred - r_sph_pred;
let dz = z_cyl_pred - z_sph_pred;
let expected_apex_z = z_sph_pred - r_sph_pred * dz / dr;
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"apex z = {}, expected {expected_apex_z}",
apex.z()
);
assert!(
expected_apex_z > z_sph_pred,
"apex should be above the contacts (mid z < apex z), got apex_z={expected_apex_z}"
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) < -1.0 + 1e-12,
"convex chamfer cone axis should be -z (apex above), got {axis:?}"
);
let want_sph = Point3::new(r_sph_pred, 0.0, z_sph_pred);
let want_cyl = Point3::new(r_cyl_pred, 0.0, z_cyl_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_sph);
let on_cone_sph = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_cyl);
let on_cone_cyl = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_sph - want_sph).length() < 1e-9,
"sphere contact must lie on chamfer cone: {on_cone_sph:?} vs {want_sph:?}"
);
assert!(
(on_cone_cyl - want_cyl).length() < 1e-9,
"cylinder contact must lie on chamfer cone: {on_cone_cyl:?} vs {want_cyl:?}"
);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: distance={dist_sph}, want R_s={big_r_s}"
);
let dist_cyl_radial = (want_cyl.x().powi(2) + want_cyl.y().powi(2)).sqrt();
assert!(
(dist_cyl_radial - r_c).abs() < 1e-9,
"cylinder contact must have radial r_c: got {dist_cyl_radial}, want {r_c}"
);
}
#[test]
fn sphere_sphere_chamfer_both_concave_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r1: f64 = 2.0;
let big_r2: f64 = 2.5;
let big_d: f64 = 3.0;
let d: f64 = 0.4;
let a0 = (big_r1 * big_r1 - big_r2 * big_r2 + big_d * big_d) / (2.0 * big_d);
let r_p_sq = big_r1 * big_r1 - a0 * a0;
let r_p = r_p_sq.sqrt();
let s1 = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r1).unwrap();
let s2 = SphericalSurface::new(Point3::new(0.0, 0.0, big_d), big_r2).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(s1.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(s2.clone()),
));
let result = sphere_sphere_chamfer(&s1, &s2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("both-concave sphere-sphere chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta1 = d / big_r1;
let delta2 = d / big_r2;
let (sin1, cos1) = delta1.sin_cos();
let (sin2, cos2) = delta2.sin_cos();
let p1_r = r_p * cos1 - a0 * sin1; let p1_z = a0 * cos1 + r_p * sin1; let p2_r = r_p * cos2 - (big_d - a0) * sin2; let p2_z = big_d - (big_d - a0) * cos2 - r_p * sin2;
assert!(
p1_z > a0,
"concave contact1 should be above spine (z > a0): got {p1_z}"
);
assert!(
p2_z < a0,
"concave contact2 should be below spine (z < a0): got {p2_z}"
);
let want_p1 = Point3::new(p1_r, 0.0, p1_z);
let want_p2 = Point3::new(p2_r, 0.0, p2_z);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_p1);
let on_cone_p1 = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_p2);
let on_cone_p2 = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_p1 - want_p1).length() < 1e-9,
"concave contact1 must lie on chamfer cone: {on_cone_p1:?} vs {want_p1:?}"
);
assert!(
(on_cone_p2 - want_p2).length() < 1e-9,
"concave contact2 must lie on chamfer cone: {on_cone_p2:?} vs {want_p2:?}"
);
let dist_s1 = (want_p1 - Point3::new(0.0, 0.0, 0.0)).length();
let dist_s2 = (want_p2 - Point3::new(0.0, 0.0, big_d)).length();
assert!(
(dist_s1 - big_r1).abs() < 1e-9,
"contact1 must lie on sphere1: distance={dist_s1}, want R1={big_r1}"
);
assert!(
(dist_s2 - big_r2).abs() < 1e-9,
"contact2 must lie on sphere2: distance={dist_s2}, want R2={big_r2}"
);
}
#[test]
fn sphere_sphere_chamfer_mixed_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let big_r1: f64 = 2.0;
let big_r2: f64 = 2.5;
let big_d: f64 = 3.0;
let d: f64 = 0.4;
let a0 = (big_r1 * big_r1 - big_r2 * big_r2 + big_d * big_d) / (2.0 * big_d);
let r_p = (big_r1 * big_r1 - a0 * a0).sqrt();
let run_case = |reverse_s1: bool, reverse_s2: bool| {
let mut topo = Topology::new();
let s1_surf = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r1).unwrap();
let s2_surf = SphericalSurface::new(Point3::new(0.0, 0.0, big_d), big_r2).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, a0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, a0), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = if reverse_s1 {
topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(s1_surf.clone()),
))
} else {
topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(s1_surf.clone())))
};
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = if reverse_s2 {
topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Sphere(s2_surf.clone()),
))
} else {
topo.add_face(Face::new(w2, vec![], FaceSurface::Sphere(s2_surf.clone())))
};
let result = sphere_sphere_chamfer(&s1_surf, &s2_surf, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("mixed sphere-sphere chamfer should produce a stripe");
let (t1_start, _) = result.stripe.contact1.domain();
let c1_point = result.stripe.contact1.evaluate(t1_start);
let dist_s1 = (c1_point - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_s1 - big_r1).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): emitted contact1 must lie on sphere1: \
distance = {dist_s1}, want R1 = {big_r1}"
);
let (t2_start, _) = result.stripe.contact2.domain();
let c2_point = result.stripe.contact2.evaluate(t2_start);
let dist_s2 = (c2_point - Point3::new(0.0, 0.0, big_d)).length();
assert!(
(dist_s2 - big_r2).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): emitted contact2 must lie on sphere2: \
distance = {dist_s2}, want R2 = {big_r2}"
);
assert!(
matches!(result.stripe.surface, FaceSurface::Cone(_)),
"({reverse_s1}, {reverse_s2}): expected Cone, got {}",
result.stripe.surface.type_tag()
);
if let FaceSurface::Cone(ref cone) = result.stripe.surface {
let (u_p, v_p) = ParametricSurface::project_point(cone, c1_point);
let on_cone_p1 = ParametricSurface::evaluate(cone, u_p, v_p);
assert!(
(on_cone_p1 - c1_point).length() < 1e-9,
"({reverse_s1}, {reverse_s2}): emitted contact1 must lie on chamfer cone"
);
let (u_q, v_q) = ParametricSurface::project_point(cone, c2_point);
let on_cone_p2 = ParametricSurface::evaluate(cone, u_q, v_q);
assert!(
(on_cone_p2 - c2_point).length() < 1e-9,
"({reverse_s1}, {reverse_s2}): emitted contact2 must lie on chamfer cone"
);
}
};
run_case(false, true); run_case(true, false); }
#[test]
fn sphere_cylinder_chamfer_both_concave_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{CylindricalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let r_c: f64 = 2.0;
let d: f64 = 0.4;
let h_s = (big_r_s * big_r_s - r_c * r_c).sqrt();
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, h_s), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, h_s), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(sph.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl.clone()),
));
let result = sphere_cylinder_chamfer(&sph, &cyl, &spine, &topo, d, d, face_sphere, face_cyl)
.unwrap()
.expect("both-concave sphere-cylinder chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta = d / big_r_s;
let (sin_d, cos_d) = delta.sin_cos();
let r_sph_pred = r_c * cos_d + h_s * sin_d; let z_sph_pred = h_s * cos_d - r_c * sin_d; let z_cyl_pred = h_s + d;
assert!(
r_sph_pred > r_c,
"concave sphere contact should have r > r_c (opposite convex case): got {r_sph_pred}"
);
assert!(
z_sph_pred < h_s,
"concave sphere contact should have z < h_s (toward cyl side): got {z_sph_pred} vs h_s={h_s}"
);
assert!(
z_cyl_pred > h_s,
"concave cyl contact should have z > h_s (opposite the convex direction): got {z_cyl_pred} vs h_s={h_s}"
);
let want_sph = Point3::new(r_sph_pred, 0.0, z_sph_pred);
let want_cyl = Point3::new(r_c, 0.0, z_cyl_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_sph);
let on_cone_sph = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_cyl);
let on_cone_cyl = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_sph - want_sph).length() < 1e-9,
"concave sphere contact must lie on chamfer cone: {on_cone_sph:?} vs {want_sph:?}"
);
assert!(
(on_cone_cyl - want_cyl).length() < 1e-9,
"concave cyl contact must lie on chamfer cone: {on_cone_cyl:?} vs {want_cyl:?}"
);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: distance={dist_sph}, want R_s={big_r_s}"
);
}
#[test]
fn sphere_cylinder_chamfer_mixed_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{CylindricalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let r_c: f64 = 2.0;
let d: f64 = 0.4;
let h_s = (big_r_s * big_r_s - r_c * r_c).sqrt();
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, h_s), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, h_s), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(sph.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl.clone()),
));
let result = sphere_cylinder_chamfer(&sph, &cyl, &spine, &topo, d, d, face_sphere, face_cyl)
.unwrap()
.expect("mixed sphere-cylinder chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta = d / big_r_s;
let (sin_d, cos_d) = delta.sin_cos();
let r_sph_pred = r_c * cos_d - h_s * sin_d; let z_sph_pred = h_s * cos_d + r_c * sin_d;
let z_cyl_pred = h_s + d;
let dr = r_c - r_sph_pred;
let dz = z_cyl_pred - z_sph_pred;
let expected_apex_z = z_sph_pred - r_sph_pred * dz / dr;
assert!(
expected_apex_z < z_sph_pred && expected_apex_z < z_cyl_pred,
"mixed apex should be below both contacts, got apex_z={expected_apex_z}, \
z_sph={z_sph_pred}, z_cyl={z_cyl_pred}"
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"mixed chamfer cone axis should be +z (apex below contacts), got {axis:?}"
);
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"apex z = {}, expected {expected_apex_z}",
apex.z()
);
let want_sph = Point3::new(r_sph_pred, 0.0, z_sph_pred);
let want_cyl = Point3::new(r_c, 0.0, z_cyl_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_sph);
let on_cone_sph = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_cyl);
let on_cone_cyl = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_sph - want_sph).length() < 1e-9,
"mixed sphere contact must lie on chamfer cone: {on_cone_sph:?} vs {want_sph:?}"
);
assert!(
(on_cone_cyl - want_cyl).length() < 1e-9,
"mixed cyl contact must lie on chamfer cone: {on_cone_cyl:?} vs {want_cyl:?}"
);
}
#[test]
fn try_analytic_chamfer_cylinder_sphere_dispatch_swaps_correctly() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{CylindricalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let r_c: f64 = 2.0;
let d1_outer: f64 = 0.5; let d2_outer: f64 = 0.4; let h_s = (big_r_s * big_r_s - r_c * r_c).sqrt();
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, h_s), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, h_s), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w_cyl = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1_cyl = topo.add_face(Face::new(w_cyl, vec![], FaceSurface::Cylinder(cyl.clone())));
let w_sph = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2_sph = topo.add_face(Face::new(w_sph, vec![], FaceSurface::Sphere(sph.clone())));
let surf1 = FaceSurface::Cylinder(cyl);
let surf2 = FaceSurface::Sphere(sph);
let result = try_analytic_chamfer(
&surf1, &surf2, &spine, &topo, d1_outer, d2_outer, face1_cyl, face2_sph,
)
.unwrap()
.expect("dispatcher should produce a stripe for (cyl, sphere) chamfer");
assert_eq!(
result.stripe.face1, face1_cyl,
"stripe.face1 should match the dispatcher's first face (cylinder), \
confirming swap_stripe_sides restored the caller-facing ordering"
);
assert_eq!(
result.stripe.face2, face2_sph,
"stripe.face2 should match the dispatcher's second face (sphere)"
);
let z_cyl_pred = h_s - d1_outer;
let delta = d2_outer / big_r_s;
let (sin_d, cos_d) = delta.sin_cos();
let r_sph_pred = r_c * cos_d - h_s * sin_d;
let z_sph_pred = h_s * cos_d + r_c * sin_d;
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let want_cyl = Point3::new(r_c, 0.0, z_cyl_pred);
let want_sph = Point3::new(r_sph_pred, 0.0, z_sph_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_cyl);
let on_cone_cyl = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_sph);
let on_cone_sph = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_cyl - want_cyl).length() < 1e-9,
"cylinder contact (using dispatcher's d1) must lie on cone: {on_cone_cyl:?} vs {want_cyl:?}"
);
assert!(
(on_cone_sph - want_sph).length() < 1e-9,
"sphere contact (using dispatcher's d2) must lie on cone: {on_cone_sph:?} vs {want_sph:?}"
);
}
#[test]
fn sphere_cone_fillet_convex_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{ConicalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let h_signed: f64 = 2.0; let beta: f64 = std::f64::consts::PI / 3.0;
let r_fillet: f64 = 0.3;
let cot_b = beta.cos() / beta.sin();
let qa = 1.0 / (beta.sin() * beta.sin());
let qb = 2.0 * h_signed * cot_b * cot_b;
let qc = h_signed * h_signed * cot_b * cot_b - big_r_s * big_r_s;
let q_disc = qb * qb - 4.0 * qa * qc;
let z_spine = (-qb + q_disc.sqrt()) / (2.0 * qa);
let r_spine = (z_spine + h_signed) * cot_b;
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cone = ConicalSurface::new(
Point3::new(0.0, 0.0, -h_signed),
Vec3::new(0.0, 0.0, 1.0),
beta,
)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(sph.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new(w2, vec![], FaceSurface::Cone(cone.clone())));
let result = sphere_cone_fillet(&sph, &cone, &spine, &topo, r_fillet, face_sphere, face_cone)
.unwrap()
.expect("convex sphere-cone fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let big_a = r_fillet + h_signed * beta.cos();
let disc = (big_r_s + r_fillet) * (big_r_s + r_fillet) - big_a * big_a;
let expected_z_b = -big_a * beta.cos() + beta.sin() * disc.sqrt();
let expected_major = (r_fillet + (expected_z_b + h_signed) * beta.cos()) / beta.sin();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-9,
"major should be (r + (c+h)·cos β)/sin β = {expected_major}, got {}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"minor should be r = {r_fillet}, got {}",
torus.minor_radius()
);
let center = torus.center();
assert!(
center.x().abs() < 1e-12 && center.y().abs() < 1e-12,
"torus center should be on z-axis, got {center:?}"
);
assert!(
(center.z() - expected_z_b).abs() < 1e-9,
"torus center z should be c_root = {expected_z_b}, got {}",
center.z()
);
let sph_axial = big_r_s * expected_z_b / (big_r_s + r_fillet);
let sph_radial = big_r_s * expected_major / (big_r_s + r_fillet);
let want_sph = Point3::new(sph_radial, 0.0, sph_axial);
let cone_axial = expected_z_b + r_fillet * beta.cos();
let cone_radial = expected_major - r_fillet * beta.sin();
let want_cone = Point3::new(cone_radial, 0.0, cone_axial);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_sph);
let on_torus_sph = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_cone);
let on_torus_cone = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_sph - want_sph).length() < 1e-9,
"sphere contact must lie on torus: {on_torus_sph:?} vs {want_sph:?}"
);
assert!(
(on_torus_cone - want_cone).length() < 1e-9,
"cone contact must lie on torus: {on_torus_cone:?} vs {want_cone:?}"
);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: distance={dist_sph}, want R_s={big_r_s}"
);
let predicted_cone_radial = (cone_axial + h_signed) * cot_b;
assert!(
(cone_radial - predicted_cone_radial).abs() < 1e-9,
"cone contact must lie on cone surface: predicted radial {predicted_cone_radial}, got {cone_radial}"
);
}
#[test]
fn sphere_cone_fillet_concave_cone_emits_smaller_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{ConicalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let h_signed: f64 = 2.0;
let beta: f64 = std::f64::consts::PI / 3.0;
let r_fillet: f64 = 0.3;
let cot_b = beta.cos() / beta.sin();
let qa_q = 1.0 / (beta.sin() * beta.sin());
let qb_q = 2.0 * h_signed * cot_b * cot_b;
let qc_q = h_signed * h_signed * cot_b * cot_b - big_r_s * big_r_s;
let q_disc = qb_q * qb_q - 4.0 * qa_q * qc_q;
let z_spine = (-qb_q + q_disc.sqrt()) / (2.0 * qa_q);
let r_spine = (z_spine + h_signed) * cot_b;
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cone = ConicalSurface::new(
Point3::new(0.0, 0.0, -h_signed),
Vec3::new(0.0, 0.0, 1.0),
beta,
)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(sph.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone.clone()),
));
let result = sphere_cone_fillet(&sph, &cone, &spine, &topo, r_fillet, face_sphere, face_cone)
.unwrap()
.expect("mixed sphere-cone fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let q_s = big_r_s + r_fillet; let big_a_pred = -r_fillet + h_signed * beta.cos(); let disc = q_s * q_s - big_a_pred * big_a_pred;
let c_root_a = -big_a_pred * beta.cos() + beta.sin() * disc.sqrt();
let c_root_b = -big_a_pred * beta.cos() - beta.sin() * disc.sqrt();
let z_b = if (c_root_a - z_spine).abs() <= (c_root_b - z_spine).abs() {
c_root_a
} else {
c_root_b
};
let expected_major = (-r_fillet + (z_b + h_signed) * beta.cos()) / beta.sin();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-9,
"concave-cone major should be {expected_major}, got {}",
torus.major_radius()
);
let big_a_convex = r_fillet + h_signed * beta.cos();
let disc_convex = (big_r_s + r_fillet) * (big_r_s + r_fillet) - big_a_convex * big_a_convex;
let c_convex = -big_a_convex * beta.cos() + beta.sin() * disc_convex.sqrt();
let convex_major = (r_fillet + (c_convex + h_signed) * beta.cos()) / beta.sin();
assert!(
torus.major_radius() < convex_major,
"concave-cone major ({}) should be smaller than convex ({convex_major})",
torus.major_radius()
);
let sph_axial = big_r_s * z_b / q_s;
let sph_radial = big_r_s * expected_major / q_s;
let want_sph = Point3::new(sph_radial, 0.0, sph_axial);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: distance={dist_sph}, want R_s={big_r_s}"
);
let cone_axial = z_b - r_fillet * beta.cos();
let cone_radial = expected_major + r_fillet * beta.sin();
let predicted_cone_radial = (cone_axial + h_signed) * cot_b;
assert!(
(cone_radial - predicted_cone_radial).abs() < 1e-9,
"cone contact must lie on cone: predicted radial {predicted_cone_radial}, got {cone_radial}"
);
let want_cone = Point3::new(cone_radial, 0.0, cone_axial);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_sph);
let on_torus_sph = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_cone);
let on_torus_cone = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_sph - want_sph).length() < 1e-9,
"sphere contact must lie on torus: {on_torus_sph:?} vs {want_sph:?}"
);
assert!(
(on_torus_cone - want_cone).length() < 1e-9,
"cone contact must lie on torus: {on_torus_cone:?} vs {want_cone:?}"
);
}
#[test]
fn sphere_cone_fillet_both_concave_emits_smaller_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{ConicalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let h_signed: f64 = 2.0;
let beta: f64 = std::f64::consts::PI / 3.0;
let r_fillet: f64 = 0.3;
let cot_b = beta.cos() / beta.sin();
let qa_q = 1.0 / (beta.sin() * beta.sin());
let qb_q = 2.0 * h_signed * cot_b * cot_b;
let qc_q = h_signed * h_signed * cot_b * cot_b - big_r_s * big_r_s;
let q_disc = qb_q * qb_q - 4.0 * qa_q * qc_q;
let z_spine = (-qb_q + q_disc.sqrt()) / (2.0 * qa_q);
let r_spine = (z_spine + h_signed) * cot_b;
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cone = ConicalSurface::new(
Point3::new(0.0, 0.0, -h_signed),
Vec3::new(0.0, 0.0, 1.0),
beta,
)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(sph.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone.clone()),
));
let result = sphere_cone_fillet(&sph, &cone, &spine, &topo, r_fillet, face_sphere, face_cone)
.unwrap()
.expect("both-concave sphere-cone fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let q_s = big_r_s - r_fillet;
let big_a = -r_fillet + h_signed * beta.cos();
let disc = q_s * q_s - big_a * big_a;
let c_root_a = -big_a * beta.cos() + beta.sin() * disc.sqrt();
let c_root_b = -big_a * beta.cos() - beta.sin() * disc.sqrt();
let z_b = if (c_root_a - z_spine).abs() <= (c_root_b - z_spine).abs() {
c_root_a
} else {
c_root_b
};
let expected_major = (-r_fillet + (z_b + h_signed) * beta.cos()) / beta.sin();
assert!(
(torus.major_radius() - expected_major).abs() < 1e-9,
"both-concave major should be {expected_major}, got {}",
torus.major_radius()
);
let convex_a = r_fillet + h_signed * beta.cos();
let convex_disc = (big_r_s + r_fillet) * (big_r_s + r_fillet) - convex_a * convex_a;
let convex_c = -convex_a * beta.cos() + beta.sin() * convex_disc.sqrt();
let convex_major = (r_fillet + (convex_c + h_signed) * beta.cos()) / beta.sin();
let concave_cone_a = -r_fillet + h_signed * beta.cos();
let concave_cone_disc =
(big_r_s + r_fillet) * (big_r_s + r_fillet) - concave_cone_a * concave_cone_a;
let concave_cone_c = -concave_cone_a * beta.cos() + beta.sin() * concave_cone_disc.sqrt();
let concave_cone_major = (-r_fillet + (concave_cone_c + h_signed) * beta.cos()) / beta.sin();
assert!(
torus.major_radius() < concave_cone_major,
"both-concave major ({}) should be smaller than concave-cone-only ({concave_cone_major})",
torus.major_radius()
);
assert!(
concave_cone_major < convex_major,
"concave-cone-only major ({concave_cone_major}) should be smaller than convex ({convex_major})"
);
let sph_axial = big_r_s * z_b / q_s;
let sph_radial = big_r_s * expected_major / q_s;
let want_sph = Point3::new(sph_radial, 0.0, sph_axial);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: {dist_sph} vs R_s={big_r_s}"
);
let cone_axial = z_b - r_fillet * beta.cos();
let cone_radial = expected_major + r_fillet * beta.sin();
let predicted_cone_radial = (cone_axial + h_signed) * cot_b;
assert!(
(cone_radial - predicted_cone_radial).abs() < 1e-9,
"cone contact must lie on cone: predicted {predicted_cone_radial}, got {cone_radial}"
);
}
#[test]
fn sphere_cone_chamfer_convex_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{ConicalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let h_signed: f64 = 2.0;
let beta: f64 = std::f64::consts::PI / 3.0;
let d: f64 = 0.3;
let cot_b = beta.cos() / beta.sin();
let qa = 1.0 / (beta.sin() * beta.sin());
let qb = 2.0 * h_signed * cot_b * cot_b;
let qc = h_signed * h_signed * cot_b * cot_b - big_r_s * big_r_s;
let q_disc = qb * qb - 4.0 * qa * qc;
let z_spine = (-qb + q_disc.sqrt()) / (2.0 * qa);
let r_spine = (z_spine + h_signed) * cot_b;
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cone = ConicalSurface::new(
Point3::new(0.0, 0.0, -h_signed),
Vec3::new(0.0, 0.0, 1.0),
beta,
)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(sph.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new(w2, vec![], FaceSurface::Cone(cone.clone())));
let result = sphere_cone_chamfer(&sph, &cone, &spine, &topo, d, d, face_sphere, face_cone)
.unwrap()
.expect("convex sphere-cone chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta = d / big_r_s;
let (sin_d, cos_d) = delta.sin_cos();
let sphere_arm_sign = -1.0_f64; let r_sph_pred = r_spine * cos_d + sphere_arm_sign * z_spine * sin_d;
let z_sph_pred = z_spine * cos_d - sphere_arm_sign * r_spine * sin_d;
let r_cone_pred = r_spine - d * beta.cos();
let z_cone_pred = z_spine - d * beta.sin();
let dr = r_cone_pred - r_sph_pred;
let dz = z_cone_pred - z_sph_pred;
let expected_apex_z = z_sph_pred - r_sph_pred * dz / dr;
let mid_z = 0.5 * (z_sph_pred + z_cone_pred);
let r_avg = 0.5 * (r_sph_pred + r_cone_pred);
let expected_beta = ((mid_z - expected_apex_z).abs() / r_avg).atan();
assert!(
expected_apex_z > z_sph_pred && expected_apex_z > z_cone_pred,
"apex should be above both contacts, got apex_z={expected_apex_z}"
);
assert!(
(chamfer_cone.half_angle() - expected_beta).abs() < 1e-9,
"chamfer half-angle should be atan(|z_apex - mid_z| / r_avg) = {expected_beta}, got {}",
chamfer_cone.half_angle()
);
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"apex z = {}, expected {expected_apex_z}",
apex.z()
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) < -1.0 + 1e-12,
"convex chamfer cone axis should be -z, got {axis:?}"
);
let want_sph = Point3::new(r_sph_pred, 0.0, z_sph_pred);
let want_cone = Point3::new(r_cone_pred, 0.0, z_cone_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_sph);
let on_cone_sph = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_cone);
let on_cone_cone = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_sph - want_sph).length() < 1e-9,
"sphere contact must lie on chamfer cone: {on_cone_sph:?} vs {want_sph:?}"
);
assert!(
(on_cone_cone - want_cone).length() < 1e-9,
"cone contact must lie on chamfer cone: {on_cone_cone:?} vs {want_cone:?}"
);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: distance={dist_sph}, want R_s={big_r_s}"
);
let cone_predicted_radial = (z_cone_pred + h_signed) * cot_b;
assert!(
(r_cone_pred - cone_predicted_radial).abs() < 1e-9,
"cone contact must lie on cone surface: predicted radial {cone_predicted_radial}, got {r_cone_pred}"
);
}
#[test]
fn sphere_cone_chamfer_both_concave_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{ConicalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let h_signed: f64 = 2.0;
let beta: f64 = std::f64::consts::PI / 3.0;
let d: f64 = 0.3;
let cot_b = beta.cos() / beta.sin();
let qa_q = 1.0 / (beta.sin() * beta.sin());
let qb_q = 2.0 * h_signed * cot_b * cot_b;
let qc_q = h_signed * h_signed * cot_b * cot_b - big_r_s * big_r_s;
let q_disc = qb_q * qb_q - 4.0 * qa_q * qc_q;
let z_spine = (-qb_q + q_disc.sqrt()) / (2.0 * qa_q);
let r_spine = (z_spine + h_signed) * cot_b;
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cone = ConicalSurface::new(
Point3::new(0.0, 0.0, -h_signed),
Vec3::new(0.0, 0.0, 1.0),
beta,
)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(sph.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone.clone()),
));
let result = sphere_cone_chamfer(&sph, &cone, &spine, &topo, d, d, face_sphere, face_cone)
.unwrap()
.expect("both-concave sphere-cone chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta = d / big_r_s;
let (sin_d, cos_d) = delta.sin_cos();
let sphere_arm_sign = 1.0_f64; let r_sph_pred = r_spine * cos_d + sphere_arm_sign * z_spine * sin_d;
let z_sph_pred = z_spine * cos_d - sphere_arm_sign * r_spine * sin_d;
let r_cone_pred = r_spine + d * beta.cos();
let z_cone_pred = z_spine + d * beta.sin();
assert!(
z_sph_pred < z_spine,
"concave sphere contact should be below spine z (toward cone): got {z_sph_pred} vs spine {z_spine}"
);
assert!(
r_cone_pred > r_spine && z_cone_pred > z_spine,
"concave cone contact should be away from apex: got ({r_cone_pred}, {z_cone_pred})"
);
let want_sph = Point3::new(r_sph_pred, 0.0, z_sph_pred);
let want_cone = Point3::new(r_cone_pred, 0.0, z_cone_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_sph);
let on_cone_sph = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_cone);
let on_cone_cone = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_sph - want_sph).length() < 1e-9,
"concave sphere contact must lie on chamfer cone: {on_cone_sph:?} vs {want_sph:?}"
);
assert!(
(on_cone_cone - want_cone).length() < 1e-9,
"concave cone contact must lie on chamfer cone: {on_cone_cone:?} vs {want_cone:?}"
);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: {dist_sph} vs R_s={big_r_s}"
);
let predicted_cone_radial = (z_cone_pred + h_signed) * cot_b;
assert!(
(r_cone_pred - predicted_cone_radial).abs() < 1e-9,
"cone contact must lie on cone surface: predicted radial {predicted_cone_radial}, got {r_cone_pred}"
);
}
#[test]
fn sphere_cone_chamfer_mixed_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{ConicalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let h_signed: f64 = 2.0;
let beta: f64 = std::f64::consts::PI / 3.0;
let d: f64 = 0.3;
let cot_b = beta.cos() / beta.sin();
let qa_q = 1.0 / (beta.sin() * beta.sin());
let qb_q = 2.0 * h_signed * cot_b * cot_b;
let qc_q = h_signed * h_signed * cot_b * cot_b - big_r_s * big_r_s;
let q_disc = qb_q * qb_q - 4.0 * qa_q * qc_q;
let z_spine = (-qb_q + q_disc.sqrt()) / (2.0 * qa_q);
let r_spine = (z_spine + h_signed) * cot_b;
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cone = ConicalSurface::new(
Point3::new(0.0, 0.0, -h_signed),
Vec3::new(0.0, 0.0, 1.0),
beta,
)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new(w1, vec![], FaceSurface::Sphere(sph.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone.clone()),
));
let result = sphere_cone_chamfer(&sph, &cone, &spine, &topo, d, d, face_sphere, face_cone)
.unwrap()
.expect("mixed sphere-cone chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let delta = d / big_r_s;
let (sin_d, cos_d) = delta.sin_cos();
let r_sph_pred = r_spine * cos_d - z_spine * sin_d;
let z_sph_pred = z_spine * cos_d + r_spine * sin_d;
let r_cone_pred = r_spine + d * beta.cos();
let z_cone_pred = z_spine + d * beta.sin();
assert!(
z_sph_pred > z_spine,
"convex sphere contact should be above spine: got {z_sph_pred}"
);
assert!(
r_cone_pred > r_spine && z_cone_pred > z_spine,
"concave cone contact should be away from apex: got ({r_cone_pred}, {z_cone_pred})"
);
let want_sph = Point3::new(r_sph_pred, 0.0, z_sph_pred);
let want_cone = Point3::new(r_cone_pred, 0.0, z_cone_pred);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_sph);
let on_cone_sph = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_cone);
let on_cone_cone = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_sph - want_sph).length() < 1e-9,
"sphere contact must lie on chamfer cone: {on_cone_sph:?} vs {want_sph:?}"
);
assert!(
(on_cone_cone - want_cone).length() < 1e-9,
"cone contact must lie on chamfer cone: {on_cone_cone:?} vs {want_cone:?}"
);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: {dist_sph} vs R_s={big_r_s}"
);
let predicted_cone_radial = (z_cone_pred + h_signed) * cot_b;
assert!(
(r_cone_pred - predicted_cone_radial).abs() < 1e-9,
"cone contact must lie on cone: predicted {predicted_cone_radial}, got {r_cone_pred}"
);
}
#[test]
fn cylinder_cylinder_fillet_parallel_axes_emits_cylinder() {
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let r1: f64 = 2.0;
let r2: f64 = 2.5;
let big_d: f64 = 3.0;
let r_fillet: f64 = 0.4;
let cyl1 =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r1).unwrap();
let cyl2 = CylindricalSurface::new(Point3::new(big_d, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r2)
.unwrap();
let x_spine = (r1 * r1 - r2 * r2 + big_d * big_d) / (2.0 * big_d);
let y_spine = (r1 * r1 - x_spine * x_spine).sqrt();
let z_lo = 0.0_f64;
let z_hi = 4.0_f64;
let p_start = Point3::new(x_spine, y_spine, z_lo);
let p_end = Point3::new(x_spine, y_spine, z_hi);
let v_start = topo.add_vertex(Vertex::new(p_start, 1e-7));
let v_end = topo.add_vertex(Vertex::new(p_end, 1e-7));
let line = brepkit_math::nurbs::curve::NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![p_start, p_end],
vec![1.0, 1.0],
)
.unwrap();
let eid = topo.add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(line)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], false).unwrap());
let face1 = topo.add_face(Face::new(w1, vec![], FaceSurface::Cylinder(cyl1.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], false).unwrap());
let face2 = topo.add_face(Face::new(w2, vec![], FaceSurface::Cylinder(cyl2.clone())));
let result = cylinder_cylinder_fillet(&cyl1, &cyl2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("parallel-axis cyl-cyl fillet should produce a stripe");
let fillet_cyl = match result.stripe.surface {
FaceSurface::Cylinder(c) => c,
other => panic!("expected Cylinder, got {}", other.type_tag()),
};
let q1 = r1 + r_fillet;
let q2 = r2 + r_fillet;
let x_ball = (q1 * q1 - q2 * q2 + big_d * big_d) / (2.0 * big_d);
let y_ball = (q1 * q1 - x_ball * x_ball).sqrt();
assert!(
(fillet_cyl.radius() - r_fillet).abs() < 1e-12,
"fillet cylinder radius should equal r = {r_fillet}, got {}",
fillet_cyl.radius()
);
let axis = fillet_cyl.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"fillet cylinder axis should be +z, got {axis:?}"
);
let origin = fillet_cyl.origin();
assert!(
(origin.x() - x_ball).abs() < 1e-12 && (origin.y() - y_ball).abs() < 1e-12,
"fillet cylinder origin should be ({x_ball}, {y_ball}, *), got {origin:?}"
);
let want_c1 = Point3::new(r1 * x_ball / q1, r1 * y_ball / q1, z_lo);
let dist_c1_axis = (want_c1.x().powi(2) + want_c1.y().powi(2)).sqrt();
assert!(
(dist_c1_axis - r1).abs() < 1e-9,
"cyl1 contact must lie on cyl1 (radial = r1): got {dist_c1_axis}, want {r1}"
);
let want_c2 = Point3::new(big_d + r2 * (x_ball - big_d) / q2, r2 * y_ball / q2, z_lo);
let dist_c2_axis = ((want_c2.x() - big_d).powi(2) + want_c2.y().powi(2)).sqrt();
assert!(
(dist_c2_axis - r2).abs() < 1e-9,
"cyl2 contact must lie on cyl2 (radial from cyl2 axis = r2): got {dist_c2_axis}, want {r2}"
);
let dist_c1_to_ball = ((want_c1.x() - x_ball).powi(2) + (want_c1.y() - y_ball).powi(2)).sqrt();
let dist_c2_to_ball = ((want_c2.x() - x_ball).powi(2) + (want_c2.y() - y_ball).powi(2)).sqrt();
assert!(
(dist_c1_to_ball - r_fillet).abs() < 1e-9,
"cyl1 contact must lie on fillet cylinder: distance from ball-line = {dist_c1_to_ball}, want r = {r_fillet}"
);
assert!(
(dist_c2_to_ball - r_fillet).abs() < 1e-9,
"cyl2 contact must lie on fillet cylinder: distance from ball-line = {dist_c2_to_ball}, want r = {r_fillet}"
);
}
#[test]
fn cylinder_cylinder_fillet_both_concave_emits_cylinder() {
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let r1: f64 = 2.0;
let r2: f64 = 2.5;
let big_d: f64 = 3.0;
let r_fillet: f64 = 0.4;
let cyl1 =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r1).unwrap();
let cyl2 = CylindricalSurface::new(Point3::new(big_d, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r2)
.unwrap();
let x_spine = (r1 * r1 - r2 * r2 + big_d * big_d) / (2.0 * big_d);
let y_spine = (r1 * r1 - x_spine * x_spine).sqrt();
let z_lo = 0.0_f64;
let z_hi = 4.0_f64;
let p_start = Point3::new(x_spine, y_spine, z_lo);
let p_end = Point3::new(x_spine, y_spine, z_hi);
let v_start = topo.add_vertex(Vertex::new(p_start, 1e-7));
let v_end = topo.add_vertex(Vertex::new(p_end, 1e-7));
let line = brepkit_math::nurbs::curve::NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![p_start, p_end],
vec![1.0, 1.0],
)
.unwrap();
let eid = topo.add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(line)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], false).unwrap());
let face1 = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cylinder(cyl1.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], false).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl2.clone()),
));
let result = cylinder_cylinder_fillet(&cyl1, &cyl2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("both-concave cyl-cyl fillet should produce a stripe");
let fillet_cyl = match result.stripe.surface {
FaceSurface::Cylinder(c) => c,
other => panic!("expected Cylinder, got {}", other.type_tag()),
};
let q1 = r1 - r_fillet;
let q2 = r2 - r_fillet;
let x_ball = (q1 * q1 - q2 * q2 + big_d * big_d) / (2.0 * big_d);
let y_ball = (q1 * q1 - x_ball * x_ball).sqrt();
assert!(
(fillet_cyl.radius() - r_fillet).abs() < 1e-12,
"fillet radius should be r = {r_fillet}, got {}",
fillet_cyl.radius()
);
let origin = fillet_cyl.origin();
assert!(
(origin.x() - x_ball).abs() < 1e-12 && (origin.y() - y_ball).abs() < 1e-12,
"concave fillet origin should be ({x_ball}, {y_ball}, *), got {origin:?}"
);
let axis = fillet_cyl.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"concave fillet axis should be +z (parallel to original cyls), got {axis:?}"
);
let actual_dist_to_cyl1_axis = (origin.x().powi(2) + origin.y().powi(2)).sqrt();
let actual_dist_to_cyl2_axis = ((origin.x() - big_d).powi(2) + origin.y().powi(2)).sqrt();
assert!(
actual_dist_to_cyl1_axis < r1 - 1e-9,
"concave: emitted fillet origin must be INSIDE cyl1 (distance {actual_dist_to_cyl1_axis} < r1 = {r1})"
);
assert!(
actual_dist_to_cyl2_axis < r2 - 1e-9,
"concave: emitted fillet origin must be INSIDE cyl2 (distance {actual_dist_to_cyl2_axis} < r2 = {r2})"
);
let want_c1 = Point3::new(r1 * x_ball / q1, r1 * y_ball / q1, z_lo);
let dist_c1_axis = (want_c1.x().powi(2) + want_c1.y().powi(2)).sqrt();
assert!(
(dist_c1_axis - r1).abs() < 1e-9,
"cyl1 contact must lie on cyl1: got {dist_c1_axis}, want {r1}"
);
let want_c2 = Point3::new(big_d + r2 * (x_ball - big_d) / q2, r2 * y_ball / q2, z_lo);
let dist_c2_axis = ((want_c2.x() - big_d).powi(2) + want_c2.y().powi(2)).sqrt();
assert!(
(dist_c2_axis - r2).abs() < 1e-9,
"cyl2 contact must lie on cyl2: got {dist_c2_axis}, want {r2}"
);
let dist_c1_to_ball =
((want_c1.x() - origin.x()).powi(2) + (want_c1.y() - origin.y()).powi(2)).sqrt();
let dist_c2_to_ball =
((want_c2.x() - origin.x()).powi(2) + (want_c2.y() - origin.y()).powi(2)).sqrt();
assert!(
(dist_c1_to_ball - r_fillet).abs() < 1e-9,
"cyl1 contact must be at distance r from fillet ball-line: got {dist_c1_to_ball}, want {r_fillet}"
);
assert!(
(dist_c2_to_ball - r_fillet).abs() < 1e-9,
"cyl2 contact must be at distance r from fillet ball-line: got {dist_c2_to_ball}, want {r_fillet}"
);
}
#[test]
fn cylinder_cylinder_fillet_mixed_emits_cylinder() {
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let r1: f64 = 2.0;
let r2: f64 = 2.5;
let big_d: f64 = 3.0;
let r_fillet: f64 = 0.4;
let x_spine = (r1 * r1 - r2 * r2 + big_d * big_d) / (2.0 * big_d);
let y_spine = (r1 * r1 - x_spine * x_spine).sqrt();
let run_case = |reverse_s1: bool, reverse_s2: bool| {
let mut topo = Topology::new();
let cyl1 =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r1)
.unwrap();
let cyl2 =
CylindricalSurface::new(Point3::new(big_d, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r2)
.unwrap();
let z_lo = 0.0_f64;
let z_hi = 4.0_f64;
let p_start = Point3::new(x_spine, y_spine, z_lo);
let p_end = Point3::new(x_spine, y_spine, z_hi);
let v_start = topo.add_vertex(Vertex::new(p_start, 1e-7));
let v_end = topo.add_vertex(Vertex::new(p_end, 1e-7));
let line = brepkit_math::nurbs::curve::NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![p_start, p_end],
vec![1.0, 1.0],
)
.unwrap();
let eid = topo.add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(line)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], false).unwrap());
let face1 = if reverse_s1 {
topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cylinder(cyl1.clone()),
))
} else {
topo.add_face(Face::new(w1, vec![], FaceSurface::Cylinder(cyl1.clone())))
};
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], false).unwrap());
let face2 = if reverse_s2 {
topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl2.clone()),
))
} else {
topo.add_face(Face::new(w2, vec![], FaceSurface::Cylinder(cyl2.clone())))
};
let result = cylinder_cylinder_fillet(&cyl1, &cyl2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("mixed cyl-cyl fillet should produce a stripe");
let fillet_cyl = match result.stripe.surface {
FaceSurface::Cylinder(c) => c,
other => panic!(
"({reverse_s1}, {reverse_s2}): expected Cylinder, got {}",
other.type_tag()
),
};
let s1_signed = if reverse_s1 { -1.0_f64 } else { 1.0_f64 };
let s2_signed = if reverse_s2 { -1.0_f64 } else { 1.0_f64 };
let q1 = r1 + s1_signed * r_fillet;
let q2 = r2 + s2_signed * r_fillet;
let x_ball = (q1 * q1 - q2 * q2 + big_d * big_d) / (2.0 * big_d);
let y_ball = (q1 * q1 - x_ball * x_ball).sqrt();
assert!(
(fillet_cyl.radius() - r_fillet).abs() < 1e-12,
"({reverse_s1}, {reverse_s2}): fillet radius should be r = {r_fillet}, got {}",
fillet_cyl.radius()
);
let origin = fillet_cyl.origin();
assert!(
(origin.x() - x_ball).abs() < 1e-12 && (origin.y() - y_ball).abs() < 1e-12,
"({reverse_s1}, {reverse_s2}): fillet origin should be ({x_ball}, {y_ball}, *), got {origin:?}"
);
let axis = fillet_cyl.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"({reverse_s1}, {reverse_s2}): fillet axis should be +z, got {axis:?}"
);
let (t1_start, _) = result.stripe.contact1.domain();
let c1_point = result.stripe.contact1.evaluate(t1_start);
let (t2_start, _) = result.stripe.contact2.domain();
let c2_point = result.stripe.contact2.evaluate(t2_start);
let dist_c1_axis = (c1_point.x().powi(2) + c1_point.y().powi(2)).sqrt();
let dist_c2_axis = ((c2_point.x() - big_d).powi(2) + c2_point.y().powi(2)).sqrt();
assert!(
(dist_c1_axis - r1).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c1 must lie on cyl1: {dist_c1_axis} vs r1 = {r1}"
);
assert!(
(dist_c2_axis - r2).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c2 must lie on cyl2: {dist_c2_axis} vs r2 = {r2}"
);
let dist_c1_to_ball =
((c1_point.x() - x_ball).powi(2) + (c1_point.y() - y_ball).powi(2)).sqrt();
let dist_c2_to_ball =
((c2_point.x() - x_ball).powi(2) + (c2_point.y() - y_ball).powi(2)).sqrt();
assert!(
(dist_c1_to_ball - r_fillet).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c1 must be at r from fillet ball-line: \
{dist_c1_to_ball} vs r = {r_fillet}"
);
assert!(
(dist_c2_to_ball - r_fillet).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c2 must be at r from fillet ball-line: \
{dist_c2_to_ball} vs r = {r_fillet}"
);
};
run_case(false, true); run_case(true, false); }
#[test]
fn cone_cone_coaxial_fillet_convex_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let beta1: f64 = std::f64::consts::PI / 3.0;
let beta2: f64 = std::f64::consts::PI / 4.0;
let h_2: f64 = 2.0;
let r_fillet: f64 = 0.3;
let sin_minus = (beta1 - beta2).sin();
let z_spine = h_2 * beta2.cos() * beta1.sin() / sin_minus;
let r_spine = z_spine * (beta1.cos() / beta1.sin());
let cone1 =
ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), beta1).unwrap();
let cone2 =
ConicalSurface::new(Point3::new(0.0, 0.0, h_2), Vec3::new(0.0, 0.0, 1.0), beta2).unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new(w1, vec![], FaceSurface::Cone(cone1.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new(w2, vec![], FaceSurface::Cone(cone2.clone())));
let result = cone_cone_coaxial_fillet(&cone1, &cone2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("convex coaxial cone-cone fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let expected_z_b =
(h_2 * beta2.cos() * beta1.sin() + r_fillet * (beta2.sin() - beta1.sin())) / sin_minus;
let expected_major = (expected_z_b * (beta1.cos() - beta2.cos()) + h_2 * beta2.cos())
/ (beta1.sin() - beta2.sin());
assert!(
(torus.major_radius() - expected_major).abs() < 1e-9,
"torus major should be {expected_major}, got {}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"minor should equal r = {r_fillet}, got {}",
torus.minor_radius()
);
assert!(
torus.major_radius() > r_spine,
"convex fillet major ({}) should be > r_spine ({r_spine})",
torus.major_radius()
);
let center = torus.center();
assert!(
center.x().abs() < 1e-12 && center.y().abs() < 1e-12,
"torus center on z-axis, got {center:?}"
);
assert!(
(center.z() - expected_z_b).abs() < 1e-9,
"torus center z should be {expected_z_b}, got {}",
center.z()
);
let tang1 = expected_major * beta1.sin() - expected_z_b * beta1.cos();
let tang2 = expected_major * beta2.sin() - (expected_z_b - h_2) * beta2.cos();
assert!(
(tang1 - r_fillet).abs() < 1e-9,
"cone1 tangency: {tang1} should equal r = {r_fillet}"
);
assert!(
(tang2 - r_fillet).abs() < 1e-9,
"cone2 tangency: {tang2} should equal r = {r_fillet}"
);
let cot_b1 = beta1.cos() / beta1.sin();
let cot_b2 = beta2.cos() / beta2.sin();
let c1_axial = expected_z_b + r_fillet * beta1.cos();
let c1_radial = expected_major - r_fillet * beta1.sin();
let c2_axial = expected_z_b + r_fillet * beta2.cos();
let c2_radial = expected_major - r_fillet * beta2.sin();
let want_c1 = Point3::new(c1_radial, 0.0, c1_axial);
let want_c2 = Point3::new(c2_radial, 0.0, c2_axial);
let pred_c1_radial = c1_axial * cot_b1;
assert!(
(c1_radial - pred_c1_radial).abs() < 1e-9,
"cone1 contact must lie on cone1 surface: predicted radial {pred_c1_radial}, got {c1_radial}"
);
let pred_c2_radial = (c2_axial - h_2) * cot_b2;
assert!(
(c2_radial - pred_c2_radial).abs() < 1e-9,
"cone2 contact must lie on cone2 surface: predicted radial {pred_c2_radial}, got {c2_radial}"
);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_c1);
let on_torus_c1 = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_c2);
let on_torus_c2 = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_c1 - want_c1).length() < 1e-9,
"cone1 contact must lie on torus: {on_torus_c1:?} vs {want_c1:?}"
);
assert!(
(on_torus_c2 - want_c2).length() < 1e-9,
"cone2 contact must lie on torus: {on_torus_c2:?} vs {want_c2:?}"
);
}
#[test]
fn cone_cone_coaxial_fillet_both_concave_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let beta1: f64 = std::f64::consts::PI / 3.0;
let beta2: f64 = std::f64::consts::PI / 4.0;
let h_2: f64 = 2.0;
let r_fillet: f64 = 0.3;
let sin_minus = (beta1 - beta2).sin();
let z_spine = h_2 * beta2.cos() * beta1.sin() / sin_minus;
let r_spine = z_spine * (beta1.cos() / beta1.sin());
let cone1 =
ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), beta1).unwrap();
let cone2 =
ConicalSurface::new(Point3::new(0.0, 0.0, h_2), Vec3::new(0.0, 0.0, 1.0), beta2).unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cone(cone1.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone2.clone()),
));
let result = cone_cone_coaxial_fillet(&cone1, &cone2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("both-concave coaxial cone-cone fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let s1 = -1.0_f64;
let s2 = -1.0_f64;
let expected_z_b = (h_2 * beta2.cos() * beta1.sin()
+ r_fillet * (s1 * beta2.sin() - s2 * beta1.sin()))
/ sin_minus;
let expected_major =
(expected_z_b * (beta1.cos() - beta2.cos()) + h_2 * beta2.cos() + (s1 - s2) * r_fillet)
/ (beta1.sin() - beta2.sin());
assert!(
(torus.major_radius() - expected_major).abs() < 1e-9,
"concave torus major should be {expected_major}, got {}",
torus.major_radius()
);
assert!(
torus.major_radius() < r_spine,
"concave fillet major ({}) should be < r_spine ({r_spine})",
torus.major_radius()
);
let tang1 = expected_major * beta1.sin() - expected_z_b * beta1.cos();
let tang2 = expected_major * beta2.sin() - (expected_z_b - h_2) * beta2.cos();
assert!(
(tang1 + r_fillet).abs() < 1e-9,
"cone1 internal tangency: {tang1} should equal -r = {}",
-r_fillet
);
assert!(
(tang2 + r_fillet).abs() < 1e-9,
"cone2 internal tangency: {tang2} should equal -r = {}",
-r_fillet
);
let cot_b1 = beta1.cos() / beta1.sin();
let cot_b2 = beta2.cos() / beta2.sin();
let c1_axial = expected_z_b + s1 * r_fillet * beta1.cos();
let c1_radial = expected_major - s1 * r_fillet * beta1.sin();
let c2_axial = expected_z_b + s2 * r_fillet * beta2.cos();
let c2_radial = expected_major - s2 * r_fillet * beta2.sin();
let pred_c1_radial = c1_axial * cot_b1;
assert!(
(c1_radial - pred_c1_radial).abs() < 1e-9,
"cone1 contact must lie on cone1: predicted {pred_c1_radial}, got {c1_radial}"
);
let pred_c2_radial = (c2_axial - h_2) * cot_b2;
assert!(
(c2_radial - pred_c2_radial).abs() < 1e-9,
"cone2 contact must lie on cone2: predicted {pred_c2_radial}, got {c2_radial}"
);
let want_c1 = Point3::new(c1_radial, 0.0, c1_axial);
let want_c2 = Point3::new(c2_radial, 0.0, c2_axial);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_c1);
let on_torus_c1 = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_c2);
let on_torus_c2 = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_c1 - want_c1).length() < 1e-9,
"cone1 contact must lie on torus: {on_torus_c1:?} vs {want_c1:?}"
);
assert!(
(on_torus_c2 - want_c2).length() < 1e-9,
"cone2 contact must lie on torus: {on_torus_c2:?} vs {want_c2:?}"
);
}
#[test]
fn cone_cone_coaxial_fillet_mixed_emits_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let beta1: f64 = std::f64::consts::PI / 3.0;
let beta2: f64 = std::f64::consts::PI / 4.0;
let h_2: f64 = 2.0;
let r_fillet: f64 = 0.3;
let sin_minus = (beta1 - beta2).sin();
let z_spine = h_2 * beta2.cos() * beta1.sin() / sin_minus;
let r_spine = z_spine * (beta1.cos() / beta1.sin());
let run_case = |reverse_s1: bool, reverse_s2: bool| {
let mut topo = Topology::new();
let cone1 =
ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), beta1)
.unwrap();
let cone2 =
ConicalSurface::new(Point3::new(0.0, 0.0, h_2), Vec3::new(0.0, 0.0, 1.0), beta2)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = if reverse_s1 {
topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cone(cone1.clone()),
))
} else {
topo.add_face(Face::new(w1, vec![], FaceSurface::Cone(cone1.clone())))
};
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = if reverse_s2 {
topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone2.clone()),
))
} else {
topo.add_face(Face::new(w2, vec![], FaceSurface::Cone(cone2.clone())))
};
let result =
cone_cone_coaxial_fillet(&cone1, &cone2, &spine, &topo, r_fillet, face1, face2)
.unwrap()
.expect("mixed coaxial cone-cone fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(ref t) => t.clone(),
ref other => panic!(
"({reverse_s1}, {reverse_s2}): expected Torus, got {}",
other.type_tag()
),
};
let s1 = if reverse_s1 { -1.0_f64 } else { 1.0 };
let s2 = if reverse_s2 { -1.0_f64 } else { 1.0 };
let expected_z_b = (h_2 * beta2.cos() * beta1.sin()
+ r_fillet * (s1 * beta2.sin() - s2 * beta1.sin()))
/ sin_minus;
let expected_major =
(expected_z_b * (beta1.cos() - beta2.cos()) + h_2 * beta2.cos() + (s1 - s2) * r_fillet)
/ (beta1.sin() - beta2.sin());
assert!(
(torus.major_radius() - expected_major).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): mixed torus major should be {expected_major}, got {}",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): minor should be {r_fillet}, got {}",
torus.minor_radius()
);
let (t1_start, _) = result.stripe.contact1.domain();
let c1_point = result.stripe.contact1.evaluate(t1_start);
let (t2_start, _) = result.stripe.contact2.domain();
let c2_point = result.stripe.contact2.evaluate(t2_start);
let pred_c1_r = expected_major - s1 * r_fillet * beta1.sin();
let pred_c1_z = expected_z_b + s1 * r_fillet * beta1.cos();
let pred_c2_r = expected_major - s2 * r_fillet * beta2.sin();
let pred_c2_z = expected_z_b + s2 * r_fillet * beta2.cos();
let c1_radial = (c1_point.x().powi(2) + c1_point.y().powi(2)).sqrt();
let c2_radial = (c2_point.x().powi(2) + c2_point.y().powi(2)).sqrt();
assert!(
(c1_radial - pred_c1_r).abs() < 1e-9 && (c1_point.z() - pred_c1_z).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): contact1 should be at \
(r={pred_c1_r}, z={pred_c1_z}); got (r={c1_radial}, z={})",
c1_point.z()
);
assert!(
(c2_radial - pred_c2_r).abs() < 1e-9 && (c2_point.z() - pred_c2_z).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): contact2 should be at \
(r={pred_c2_r}, z={pred_c2_z}); got (r={c2_radial}, z={})",
c2_point.z()
);
};
run_case(false, true); run_case(true, false); }
#[test]
fn cylinder_cylinder_chamfer_convex_emits_plane() {
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let r1: f64 = 2.0;
let r2: f64 = 2.5;
let big_d: f64 = 3.0;
let d: f64 = 0.4;
let cyl1 =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r1).unwrap();
let cyl2 = CylindricalSurface::new(Point3::new(big_d, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r2)
.unwrap();
let x_spine = (r1 * r1 - r2 * r2 + big_d * big_d) / (2.0 * big_d);
let y_spine = (r1 * r1 - x_spine * x_spine).sqrt();
let z_lo = 0.0_f64;
let z_hi = 4.0_f64;
let p_start = Point3::new(x_spine, y_spine, z_lo);
let p_end = Point3::new(x_spine, y_spine, z_hi);
let v_start = topo.add_vertex(Vertex::new(p_start, 1e-7));
let v_end = topo.add_vertex(Vertex::new(p_end, 1e-7));
let line = brepkit_math::nurbs::curve::NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![p_start, p_end],
vec![1.0, 1.0],
)
.unwrap();
let eid = topo.add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(line)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], false).unwrap());
let face1 = topo.add_face(Face::new(w1, vec![], FaceSurface::Cylinder(cyl1.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], false).unwrap());
let face2 = topo.add_face(Face::new(w2, vec![], FaceSurface::Cylinder(cyl2.clone())));
let result = cylinder_cylinder_chamfer(&cyl1, &cyl2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("convex parallel-axis cyl-cyl chamfer should produce a stripe");
let (chamfer_normal, chamfer_d) = match result.stripe.surface {
FaceSurface::Plane { normal, d } => (normal, d),
other => panic!("expected Plane, got {}", other.type_tag()),
};
let dtheta1 = d / r1; let dtheta2 = -d / r2; let (sin1, cos1) = dtheta1.sin_cos();
let (sin2, cos2) = dtheta2.sin_cos();
let c1_x = x_spine * cos1 - y_spine * sin1;
let c1_y = y_spine * cos1 + x_spine * sin1;
let c2_local_x = (x_spine - big_d) * cos2 - y_spine * sin2;
let c2_local_y = y_spine * cos2 + (x_spine - big_d) * sin2;
let c2_x = c2_local_x + big_d;
let c2_y = c2_local_y;
let dist_c1_axis = (c1_x.powi(2) + c1_y.powi(2)).sqrt();
assert!(
(dist_c1_axis - r1).abs() < 1e-9,
"cyl1 contact must lie on cyl1: distance = {dist_c1_axis}, want r1 = {r1}"
);
let dist_c2_axis = ((c2_x - big_d).powi(2) + c2_y.powi(2)).sqrt();
assert!(
(dist_c2_axis - r2).abs() < 1e-9,
"cyl2 contact must lie on cyl2: distance = {dist_c2_axis}, want r2 = {r2}"
);
let p1 = Point3::new(c1_x, c1_y, z_lo);
let p2 = Point3::new(c2_x, c2_y, z_lo);
let on_plane_1 = chamfer_normal.dot(Vec3::new(p1.x(), p1.y(), p1.z())) - chamfer_d;
let on_plane_2 = chamfer_normal.dot(Vec3::new(p2.x(), p2.y(), p2.z())) - chamfer_d;
assert!(
on_plane_1.abs() < 1e-9,
"cyl1 contact must lie on chamfer plane: residual {on_plane_1}"
);
assert!(
on_plane_2.abs() < 1e-9,
"cyl2 contact must lie on chamfer plane: residual {on_plane_2}"
);
assert!(
chamfer_normal.z().abs() < 1e-12,
"chamfer plane normal should be perpendicular to z axis, got {chamfer_normal:?}"
);
}
#[test]
fn cylinder_cylinder_chamfer_both_concave_negative_y_emits_plane() {
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let r1: f64 = 2.0;
let r2: f64 = 2.5;
let big_d: f64 = 3.0;
let d: f64 = 0.4;
let cyl1 =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r1).unwrap();
let cyl2 = CylindricalSurface::new(Point3::new(big_d, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r2)
.unwrap();
let x_spine = (r1 * r1 - r2 * r2 + big_d * big_d) / (2.0 * big_d);
let y_spine = -((r1 * r1 - x_spine * x_spine).sqrt()); let z_lo = 0.0_f64;
let z_hi = 4.0_f64;
let p_start = Point3::new(x_spine, y_spine, z_lo);
let p_end = Point3::new(x_spine, y_spine, z_hi);
let v_start = topo.add_vertex(Vertex::new(p_start, 1e-7));
let v_end = topo.add_vertex(Vertex::new(p_end, 1e-7));
let line = brepkit_math::nurbs::curve::NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![p_start, p_end],
vec![1.0, 1.0],
)
.unwrap();
let eid = topo.add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(line)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], false).unwrap());
let face1 = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cylinder(cyl1.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], false).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl2.clone()),
));
let result = cylinder_cylinder_chamfer(&cyl1, &cyl2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("both-concave −y-spine cyl-cyl chamfer should produce a stripe");
let (chamfer_normal, chamfer_d) = match result.stripe.surface {
FaceSurface::Plane { normal, d } => (normal, d),
other => panic!("expected Plane, got {}", other.type_tag()),
};
let dtheta1 = d / r1;
let dtheta2 = -d / r2;
let (sin1, cos1) = dtheta1.sin_cos();
let (sin2, cos2) = dtheta2.sin_cos();
let c1_x = x_spine * cos1 - y_spine * sin1;
let c1_y = y_spine * cos1 + x_spine * sin1;
let c2_local_x = (x_spine - big_d) * cos2 - y_spine * sin2;
let c2_local_y = y_spine * cos2 + (x_spine - big_d) * sin2;
let c2_x = c2_local_x + big_d;
let c2_y = c2_local_y;
let dist_c1_axis = (c1_x.powi(2) + c1_y.powi(2)).sqrt();
assert!(
(dist_c1_axis - r1).abs() < 1e-9,
"cyl1 contact must lie on cyl1: distance = {dist_c1_axis}, want r1 = {r1}"
);
let dist_c2_axis = ((c2_x - big_d).powi(2) + c2_y.powi(2)).sqrt();
assert!(
(dist_c2_axis - r2).abs() < 1e-9,
"cyl2 contact must lie on cyl2: distance = {dist_c2_axis}, want r2 = {r2}"
);
let p1 = Point3::new(c1_x, c1_y, z_lo);
let p2 = Point3::new(c2_x, c2_y, z_lo);
let on_plane_1 = chamfer_normal.dot(Vec3::new(p1.x(), p1.y(), p1.z())) - chamfer_d;
let on_plane_2 = chamfer_normal.dot(Vec3::new(p2.x(), p2.y(), p2.z())) - chamfer_d;
assert!(
on_plane_1.abs() < 1e-9,
"cyl1 contact must lie on chamfer plane: residual {on_plane_1}"
);
assert!(
on_plane_2.abs() < 1e-9,
"cyl2 contact must lie on chamfer plane: residual {on_plane_2}"
);
assert!(
c1_y > y_spine,
"concave cyl1 contact should pull TOWARD y=0: got {c1_y} vs spine {y_spine}"
);
assert!(
c2_y > y_spine,
"concave cyl2 contact should pull TOWARD y=0: got {c2_y} vs spine {y_spine}"
);
assert!(
chamfer_normal.z().abs() < 1e-12,
"chamfer plane normal must be perpendicular to z, got {chamfer_normal:?}"
);
}
#[test]
fn cylinder_cylinder_chamfer_mixed_emits_plane() {
use brepkit_math::surfaces::CylindricalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let r1: f64 = 2.0;
let r2: f64 = 2.5;
let big_d: f64 = 3.0;
let d: f64 = 0.4;
let x_spine = (r1 * r1 - r2 * r2 + big_d * big_d) / (2.0 * big_d);
let y_spine = (r1 * r1 - x_spine * x_spine).sqrt();
let run_case = |reverse_s1: bool, reverse_s2: bool| {
let mut topo = Topology::new();
let cyl1 =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r1)
.unwrap();
let cyl2 =
CylindricalSurface::new(Point3::new(big_d, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r2)
.unwrap();
let z_lo = 0.0_f64;
let z_hi = 4.0_f64;
let p_start = Point3::new(x_spine, y_spine, z_lo);
let p_end = Point3::new(x_spine, y_spine, z_hi);
let v_start = topo.add_vertex(Vertex::new(p_start, 1e-7));
let v_end = topo.add_vertex(Vertex::new(p_end, 1e-7));
let line = brepkit_math::nurbs::curve::NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![p_start, p_end],
vec![1.0, 1.0],
)
.unwrap();
let eid = topo.add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(line)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], false).unwrap());
let face1 = if reverse_s1 {
topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cylinder(cyl1.clone()),
))
} else {
topo.add_face(Face::new(w1, vec![], FaceSurface::Cylinder(cyl1.clone())))
};
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], false).unwrap());
let face2 = if reverse_s2 {
topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl2.clone()),
))
} else {
topo.add_face(Face::new(w2, vec![], FaceSurface::Cylinder(cyl2.clone())))
};
let result = cylinder_cylinder_chamfer(&cyl1, &cyl2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("mixed cyl-cyl chamfer should produce a stripe");
let (chamfer_normal, chamfer_d) = match result.stripe.surface {
FaceSurface::Plane { normal, d } => (normal, d),
ref other => panic!(
"({reverse_s1}, {reverse_s2}): expected Plane, got {}",
other.type_tag()
),
};
let (t1_start, _) = result.stripe.contact1.domain();
let c1_point = result.stripe.contact1.evaluate(t1_start);
let (t2_start, _) = result.stripe.contact2.domain();
let c2_point = result.stripe.contact2.evaluate(t2_start);
let dist_c1_axis = (c1_point.x().powi(2) + c1_point.y().powi(2)).sqrt();
let dist_c2_axis = ((c2_point.x() - big_d).powi(2) + c2_point.y().powi(2)).sqrt();
assert!(
(dist_c1_axis - r1).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c1 must lie on cyl1: \
distance = {dist_c1_axis}, want r1 = {r1}"
);
assert!(
(dist_c2_axis - r2).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c2 must lie on cyl2: \
distance = {dist_c2_axis}, want r2 = {r2}"
);
let on_plane_1 =
chamfer_normal.dot(Vec3::new(c1_point.x(), c1_point.y(), c1_point.z())) - chamfer_d;
let on_plane_2 =
chamfer_normal.dot(Vec3::new(c2_point.x(), c2_point.y(), c2_point.z())) - chamfer_d;
assert!(
on_plane_1.abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c1 on chamfer plane: residual {on_plane_1}"
);
assert!(
on_plane_2.abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): c2 on chamfer plane: residual {on_plane_2}"
);
assert!(
chamfer_normal.z().abs() < 1e-12,
"({reverse_s1}, {reverse_s2}): plane normal should be perpendicular to z, got {chamfer_normal:?}"
);
};
run_case(false, true); run_case(true, false); }
#[test]
fn cone_cone_coaxial_chamfer_convex_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let beta1: f64 = std::f64::consts::PI / 3.0;
let beta2: f64 = std::f64::consts::PI / 4.0;
let h_2: f64 = 2.0;
let d: f64 = 0.3;
let sin_minus = (beta1 - beta2).sin();
let z_spine = h_2 * beta2.cos() * beta1.sin() / sin_minus;
let r_spine = z_spine * (beta1.cos() / beta1.sin());
let cone1 =
ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), beta1).unwrap();
let cone2 =
ConicalSurface::new(Point3::new(0.0, 0.0, h_2), Vec3::new(0.0, 0.0, 1.0), beta2).unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new(w1, vec![], FaceSurface::Cone(cone1.clone())));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new(w2, vec![], FaceSurface::Cone(cone2.clone())));
let result = cone_cone_coaxial_chamfer(&cone1, &cone2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("convex coaxial cone-cone chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let r_c1 = r_spine - d * beta1.cos();
let z_c1 = z_spine - d * beta1.sin();
let r_c2 = r_spine + d * beta2.cos();
let z_c2 = z_spine + d * beta2.sin();
assert!(
r_c1 < r_spine && z_c1 < z_spine,
"cone1 contact should retreat toward apex1: got ({r_c1}, {z_c1}) vs spine ({r_spine}, {z_spine})"
);
assert!(
r_c2 > r_spine && z_c2 > z_spine,
"cone2 contact should extend away from apex2: got ({r_c2}, {z_c2}) vs spine"
);
let dr = r_c2 - r_c1;
let dz = z_c2 - z_c1;
let expected_apex_z = z_c1 - r_c1 * dz / dr;
let mid_z = 0.5 * (z_c1 + z_c2);
let r_avg = 0.5 * (r_c1 + r_c2);
let expected_beta = ((mid_z - expected_apex_z).abs() / r_avg).atan();
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"apex z = {}, expected {expected_apex_z}",
apex.z()
);
assert!(
(chamfer_cone.half_angle() - expected_beta).abs() < 1e-9,
"chamfer half-angle should be {expected_beta}, got {}",
chamfer_cone.half_angle()
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"convex chamfer cone axis should be +z (contacts above apex), got {axis:?}"
);
let want_c1 = Point3::new(r_c1, 0.0, z_c1);
let want_c2 = Point3::new(r_c2, 0.0, z_c2);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_c1);
let on_cone_c1 = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_c2);
let on_cone_c2 = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_c1 - want_c1).length() < 1e-9,
"cone1 contact must lie on chamfer cone: {on_cone_c1:?} vs {want_c1:?}"
);
assert!(
(on_cone_c2 - want_c2).length() < 1e-9,
"cone2 contact must lie on chamfer cone: {on_cone_c2:?} vs {want_c2:?}"
);
let cot_b1 = beta1.cos() / beta1.sin();
let cot_b2 = beta2.cos() / beta2.sin();
let pred_r_c1 = z_c1 * cot_b1;
let pred_r_c2 = (z_c2 - h_2) * cot_b2;
assert!(
(r_c1 - pred_r_c1).abs() < 1e-9,
"cone1 contact must lie on cone1: predicted radial {pred_r_c1}, got {r_c1}"
);
assert!(
(r_c2 - pred_r_c2).abs() < 1e-9,
"cone2 contact must lie on cone2: predicted radial {pred_r_c2}, got {r_c2}"
);
}
#[test]
fn cone_cone_coaxial_chamfer_both_concave_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let beta1: f64 = std::f64::consts::PI / 3.0;
let beta2: f64 = std::f64::consts::PI / 4.0;
let h_2: f64 = 2.0;
let d: f64 = 0.3;
let sin_minus = (beta1 - beta2).sin();
let z_spine = h_2 * beta2.cos() * beta1.sin() / sin_minus;
let r_spine = z_spine * (beta1.cos() / beta1.sin());
let cone1 =
ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), beta1).unwrap();
let cone2 =
ConicalSurface::new(Point3::new(0.0, 0.0, h_2), Vec3::new(0.0, 0.0, 1.0), beta2).unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cone(cone1.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone2.clone()),
));
let result = cone_cone_coaxial_chamfer(&cone1, &cone2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("both-concave coaxial cone-cone chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let r_c1 = r_spine + d * beta1.cos();
let z_c1 = z_spine + d * beta1.sin();
let r_c2 = r_spine - d * beta2.cos();
let z_c2 = z_spine - d * beta2.sin();
assert!(
r_c1 > r_spine && z_c1 > z_spine,
"concave cone1 contact should extend away from apex1: got ({r_c1}, {z_c1}) vs spine ({r_spine}, {z_spine})"
);
assert!(
r_c2 < r_spine && z_c2 < z_spine,
"concave cone2 contact should retreat toward apex2: got ({r_c2}, {z_c2}) vs spine"
);
let dr = r_c2 - r_c1;
let dz = z_c2 - z_c1;
let expected_apex_z = z_c1 - r_c1 * dz / dr;
let mid_z = 0.5 * (z_c1 + z_c2);
let r_avg = 0.5 * (r_c1 + r_c2);
let expected_beta = ((mid_z - expected_apex_z).abs() / r_avg).atan();
let apex = chamfer_cone.apex();
assert!(
apex.x().abs() < 1e-12 && apex.y().abs() < 1e-12,
"concave apex should be on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"concave apex z = {}, expected {expected_apex_z}",
apex.z()
);
assert!(
(chamfer_cone.half_angle() - expected_beta).abs() < 1e-9,
"concave chamfer half-angle should be {expected_beta}, got {}",
chamfer_cone.half_angle()
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"concave chamfer cone axis should be +z (apex below contacts), got {axis:?}"
);
let want_c1 = Point3::new(r_c1, 0.0, z_c1);
let want_c2 = Point3::new(r_c2, 0.0, z_c2);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_c1);
let on_cone_c1 = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&chamfer_cone, want_c2);
let on_cone_c2 = ParametricSurface::evaluate(&chamfer_cone, u_q, v_q);
assert!(
(on_cone_c1 - want_c1).length() < 1e-9,
"cone1 contact must lie on chamfer cone: {on_cone_c1:?} vs {want_c1:?}"
);
assert!(
(on_cone_c2 - want_c2).length() < 1e-9,
"cone2 contact must lie on chamfer cone: {on_cone_c2:?} vs {want_c2:?}"
);
let cot_b1 = beta1.cos() / beta1.sin();
let cot_b2 = beta2.cos() / beta2.sin();
let pred_r_c1 = z_c1 * cot_b1;
let pred_r_c2 = (z_c2 - h_2) * cot_b2;
assert!(
(r_c1 - pred_r_c1).abs() < 1e-9,
"cone1 contact must lie on cone1: predicted radial {pred_r_c1}, got {r_c1}"
);
assert!(
(r_c2 - pred_r_c2).abs() < 1e-9,
"cone2 contact must lie on cone2: predicted radial {pred_r_c2}, got {r_c2}"
);
}
#[test]
fn cone_cone_coaxial_chamfer_mixed_emits_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let beta1: f64 = std::f64::consts::PI / 3.0;
let beta2: f64 = std::f64::consts::PI / 4.0;
let h_2: f64 = 2.0;
let d: f64 = 0.3;
let sin_minus = (beta1 - beta2).sin();
let z_spine = h_2 * beta2.cos() * beta1.sin() / sin_minus;
let r_spine = z_spine * (beta1.cos() / beta1.sin());
let run_case = |reverse_s1: bool, reverse_s2: bool| {
let mut topo = Topology::new();
let cone1 =
ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), beta1)
.unwrap();
let cone2 =
ConicalSurface::new(Point3::new(0.0, 0.0, h_2), Vec3::new(0.0, 0.0, 1.0), beta2)
.unwrap();
let spine_circle = Circle3D::new(
Point3::new(0.0, 0.0, z_spine),
Vec3::new(0.0, 0.0, 1.0),
r_spine,
)
.unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_spine, 0.0, z_spine), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face1 = if reverse_s1 {
topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Cone(cone1.clone()),
))
} else {
topo.add_face(Face::new(w1, vec![], FaceSurface::Cone(cone1.clone())))
};
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face2 = if reverse_s2 {
topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone2.clone()),
))
} else {
topo.add_face(Face::new(w2, vec![], FaceSurface::Cone(cone2.clone())))
};
let result = cone_cone_coaxial_chamfer(&cone1, &cone2, &spine, &topo, d, d, face1, face2)
.unwrap()
.expect("mixed coaxial cone-cone chamfer should produce a stripe");
let (t1_start, _) = result.stripe.contact1.domain();
let c1_point = result.stripe.contact1.evaluate(t1_start);
let (t2_start, _) = result.stripe.contact2.domain();
let c2_point = result.stripe.contact2.evaluate(t2_start);
let s1_signed = if reverse_s1 { -1.0_f64 } else { 1.0_f64 };
let s2_signed = if reverse_s2 { -1.0_f64 } else { 1.0_f64 };
let pred_c1_r = r_spine - s1_signed * d * beta1.cos();
let pred_c1_z = z_spine - s1_signed * d * beta1.sin();
let pred_c2_r = r_spine + s2_signed * d * beta2.cos();
let pred_c2_z = z_spine + s2_signed * d * beta2.sin();
let c1_radial = (c1_point.x().powi(2) + c1_point.y().powi(2)).sqrt();
let c2_radial = (c2_point.x().powi(2) + c2_point.y().powi(2)).sqrt();
assert!(
(c1_radial - pred_c1_r).abs() < 1e-9 && (c1_point.z() - pred_c1_z).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): contact1 should be at (r={pred_c1_r}, z={pred_c1_z}); \
got (r={c1_radial}, z={})",
c1_point.z()
);
assert!(
(c2_radial - pred_c2_r).abs() < 1e-9 && (c2_point.z() - pred_c2_z).abs() < 1e-9,
"({reverse_s1}, {reverse_s2}): contact2 should be at (r={pred_c2_r}, z={pred_c2_z}); \
got (r={c2_radial}, z={})",
c2_point.z()
);
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(ref c) => c,
ref other => panic!(
"({reverse_s1}, {reverse_s2}): expected Cone, got {}",
other.type_tag()
),
};
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) < -1.0 + 1e-12,
"({reverse_s1}, {reverse_s2}): chamfer cone axis should be −z (mixed = apex above), got {axis:?}"
);
let (u_p, v_p) = ParametricSurface::project_point(chamfer_cone, c1_point);
let on_cone_p1 = ParametricSurface::evaluate(chamfer_cone, u_p, v_p);
assert!(
(on_cone_p1 - c1_point).length() < 1e-9,
"({reverse_s1}, {reverse_s2}): contact1 must lie on chamfer cone"
);
let (u_q, v_q) = ParametricSurface::project_point(chamfer_cone, c2_point);
let on_cone_p2 = ParametricSurface::evaluate(chamfer_cone, u_q, v_q);
assert!(
(on_cone_p2 - c2_point).length() < 1e-9,
"({reverse_s1}, {reverse_s2}): contact2 must lie on chamfer cone"
);
};
run_case(false, true); run_case(true, false); }
#[test]
fn sphere_cylinder_fillet_both_concave_emits_smaller_torus() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::{CylindricalSurface, SphericalSurface};
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let big_r_s: f64 = 3.0;
let r_c: f64 = 2.0;
let r_fillet: f64 = 0.4;
let h_s = (big_r_s * big_r_s - r_c * r_c).sqrt();
let sph = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), big_r_s).unwrap();
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let spine_circle =
Circle3D::new(Point3::new(0.0, 0.0, h_s), Vec3::new(0.0, 0.0, 1.0), r_c).unwrap();
let v = topo.add_vertex(Vertex::new(Point3::new(r_c, 0.0, h_s), 1e-7));
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(spine_circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_sphere = topo.add_face(Face::new_reversed(
w1,
vec![],
FaceSurface::Sphere(sph.clone()),
));
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cyl = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cylinder(cyl.clone()),
));
let result = sphere_cylinder_fillet(&sph, &cyl, &spine, &topo, r_fillet, face_sphere, face_cyl)
.unwrap()
.expect("both-concave sphere-cylinder fillet should produce a stripe");
let torus = match result.stripe.surface {
FaceSurface::Torus(t) => t,
other => panic!("expected Torus, got {}", other.type_tag()),
};
let q_sph = big_r_s - r_fillet;
let q_cyl = r_c - r_fillet;
let expected_a_ball = (q_sph * q_sph - q_cyl * q_cyl).sqrt();
let expected_major = q_cyl;
assert!(
(torus.major_radius() - expected_major).abs() < 1e-12,
"concave major should be Q_cyl = {expected_major}, got {}",
torus.major_radius()
);
assert!(
torus.major_radius() < r_c,
"concave major ({}) must be < r_c = {r_c} (vs convex which would be > r_c)",
torus.major_radius()
);
assert!(
(torus.minor_radius() - r_fillet).abs() < 1e-12,
"minor should equal r_fillet = {r_fillet}, got {}",
torus.minor_radius()
);
let center = torus.center();
assert!(
center.x().abs() < 1e-12 && center.y().abs() < 1e-12,
"torus center on z-axis, got {center:?}"
);
assert!(
(center.z() - expected_a_ball).abs() < 1e-12,
"torus center z should be a_ball = {expected_a_ball}, got {}",
center.z()
);
let actual_dist_to_sphere_center =
(center.x().powi(2) + center.y().powi(2) + center.z().powi(2)).sqrt();
assert!(
actual_dist_to_sphere_center < big_r_s - 1e-9,
"concave: ball must be INSIDE sphere (distance {actual_dist_to_sphere_center} < R_s = {big_r_s})"
);
let actual_dist_to_cyl_axis = (center.x().powi(2) + center.y().powi(2)).sqrt();
assert!(
actual_dist_to_cyl_axis < r_c - 1e-9,
"concave: ball must be INSIDE cyl (distance {actual_dist_to_cyl_axis} < r_c = {r_c})"
);
let sph_axial = big_r_s * expected_a_ball / q_sph;
let sph_radial = big_r_s * expected_major / q_sph;
let want_sph = Point3::new(sph_radial, 0.0, sph_axial);
let dist_sph = (want_sph - Point3::new(0.0, 0.0, 0.0)).length();
assert!(
(dist_sph - big_r_s).abs() < 1e-9,
"sphere contact must lie on sphere: {dist_sph} vs R_s = {big_r_s}"
);
let want_cyl = Point3::new(r_c, 0.0, expected_a_ball);
let cyl_radial = (want_cyl.x().powi(2) + want_cyl.y().powi(2)).sqrt();
assert!(
(cyl_radial - r_c).abs() < 1e-9,
"cyl contact must have radial r_c: got {cyl_radial}, want {r_c}"
);
let (u_p, v_p) = ParametricSurface::project_point(&torus, want_sph);
let on_torus_sph = ParametricSurface::evaluate(&torus, u_p, v_p);
let (u_q, v_q) = ParametricSurface::project_point(&torus, want_cyl);
let on_torus_cyl = ParametricSurface::evaluate(&torus, u_q, v_q);
assert!(
(on_torus_sph - want_sph).length() < 1e-9,
"sphere contact on torus: {on_torus_sph:?} vs {want_sph:?}"
);
assert!(
(on_torus_cyl - want_cyl).length() < 1e-9,
"cyl contact on torus: {on_torus_cyl:?} vs {want_cyl:?}"
);
}
#[test]
fn plane_cone_chamfer_concave_emits_chamfer_cone() {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::ConicalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut topo = Topology::new();
let alpha: f64 = std::f64::consts::FRAC_PI_4;
let h: f64 = 4.0;
let r_p: f64 = h * (alpha.cos() / alpha.sin());
let d: f64 = 1.0;
let v = topo.add_vertex(Vertex::new(Point3::new(r_p, 0.0, 0.0), 1e-7));
let circle = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r_p).unwrap();
let eid = topo.add_edge(Edge::new(v, v, EdgeCurve::Circle(circle)));
let spine = Spine::from_single_edge(&topo, eid).unwrap();
let w1 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap());
let face_plate = topo.add_face(Face::new(
w1,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let cone_surf =
ConicalSurface::new(Point3::new(0.0, 0.0, h), Vec3::new(0.0, 0.0, -1.0), alpha).unwrap();
let w2 = topo.add_wire(Wire::new(vec![OrientedEdge::new(eid, false)], true).unwrap());
let face_cone = topo.add_face(Face::new_reversed(
w2,
vec![],
FaceSurface::Cone(cone_surf.clone()),
));
let n_p_inward = Vec3::new(0.0, 0.0, 1.0);
let result = plane_cone_chamfer(
n_p_inward, 0.0, &cone_surf, &spine, &topo, d, d, face_plate, face_cone,
)
.unwrap()
.expect("concave plane-cone chamfer should produce a stripe");
let chamfer_cone = match result.stripe.surface {
FaceSurface::Cone(c) => c,
other => panic!("expected Cone, got {}", other.type_tag()),
};
let expected_beta = std::f64::consts::FRAC_PI_2 - alpha * 0.5;
assert!(
(chamfer_cone.half_angle() - expected_beta).abs() < 1e-12,
"chamfer cone half-angle should be 3π/8 for α=π/4, d1=d2; got {}",
chamfer_cone.half_angle()
);
let apex = chamfer_cone.apex();
let dz = alpha.sin();
let dr = 1.0 - alpha.cos();
let expected_apex_z = -(r_p + d) * dz / dr;
assert!(
(apex.x()).abs() < 1e-12 && (apex.y()).abs() < 1e-12,
"apex should lie on z-axis, got {apex:?}"
);
assert!(
(apex.z() - expected_apex_z).abs() < 1e-9,
"apex z = {}, expected {}",
apex.z(),
expected_apex_z
);
let axis = chamfer_cone.axis();
assert!(
axis.dot(Vec3::new(0.0, 0.0, 1.0)) > 1.0 - 1e-12,
"chamfer cone axis should be +z, got {axis:?}"
);
let want_plate = Point3::new(r_p + d, 0.0, 0.0);
let cone_contact_axial = -d * alpha.sin();
let cone_contact_radial = r_p + d * alpha.cos();
let want_cone = Point3::new(cone_contact_radial, 0.0, cone_contact_axial);
let (u_p, v_p) = ParametricSurface::project_point(&chamfer_cone, want_plate);
let on_surf_plate = ParametricSurface::evaluate(&chamfer_cone, u_p, v_p);
let (u_c, v_c) = ParametricSurface::project_point(&chamfer_cone, want_cone);
let on_surf_cone = ParametricSurface::evaluate(&chamfer_cone, u_c, v_c);
assert!(
(on_surf_plate - want_plate).length() < 1e-9,
"plate contact must lie on chamfer cone: project→eval gave {on_surf_plate:?}, want {want_plate:?}"
);
assert!(
(on_surf_cone - want_cone).length() < 1e-9,
"cone-side contact must lie on chamfer cone: project→eval gave {on_surf_cone:?}, want {want_cone:?}"
);
}