use super::*;
use crate::projection::project_point_to_surface_general;
use crate::{
make_cone_surface, make_cylinder_surface, make_plane, make_sphere_surface,
make_torus_surface,
};
#[test]
fn general_revolution_projection_matches_newton() {
let generatrix = crate::make_line(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.5, 0.0, 4.0))
.unwrap();
let surface = make_revolution(
Vec3::default(),
Vec3::new(0.0, 0.0, 1.0),
&generatrix,
2.1,
)
.unwrap();
match surface.analytic() {
Some(AnalyticSurface::Revolution { sweep, .. }) => {
assert!((sweep - 2.1).abs() < 1e-9);
}
other => panic!(
"expected general Revolution recognition, got {:?}",
other.map(|analytic| format!("{analytic:?}"))
),
}
let probes = [
Vec3::new(2.5, 1.0, 1.0), Vec3::new(0.5, -2.0, 2.0), Vec3::new(-3.0, -0.5, 5.0), Vec3::new(0.01, 0.02, 2.0), Vec3::new(4.0, 2.0, 6.0), ];
for probe in probes {
let analytic = surface
.analytic()
.unwrap()
.project(&surface, probe)
.expect("analytic projection");
let general = project_point_to_surface_general(&surface, probe).unwrap();
assert!(
analytic.distance <= general.distance + 1e-7,
"probe {probe:?}: analytic {} vs newton {}",
analytic.distance,
general.distance
);
}
}
#[test]
fn skew_revolution_uses_general_projection() {
let axis_origin = Vec3::new(-0.012196157, -0.025629012, 0.0908);
let generatrix = NurbsCurve::new(
3,
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
[
(Vec3::new(-0.012196157, -0.022929012, 0.072001563), 1.0),
(
Vec3::new(-0.018454037, -0.022929012, 0.079800419),
0.853300733,
),
(
Vec3::new(-0.018454037, -0.022929012, 0.089799581),
0.853300733,
),
(Vec3::new(-0.012196157, -0.022929012, 0.097598437), 1.0),
]
.into_iter()
.map(|(point, weight)| crate::Vec4::from_point(point, weight))
.collect(),
)
.unwrap();
let surface = make_revolution(
axis_origin,
Vec3::new(0.0, 0.0, -1.0),
&generatrix,
std::f64::consts::TAU,
)
.unwrap();
let analytic = surface.analytic().expect("recognized revolution");
let AnalyticSurface::Revolution {
frame,
generatrix,
..
} = &analytic
else {
panic!("expected general revolution");
};
assert!(!generatrix_is_meridional_half_ray(generatrix, frame));
let point = surface.evaluate(0.37, 0.61).unwrap();
assert!(analytic.project(&surface, point).is_none());
let projection = crate::project_point_to_surface(&surface, point).unwrap();
assert!(projection.distance < 1e-8, "{}", projection.distance);
}
#[test]
fn skew_line_is_not_promoted_to_ruled_revolution() {
let generatrix =
crate::make_line(Vec3::new(2.0, 1.0, 0.0), Vec3::new(3.0, -1.0, 4.0)).unwrap();
let surface = make_revolution(
Vec3::default(),
Vec3::new(0.0, 0.0, 1.0),
&generatrix,
std::f64::consts::TAU,
)
.unwrap();
let analytic = surface.analytic().expect("recognized revolution");
let AnalyticSurface::Revolution {
frame,
generatrix,
..
} = &analytic
else {
panic!("skew line must remain a general revolution");
};
assert!(!generatrix_is_meridional_half_ray(generatrix, frame));
let point = surface.evaluate(0.42, 0.57).unwrap();
assert!(analytic.project(&surface, point).is_none());
assert!(crate::project_point_to_surface(&surface, point)
.unwrap()
.distance
< 1e-8);
let axial_plane = make_plane(
Vec3::new(-5.0, 0.0, -5.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
10.0,
10.0,
)
.unwrap();
assert!(intersect_analytic_pair(&axial_plane, &surface, 1e-8).is_none());
}
#[test]
fn opposite_half_ray_line_refuses_analytic_shortcuts() {
let generatrix =
crate::make_line(Vec3::new(2.0, 0.0, 0.0), Vec3::new(-3.0, 0.0, 4.0)).unwrap();
let surface = make_revolution(
Vec3::default(),
Vec3::new(0.0, 0.0, 1.0),
&generatrix,
std::f64::consts::TAU,
)
.unwrap();
let analytic = surface.analytic().expect("recognized revolution");
let AnalyticSurface::Revolution {
frame,
generatrix,
..
} = &analytic
else {
panic!("axis-crossing line must remain a general revolution");
};
assert!(!generatrix_is_meridional_half_ray(generatrix, frame));
let point = surface.evaluate(0.31, 0.82).unwrap();
assert!(analytic.project(&surface, point).is_none());
assert!(crate::project_point_to_surface(&surface, point)
.unwrap()
.distance
< 1e-8);
}
#[test]
fn meridional_revolution_keeps_analytic_projection() {
let generatrix =
crate::make_line(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 4.0)).unwrap();
let surface = make_revolution(
Vec3::default(),
Vec3::new(0.0, 0.0, 1.0),
&generatrix,
2.1,
)
.unwrap();
let analytic = surface.analytic().expect("recognized revolution");
let AnalyticSurface::Revolution {
frame,
generatrix,
..
} = &analytic
else {
panic!("expected general revolution");
};
assert!(generatrix_is_meridional_half_ray(generatrix, frame));
assert!(analytic
.project(&surface, Vec3::new(2.5, 1.0, 1.0))
.is_some());
}
#[test]
#[ignore = "slow: skew-revolution corpus import (also known-failing); run with cargo test -- --include-ignored"]
fn optional_skew_revolution_corpus_models_import() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
for relative in [
"test-data/cache/abc/files/abc_0000_step_v00/00006096/\
00006096_b6e075a7dd534de3be9c0300_step_012.step",
"test-data/cache/abc/files/abc_0002_step_v00/00027079/\
00027079_73fec378369d4476b0fe603a_step_012.step",
] {
let path = root.join(relative);
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let (solids, failed, error) = crate::import_step_report(&text)
.unwrap_or_else(|message| panic!("{}: {message}", path.display()));
assert_eq!(failed, 0, "{}: {error:?}", path.display());
assert_eq!(solids.len(), 1, "{}", path.display());
assert!(
solids[0].validate().is_empty(),
"{}: {:?}",
path.display(),
solids[0].validate()
);
}
}
fn assert_projection_parity(surface: &NurbsSurface, points: &[Vec3]) {
let analytic = recognize(surface).expect("surface should be recognized");
for &point in points {
let fast = analytic
.project(surface, point)
.expect("analytic projection");
let reference = project_point_to_surface_general(surface, point).unwrap();
assert!(
fast.distance <= reference.distance + 1e-7,
"analytic distance {} worse than Newton {} at {:?}",
fast.distance,
reference.distance,
point
);
let evaluated = surface.evaluate(fast.u, fast.v).unwrap();
assert!(
evaluated.sub(fast.point).length() <= 1e-12,
"returned point must come from the NURBS evaluator"
);
}
}
#[test]
fn recognizes_and_projects_cylinder() {
let surface = make_cylinder_surface(
Vec3::new(1.0, -2.0, 0.5),
Vec3::new(0.3, 0.1, 1.0),
2.0,
5.0,
)
.unwrap();
assert!(matches!(
recognize(&surface),
Some(AnalyticSurface::RuledRevolution { .. })
));
assert_projection_parity(
&surface,
&[
Vec3::new(4.0, 0.0, 2.0),
Vec3::new(1.0, -2.0, 9.0),
Vec3::new(0.9, -1.9, 0.6),
Vec3::new(-3.0, 4.0, -1.0),
Vec3::new(3.1, -2.0, 0.5),
],
);
}
#[test]
fn recognizes_and_projects_cone() {
let surface = make_cone_surface(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
3.0,
1.0,
4.0,
)
.unwrap();
assert!(matches!(
recognize(&surface),
Some(AnalyticSurface::RuledRevolution { .. })
));
assert_projection_parity(
&surface,
&[
Vec3::new(5.0, 1.0, 1.0),
Vec3::new(0.1, 0.1, 6.0),
Vec3::new(-2.0, -2.0, 2.0),
Vec3::new(0.0, 3.0, 0.0),
],
);
}
#[test]
fn recognizes_and_projects_sphere() {
let surface =
make_sphere_surface(Vec3::new(2.0, 1.0, -1.0), 1.5, Vec3::new(0.2, 1.0, 0.4)).unwrap();
assert!(matches!(
recognize(&surface),
Some(AnalyticSurface::Sphere { .. })
));
assert_projection_parity(
&surface,
&[
Vec3::new(4.0, 4.0, 0.0),
Vec3::new(2.0, 1.0, -1.1),
Vec3::new(2.1, 2.9, -0.5),
Vec3::new(0.0, 0.0, 0.0),
],
);
}
#[test]
fn recognizes_and_projects_torus() {
let surface =
make_torus_surface(Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 0.1, 1.0), 4.0, 1.0)
.unwrap();
assert!(matches!(
recognize(&surface),
Some(AnalyticSurface::Torus { .. })
));
assert_projection_parity(
&surface,
&[
Vec3::new(6.0, 0.0, 1.0),
Vec3::new(2.0, 2.0, 3.0),
Vec3::new(-4.5, 0.2, 0.4),
Vec3::new(0.5, -0.5, 1.2),
],
);
}
#[test]
fn recognizes_plane_and_rejects_freeform() {
let plane = make_plane(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
4.0,
3.0,
)
.unwrap();
assert!(matches!(
recognize(&plane),
Some(AnalyticSurface::Plane { .. })
));
assert_projection_parity(
&plane,
&[Vec3::new(1.0, 1.0, 2.0), Vec3::new(-3.0, 8.0, -1.0)],
);
let mut freeform = make_plane(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
4.0,
3.0,
)
.unwrap();
freeform.control_points[1][1].z += 0.5;
assert!(recognize(&freeform).is_none());
}
#[test]
fn kind_label_names_each_carrier() {
let plane = make_plane(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
4.0,
3.0,
)
.unwrap();
assert_eq!(recognize(&plane).unwrap().kind_label(), "Plane");
let cylinder = make_cylinder_surface(
Vec3::new(1.0, -2.0, 0.5),
Vec3::new(0.3, 0.1, 1.0),
2.0,
5.0,
)
.unwrap();
assert_eq!(recognize(&cylinder).unwrap().kind_label(), "Cylinder");
let cone = make_cone_surface(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
3.0,
1.0,
4.0,
)
.unwrap();
assert_eq!(recognize(&cone).unwrap().kind_label(), "Cone");
let sphere =
make_sphere_surface(Vec3::new(2.0, 1.0, -1.0), 1.5, Vec3::new(0.2, 1.0, 0.4)).unwrap();
assert_eq!(recognize(&sphere).unwrap().kind_label(), "Sphere");
let torus =
make_torus_surface(Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 0.1, 1.0), 4.0, 1.0).unwrap();
assert_eq!(recognize(&torus).unwrap().kind_label(), "Torus");
}
fn assert_curves_lie_on_both(
curves: &[NurbsCurve],
first: &NurbsSurface,
second: &NurbsSurface,
expected_count: usize,
) {
assert_eq!(curves.len(), expected_count);
for curve in curves {
let [start, end] = curve.domain().unwrap();
for i in 0..=32 {
let t = start + (end - start) * i as f64 / 32.0;
let point = curve.evaluate(t).unwrap();
for surface in [first, second] {
let distance = project_point_to_surface_general(surface, point)
.unwrap()
.distance;
assert!(
distance <= 5e-7 * (1.0 + point.length()),
"curve point {point:?} off surface by {distance}"
);
}
}
}
}
#[test]
fn plane_cylinder_sections_are_exact() {
let cylinder = make_cylinder_surface(
Vec3::new(0.0, 0.0, -3.0),
Vec3::new(0.0, 0.0, 1.0),
2.0,
6.0,
)
.unwrap();
let oblique = make_plane(
Vec3::new(-5.0, -5.0, -1.0),
Vec3::new(1.0, 0.0, 0.4),
Vec3::new(0.0, 1.0, 0.1),
10.0,
10.0,
)
.unwrap();
let ellipse = intersect_analytic_pair(&oblique, &cylinder, 1e-7).unwrap();
assert_curves_lie_on_both(&ellipse, &oblique, &cylinder, 1);
let parallel = make_plane(
Vec3::new(1.0, -5.0, -3.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
10.0,
6.0,
)
.unwrap();
let lines = intersect_analytic_pair(¶llel, &cylinder, 1e-7).unwrap();
assert_curves_lie_on_both(&lines, ¶llel, &cylinder, 2);
let outside = make_plane(
Vec3::new(3.0, -5.0, -3.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
10.0,
6.0,
)
.unwrap();
assert!(intersect_analytic_pair(&outside, &cylinder, 1e-7)
.unwrap()
.is_empty());
}
#[test]
fn plane_cone_section_is_exact() {
let cone = make_cone_surface(
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
3.0,
1.0,
4.0,
)
.unwrap();
let oblique = make_plane(
Vec3::new(-6.0, -6.0, 2.0),
Vec3::new(1.0, 0.0, 0.15),
Vec3::new(0.0, 1.0, -0.1),
14.0,
14.0,
)
.unwrap();
let conic = intersect_analytic_pair(&oblique, &cone, 1e-7).unwrap();
assert_curves_lie_on_both(&conic, &oblique, &cone, 1);
}
#[test]
fn plane_sphere_and_sphere_sphere_circles_are_exact() {
let sphere =
make_sphere_surface(Vec3::new(1.0, 2.0, 3.0), 2.0, Vec3::new(0.0, 0.3, 1.0)).unwrap();
let plane = make_plane(
Vec3::new(-4.0, -3.0, 3.2),
Vec3::new(1.0, 0.0, 0.2),
Vec3::new(0.0, 1.0, 0.1),
10.0,
10.0,
)
.unwrap();
let circle = intersect_analytic_pair(&plane, &sphere, 1e-7).unwrap();
assert_curves_lie_on_both(&circle, &plane, &sphere, 1);
let other =
make_sphere_surface(Vec3::new(3.5, 2.0, 3.0), 1.5, Vec3::new(1.0, 0.0, 0.0)).unwrap();
let lens = intersect_analytic_pair(&sphere, &other, 1e-7).unwrap();
assert_curves_lie_on_both(&lens, &sphere, &other, 1);
}
#[test]
fn perpendicular_plane_torus_yields_two_circles() {
let torus =
make_torus_surface(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 4.0, 1.0)
.unwrap();
let plane = make_plane(
Vec3::new(-6.0, -6.0, 0.5),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
12.0,
12.0,
)
.unwrap();
let circles = intersect_analytic_pair(&plane, &torus, 1e-7).unwrap();
assert_curves_lie_on_both(&circles, &plane, &torus, 2);
}
#[test]
fn angle_parameter_map_matches_surface_evaluation() {
let surface =
make_cylinder_surface(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0, 1.0)
.unwrap();
let analytic = recognize(&surface).unwrap();
let frame = analytic.frame().unwrap();
for i in 0..=64 {
let theta = std::f64::consts::TAU * i as f64 / 64.0;
let u = circle_angle_to_parameter(4, std::f64::consts::TAU, theta);
let expected = frame
.origin
.add(frame.x_axis.scale(2.0 * theta.cos()))
.add(frame.y_axis.scale(2.0 * theta.sin()));
let actual = surface.evaluate(u, 0.0).unwrap();
assert!(
actual.sub(expected).length() <= 1e-9,
"theta {theta}: {actual:?} != {expected:?}"
);
}
}
fn reflect_across_x_half() -> crate::AffineTransform {
crate::AffineTransform::new([
-1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
])
.unwrap()
}
#[test]
fn reflected_sphere_keeps_its_closed_form_sections() {
let sphere =
make_sphere_surface(Vec3::new(1.0, 2.0, 3.0), 2.0, Vec3::new(0.0, 0.3, 1.0)).unwrap();
let reflected =
crate::transform_topology::transform_surface(&sphere, reflect_across_x_half()).unwrap();
let center = Vec3::new(0.0, 2.0, 3.0);
assert!(
matches!(
recognize(&reflected),
Some(AnalyticSurface::Revolution { .. })
),
"a reflected sphere carries the general revolution representation, got {:?}",
recognize(&reflected).map(|analytic| analytic.kind_label())
);
let (got_center, got_radius) = reflected
.analytic()
.and_then(AnalyticSurface::sphere_geometry)
.expect("the reflected sphere is still a sphere");
assert!(
got_center.sub(center).length() <= 1e-9,
"centre {got_center:?}"
);
assert!((got_radius - 2.0).abs() <= 1e-9, "radius {got_radius}");
assert!(
sphere
.analytic()
.and_then(AnalyticSurface::torus_geometry)
.is_none()
&& reflected
.analytic()
.and_then(AnalyticSurface::torus_geometry)
.is_none(),
"a sphere is not a torus"
);
let kissing = make_plane(
Vec3::new(-5.0, -5.0, 5.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
10.0,
10.0,
)
.unwrap();
for (first, second) in [(&reflected, &kissing), (&kissing, &reflected)] {
let section = intersect_analytic_pair(first, second, 1e-7);
assert!(
section.as_ref().is_some_and(Vec::is_empty),
"a point contact is a proven-empty section, got {:?}",
section.map(|curves| curves.len())
);
}
let cutting = make_plane(
Vec3::new(-5.0, -5.0, 4.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
10.0,
10.0,
)
.unwrap();
let circle = intersect_analytic_pair(&reflected, &cutting, 1e-7).unwrap();
assert_curves_lie_on_both(&circle, &reflected, &cutting, 1);
let other =
make_sphere_surface(Vec3::new(2.5, 2.0, 3.0), 1.5, Vec3::new(1.0, 0.0, 0.0)).unwrap();
let lens = intersect_analytic_pair(&other, &reflected, 1e-7).unwrap();
assert_curves_lie_on_both(&lens, &other, &reflected, 1);
}
#[test]
fn reflected_torus_keeps_its_closed_form_sections() {
let torus =
make_torus_surface(Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 0.0, 1.0), 4.0, 1.0).unwrap();
let reflected =
crate::transform_topology::transform_surface(&torus, reflect_across_x_half()).unwrap();
assert!(
matches!(
recognize(&reflected),
Some(AnalyticSurface::Revolution { .. })
),
"a reflected torus carries the general revolution representation, got {:?}",
recognize(&reflected).map(|analytic| analytic.kind_label())
);
let (frame, major, minor) = reflected
.analytic()
.and_then(AnalyticSurface::torus_geometry)
.expect("the reflected torus is still a torus");
assert!(
frame.origin.sub(Vec3::new(1.0, 0.0, 1.0)).length() <= 1e-9,
"tube-centre origin {:?}",
frame.origin
);
assert!(
(frame.axis.z.abs() - 1.0).abs() <= 1e-12,
"axis {:?}",
frame.axis
);
assert!((major - 4.0).abs() <= 1e-9 && (minor - 1.0).abs() <= 1e-9);
assert!(
reflected
.analytic()
.and_then(AnalyticSurface::sphere_geometry)
.is_none(),
"a torus is not a sphere"
);
let plane_at = |z: f64| {
make_plane(
Vec3::new(-6.0, -6.0, z),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
14.0,
14.0,
)
.unwrap()
};
let circles = intersect_analytic_pair(&plane_at(1.5), &reflected, 1e-7).unwrap();
assert_curves_lie_on_both(&circles, &plane_at(1.5), &reflected, 2);
let above = intersect_analytic_pair(&reflected, &plane_at(2.5), 1e-7);
assert!(
above.as_ref().is_some_and(Vec::is_empty),
"a plane clear of the tube is a proven-empty section, got {:?}",
above.map(|curves| curves.len())
);
}