#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::f64::consts::TAU;
use brepkit_math::analytic_intersection::{
AnalyticSurface, ExactIntersectionCurve, exact_plane_analytic, intersect_analytic_analytic,
intersect_plane_cone, intersect_plane_sphere, intersect_plane_torus,
};
use brepkit_math::nurbs::curve::NurbsCurve;
use brepkit_math::nurbs::intersection::{
intersect_curve_surface, intersect_line_nurbs, intersect_nurbs_nurbs, intersect_plane_nurbs,
};
use brepkit_math::nurbs::surface::NurbsSurface;
use brepkit_math::surfaces::{
ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface,
};
use brepkit_math::vec::{Point3, Vec3};
fn bilinear_patch(p00: Point3, p01: Point3, p10: Point3, p11: Point3) -> NurbsSurface {
NurbsSurface::new(
1,
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0],
vec![vec![p00, p01], vec![p10, p11]],
vec![vec![1.0, 1.0], vec![1.0, 1.0]],
)
.unwrap()
}
fn line_curve(a: Point3, b: Point3) -> NurbsCurve {
NurbsCurve::new(1, vec![0.0, 0.0, 1.0, 1.0], vec![a, b], vec![1.0, 1.0]).unwrap()
}
fn assert_on_sphere(pt: Point3, center: Point3, radius: f64, tol: f64) {
let dist = (pt - center).length();
assert!(
(dist - radius).abs() < tol,
"point {pt:?} is at distance {dist} from center, expected radius {radius}"
);
}
fn assert_on_cylinder(pt: Point3, origin: Point3, axis: Vec3, radius: f64, tol: f64) {
let v = pt - origin;
let axial = Vec3::new(v.x(), v.y(), v.z());
let along = axis.dot(axial);
let radial_vec = axial - axis * along;
let radial_dist = radial_vec.length();
assert!(
(radial_dist - radius).abs() < tol,
"point {pt:?} radial distance {radial_dist}, expected {radius}"
);
}
#[test]
fn plane_cylinder_perpendicular_gives_circle() {
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let results = exact_plane_analytic(
AnalyticSurface::Cylinder(&cyl),
Vec3::new(0.0, 0.0, 1.0),
5.0,
)
.unwrap();
assert_eq!(
results.len(),
1,
"should get exactly one intersection curve"
);
match &results[0] {
ExactIntersectionCurve::Circle(c) => {
assert!(
(c.radius() - 1.0).abs() < 1e-10,
"circle radius should be 1"
);
assert!(
(c.center().z() - 5.0).abs() < 1e-10,
"circle center should be at z=5"
);
}
other => panic!("expected Circle, got {other:?}"),
}
}
#[test]
fn plane_cylinder_oblique_gives_ellipse() {
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let angle = std::f64::consts::FRAC_PI_4;
let sin45 = angle.sin();
let cos45 = angle.cos();
let normal = Vec3::new(sin45, 0.0, cos45);
let results = exact_plane_analytic(AnalyticSurface::Cylinder(&cyl), normal, 0.0).unwrap();
assert_eq!(
results.len(),
1,
"should get exactly one intersection curve"
);
match &results[0] {
ExactIntersectionCurve::Ellipse(e) => {
let expected_major = 2.0 / cos45;
assert!(
(e.semi_major() - expected_major).abs() < 1e-6,
"semi-major should be r/cos(45)={expected_major}, got {}",
e.semi_major()
);
assert!(
(e.semi_minor() - 2.0).abs() < 1e-6,
"semi-minor should be radius=2, got {}",
e.semi_minor()
);
}
other => panic!("expected Ellipse, got {other:?}"),
}
}
#[test]
fn plane_sphere_latitude_circle() {
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
let results = exact_plane_analytic(
AnalyticSurface::Sphere(&sphere),
Vec3::new(0.0, 0.0, 1.0),
0.5,
)
.unwrap();
assert_eq!(results.len(), 1, "should get one intersection");
match &results[0] {
ExactIntersectionCurve::Circle(c) => {
let expected_r = (1.0_f64 - 0.25).sqrt();
assert!(
(c.radius() - expected_r).abs() < 1e-10,
"circle radius should be sqrt(0.75)={expected_r}, got {}",
c.radius()
);
assert!(
(c.center().z() - 0.5).abs() < 1e-10,
"circle center z should be 0.5"
);
}
other => panic!("expected Circle, got {other:?}"),
}
}
#[test]
fn plane_cone_intersection() {
let cone = ConicalSurface::new(
Point3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
std::f64::consts::FRAC_PI_4,
)
.unwrap();
let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
assert!(!curves.is_empty(), "should find intersection with cone");
for curve in &curves {
for pt in &curve.points {
assert!(
(pt.point.z() - 1.0).abs() < 0.1,
"intersection point z={} should be near 1.0",
pt.point.z()
);
}
}
}
#[test]
fn cylinder_cylinder_perpendicular() {
let cyl_z =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let cyl_x =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0).unwrap();
let curves = intersect_analytic_analytic(
AnalyticSurface::Cylinder(&cyl_z),
AnalyticSurface::Cylinder(&cyl_x),
20,
)
.unwrap();
assert!(
!curves.is_empty(),
"perpendicular equal cylinders should intersect"
);
let tol = 1e-4;
for curve in &curves {
for pt in &curve.points {
assert_on_cylinder(
pt.point,
Point3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
1.0,
tol,
);
assert_on_cylinder(
pt.point,
Point3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
1.0,
tol,
);
}
}
}
#[test]
fn sphere_cylinder_intersection_on_both_surfaces() {
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
let cyl =
CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let curves = intersect_analytic_analytic(
AnalyticSurface::Sphere(&sphere),
AnalyticSurface::Cylinder(&cyl),
20,
)
.unwrap();
assert!(
!curves.is_empty(),
"sphere R=2 and cylinder r=1 must intersect"
);
let tol = 0.5;
for curve in &curves {
for pt in &curve.points {
assert_on_sphere(pt.point, Point3::new(0.0, 0.0, 0.0), 2.0, tol);
assert_on_cylinder(
pt.point,
Point3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
1.0,
tol,
);
}
}
}
#[test]
fn plane_sphere_tangent() {
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
let results = exact_plane_analytic(
AnalyticSurface::Sphere(&sphere),
Vec3::new(0.0, 0.0, 1.0),
1.0,
)
.unwrap();
if !results.is_empty()
&& let ExactIntersectionCurve::Circle(c) = &results[0]
{
assert!(
c.radius() < 1e-6,
"tangent circle radius should be ~0, got {}",
c.radius()
);
}
}
#[test]
fn plane_sphere_no_intersection() {
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
let results = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
assert!(
results.is_empty(),
"plane at z=2 should not intersect unit sphere, got {} curves",
results.len()
);
}
#[test]
fn plane_torus_equatorial_two_circles() {
let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
assert!(
!curves.is_empty(),
"equatorial plane through torus should produce at least 1 curve"
);
let mut has_inner = false;
let mut has_outer = false;
for curve in &curves {
for pt in &curve.points {
let r = (pt.point.x() * pt.point.x() + pt.point.y() * pt.point.y()).sqrt();
if (r - 4.0).abs() < 0.5 {
has_inner = true;
}
if (r - 6.0).abs() < 0.5 {
has_outer = true;
}
}
}
let radii: Vec<f64> = curves
.iter()
.flat_map(|c| c.points.iter())
.map(|pt| (pt.point.x() * pt.point.x() + pt.point.y() * pt.point.y()).sqrt())
.collect();
assert!(
has_inner,
"should have points near inner circle R-r=4, radii={radii:?}"
);
assert!(
has_outer,
"should have points near outer circle R+r=6, radii={radii:?}"
);
}
#[test]
fn two_planar_nurbs_patches_intersection_line() {
let patch_a = bilinear_patch(
Point3::new(-2.0, -2.0, 0.0),
Point3::new(-2.0, 2.0, 0.0),
Point3::new(2.0, -2.0, 0.0),
Point3::new(2.0, 2.0, 0.0),
);
let patch_b = bilinear_patch(
Point3::new(-2.0, 0.0, -2.0),
Point3::new(-2.0, 0.0, 2.0),
Point3::new(2.0, 0.0, -2.0),
Point3::new(2.0, 0.0, 2.0),
);
let curves = intersect_nurbs_nurbs(&patch_a, &patch_b, 20, 0.0).unwrap();
assert_eq!(
curves.len(),
1,
"two perpendicular planes should intersect in one line, got {}",
curves.len()
);
let tol = 1e-4;
for pt in &curves[0].points {
assert!(
pt.point.y().abs() < tol,
"intersection point y={} should be ~0",
pt.point.y()
);
assert!(
pt.point.z().abs() < tol,
"intersection point z={} should be ~0",
pt.point.z()
);
}
}
#[test]
fn plane_nurbs_horizontal_cut() {
let patch = NurbsSurface::new(
2,
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
vec![
Point3::new(-1.0, -1.0, 1.0),
Point3::new(-1.0, 0.0, 0.0),
Point3::new(-1.0, 1.0, -1.0),
],
vec![
Point3::new(0.0, -1.0, 0.0),
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
],
vec![
Point3::new(1.0, -1.0, -1.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 1.0),
],
],
vec![
vec![1.0, 1.0, 1.0],
vec![1.0, 1.0, 1.0],
vec![1.0, 1.0, 1.0],
],
)
.unwrap();
let curves = intersect_plane_nurbs(&patch, Vec3::new(0.0, 0.0, 1.0), 0.0, 30).unwrap();
assert!(
!curves.is_empty(),
"z=0 plane should intersect the saddle surface"
);
let tol = 1e-3;
for curve in &curves {
for pt in &curve.points {
assert!(
pt.point.z().abs() < tol,
"intersection point z={} should be ~0",
pt.point.z()
);
}
}
}
#[test]
fn line_through_nurbs_sphere_two_hits() {
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
let n = 5;
let mut cps = Vec::new();
let mut ws = Vec::new();
#[allow(clippy::cast_precision_loss)]
for i in 0..n {
let mut row = Vec::new();
let mut wrow = Vec::new();
let u = TAU * (i as f64) / ((n - 1) as f64);
for j in 0..n {
let v =
-std::f64::consts::FRAC_PI_2 + std::f64::consts::PI * (j as f64) / ((n - 1) as f64);
row.push(sphere.evaluate(u, v));
wrow.push(1.0);
}
cps.push(row);
ws.push(wrow);
}
let knots_u = vec![0.0, 0.0, 0.0, 0.25, 0.75, 1.0, 1.0, 1.0];
let knots_v = vec![0.0, 0.0, 0.0, 0.25, 0.75, 1.0, 1.0, 1.0];
let surface = NurbsSurface::new(2, 2, knots_u, knots_v, cps, ws).unwrap();
let hits = intersect_line_nurbs(
&surface,
Point3::new(-3.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
30,
)
.unwrap();
assert!(
!hits.is_empty(),
"line along X axis should intersect sphere-like NURBS surface"
);
}
#[test]
fn line_curve_vs_planar_patch_one_hit() {
let patch = bilinear_patch(
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
);
let curve = line_curve(Point3::new(0.5, 0.5, -1.0), Point3::new(0.5, 0.5, 1.0));
let hits = intersect_curve_surface(&curve, &patch, 1e-7).unwrap();
assert_eq!(
hits.len(),
1,
"should find exactly one hit, got {}",
hits.len()
);
assert!(
hits[0].point.z().abs() < 1e-4,
"hit z={} should be ~0",
hits[0].point.z()
);
assert!(
(hits[0].point.x() - 0.5).abs() < 1e-4,
"hit x={} should be ~0.5",
hits[0].point.x()
);
}
#[test]
fn line_tangent_to_sphere_nurbs() {
let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
let n = 5;
let mut cps = Vec::new();
let mut ws = Vec::new();
#[allow(clippy::cast_precision_loss)]
for i in 0..n {
let mut row = Vec::new();
let mut wrow = Vec::new();
let u = TAU * (i as f64) / ((n - 1) as f64);
for j in 0..n {
let v =
-std::f64::consts::FRAC_PI_2 + std::f64::consts::PI * (j as f64) / ((n - 1) as f64);
row.push(sphere.evaluate(u, v));
wrow.push(1.0);
}
cps.push(row);
ws.push(wrow);
}
let knots_u = vec![0.0, 0.0, 0.0, 0.25, 0.75, 1.0, 1.0, 1.0];
let knots_v = vec![0.0, 0.0, 0.0, 0.25, 0.75, 1.0, 1.0, 1.0];
let surface = NurbsSurface::new(2, 2, knots_u, knots_v, cps, ws).unwrap();
let hits = intersect_line_nurbs(
&surface,
Point3::new(1.0, 0.0, -2.0),
Vec3::new(0.0, 0.0, 1.0),
30,
)
.unwrap();
assert!(
hits.len() <= 1,
"tangent line should produce 0 or 1 hit, got {}",
hits.len()
);
}
#[test]
fn line_missing_patch_no_hits() {
let patch = bilinear_patch(
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
);
let hits = intersect_line_nurbs(
&patch,
Point3::new(5.0, 5.0, -1.0),
Vec3::new(0.0, 0.0, 1.0),
20,
)
.unwrap();
assert!(
hits.is_empty(),
"line far from patch should produce no hits, got {}",
hits.len()
);
}