#![allow(clippy::unwrap_used)]
use std::collections::HashMap;
use brepkit_math::nurbs::curve::NurbsCurve;
use brepkit_math::tolerance::Tolerance;
use brepkit_math::vec::Point3;
use brepkit_topology::Topology;
use brepkit_topology::face::FaceSurface;
use brepkit_topology::test_utils::make_unit_square_face;
use super::*;
fn straight_z_path(length: f64) -> NurbsCurve {
NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, length)],
vec![1.0, 1.0],
)
.unwrap()
}
fn quarter_circle_xz_path(radius: f64) -> NurbsCurve {
let w = std::f64::consts::FRAC_1_SQRT_2;
NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(radius, 0.0, 0.0),
Point3::new(radius, 0.0, radius),
],
vec![1.0, w, 1.0],
)
.unwrap()
}
fn make_square(topo: &mut Topology, size: f64) -> FaceId {
let hs = size / 2.0;
let t = 1e-7;
let v0 = topo.add_vertex(Vertex::new(Point3::new(-hs, -hs, 0.0), t));
let v1 = topo.add_vertex(Vertex::new(Point3::new(hs, -hs, 0.0), t));
let v2 = topo.add_vertex(Vertex::new(Point3::new(hs, hs, 0.0), t));
let v3 = topo.add_vertex(Vertex::new(Point3::new(-hs, hs, 0.0), t));
let e0 = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let e1 = topo.add_edge(Edge::new(v1, v2, EdgeCurve::Line));
let e2 = topo.add_edge(Edge::new(v2, v3, EdgeCurve::Line));
let e3 = topo.add_edge(Edge::new(v3, v0, EdgeCurve::Line));
let wire = Wire::new(
vec![
OrientedEdge::new(e0, true),
OrientedEdge::new(e1, true),
OrientedEdge::new(e2, true),
OrientedEdge::new(e3, true),
],
true,
)
.unwrap();
let wid = topo.add_wire(wire);
topo.add_face(Face::new(
wid,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
))
}
#[test]
fn multi_section_sweep_line_spine_tapered_volume() {
let mut topo = Topology::new();
let big = make_square(&mut topo, 10.0);
let small = make_square(&mut topo, 6.0);
let spine = straight_z_path(20.0);
let solid = multi_section_sweep(&mut topo, &spine, &[(big, 0.0), (small, 1.0)], true).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 720.0 && vol < 2000.0,
"expected tapered volume, got {vol}"
);
}
#[test]
fn multi_section_sweep_curved_spine_positive_volume() {
let mut topo = Topology::new();
let a = make_square(&mut topo, 4.0);
let b = make_square(&mut topo, 4.0);
let c = make_square(&mut topo, 4.0);
let spine = quarter_circle_xz_path(20.0);
let solid =
multi_section_sweep(&mut topo, &spine, &[(a, 0.0), (b, 0.5), (c, 1.0)], true).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0,
"curved-spine multi-section sweep volume, got {vol}"
);
}
#[test]
fn multi_section_sweep_rejects_single_section() {
let mut topo = Topology::new();
let only = make_square(&mut topo, 4.0);
let spine = straight_z_path(10.0);
assert!(multi_section_sweep(&mut topo, &spine, &[(only, 0.0)], true).is_err());
}
#[test]
fn multi_section_sweep_rejects_out_of_range_param() {
let mut topo = Topology::new();
let a = make_square(&mut topo, 4.0);
let b = make_square(&mut topo, 4.0);
let spine = straight_z_path(10.0);
assert!(multi_section_sweep(&mut topo, &spine, &[(a, 0.0), (b, 1.5)], true).is_err());
}
#[test]
fn multi_section_sweep_unsorted_params_match_sorted() {
let mut t1 = Topology::new();
let (a1, b1) = (make_square(&mut t1, 10.0), make_square(&mut t1, 4.0));
let s1 = multi_section_sweep(
&mut t1,
&straight_z_path(20.0),
&[(a1, 0.0), (b1, 1.0)],
true,
)
.unwrap();
let v_sorted = crate::measure::solid_volume(&t1, s1, 0.1).unwrap();
let mut t2 = Topology::new();
let (a2, b2) = (make_square(&mut t2, 10.0), make_square(&mut t2, 4.0));
let s2 = multi_section_sweep(
&mut t2,
&straight_z_path(20.0),
&[(b2, 1.0), (a2, 0.0)],
true,
)
.unwrap();
let v_unsorted = crate::measure::solid_volume(&t2, s2, 0.1).unwrap();
assert!(
(v_sorted - v_unsorted).abs() < 1e-6,
"{v_sorted} vs {v_unsorted}"
);
}
#[test]
fn profile_to_frame_matrix_is_proper_rotation() {
let mut topo = Topology::new();
let face = make_square(&mut topo, 4.0);
let tangent = Vec3::new(1.0, 1.0, 1.0).normalize().unwrap();
let up = orthogonalize(Vec3::new(0.0, 0.0, 1.0), tangent);
let frame = Frame {
origin: Point3::new(5.0, 6.0, 7.0),
tangent,
up,
right: tangent.cross(up),
};
let m = profile_to_frame_matrix(&topo, face, &frame).unwrap();
let r = &m.0;
let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
- r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
+ r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
assert!(
(det - 1.0).abs() < 1e-9,
"rotation det should be +1, got {det}"
);
}
fn make_rect(topo: &mut Topology, hx: f64, hy: f64) -> FaceId {
let t = 1e-7;
let v0 = topo.add_vertex(Vertex::new(Point3::new(-hx, -hy, 0.0), t));
let v1 = topo.add_vertex(Vertex::new(Point3::new(hx, -hy, 0.0), t));
let v2 = topo.add_vertex(Vertex::new(Point3::new(hx, hy, 0.0), t));
let v3 = topo.add_vertex(Vertex::new(Point3::new(-hx, hy, 0.0), t));
let e0 = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let e1 = topo.add_edge(Edge::new(v1, v2, EdgeCurve::Line));
let e2 = topo.add_edge(Edge::new(v2, v3, EdgeCurve::Line));
let e3 = topo.add_edge(Edge::new(v3, v0, EdgeCurve::Line));
let wire = Wire::new(
vec![
OrientedEdge::new(e0, true),
OrientedEdge::new(e1, true),
OrientedEdge::new(e2, true),
OrientedEdge::new(e3, true),
],
true,
)
.unwrap();
let wid = topo.add_wire(wire);
topo.add_face(Face::new(
wid,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
))
}
fn guide_line(x0: f64, y0: f64, x1: f64, y1: f64) -> NurbsCurve {
NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(x0, y0, 0.0), Point3::new(x1, y1, 10.0)],
vec![1.0, 1.0],
)
.unwrap()
}
#[test]
fn sweep_guided_produces_valid_solid() {
let mut topo = Topology::new();
let profile = make_square(&mut topo, 4.0);
let spine = straight_z_path(10.0);
let aux = guide_line(10.0, 0.0, 10.0, 0.0);
let solid = sweep_guided(&mut topo, profile, &spine, aux).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(vol > 0.0, "guided sweep volume, got {vol}");
}
#[test]
fn sweep_guided_rotating_aux_rolls_profile() {
let mut t_plain = Topology::new();
let p_plain = make_rect(&mut t_plain, 8.0, 1.0);
let s_plain = sweep_with_options(
&mut t_plain,
p_plain,
&straight_z_path(10.0),
&SweepOptions::default(),
)
.unwrap();
let bb_plain = crate::measure::solid_bounding_box(&t_plain, s_plain).unwrap();
let y_plain = bb_plain.max.y() - bb_plain.min.y();
let mut t_guided = Topology::new();
let p_guided = make_rect(&mut t_guided, 8.0, 1.0);
let aux = guide_line(30.0, 0.0, 0.0, 30.0);
let s_guided = sweep_guided(&mut t_guided, p_guided, &straight_z_path(10.0), aux).unwrap();
let bb_guided = crate::measure::solid_bounding_box(&t_guided, s_guided).unwrap();
let y_guided = bb_guided.max.y() - bb_guided.min.y();
assert!(
y_plain < 4.0,
"plain sweep keeps the rectangle flat in Y, got {y_plain}"
);
assert!(
y_guided > 8.0,
"the rotating guide should roll the wide axis into Y, got {y_guided}"
);
}
#[test]
fn sweep_guided_handles_guide_meeting_spine() {
let mut topo = Topology::new();
let profile = make_square(&mut topo, 3.0);
let spine = straight_z_path(10.0);
let aux = guide_line(0.0, 0.0, 10.0, 0.0);
let solid = sweep_guided(&mut topo, profile, &spine, aux).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0 && vol.is_finite(),
"guide-meets-spine should still yield a valid solid, got {vol}"
);
}
#[test]
fn sweep_circle_along_straight_line_is_exact_cylinder() {
use brepkit_math::vec::Vec3;
use brepkit_topology::builder::make_circle_edge;
use brepkit_topology::face::Face;
use brepkit_topology::wire::{OrientedEdge, Wire};
let tol = 1e-7;
let mut topo = Topology::new();
let circle = make_circle_edge(
&mut topo,
Point3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
2.0,
tol,
)
.unwrap();
let wid = topo.add_wire(Wire::new(vec![OrientedEdge::new(circle, true)], true).unwrap());
let profile = topo.add_face(Face::new(
wid,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let path = straight_z_path(20.0);
let solid = sweep(&mut topo, profile, &path).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.01).unwrap();
let expected = std::f64::consts::PI * 4.0 * 20.0;
assert!(
(vol - expected).abs() / expected < 1e-6,
"expected exact cylinder volume {expected}, got {vol}"
);
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid()
);
}
#[test]
fn sweep_square_along_line() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let path = straight_z_path(2.0);
let solid = sweep(&mut topo, face, &path).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
assert_eq!(shell.faces().len(), 6, "a straight square sweep is a box");
for &fid in shell.faces() {
let f = topo.face(fid).unwrap();
assert!(
matches!(f.surface(), FaceSurface::Plane { .. }),
"all box faces should be planar"
);
}
let vol = crate::measure::solid_volume(&topo, solid, 0.01).unwrap();
assert!(
(vol - 2.0).abs() < 1e-9,
"expected box volume 2.0, got {vol}"
);
}
#[test]
fn sweep_square_along_quarter_circle() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let path = quarter_circle_xz_path(5.0);
let solid = sweep(&mut topo, face, &path).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
let num_segs = (path.control_points().len() * 2).max(4);
let expected_faces = num_segs * 4 + 2;
assert_eq!(shell.faces().len(), expected_faces);
let mut edge_counts: HashMap<usize, usize> = HashMap::new();
for &fid in shell.faces() {
let f = topo.face(fid).unwrap();
let wire = topo.wire(f.outer_wire()).unwrap();
for oe in wire.edges() {
*edge_counts.entry(oe.edge().index()).or_insert(0) += 1;
}
}
for (&edge_idx, &count) in &edge_counts {
assert_eq!(
count, 2,
"edge {edge_idx} shared by {count} faces, expected 2"
);
}
}
#[test]
fn sweep_insufficient_control_points_error() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let path = NurbsCurve::new(
0,
vec![0.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0)],
vec![1.0],
)
.unwrap();
let result = sweep(&mut topo, face, &path);
assert!(result.is_err());
}
#[test]
fn sweep_zero_path_error() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(1.0, 2.0, 3.0), Point3::new(1.0, 2.0, 3.0)],
vec![1.0, 1.0],
)
.unwrap();
let result = sweep(&mut topo, face, &path);
assert!(result.is_err());
}
#[test]
fn sweep_and_tessellate_roundtrip() {
use crate::tessellate::tessellate;
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let path = quarter_circle_xz_path(5.0);
let solid = sweep(&mut topo, face, &path).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
let tol = Tolerance::new();
for &fid in shell.faces() {
let mesh = tessellate(&topo, fid, 0.25).unwrap();
assert!(!mesh.positions.is_empty());
assert!(!mesh.indices.is_empty());
assert_eq!(mesh.positions.len(), mesh.normals.len());
for normal in &mesh.normals {
let len = normal.length();
assert!(
tol.approx_eq(len, 1.0) || tol.approx_eq(len, 0.0),
"normal length should be ~1.0, got {len}"
);
}
}
}
#[test]
fn sweep_with_default_options_matches_basic() {
let mut topo = Topology::new();
let face = crate::primitives::make_box(&mut topo, 0.5, 0.5, 0.01).unwrap();
let solid = topo.solid(face).unwrap();
let shell = topo.shell(solid.outer_shell()).unwrap();
let profile = shell.faces()[0];
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 5.0)],
vec![1.0, 1.0],
)
.unwrap();
let options = SweepOptions::default();
let result = sweep_with_options(&mut topo, profile, &path, &options);
assert!(result.is_ok());
}
#[test]
fn sweep_with_linear_scale() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 5.0)],
vec![1.0, 1.0],
)
.unwrap();
let options = SweepOptions {
scale_law: Some(Box::new(|t| 0.5f64.mul_add(-t, 1.0))), segments: 8,
..Default::default()
};
let result = sweep_with_options(&mut topo, profile, &path, &options).unwrap();
let vol = crate::measure::solid_volume(&topo, result, 0.5).unwrap();
assert!(vol > 0.0, "tapered sweep should have positive volume");
}
#[test]
fn sweep_fixed_contact_mode() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 5.0)],
vec![1.0, 1.0],
)
.unwrap();
let options = SweepOptions {
contact_mode: SweepContactMode::Fixed,
..Default::default()
};
let result = sweep_with_options(&mut topo, profile, &path, &options);
assert!(result.is_ok());
}
#[test]
fn sweep_constant_normal_mode() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 5.0)],
vec![1.0, 1.0],
)
.unwrap();
let options = SweepOptions {
contact_mode: SweepContactMode::ConstantNormal(Vec3::new(0.0, 1.0, 0.0)),
..Default::default()
};
let result = sweep_with_options(&mut topo, profile, &path, &options);
assert!(result.is_ok());
}
#[test]
fn sweep_smooth_produces_nurbs_sides() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = straight_z_path(2.0);
let solid = sweep_smooth(&mut topo, profile, &path).unwrap();
let s = topo.solid(solid).unwrap();
let sh = topo.shell(s.outer_shell()).unwrap();
let nurbs_count = sh
.faces()
.iter()
.filter(|&&fid| matches!(topo.face(fid).unwrap().surface(), FaceSurface::Nurbs(_)))
.count();
assert!(
nurbs_count > 0,
"smooth sweep should produce NURBS side faces"
);
let profile_edge_count = 4; let expected_face_count = profile_edge_count + 2; assert_eq!(
sh.faces().len(),
expected_face_count,
"smooth sweep should have {expected_face_count} faces, got {}",
sh.faces().len()
);
}
#[test]
fn sweep_smooth_positive_volume() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = straight_z_path(3.0);
let solid = sweep_smooth(&mut topo, profile, &path).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0,
"smooth sweep should have positive volume, got {vol}"
);
}
#[test]
fn sweep_smooth_curved_path() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = quarter_circle_xz_path(5.0);
let solid = sweep_smooth(&mut topo, profile, &path).unwrap();
let s = topo.solid(solid).unwrap();
let sh = topo.shell(s.outer_shell()).unwrap();
assert_eq!(
sh.faces().len(),
6,
"smooth curved sweep should have 6 faces"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 7.0 && vol < 8.5,
"curved smooth sweep volume should be ~7.85, got {vol}"
);
}
fn closed_circle_path(radius: f64) -> NurbsCurve {
let w = std::f64::consts::FRAC_1_SQRT_2;
let r = radius;
NurbsCurve::new(
2,
vec![
0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
],
vec![
Point3::new(r, 0.0, 0.0),
Point3::new(r, 0.0, r),
Point3::new(0.0, 0.0, r),
Point3::new(-r, 0.0, r),
Point3::new(-r, 0.0, 0.0),
Point3::new(-r, 0.0, -r),
Point3::new(0.0, 0.0, -r),
Point3::new(r, 0.0, -r),
Point3::new(r, 0.0, 0.0),
],
vec![1.0, w, 1.0, w, 1.0, w, 1.0, w, 1.0],
)
.unwrap()
}
#[test]
fn sweep_closed_circular_path() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = closed_circle_path(5.0);
let solid = sweep(&mut topo, profile, &path).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
let num_segs = (path.control_points().len() * 2).max(4);
let expected_faces = num_segs * 4; assert_eq!(
shell.faces().len(),
expected_faces,
"closed sweep should have {expected_faces} side faces (no caps)"
);
let mut edge_counts: HashMap<usize, usize> = HashMap::new();
for &fid in shell.faces() {
let f = topo.face(fid).unwrap();
let wire = topo.wire(f.outer_wire()).unwrap();
for oe in wire.edges() {
*edge_counts.entry(oe.edge().index()).or_insert(0) += 1;
}
}
for (&edge_idx, &count) in &edge_counts {
assert_eq!(
count, 2,
"edge {edge_idx} shared by {count} faces, expected 2 (manifold)"
);
}
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0,
"closed sweep should have positive volume, got {vol}"
);
}
fn make_square_face_with_hole(topo: &mut Topology) -> FaceId {
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::{Face, FaceSurface};
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let lin = Tolerance::new().linear;
let ov0 = topo.add_vertex(Vertex::new(Point3::new(-1.0, -1.0, 0.0), lin));
let ov1 = topo.add_vertex(Vertex::new(Point3::new(1.0, -1.0, 0.0), lin));
let ov2 = topo.add_vertex(Vertex::new(Point3::new(1.0, 1.0, 0.0), lin));
let ov3 = topo.add_vertex(Vertex::new(Point3::new(-1.0, 1.0, 0.0), lin));
let oe0 = topo.add_edge(Edge::new(ov0, ov1, EdgeCurve::Line));
let oe1 = topo.add_edge(Edge::new(ov1, ov2, EdgeCurve::Line));
let oe2 = topo.add_edge(Edge::new(ov2, ov3, EdgeCurve::Line));
let oe3 = topo.add_edge(Edge::new(ov3, ov0, EdgeCurve::Line));
let outer_wire = topo.add_wire(
Wire::new(
vec![
OrientedEdge::new(oe0, true),
OrientedEdge::new(oe1, true),
OrientedEdge::new(oe2, true),
OrientedEdge::new(oe3, true),
],
true,
)
.unwrap(),
);
let iv0 = topo.add_vertex(Vertex::new(Point3::new(-0.25, -0.25, 0.0), lin));
let iv1 = topo.add_vertex(Vertex::new(Point3::new(0.25, -0.25, 0.0), lin));
let iv2 = topo.add_vertex(Vertex::new(Point3::new(0.25, 0.25, 0.0), lin));
let iv3 = topo.add_vertex(Vertex::new(Point3::new(-0.25, 0.25, 0.0), lin));
let ie0 = topo.add_edge(Edge::new(iv0, iv1, EdgeCurve::Line));
let ie1 = topo.add_edge(Edge::new(iv1, iv2, EdgeCurve::Line));
let ie2 = topo.add_edge(Edge::new(iv2, iv3, EdgeCurve::Line));
let ie3 = topo.add_edge(Edge::new(iv3, iv0, EdgeCurve::Line));
let inner_wire = topo.add_wire(
Wire::new(
vec![
OrientedEdge::new(ie0, true),
OrientedEdge::new(ie1, true),
OrientedEdge::new(ie2, true),
OrientedEdge::new(ie3, true),
],
true,
)
.unwrap(),
);
topo.add_face(Face::new(
outer_wire,
vec![inner_wire],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
))
}
#[test]
fn sweep_closed_path_with_inner_hole() {
let mut topo = Topology::new();
let profile = make_square_face_with_hole(&mut topo);
let path = closed_circle_path(5.0);
let solid = sweep(&mut topo, profile, &path).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0,
"closed sweep with inner hole should have positive volume, got {vol}"
);
}
#[test]
fn sweep_smooth_closed_path() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = closed_circle_path(5.0);
let solid = sweep_smooth(&mut topo, profile, &path).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0,
"smooth closed sweep should have positive volume, got {vol}"
);
}
#[test]
fn sweep_cw_profile_produces_correct_solid() {
let path = straight_z_path(3.0);
crate::test_helpers::assert_cw_profile_produces_valid_solid(
|topo, face| sweep(topo, face, &path).unwrap(),
3.0,
0.05,
);
}
#[test]
fn sweep_cw_profile_translation_invariant() {
use brepkit_topology::test_utils::make_cw_unit_square_face;
let mut topo1 = Topology::new();
let face1 = make_cw_unit_square_face(&mut topo1);
let path1 = straight_z_path(3.0);
let solid1 = sweep(&mut topo1, face1, &path1).unwrap();
let vol1 = crate::measure::solid_volume(&topo1, solid1, 0.1).unwrap();
let mut topo2 = Topology::new();
let face2 = make_cw_unit_square_face(&mut topo2);
let path2 = straight_z_path(3.0);
let solid2 = sweep(&mut topo2, face2, &path2).unwrap();
crate::transform::transform_solid(
&mut topo2,
solid2,
&brepkit_math::mat::Mat4::translation(1000.0, 1000.0, 1000.0),
)
.unwrap();
let vol2 = crate::measure::solid_volume(&topo2, solid2, 0.1).unwrap();
let rel_err = (vol1 - vol2).abs() / vol1.max(1e-12);
assert!(
rel_err < 0.01,
"CW sweep volumes should match: origin={vol1}, translated={vol2}, \
rel_err={rel_err:.2e}"
);
}
#[test]
fn sweep_cw_profile_nonparallel_axis() {
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 tol_val = 1e-7;
let v0 = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), tol_val));
let v1 = topo.add_vertex(Vertex::new(Point3::new(0.0, 2.0, 0.0), tol_val));
let v2 = topo.add_vertex(Vertex::new(Point3::new(1.0, 2.0, 0.0), tol_val));
let v3 = topo.add_vertex(Vertex::new(Point3::new(1.0, 0.0, 0.0), tol_val));
let e0 = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let e1 = topo.add_edge(Edge::new(v1, v2, EdgeCurve::Line));
let e2 = topo.add_edge(Edge::new(v2, v3, EdgeCurve::Line));
let e3 = topo.add_edge(Edge::new(v3, v0, EdgeCurve::Line));
let wire = Wire::new(
vec![
OrientedEdge::new(e0, true),
OrientedEdge::new(e1, true),
OrientedEdge::new(e2, true),
OrientedEdge::new(e3, true),
],
true,
)
.unwrap();
let wid = topo.add_wire(wire);
let face = topo.add_face(Face::new(
wid,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, -1.0),
d: 0.0,
},
));
let path = straight_z_path(5.0);
let solid = sweep(&mut topo, face, &path).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
(vol - 10.0).abs() < 0.5,
"CW 1×2 rectangle swept along Z should produce volume ~10.0, got {vol}"
);
}
fn l_shaped_path() -> NurbsCurve {
NurbsCurve::new(
1,
vec![0.0, 0.0, 0.5, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 5.0),
Point3::new(5.0, 0.0, 5.0),
],
vec![1.0, 1.0, 1.0],
)
.unwrap()
}
#[test]
fn detect_kinks_l_shaped_path() {
let path = l_shaped_path();
let kinks = detect_kinks(&path);
assert_eq!(kinks.len(), 1, "L-shaped path should have one kink");
assert!((kinks[0] - 0.5).abs() < 1e-6, "kink should be at t=0.5");
}
#[test]
fn detect_kinks_no_kinks_for_smooth_path() {
let path = quarter_circle_xz_path(5.0);
let kinks = detect_kinks(&path);
assert!(kinks.is_empty(), "smooth path should have no kinks");
}
#[test]
fn detect_kinks_straight_line_no_kinks() {
let path = straight_z_path(5.0);
let kinks = detect_kinks(&path);
assert!(kinks.is_empty(), "straight line should have no kinks");
}
#[test]
fn detect_kinks_collinear_polyline_no_kinks() {
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 0.5, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(5.0, 0.0, 0.0),
Point3::new(10.0, 0.0, 0.0),
],
vec![1.0, 1.0, 1.0],
)
.unwrap();
let kinks = detect_kinks(&path);
assert!(kinks.is_empty(), "collinear polyline should have no kinks");
}
#[test]
fn sweep_miter_l_shaped_path() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = l_shaped_path();
let options = SweepOptions {
corner_mode: SweepCornerMode::Miter,
..Default::default()
};
let solid = sweep_with_options(&mut topo, profile, &path, &options).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0,
"miter sweep should have positive volume, got {vol}"
);
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
let mut edge_counts: HashMap<usize, usize> = HashMap::new();
for &fid in shell.faces() {
let f = topo.face(fid).unwrap();
let wire = topo.wire(f.outer_wire()).unwrap();
for oe in wire.edges() {
*edge_counts.entry(oe.edge().index()).or_insert(0) += 1;
}
}
for (&edge_idx, &count) in &edge_counts {
assert_eq!(
count, 2,
"edge {edge_idx} shared by {count} faces, expected 2 (manifold)"
);
}
}
#[test]
fn sweep_miter_l_shaped_volume_correct() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = l_shaped_path();
let options = SweepOptions {
corner_mode: SweepCornerMode::Miter,
..Default::default()
};
let solid = sweep_with_options(&mut topo, profile, &path, &options).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 5.0 && vol < 15.0,
"L-sweep volume should be roughly 10 (two 5-unit legs), got {vol}"
);
}
#[test]
fn sweep_miter_u_shaped_path() {
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 5.0),
Point3::new(5.0, 0.0, 5.0),
Point3::new(5.0, 0.0, 0.0),
],
vec![1.0, 1.0, 1.0, 1.0],
)
.unwrap();
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let options = SweepOptions {
corner_mode: SweepCornerMode::Miter,
..Default::default()
};
let solid = sweep_with_options(&mut topo, profile, &path, &options).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.0,
"U-shaped miter sweep should have positive volume, got {vol}"
);
}
#[test]
fn sweep_miter_fallback_smooth_on_no_kinks() {
let mut topo = Topology::new();
let profile = make_unit_square_face(&mut topo);
let path = straight_z_path(5.0);
let options = SweepOptions {
corner_mode: SweepCornerMode::Miter,
..Default::default()
};
let solid = sweep_with_options(&mut topo, profile, &path, &options).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
(vol - 5.0).abs() < 1.0,
"straight-path miter fallback should produce volume ~5.0, got {vol}"
);
}
fn sparse_rounded_rect(half_x: f64, half_y: f64, r: f64) -> Vec<Point3> {
let (cx, cy) = (half_x - r, half_y - r);
let hp = std::f64::consts::FRAC_PI_2;
let corners = [
(cx, -cy, -hp, 0.0),
(cx, cy, 0.0, hp),
(-cx, cy, hp, std::f64::consts::PI),
(-cx, -cy, std::f64::consts::PI, 3.0 * hp),
];
let mut pts: Vec<Point3> = Vec::new();
let push = |p: Point3, pts: &mut Vec<Point3>| {
if pts.last().is_none_or(|l: &Point3| (*l - p).length() > 1e-7) {
pts.push(p);
}
};
for &(ccx, ccy, a0, a1) in &corners {
for k in 0..=8 {
let a = a0 + (a1 - a0) * f64::from(k) / 8.0;
push(
Point3::new(ccx + r * a.cos(), ccy + r * a.sin(), 0.0),
&mut pts,
);
}
}
if let Some(first) = pts.first().copied() {
push(first, &mut pts);
}
pts
}
fn path_x_extent(path: &NurbsCurve) -> (f64, f64) {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for k in 0..=400 {
let x = path.evaluate(f64::from(k) / 400.0).x();
lo = lo.min(x);
hi = hi.max(x);
}
(lo, hi)
}
#[test]
fn densify_fixes_nonsquare_spine_overshoot() {
use brepkit_math::nurbs::fitting::interpolate;
let pts = sparse_rounded_rect(21.0, 63.0, 3.75);
let raw = interpolate(&pts, 3).unwrap();
let (_, raw_hi) = path_x_extent(&raw);
assert!(
raw_hi > 50.0,
"expected the un-densified fit to overshoot (got x_max {raw_hi}); \
if this no longer overshoots the regression guard is moot"
);
let dense = densify_path_points(&pts);
let fixed = interpolate(&dense, 3).unwrap();
let (fixed_lo, fixed_hi) = path_x_extent(&fixed);
assert!(
fixed_hi < 22.0 && fixed_lo > -22.0,
"densified fit must stay near the true ±21 bound, got x[{fixed_lo:.2},{fixed_hi:.2}]"
);
}
#[test]
fn densify_leaves_uniform_polyline_unchanged() {
let pts: Vec<Point3> = (0..10)
.map(|i| Point3::new(f64::from(i), 0.0, 0.0))
.collect();
assert_eq!(densify_path_points(&pts).len(), pts.len());
}
#[test]
fn densify_short_input_is_identity() {
let pts = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 0.0, 0.0)];
assert_eq!(densify_path_points(&pts), pts);
}
#[test]
fn sweep_planar_profile_caps_are_planar() {
let mut topo = Topology::new();
let profile = make_square(&mut topo, 2.0);
let path = NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 3.0),
Point3::new(2.0, 0.0, 5.0),
],
vec![1.0, 1.0, 1.0],
)
.unwrap();
let solid = sweep(&mut topo, profile, &path).unwrap();
let sh = topo
.shell(topo.solid(solid).unwrap().outer_shell())
.unwrap();
let planar = sh
.faces()
.iter()
.filter(|&&fid| topo.face(fid).unwrap().surface().is_planar())
.count();
assert_eq!(planar, sh.faces().len(), "planar sweep stays all-planar");
}
#[test]
fn sweep_nonplanar_saddle_profile_is_valid_solid() {
let mut topo = Topology::new();
let profile = crate::test_helpers::make_saddle_profile(&mut topo, 2.0);
assert!(
!topo.face(profile).unwrap().surface().is_planar(),
"saddle profile must be a non-planar face"
);
let path = straight_z_path(6.0);
let solid = sweep(&mut topo, profile, &path).unwrap();
let sh = topo
.shell(topo.solid(solid).unwrap().outer_shell())
.unwrap();
let nurbs_caps = sh
.faces()
.iter()
.filter(|&&fid| matches!(topo.face(fid).unwrap().surface(), FaceSurface::Nurbs(_)))
.count();
assert_eq!(
nurbs_caps, 2,
"non-planar ring caps are bilinear NURBS fills"
);
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid(),
"non-planar sweep must be a valid solid"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 85.0 && vol < 110.0,
"non-planar sweep volume out of expected range, got {vol}"
);
}
#[test]
fn sweep_smooth_straight_path_exact_volume() {
let mut topo = Topology::new();
let profile = make_square(&mut topo, 2.0);
let solid = sweep_smooth(&mut topo, profile, &straight_z_path(6.0)).unwrap();
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid(),
"smooth sweep must be a valid (manifold) solid"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.05).unwrap();
assert!(
(vol - 24.0).abs() / 24.0 < 0.01,
"straight smooth-sweep prism volume should be 24, got {vol}"
);
}
#[test]
fn sweep_smooth_gentle_curve_is_valid_with_sane_volume() {
let mut topo = Topology::new();
let profile = make_square(&mut topo, 2.0);
let path = NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 3.0),
Point3::new(1.5, 0.0, 6.0),
],
vec![1.0, 1.0, 1.0],
)
.unwrap();
let solid = sweep_smooth(&mut topo, profile, &path).unwrap();
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid(),
"curved smooth sweep must be a valid solid"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.05).unwrap();
assert!(
vol > 24.0 && vol < 26.0,
"gently curved smooth-sweep volume should be ~25, got {vol}"
);
}
#[test]
fn sweep_edge_on_profile_is_auto_oriented() {
let mut topo = Topology::new();
let profile = make_square(&mut topo, 1.0);
let path = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(5.0, 0.0, 0.0)],
vec![1.0, 1.0],
)
.unwrap();
let solid = sweep(&mut topo, profile, &path).unwrap();
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid(),
"edge-on sweep must be a valid solid"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.05).unwrap();
assert!(
(vol - 5.0).abs() / 5.0 < 0.02,
"edge-on profile should sweep to a 1×1×5 prism (volume 5), got {vol}"
);
}
#[test]
fn sweep_smooth_nonplanar_saddle_is_valid_solid() {
let mut topo = Topology::new();
let profile = crate::test_helpers::make_saddle_profile(&mut topo, 2.0);
assert!(!topo.face(profile).unwrap().surface().is_planar());
let solid = sweep_smooth(&mut topo, profile, &straight_z_path(6.0)).unwrap();
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid(),
"non-planar smooth sweep must be a valid solid"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
(vol - 96.0).abs() / 96.0 < 0.05,
"non-planar smooth sweep volume should be ~96, got {vol}"
);
}
#[test]
fn sweep_with_options_nonplanar_saddle_is_valid_solid() {
let mut topo = Topology::new();
let profile = crate::test_helpers::make_saddle_profile(&mut topo, 2.0);
let solid = sweep_with_options(
&mut topo,
profile,
&straight_z_path(6.0),
&SweepOptions::default(),
)
.unwrap();
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid(),
"non-planar sweep_with_options must be a valid solid"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
(vol - 96.0).abs() / 96.0 < 0.05,
"non-planar sweep_with_options volume should be ~96, got {vol}"
);
}
#[test]
fn multi_section_sweep_nonplanar_sections_is_valid_solid() {
let mut topo = Topology::new();
let a = crate::test_helpers::make_saddle_profile(&mut topo, 2.0);
let b = crate::test_helpers::make_saddle_profile(&mut topo, 2.0);
let solid = multi_section_sweep(
&mut topo,
&straight_z_path(6.0),
&[(a, 0.0), (b, 1.0)],
true,
)
.unwrap();
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid(),
"non-planar multi-section sweep must be a valid solid"
);
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 80.0 && vol < 110.0,
"non-planar multi-section sweep volume should be ~96, got {vol}"
);
}