#![allow(clippy::unwrap_used, clippy::print_stderr)]
use std::collections::HashMap;
use brepkit_math::tolerance::Tolerance;
use brepkit_topology::Topology;
use brepkit_topology::face::FaceSurface;
use brepkit_topology::test_utils::{make_unit_square_face, make_unit_triangle_face};
use super::*;
use crate::test_helpers::assert_euler_genus0;
#[test]
fn extrude_square_creates_box() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
assert_eq!(shell.faces().len(), 6);
assert_eq!(topo.edges().len(), 12);
assert_eq!(topo.vertices().len(), 8);
}
#[test]
fn extrude_triangle_creates_prism() {
let mut topo = Topology::new();
let face = make_unit_triangle_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
assert_eq!(shell.faces().len(), 5);
assert_eq!(topo.edges().len(), 9);
assert_eq!(topo.vertices().len(), 6);
}
#[test]
fn extrude_rect_with_circular_hole_uses_exact_cylinder() {
use brepkit_math::vec::Point3;
use brepkit_topology::builder::{make_circle_edge, make_polygon_wire};
use brepkit_topology::face::Face;
use brepkit_topology::wire::{OrientedEdge, Wire};
let tol = 1e-7;
let mut topo = Topology::new();
let outer = make_polygon_wire(
&mut topo,
&[
Point3::new(0.0, 0.0, 0.0),
Point3::new(20.0, 0.0, 0.0),
Point3::new(20.0, 20.0, 0.0),
Point3::new(0.0, 20.0, 0.0),
],
tol,
)
.unwrap();
let hole = make_circle_edge(
&mut topo,
Point3::new(10.0, 10.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
3.0,
tol,
)
.unwrap();
let inner = topo.add_wire(Wire::new(vec![OrientedEdge::new(hole, false)], true).unwrap());
let face = topo.add_face(Face::new(
outer,
vec![inner],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 10.0).unwrap();
let shell = topo
.shell(topo.solid(solid).unwrap().outer_shell())
.unwrap();
let cyl_count = shell
.faces()
.iter()
.filter(|&&fid| matches!(topo.face(fid).unwrap().surface(), FaceSurface::Cylinder(_)))
.count();
assert_eq!(cyl_count, 1, "hole wall must be one exact cylinder face");
let vol = crate::measure::solid_volume(&topo, solid, 0.01).unwrap();
let expected = (400.0 - std::f64::consts::PI * 9.0) * 10.0;
assert!(
(vol - expected).abs() / expected < 1e-3,
"expected ~{expected}, got {vol}"
);
assert!(
crate::validate::validate_solid(&topo, solid)
.unwrap()
.is_valid()
);
}
#[test]
fn extrude_zero_direction_error() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let result = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 0.0), 1.0);
assert!(result.is_err());
}
#[test]
fn extrude_zero_distance_error() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let result = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 0.0);
assert!(result.is_err());
}
#[test]
fn extrude_orientation_correct() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let tol = Tolerance::new();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
let mut found_bottom = false;
let mut found_top = false;
for &fid in shell.faces() {
let f = topo.face(fid).unwrap();
if let FaceSurface::Plane { normal, .. } = f.surface() {
if tol.approx_eq(normal.z(), -1.0)
&& tol.approx_eq(normal.x(), 0.0)
&& tol.approx_eq(normal.y(), 0.0)
{
found_bottom = true;
}
if tol.approx_eq(normal.z(), 1.0)
&& tol.approx_eq(normal.x(), 0.0)
&& tol.approx_eq(normal.y(), 0.0)
{
found_top = true;
}
}
}
assert!(found_bottom, "bottom face should have -Z normal");
assert!(found_top, "top face should have +Z normal");
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"
);
}
}
fn make_nurbs_face(topo: &mut Topology) -> FaceId {
use brepkit_math::nurbs::surface::NurbsSurface;
let cps = 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.5), Point3::new(1.0, 1.0, 0.5)],
];
let weights = vec![vec![1.0, 1.0], vec![1.0, 1.0]];
let knots = vec![0.0, 0.0, 1.0, 1.0];
let surface = NurbsSurface::new(1, 1, knots.clone(), knots, cps, weights).unwrap();
let tol = 1e-7;
let v0 = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), tol));
let v1 = topo.add_vertex(Vertex::new(Point3::new(1.0, 0.0, 0.0), tol));
let v2 = topo.add_vertex(Vertex::new(Point3::new(1.0, 1.0, 0.5), tol));
let v3 = topo.add_vertex(Vertex::new(Point3::new(0.0, 1.0, 0.5), tol));
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::Nurbs(surface)))
}
#[test]
fn extrude_nurbs_face_creates_solid() {
let mut topo = Topology::new();
let face = make_nurbs_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let s = topo.solid(solid).unwrap();
let sh = topo.shell(s.outer_shell()).unwrap();
assert_eq!(
sh.faces().len(),
6,
"extruded NURBS face should have 6 faces"
);
}
#[test]
fn extrude_nurbs_face_top_is_nurbs() {
let mut topo = Topology::new();
let face = make_nurbs_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 2.0).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 >= 2,
"extruded NURBS face should have at least 2 NURBS caps, got {nurbs_count}"
);
}
#[test]
fn extrude_nurbs_face_top_translated() {
let mut topo = Topology::new();
let face = make_nurbs_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
let s = topo.solid(solid).unwrap();
let sh = topo.shell(s.outer_shell()).unwrap();
let mut top_z = f64::MIN;
for &fid in sh.faces() {
let f = topo.face(fid).unwrap();
if let FaceSurface::Nurbs(surface) = f.surface() {
let pt = surface.evaluate(0.0, 0.0);
if pt.z() > top_z {
top_z = pt.z();
}
}
}
assert!(
(top_z - 3.0).abs() < 1e-7,
"top NURBS surface should be at z≈3.0, got z={top_z}"
);
}
#[test]
fn extrude_nurbs_face_positive_volume() {
let mut topo = Topology::new();
let face = make_nurbs_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
assert!(
vol > 0.3 && vol < 1.5,
"extruded NURBS solid should have positive volume in (0.3, 1.5), got {vol}"
);
assert_euler_genus0(&topo, solid);
}
fn make_face_with_hole(topo: &mut Topology) -> FaceId {
let outer_pts = vec![
Point3::new(-1.0, -1.0, 0.0),
Point3::new(1.0, -1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(-1.0, 1.0, 0.0),
];
let outer_wire = brepkit_topology::builder::make_polygon_wire(topo, &outer_pts, 1e-7).unwrap();
let inner_pts = vec![
Point3::new(-0.25, -0.25, 0.0),
Point3::new(-0.25, 0.25, 0.0),
Point3::new(0.25, 0.25, 0.0),
Point3::new(0.25, -0.25, 0.0),
];
let inner_wire = brepkit_topology::builder::make_polygon_wire(topo, &inner_pts, 1e-7).unwrap();
let normal = Vec3::new(0.0, 0.0, 1.0);
let d = 0.0;
let face = Face::new(
outer_wire,
vec![inner_wire],
FaceSurface::Plane { normal, d },
);
topo.add_face(face)
}
#[test]
fn extrude_face_with_hole_produces_more_faces() {
let mut topo = Topology::new();
let face = make_face_with_hole(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
assert_eq!(
shell.faces().len(),
10,
"extruded face with hole should have 10 faces (6 outer + 4 inner)"
);
}
#[test]
fn extrude_face_with_hole_caps_have_inner_wires() {
let mut topo = Topology::new();
let face = make_face_with_hole(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let solid_data = topo.solid(solid).unwrap();
let shell = topo.shell(solid_data.outer_shell()).unwrap();
let faces_with_holes_count = shell
.faces()
.iter()
.filter(|&&fid| !topo.face(fid).unwrap().inner_wires().is_empty())
.count();
assert_eq!(
faces_with_holes_count, 2,
"bottom and top caps should both have inner wire holes"
);
}
#[test]
fn extrude_zero_distance_errors() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let result = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 0.0);
assert!(result.is_err(), "zero distance extrusion should error");
}
#[test]
fn extrude_face_with_hole_has_correct_volume() {
let mut topo_solid = Topology::new();
let solid_face = make_unit_square_face(&mut topo_solid);
let solid_box = extrude(&mut topo_solid, solid_face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let mut topo_hollow = Topology::new();
let hollow_face = make_face_with_hole(&mut topo_hollow);
let hollow_solid =
extrude(&mut topo_hollow, hollow_face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let vol_solid = crate::measure::solid_volume(&topo_solid, solid_box, 0.1).unwrap();
let vol_hollow = crate::measure::solid_volume(&topo_hollow, hollow_solid, 0.1).unwrap();
let rel_solid = (vol_solid - 1.0).abs() / 1.0;
assert!(
rel_solid < 1e-8,
"unit box volume should be 1.0, got {vol_solid} (rel_err={rel_solid:.2e})"
);
let expected_hollow = 3.75;
let rel_hollow = (vol_hollow - expected_hollow).abs() / expected_hollow;
assert!(
rel_hollow < 0.01,
"hollow extrusion volume should be {expected_hollow}, got {vol_hollow} \
(rel_err={rel_hollow:.2e}). If > 1%, inner-wire volume subtraction may be buggy."
);
}
#[test]
fn extrude_square_volume_exact() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
let rel_err = (vol - 5.0).abs() / 5.0;
assert!(
rel_err < 1e-8,
"extruded unit square by 5 should have volume 5.0, got {vol} (rel_err={rel_err:.2e})"
);
}
#[test]
fn extrude_triangle_volume_exact() {
let mut topo = Topology::new();
let face = make_unit_triangle_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
let expected = 1.5;
let rel_err = (vol - expected).abs() / expected;
assert!(
rel_err < 1e-8,
"extruded unit triangle by 3 should have volume {expected}, got {vol} (rel_err={rel_err:.2e})"
);
}
#[test]
fn extrude_oblique_direction_volume() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let solid = extrude(&mut topo, face, Vec3::new(1.0, 0.0, 1.0), 2.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
let expected = 2.0;
let rel_err = (vol - expected).abs() / expected;
assert!(
rel_err < 1e-8,
"oblique extrusion volume should be {expected}, got {vol} (rel_err={rel_err:.2e})"
);
}
#[test]
fn extrude_face_with_ccw_circle_hole_volume() {
let mut topo = Topology::new();
let outer_pts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(20.0, 0.0, 0.0),
Point3::new(20.0, 20.0, 0.0),
Point3::new(0.0, 20.0, 0.0),
];
let outer_wire =
brepkit_topology::builder::make_polygon_wire(&mut topo, &outer_pts, 1e-7).unwrap();
let n_segments = 32;
let cx = 10.0;
let cy = 10.0;
let r = 3.0;
let inner_pts: Vec<Point3> = (0..n_segments)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let theta = 2.0 * std::f64::consts::PI * (i as f64) / (n_segments as f64);
Point3::new(cx + r * theta.cos(), cy + r * theta.sin(), 0.0)
})
.collect();
let inner_wire =
brepkit_topology::builder::make_polygon_wire(&mut topo, &inner_pts, 1e-7).unwrap();
let normal = Vec3::new(0.0, 0.0, 1.0);
let face = Face::new(
outer_wire,
vec![inner_wire],
FaceSurface::Plane { normal, d: 0.0 },
);
let face_id = topo.add_face(face);
let solid = extrude(&mut topo, face_id, Vec3::new(0.0, 0.0, 1.0), 10.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.1).unwrap();
let polygon_area: f64 = (0..n_segments)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let theta1 = 2.0 * std::f64::consts::PI * (i as f64) / (n_segments as f64);
#[allow(clippy::cast_precision_loss)]
let theta2 = 2.0 * std::f64::consts::PI * ((i + 1) as f64) / (n_segments as f64);
0.5 * r * r * (theta2 - theta1).sin().abs()
})
.sum();
let expected = 20.0 * 20.0 * 10.0 - polygon_area * 10.0;
let rel_err = (vol - expected).abs() / expected;
assert!(
rel_err < 0.01,
"CCW circle hole extrusion volume should be ~{expected:.1}, got {vol:.1} \
(rel_err={rel_err:.2e})"
);
}
#[test]
fn extrude_analytic_circle_hole_volume_is_exact() {
use brepkit_math::curves::Circle3D;
let mut topo = Topology::new();
let outer_pts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(20.0, 0.0, 0.0),
Point3::new(20.0, 20.0, 0.0),
Point3::new(0.0, 20.0, 0.0),
];
let outer_wire =
brepkit_topology::builder::make_polygon_wire(&mut topo, &outer_pts, 1e-7).unwrap();
let (cx, cy, r) = (10.0, 10.0, 3.0);
let mk = |topo: &mut Topology, x: f64, y: f64| {
topo.add_vertex(Vertex::new(Point3::new(x, y, 0.0), 1e-7))
};
let p = [
mk(&mut topo, cx + r, cy),
mk(&mut topo, cx, cy + r),
mk(&mut topo, cx - r, cy),
mk(&mut topo, cx, cy - r),
];
let circ = || {
Circle3D::with_axes(
Point3::new(cx, cy, 0.0),
Vec3::new(0.0, 0.0, 1.0),
r,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
)
.unwrap()
};
let arcs: Vec<_> = (0..4)
.map(|i| {
let e = topo.add_edge(Edge::new(p[i], p[(i + 1) % 4], EdgeCurve::Circle(circ())));
OrientedEdge::new(e, true)
})
.collect();
let inner_wire = topo.add_wire(Wire::new(arcs, true).unwrap());
let face = topo.add_face(Face::new(
outer_wire,
vec![inner_wire],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 10.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.5).unwrap();
let expected = 20.0 * 20.0 * 10.0 - std::f64::consts::PI * r * r * 10.0;
let rel_err = (vol - expected).abs() / expected;
assert!(
rel_err < 1e-3,
"analytic circular-hole volume should be ~{expected:.4}, got {vol:.4} \
(rel_err={rel_err:.2e}) — coarse deflection must be clamped fine"
);
}
#[test]
fn extrude_cw_profile_produces_correct_solid() {
crate::test_helpers::assert_cw_profile_produces_valid_solid(
|topo, face| extrude(topo, face, Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap(),
2.0,
0.01,
);
}
#[test]
fn extrude_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 solid1 = extrude(&mut topo1, face1, Vec3::new(0.0, 0.0, 1.0), 2.0).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 solid2 = extrude(&mut topo2, face2, Vec3::new(0.0, 0.0, 1.0), 2.0).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 extrusion volumes should match: origin={vol1}, translated={vol2}, \
rel_err={rel_err:.2e}"
);
}
#[test]
fn extrude_recognized_circle_nurbs_arc_uses_cylinder_side_face() {
use brepkit_math::nurbs::curve::NurbsCurve;
let mut topo = Topology::new();
let tol = Tolerance::new();
let v0 = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), tol.linear));
let v1 = topo.add_vertex(Vertex::new(Point3::new(1.0, 0.0, 0.0), tol.linear));
let v2 = topo.add_vertex(Vertex::new(Point3::new(1.0, 1.0, 0.0), tol.linear));
let v3 = topo.add_vertex(Vertex::new(Point3::new(0.0, 1.0, 0.0), tol.linear));
let arc_curve = NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.5, 0.5, 0.0),
Point3::new(1.0, 1.0, 0.0),
],
vec![1.0, std::f64::consts::FRAC_1_SQRT_2, 1.0],
)
.unwrap();
let e0 = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let e1 = topo.add_edge(Edge::new(v1, v2, EdgeCurve::NurbsCurve(arc_curve)));
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 wire_id = topo.add_wire(wire);
let face = topo.add_face(Face::new(
wire_id,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let s = topo.solid(solid).unwrap();
let sh = topo.shell(s.outer_shell()).unwrap();
assert_eq!(sh.faces().len(), 6, "should have 6 faces");
let nurbs_count = sh
.faces()
.iter()
.filter(|&&fid| matches!(topo.face(fid).unwrap().surface(), FaceSurface::Nurbs(_)))
.count();
let cylinder_count = sh
.faces()
.iter()
.filter(|&&fid| matches!(topo.face(fid).unwrap().surface(), FaceSurface::Cylinder(_)))
.count();
assert_eq!(
cylinder_count, 1,
"rational-quadratic circular arc should produce 1 Cylinder side face"
);
assert_eq!(
nurbs_count, 0,
"no NURBS side face expected once the arc is recognized as a Circle"
);
}
#[test]
fn extrude_circle_edge_produces_cylinder_side_face() {
use brepkit_math::curves::Circle3D;
let mut topo = Topology::new();
let tol = Tolerance::new();
let v0 = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), tol.linear));
let v1 = topo.add_vertex(Vertex::new(Point3::new(1.0, 0.0, 0.0), tol.linear));
let v2 = topo.add_vertex(Vertex::new(Point3::new(1.0, 1.0, 0.0), tol.linear));
let v3 = topo.add_vertex(Vertex::new(Point3::new(0.0, 1.0, 0.0), tol.linear));
let circle = Circle3D::with_axes(
Point3::new(1.0, 0.5, 0.0),
Vec3::new(0.0, 0.0, 1.0),
0.5,
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
)
.unwrap();
let e0 = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let e1 = topo.add_edge(Edge::new(v1, v2, EdgeCurve::Circle(circle)));
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 wire_id = topo.add_wire(wire);
let face = topo.add_face(Face::new(
wire_id,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
let s = topo.solid(solid).unwrap();
let sh = topo.shell(s.outer_shell()).unwrap();
let cyl_count = sh
.faces()
.iter()
.filter(|&&fid| matches!(topo.face(fid).unwrap().surface(), FaceSurface::Cylinder(_)))
.count();
assert_eq!(
cyl_count, 1,
"exactly 1 side face should be a cylinder, got {cyl_count}"
);
}
#[test]
fn nurbs_arc_extrude_volume() {
use brepkit_math::nurbs::fitting::interpolate;
use brepkit_math::tolerance::Tolerance;
let mut topo = Topology::new();
let tol = Tolerance::new();
let w = 41.5_f64;
let d = 41.5_f64;
let r = 3.75_f64;
let h = 21.0_f64;
let hw = w / 2.0;
let hd = d / 2.0;
let make_arc_nurbs = |cx: f64, cy: f64, start_angle: f64| {
let n_pts = 24;
let mut pts = Vec::new();
for i in 0..=n_pts {
let angle = start_angle + std::f64::consts::FRAC_PI_2 * i as f64 / n_pts as f64;
pts.push(Point3::new(cx + r * angle.cos(), cy + r * angle.sin(), 0.0));
}
interpolate(&pts, 3).unwrap()
};
let positions = [
Point3::new(hw - r, -hd, 0.0),
Point3::new(hw, -hd + r, 0.0),
Point3::new(hw, hd - r, 0.0),
Point3::new(hw - r, hd, 0.0),
Point3::new(-hw + r, hd, 0.0),
Point3::new(-hw, hd - r, 0.0),
Point3::new(-hw, -hd + r, 0.0),
Point3::new(-hw + r, -hd, 0.0),
];
let vids: Vec<_> = positions
.iter()
.map(|p| topo.add_vertex(Vertex::new(*p, tol.linear)))
.collect();
let arcs = [
make_arc_nurbs(hw - r, -hd + r, -std::f64::consts::FRAC_PI_2),
make_arc_nurbs(hw - r, hd - r, 0.0),
make_arc_nurbs(-hw + r, hd - r, std::f64::consts::FRAC_PI_2),
make_arc_nurbs(-hw + r, -hd + r, std::f64::consts::PI),
];
let e_bot = topo.add_edge(Edge::new(vids[7], vids[0], EdgeCurve::Line));
let e_br = topo.add_edge(Edge::new(
vids[0],
vids[1],
EdgeCurve::NurbsCurve(arcs[0].clone()),
));
let e_right = topo.add_edge(Edge::new(vids[1], vids[2], EdgeCurve::Line));
let e_tr = topo.add_edge(Edge::new(
vids[2],
vids[3],
EdgeCurve::NurbsCurve(arcs[1].clone()),
));
let e_top = topo.add_edge(Edge::new(vids[3], vids[4], EdgeCurve::Line));
let e_tl = topo.add_edge(Edge::new(
vids[4],
vids[5],
EdgeCurve::NurbsCurve(arcs[2].clone()),
));
let e_left = topo.add_edge(Edge::new(vids[5], vids[6], EdgeCurve::Line));
let e_bl = topo.add_edge(Edge::new(
vids[6],
vids[7],
EdgeCurve::NurbsCurve(arcs[3].clone()),
));
let wire = Wire::new(
vec![
OrientedEdge::new(e_bot, true),
OrientedEdge::new(e_br, true),
OrientedEdge::new(e_right, true),
OrientedEdge::new(e_tr, true),
OrientedEdge::new(e_top, true),
OrientedEdge::new(e_tl, true),
OrientedEdge::new(e_left, true),
OrientedEdge::new(e_bl, true),
],
true,
)
.unwrap();
let wire_id = topo.add_wire(wire);
let normal = Vec3::new(0.0, 0.0, 1.0);
let face = Face::new(wire_id, vec![], FaceSurface::Plane { normal, d: 0.0 });
let face_id = topo.add_face(face);
let solid = crate::extrude::extrude(&mut topo, face_id, Vec3::new(0.0, 0.0, 1.0), h).unwrap();
let sh = topo
.shell(topo.solid(solid).unwrap().outer_shell())
.unwrap();
assert_eq!(sh.faces().len(), 10, "should have 10 faces");
let vol_circle = {
let mut t2 = Topology::new();
let tol2 = Tolerance::new();
let z_axis = Vec3::new(0.0, 0.0, 1.0);
let vids2: Vec<_> = positions
.iter()
.map(|p| t2.add_vertex(Vertex::new(*p, tol2.linear)))
.collect();
let mk_line2 = |topo: &mut Topology, s, e| topo.add_edge(Edge::new(s, e, EdgeCurve::Line));
let mk_arc2 = |topo: &mut Topology, s, e, center: Point3| {
let circle = brepkit_math::curves::Circle3D::new(center, z_axis, r).unwrap();
topo.add_edge(Edge::new(s, e, EdgeCurve::Circle(circle)))
};
let e2_bot = mk_line2(&mut t2, vids2[7], vids2[0]);
let e2_br = mk_arc2(
&mut t2,
vids2[0],
vids2[1],
Point3::new(hw - r, -hd + r, 0.0),
);
let e2_right = mk_line2(&mut t2, vids2[1], vids2[2]);
let e2_tr = mk_arc2(
&mut t2,
vids2[2],
vids2[3],
Point3::new(hw - r, hd - r, 0.0),
);
let e2_top = mk_line2(&mut t2, vids2[3], vids2[4]);
let e2_tl = mk_arc2(
&mut t2,
vids2[4],
vids2[5],
Point3::new(-hw + r, hd - r, 0.0),
);
let e2_left = mk_line2(&mut t2, vids2[5], vids2[6]);
let e2_bl = mk_arc2(
&mut t2,
vids2[6],
vids2[7],
Point3::new(-hw + r, -hd + r, 0.0),
);
let wire2 = Wire::new(
vec![
OrientedEdge::new(e2_bot, true),
OrientedEdge::new(e2_br, true),
OrientedEdge::new(e2_right, true),
OrientedEdge::new(e2_tr, true),
OrientedEdge::new(e2_top, true),
OrientedEdge::new(e2_tl, true),
OrientedEdge::new(e2_left, true),
OrientedEdge::new(e2_bl, true),
],
true,
)
.unwrap();
let wid2 = t2.add_wire(wire2);
let face2 = Face::new(wid2, vec![], FaceSurface::Plane { normal, d: 0.0 });
let fid2 = t2.add_face(face2);
let solid2 = crate::extrude::extrude(&mut t2, fid2, Vec3::new(0.0, 0.0, 1.0), h).unwrap();
crate::measure::solid_volume(&t2, solid2, 0.01).unwrap()
};
eprintln!("[nurbs_arc] Circle3D volume: {vol_circle:.2}");
for &fid in sh.faces() {
let f = topo.face(fid).unwrap();
let kind = match f.surface() {
FaceSurface::Plane { .. } => "Plane",
FaceSurface::Nurbs(_) => "Nurbs",
FaceSurface::Cylinder(_) => "Cylinder",
_ => "Other",
};
let w2 = topo.wire(f.outer_wire()).unwrap();
let mesh = crate::tessellate::tessellate(&topo, fid, 0.01).unwrap();
let tri_count = mesh.indices.len() / 3;
let mut face_vol = 0.0_f64;
for t in 0..tri_count {
let v0 = mesh.positions[mesh.indices[t * 3] as usize];
let v1 = mesh.positions[mesh.indices[t * 3 + 1] as usize];
let v2 = mesh.positions[mesh.indices[t * 3 + 2] as usize];
let a = Vec3::new(v0.x(), v0.y(), v0.z());
let b = Vec3::new(v1.x(), v1.y(), v1.z());
let c = Vec3::new(v2.x(), v2.y(), v2.z());
face_vol += a.dot(b.cross(c));
}
face_vol /= 6.0;
eprintln!(
"[nurbs_arc] Face {}: {kind}, {} edges, {} tris, vol_contrib={face_vol:.2}",
fid.index(),
w2.edges().len(),
tri_count
);
}
let vol = crate::measure::solid_volume(&topo, solid, 0.01).unwrap();
let corner_area = (4.0 - std::f64::consts::PI) * r * r;
let expected_vol = (w * d - corner_area) * h;
let pct = (vol - expected_vol).abs() / expected_vol;
eprintln!(
"[nurbs_arc] Volume: {vol:.2}, expected: {expected_vol:.2}, err: {:.2}%",
pct * 100.0
);
assert!(
pct < 0.05,
"NURBS arc extrude volume error too large: {vol:.2} vs {expected_vol:.2} ({:.2}%)",
pct * 100.0
);
}
#[test]
fn extrude_half_circle_face_volume() {
use brepkit_math::curves::Circle3D;
use brepkit_math::vec::{Point3, Vec3};
let mut topo = Topology::new();
let tol = Tolerance::new();
let r = 5.0_f64;
let v0 = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), tol.linear));
let v1 = topo.add_vertex(Vertex::new(Point3::new(10.0, 0.0, 0.0), tol.linear));
let circle = Circle3D::with_axes(
Point3::new(5.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
r,
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
)
.unwrap();
let arc = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Circle(circle)));
let line = topo.add_edge(Edge::new(v1, v0, EdgeCurve::Line));
let wire = Wire::new(
vec![OrientedEdge::new(arc, true), OrientedEdge::new(line, 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 expected = 0.5 * std::f64::consts::PI * r * r;
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.001).unwrap();
assert!(
(vol - expected).abs() / expected < 0.01,
"extruded half-circle volume {vol:.4} != {expected:.4}"
);
}
#[test]
fn extrude_half_circle_reversed_edge_volume() {
use brepkit_math::curves::Circle3D;
use brepkit_math::vec::{Point3, Vec3};
let mut topo = Topology::new();
let tol = Tolerance::new();
let r = 5.0_f64;
let v0 = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), tol.linear));
let v1 = topo.add_vertex(Vertex::new(Point3::new(10.0, 0.0, 0.0), tol.linear));
let circle = Circle3D::with_axes(
Point3::new(5.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
r,
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
)
.unwrap();
let arc = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Circle(circle)));
let line = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let wire = Wire::new(
vec![OrientedEdge::new(arc, false), OrientedEdge::new(line, 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 expected = 0.5 * std::f64::consts::PI * r * r;
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.001).unwrap();
assert!(
(vol - expected).abs() / expected < 0.01,
"reversed-edge half-circle volume {vol:.4} != {expected:.4}"
);
}
#[test]
fn extrude_half_disc_reversed_nurbs_edge_volume() {
use brepkit_math::nurbs::fitting::interpolate;
use brepkit_math::vec::{Point3, Vec3};
let mut topo = Topology::new();
let tol = Tolerance::new();
let r = 5.0_f64;
let cx = 5.0_f64;
let n_pts = 24;
let pts: Vec<Point3> = (0..=n_pts)
.map(|i| {
let angle = std::f64::consts::PI * (1.0 - f64::from(i) / f64::from(n_pts));
Point3::new(cx + r * angle.cos(), r * angle.sin(), 0.0)
})
.collect();
let nurbs = interpolate(&pts, 3).unwrap();
let v0 = topo.add_vertex(Vertex::new(Point3::new(0.0, 0.0, 0.0), tol.linear));
let v1 = topo.add_vertex(Vertex::new(Point3::new(10.0, 0.0, 0.0), tol.linear));
let arc = topo.add_edge(Edge::new(v0, v1, EdgeCurve::NurbsCurve(nurbs)));
let line = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let wire = Wire::new(
vec![OrientedEdge::new(arc, false), OrientedEdge::new(line, 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 expected = 0.5 * std::f64::consts::PI * r * r;
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.001).unwrap();
assert!(
(vol - expected).abs() / expected < 0.02,
"reversed-edge half-disc (NURBS) volume {vol:.4} != {expected:.4}"
);
}
#[test]
fn extrude_half_ellipse_face_volume() {
use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::builder::make_ellipse_arc;
let mut topo = Topology::new();
let center = Point3::new(5.0, 0.0, 0.0);
let axis = Vec3::new(0.0, 0.0, -1.0);
let ref_dir = Vec3::new(0.0, 1.0, 0.0);
let start = Point3::new(0.0, 0.0, 0.0);
let end = Point3::new(10.0, 0.0, 0.0);
let arc = make_ellipse_arc(
&mut topo,
center,
axis,
8.333_333_333_333_334,
5.0,
ref_dir,
start,
end,
1e-7,
)
.unwrap();
let (v0, v1) = {
let e = topo.edge(arc).unwrap();
(e.start(), e.end())
};
let line = topo.add_edge(Edge::new(v1, v0, EdgeCurve::Line));
let wire = Wire::new(
vec![OrientedEdge::new(arc, true), OrientedEdge::new(line, 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 expected = 0.5 * std::f64::consts::PI * 8.333_333_333_333_334 * 5.0;
let face_area = crate::measure::face_area(&topo, face, 0.001).unwrap();
assert!(
(face_area - expected).abs() / expected < 0.01,
"half-ellipse cap area {face_area:.4} != {expected:.4}"
);
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.001).unwrap();
assert!(
(vol - expected).abs() / expected < 0.01,
"extruded half-ellipse volume {vol:.4} != {expected:.4}"
);
}
#[test]
fn extrude_half_ellipse_reversed_edge_volume() {
use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::builder::make_ellipse_arc;
let mut topo = Topology::new();
let center = Point3::new(5.0, 0.0, 0.0);
let axis = Vec3::new(0.0, 0.0, -1.0);
let ref_dir = Vec3::new(0.0, 1.0, 0.0);
let start = Point3::new(0.0, 0.0, 0.0);
let end = Point3::new(10.0, 0.0, 0.0);
let arc = make_ellipse_arc(
&mut topo,
center,
axis,
8.333_333_333_333_334,
5.0,
ref_dir,
start,
end,
1e-7,
)
.unwrap();
let (v0, v1) = {
let e = topo.edge(arc).unwrap();
(e.start(), e.end())
};
let line = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let wire = Wire::new(
vec![OrientedEdge::new(arc, false), OrientedEdge::new(line, 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 expected = 0.5 * std::f64::consts::PI * 8.333_333_333_333_334 * 5.0;
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let vol = crate::measure::solid_volume(&topo, solid, 0.001).unwrap();
assert!(
(vol - expected).abs() / expected < 0.01,
"reversed-edge half-ellipse volume {vol:.4} != {expected:.4}"
);
}
#[test]
fn extrude_spline_encoded_profile_recovers_analytic_walls() {
use brepkit_geometry::convert::{circle_to_nurbs, line_to_nurbs};
use brepkit_topology::edge::Edge;
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::Wire;
let mut topo = Topology::new();
let tol = Tolerance::new().linear;
let pts = [
Point3::new(0.0, 0.0, 0.0),
Point3::new(10.0, 0.0, 0.0),
Point3::new(10.0, 4.0, 0.0), Point3::new(8.0, 6.0, 0.0), Point3::new(2.0, 6.0, 0.0), Point3::new(0.0, 4.0, 0.0), ];
let vids: Vec<_> = pts
.iter()
.map(|&p| topo.add_vertex(Vertex::new(p, tol)))
.collect();
let nurbs_line = |a: Point3, b: Point3| EdgeCurve::NurbsCurve(line_to_nurbs(a, b).unwrap());
let fillet_circle = brepkit_math::curves::Circle3D::new(
Point3::new(2.0, 4.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
2.0,
)
.unwrap();
let t0 = fillet_circle.project(pts[4]);
let mut t1 = fillet_circle.project(pts[5]);
if t1 < t0 {
t1 += std::f64::consts::TAU;
}
let fillet_arc = EdgeCurve::NurbsCurve(circle_to_nurbs(&fillet_circle, t0, t1).unwrap());
let curves = [
nurbs_line(pts[0], pts[1]),
nurbs_line(pts[1], pts[2]),
nurbs_line(pts[2], pts[3]), nurbs_line(pts[3], pts[4]),
fillet_arc,
nurbs_line(pts[5], pts[0]),
];
let mut oes = Vec::new();
for (i, c) in curves.into_iter().enumerate() {
let a = vids[i];
let b = vids[(i + 1) % vids.len()];
let e = topo.add_edge(Edge::new(a, b, c));
oes.push(OrientedEdge::new(e, true));
}
let wid = topo.add_wire(Wire::new(oes, true).unwrap());
let face = topo.add_face(Face::new(
wid,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
let mut nurbs_walls = 0usize;
let mut cylinders = 0usize;
for fid in brepkit_topology::explorer::solid_faces(&topo, solid).unwrap() {
match topo.face(fid).unwrap().surface() {
FaceSurface::Nurbs(_) => nurbs_walls += 1,
FaceSurface::Cylinder(_) => cylinders += 1,
_ => {}
}
}
assert_eq!(
nurbs_walls, 0,
"spline-encoded straight/arc profile edges must extrude as analytic walls"
);
assert_eq!(
cylinders, 1,
"the fillet arc must extrude as a true cylinder"
);
let expected = (60.0 - 2.0 - (4.0 - std::f64::consts::PI)) * 3.0;
let vol = crate::measure::solid_volume(&topo, solid, 0.001).unwrap();
assert!(
(vol - expected).abs() / expected < 1e-4,
"analytic prism volume {vol:.9} != exact {expected:.9}"
);
}
#[test]
fn extrude_reversed_spline_arc_profile_recovers_analytic_walls() {
use brepkit_geometry::convert::{circle_to_nurbs, line_to_nurbs};
use brepkit_topology::edge::Edge;
use brepkit_topology::face::Face;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::Wire;
let mut topo = Topology::new();
let tol = Tolerance::new().linear;
let pts = [
Point3::new(0.0, 0.0, 0.0),
Point3::new(10.0, 0.0, 0.0),
Point3::new(10.0, 6.0, 0.0),
Point3::new(2.0, 6.0, 0.0),
Point3::new(0.0, 4.0, 0.0),
];
let vids: Vec<_> = pts
.iter()
.map(|&p| topo.add_vertex(Vertex::new(p, tol)))
.collect();
let nurbs_line = |a: Point3, b: Point3| EdgeCurve::NurbsCurve(line_to_nurbs(a, b).unwrap());
let fillet_circle = brepkit_math::curves::Circle3D::new(
Point3::new(2.0, 4.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
2.0,
)
.unwrap();
let t0 = fillet_circle.project(pts[4]);
let t1 = t0 - std::f64::consts::FRAC_PI_2;
let reversed_arc = EdgeCurve::NurbsCurve(circle_to_nurbs(&fillet_circle, t0, t1).unwrap());
let curves = [
nurbs_line(pts[0], pts[1]),
nurbs_line(pts[1], pts[2]),
nurbs_line(pts[2], pts[3]),
reversed_arc,
nurbs_line(pts[4], pts[0]),
];
let mut oes = Vec::new();
for (i, c) in curves.into_iter().enumerate() {
let a = vids[i];
let b = vids[(i + 1) % vids.len()];
let e = topo.add_edge(Edge::new(a, b, c));
oes.push(OrientedEdge::new(e, true));
}
let wid = topo.add_wire(Wire::new(oes, true).unwrap());
let face = topo.add_face(Face::new(
wid,
vec![],
FaceSurface::Plane {
normal: Vec3::new(0.0, 0.0, 1.0),
d: 0.0,
},
));
let solid = extrude(&mut topo, face, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
let mut nurbs_walls = 0usize;
for fid in brepkit_topology::explorer::solid_faces(&topo, solid).unwrap() {
if matches!(topo.face(fid).unwrap().surface(), FaceSurface::Nurbs(_)) {
nurbs_walls += 1;
}
}
assert_eq!(
nurbs_walls, 0,
"reversed spline arc must still recover analytic walls"
);
let expected = (60.0 - (4.0 - std::f64::consts::PI)) * 3.0;
let vol = crate::measure::solid_volume(&topo, solid, 0.001).unwrap();
assert!(
(vol - expected).abs() / expected < 1e-4,
"reversed-arc prism volume {vol:.9} != exact {expected:.9}"
);
}