use crate::errors::ValidationError;
use crate::float_types::{EPSILON, FRAC_PI_2, PI, Real};
use crate::mesh::Mesh;
use crate::mesh::bsp::Node;
use crate::mesh::plane::Plane;
use crate::mesh::polygon::Polygon;
use crate::mesh::vertex::{Vertex, VertexCluster};
use crate::sketch::Sketch;
use crate::traits::CSG;
use geo::{Area, Geometry, HasDimensions};
use hashbrown::HashMap;
use nalgebra::{Point3, Vector3};
fn bounding_box(polygons: &[Polygon<()>]) -> [Real; 6] {
let mut min_x = Real::MAX;
let mut min_y = Real::MAX;
let mut min_z = Real::MAX;
let mut max_x = Real::MIN;
let mut max_y = Real::MIN;
let mut max_z = Real::MIN;
for poly in polygons {
for v in &poly.vertices {
let p = v.pos;
if p.x < min_x {
min_x = p.x;
}
if p.y < min_y {
min_y = p.y;
}
if p.z < min_z {
min_z = p.z;
}
if p.x > max_x {
max_x = p.x;
}
if p.y > max_y {
max_y = p.y;
}
if p.z > max_z {
max_z = p.z;
}
}
}
[min_x, min_y, min_z, max_x, max_y, max_z]
}
fn approx_eq(a: Real, b: Real, eps: Real) -> bool {
(a - b).abs() < eps
}
#[test]
fn test_vertex_flip() {
let mut v = Vertex::new(Point3::new(1.0, 2.0, 3.0), Vector3::x());
v.flip();
assert_eq!(v.pos, Point3::new(1.0, 2.0, 3.0));
assert_eq!(v.normal, -Vector3::x());
}
#[test]
fn test_polygon_construction() {
let v1 = Vertex::new(Point3::origin(), Vector3::y());
let v2 = Vertex::new(Point3::new(1.0, 0.0, 1.0), Vector3::y());
let v3 = Vertex::new(Point3::new(1.0, 0.0, -1.0), Vector3::y());
let poly: Polygon<()> = Polygon::new(vec![v1.clone(), v2.clone(), v3.clone()], None);
assert_eq!(poly.vertices.len(), 3);
assert!(
approx_eq(poly.plane.normal().dot(&Vector3::y()).abs(), 1.0, 1e-8),
"Expected plane normal to match ±Y"
);
}
#[test]
#[cfg(feature = "stl-io")]
fn test_to_stl_ascii() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let stl_str = cube.to_stl_ascii("test_cube");
assert!(stl_str.contains("solid test_cube"));
assert!(stl_str.contains("endsolid test_cube"));
assert!(stl_str.contains("facet normal"));
assert!(stl_str.contains("vertex"));
}
#[test]
fn test_degenerate_polygon_after_clipping() {
let vertices = vec![
Vertex::new(Point3::origin(), Vector3::y()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::y()),
Vertex::new(Point3::new(0.5, 1.0, 0.0), Vector3::y()),
];
let polygon: Polygon<()> = Polygon::new(vertices.clone(), None);
let plane = Plane::from_normal(Vector3::new(0.0, 0.0, 1.0), 0.0);
eprintln!("Original polygon: {:?}", polygon);
eprintln!("Clipping plane: {:?}", plane);
let (_coplanar_front, _coplanar_back, front, back) = plane.split_polygon(&polygon);
eprintln!("Front polygons: {:?}", front);
eprintln!("Back polygons: {:?}", back);
assert!(front.is_empty(), "Front should be empty for this test");
assert!(back.is_empty(), "Back should be empty for this test");
}
#[test]
fn test_valid_polygon_clipping() {
let vertices = vec![
Vertex::new(Point3::origin(), Vector3::y()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::y()),
Vertex::new(Point3::new(0.5, 1.0, 0.0), Vector3::y()),
];
let polygon: Polygon<()> = Polygon::new(vertices, None);
let plane = Plane::from_normal(-Vector3::y(), -0.5);
eprintln!("Polygon before clipping: {:?}", polygon);
eprintln!("Clipping plane: {:?}", plane);
let (_coplanar_front, _coplanar_back, front, back) = plane.split_polygon(&polygon);
eprintln!("Front polygons: {:?}", front);
eprintln!("Back polygons: {:?}", back);
assert!(!front.is_empty(), "Front should not be empty");
assert!(!back.is_empty(), "Back should not be empty");
}
#[test]
fn test_vertex_new() {
let pos = Point3::new(1.0, 2.0, 3.0);
let normal = Vector3::new(0.0, 1.0, 0.0);
let v = Vertex::new(pos, normal);
assert_eq!(v.pos, pos);
assert_eq!(v.normal, normal);
}
#[test]
fn test_vertex_interpolate() {
let v1 = Vertex::new(Point3::origin(), Vector3::x());
let v2 = Vertex::new(Point3::new(2.0, 2.0, 2.0), Vector3::y());
let v_mid = v1.interpolate(&v2, 0.5);
assert!(approx_eq(v_mid.pos.x, 1.0, EPSILON));
assert!(approx_eq(v_mid.pos.y, 1.0, EPSILON));
assert!(approx_eq(v_mid.pos.z, 1.0, EPSILON));
assert!(approx_eq(v_mid.normal.x, 0.5, EPSILON));
assert!(approx_eq(v_mid.normal.y, 0.5, EPSILON));
assert!(approx_eq(v_mid.normal.z, 0.0, EPSILON));
}
#[test]
fn test_plane_flip() {
let mut plane = Plane::from_normal(Vector3::y(), 2.0);
plane.flip();
assert_eq!(plane.normal(), Vector3::new(0.0, -1.0, 0.0));
assert_eq!(plane.offset(), -2.0);
}
#[test]
fn test_plane_split_polygon() {
let plane = Plane::from_normal(Vector3::new(0.0, 1.0, 0.0), 0.0);
let poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::new(-1.0, -1.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(1.0, -1.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 1.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(-1.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let (cf, cb, f, b) = plane.split_polygon(&poly);
assert_eq!(cf.len(), 0);
assert_eq!(cb.len(), 0);
assert_eq!(f.len(), 1);
assert_eq!(b.len(), 1);
let front_poly = &f[0];
let back_poly = &b[0];
assert!(front_poly.vertices.len() >= 3);
assert!(back_poly.vertices.len() >= 3);
for v in &front_poly.vertices {
assert!(v.pos.y >= -EPSILON);
}
for v in &back_poly.vertices {
assert!(v.pos.y <= EPSILON);
}
}
#[test]
fn test_polygon_new() {
let vertices = vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
];
let poly: Polygon<()> = Polygon::new(vertices.clone(), None);
assert_eq!(poly.vertices.len(), 3);
assert_eq!(poly.metadata, None);
assert!(approx_eq(poly.plane.normal().x, 0.0, EPSILON));
assert!(approx_eq(poly.plane.normal().y, 0.0, EPSILON));
assert!(approx_eq(poly.plane.normal().z, 1.0, EPSILON));
}
#[test]
fn test_polygon_flip() {
let mut poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let plane_normal_before = poly.plane.normal();
poly.flip();
assert_eq!(poly.vertices.len(), 3);
assert!(approx_eq(
poly.plane.normal().x,
-plane_normal_before.x,
EPSILON
));
assert!(approx_eq(
poly.plane.normal().y,
-plane_normal_before.y,
EPSILON
));
assert!(approx_eq(
poly.plane.normal().z,
-plane_normal_before.z,
EPSILON
));
}
#[test]
fn test_polygon_triangulate() {
let poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 1.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let triangles = poly.triangulate();
assert_eq!(
triangles.len(),
2,
"A quad should triangulate into 2 triangles"
);
}
#[test]
fn test_polygon_subdivide_triangles() {
let poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let subs = poly.subdivide_triangles(1.try_into().expect("not 0"));
assert_eq!(subs.len(), 4);
let subs2 = poly.subdivide_triangles(2.try_into().expect("not 0"));
assert_eq!(subs2.len(), 16);
}
#[test]
fn test_polygon_recalc_plane_and_normals() {
let mut poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::zeros()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::zeros()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::zeros()),
],
None,
);
poly.set_new_normal();
assert!(approx_eq(poly.plane.normal().z, 1.0, EPSILON));
for v in &poly.vertices {
assert!(approx_eq(v.normal.x, 0.0, EPSILON));
assert!(approx_eq(v.normal.y, 0.0, EPSILON));
assert!(approx_eq(v.normal.z, 1.0, EPSILON));
}
}
#[test]
fn test_node_new_and_build() {
let p: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let node: Node<()> = Node::from_polygons(&[p.clone()]);
assert!(node.plane.is_some());
assert_eq!(node.polygons.len(), 1);
assert!(node.front.is_none());
assert!(node.back.is_none());
}
#[test]
fn test_node_invert() {
let p: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let mut node: Node<()> = Node::from_polygons(&[p.clone()]);
let original_count = node.polygons.len();
let original_normal = node.plane.as_ref().unwrap().normal();
node.invert();
let flipped_normal = node.plane.as_ref().unwrap().normal();
assert!(approx_eq(flipped_normal.x, -original_normal.x, EPSILON));
assert!(approx_eq(flipped_normal.y, -original_normal.y, EPSILON));
assert!(approx_eq(flipped_normal.z, -original_normal.z, EPSILON));
assert_eq!(node.polygons.len(), original_count);
node.invert();
assert_eq!(node.polygons.len(), original_count);
}
#[test]
fn test_node_clip_polygons2() {
let plane = Plane::from_normal(Vector3::z(), 0.0);
let mut node: Node<()> = Node {
plane: Some(plane),
front: None,
back: None,
polygons: Vec::new(),
};
let poly_in_plane: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let poly_above: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::new(0.0, 0.0, 1.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 1.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 1.0), Vector3::z()),
],
None,
);
let poly_below: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::new(0.0, 0.0, -1.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, -1.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, -1.0), Vector3::z()),
],
None,
);
node.build(&[
poly_in_plane.clone(),
poly_above.clone(),
poly_below.clone(),
]);
let crossing_poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::new(-1.0, -1.0, -0.5), Vector3::z()),
Vertex::new(Point3::new(2.0, -1.0, 0.5), Vector3::z()),
Vertex::new(Point3::new(-1.0, 2.0, 0.5), Vector3::z()),
],
None,
);
let clipped = node.clip_polygons(&[crossing_poly.clone()]);
assert!(!clipped.is_empty());
}
#[test]
fn test_node_clip_to() {
let poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::new(-0.5, -0.5, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.5, -0.5, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 0.5, 0.0), Vector3::z()),
],
None,
);
let mut node_a: Node<()> = Node::from_polygons(&[poly]);
let big_poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::new(-1.0, -1.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(1.0, -1.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 1.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(-1.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let node_b: Node<()> = Node::from_polygons(&[big_poly]);
node_a.clip_to(&node_b);
let all_a = node_a.all_polygons();
assert_eq!(all_a.len(), 1);
}
#[test]
fn test_node_all_polygons() {
let poly1: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let poly2: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::new(0.0, 0.0, 1.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 1.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 1.0), Vector3::z()),
],
None,
);
let node: Node<()> = Node::from_polygons(&[poly1.clone(), poly2.clone()]);
let all_polys = node.all_polygons();
assert_eq!(all_polys.len(), 2);
}
#[test]
fn test_csg_from_polygons_and_to_polygons() {
let poly: Polygon<()> = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
None,
);
let csg: Mesh<()> = Mesh::from_polygons(&[poly.clone()], None);
assert_eq!(csg.polygons.len(), 1);
assert_eq!(csg.polygons[0].vertices.len(), 3);
}
#[test]
fn test_csg_union() {
let cube1: Mesh<()> = Mesh::cube(2.0, None).translate(-1.0, -1.0, -1.0); let cube2: Mesh<()> = Mesh::cube(1.0, None).translate(0.5, 0.5, 0.5);
let union_csg = cube1.union(&cube2);
assert!(
!union_csg.polygons.is_empty(),
"Union of two cubes should produce polygons"
);
let bb = bounding_box(&union_csg.polygons);
assert!(approx_eq(bb[0], -1.0, 1e-8));
assert!(approx_eq(bb[1], -1.0, 1e-8));
assert!(approx_eq(bb[2], -1.0, 1e-8));
assert!(approx_eq(bb[3], 1.5, 1e-8));
assert!(approx_eq(bb[4], 1.5, 1e-8));
assert!(approx_eq(bb[5], 1.5, 1e-8));
}
#[test]
fn test_csg_difference() {
let big_cube: Mesh<()> = Mesh::cube(4.0, None).translate(-2.0, -2.0, -2.0); let small_cube: Mesh<()> = Mesh::cube(2.0, None).translate(-1.0, -1.0, -1.0);
let result = big_cube.difference(&small_cube);
assert!(
!result.polygons.is_empty(),
"Subtracting a smaller cube should leave polygons"
);
let bb = bounding_box(&result.polygons);
assert!(approx_eq(bb[0], -2.0, 1e-8));
assert!(approx_eq(bb[3], 2.0, 1e-8));
}
#[test]
fn test_csg_union2() {
let c1: Mesh<()> = Mesh::cube(2.0, None); let c2: Mesh<()> = Mesh::sphere(1.0, 16, 8, None); let unioned = c1.union(&c2);
let bb_union = unioned.bounding_box();
let bb_cube = c1.bounding_box();
let bb_sphere = c2.bounding_box();
assert!(bb_union.mins.x <= bb_cube.mins.x.min(bb_sphere.mins.x));
assert!(bb_union.maxs.x >= bb_cube.maxs.x.max(bb_sphere.maxs.x));
}
#[test]
fn test_csg_intersect() {
let c1: Mesh<()> = Mesh::cube(2.0, None);
let c2: Mesh<()> = Mesh::sphere(1.0, 16, 8, None);
let isect = c1.intersection(&c2);
let bb_isect = isect.bounding_box();
let bb_cube = c1.bounding_box();
let bb_sphere = c2.bounding_box();
assert!(bb_isect.mins.x >= bb_cube.mins.x - EPSILON);
assert!(bb_isect.mins.x >= bb_sphere.mins.x - EPSILON);
assert!(bb_isect.maxs.x <= bb_cube.maxs.x + EPSILON);
assert!(bb_isect.maxs.x <= bb_sphere.maxs.x + EPSILON);
}
#[test]
fn test_csg_intersect2() {
let sphere: Mesh<()> = Mesh::sphere(1.0, 16, 8, None);
let cube: Mesh<()> = Mesh::cube(2.0, None);
let intersection = sphere.intersection(&cube);
assert!(
!intersection.polygons.is_empty(),
"Sphere ∩ Cube should produce the portion of the sphere inside the cube"
);
let bb = bounding_box(&intersection.polygons);
for &val in &bb[..3] {
assert!(val >= -1.0 - 1e-1);
}
for &val in &bb[3..] {
assert!(val <= 1.0 + 1e-1);
}
}
#[test]
fn test_csg_inverse() {
let c1: Mesh<()> = Mesh::cube(2.0, None);
let inv = c1.inverse();
let orig_poly = &c1.polygons[0];
let inv_poly = &inv.polygons[0];
assert!(approx_eq(
orig_poly.plane.normal().x,
-inv_poly.plane.normal().x,
EPSILON
));
assert!(approx_eq(
orig_poly.plane.normal().y,
-inv_poly.plane.normal().y,
EPSILON
));
assert!(approx_eq(
orig_poly.plane.normal().z,
-inv_poly.plane.normal().z,
EPSILON
));
assert_eq!(
c1.polygons.len(),
inv.polygons.len(),
"Inverse should keep the same polygon count, but flip them"
);
}
#[test]
fn test_csg_cube() {
let c: Mesh<()> = Mesh::cube(2.0, None);
assert_eq!(c.polygons.len(), 6);
let bb = c.bounding_box();
assert!(approx_eq(bb.mins.x, 0.0, EPSILON));
assert!(approx_eq(bb.maxs.x, 2.0, EPSILON));
}
#[test]
fn test_csg_sphere() {
let sphere: Mesh<()> = Mesh::sphere(1.0, 16, 8, None);
assert!(!sphere.polygons.is_empty(), "Sphere should generate polygons");
let bb = bounding_box(&sphere.polygons);
assert!(approx_eq(bb[0], -1.0, 1e-1));
assert!(approx_eq(bb[1], -1.0, 1e-1));
assert!(approx_eq(bb[2], -1.0, 1e-1));
assert!(approx_eq(bb[3], 1.0, 1e-1));
assert!(approx_eq(bb[4], 1.0, 1e-1));
assert!(approx_eq(bb[5], 1.0, 1e-1));
assert_eq!(sphere.polygons.len(), 16 * 8);
}
#[test]
fn test_csg_cylinder() {
let cylinder: Mesh<()> = Mesh::cylinder(1.0, 2.0, 16, None);
assert!(
!cylinder.polygons.is_empty(),
"Cylinder should generate polygons"
);
let bb = bounding_box(&cylinder.polygons);
assert!(approx_eq(bb[0], -1.0, 1e-8), "min X");
assert!(approx_eq(bb[1], -1.0, 1e-8), "min Y");
assert!(approx_eq(bb[2], 0.0, 1e-8), "min Z");
assert!(approx_eq(bb[3], 1.0, 1e-8), "max X");
assert!(approx_eq(bb[4], 1.0, 1e-8), "max Y");
assert!(approx_eq(bb[5], 2.0, 1e-8), "max Z");
assert_eq!(cylinder.polygons.len(), 48);
}
#[test]
fn test_csg_polyhedron() {
let pts = &[
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ];
let faces: [&[usize]; 4] = [&[0, 1, 2], &[0, 1, 3], &[1, 2, 3], &[2, 0, 3]];
let csg_tetra: Mesh<()> = Mesh::polyhedron(pts, &faces, None).unwrap();
assert_eq!(csg_tetra.polygons.len(), 4);
}
#[test]
fn test_csg_transform_translate_rotate_scale() {
let c: Mesh<()> = Mesh::cube(2.0, None).center();
let translated = c.translate(1.0, 2.0, 3.0);
let rotated = c.rotate(90.0, 0.0, 0.0); let scaled = c.scale(2.0, 1.0, 1.0);
let bb_t = translated.bounding_box();
assert!(approx_eq(bb_t.mins.x, -1.0 + 1.0, EPSILON));
assert!(approx_eq(bb_t.mins.y, -1.0 + 2.0, EPSILON));
assert!(approx_eq(bb_t.mins.z, -1.0 + 3.0, EPSILON));
let bb_s = scaled.bounding_box();
assert!(approx_eq(bb_s.mins.x, -2.0, EPSILON)); assert!(approx_eq(bb_s.maxs.x, 2.0, EPSILON));
assert!(approx_eq(bb_s.mins.y, -1.0, EPSILON));
assert!(approx_eq(bb_s.maxs.y, 1.0, EPSILON));
let poly0 = &rotated.polygons[0];
for v in &poly0.vertices {
assert_ne!(v.pos.y, 0.0); }
}
#[test]
fn test_csg_mirror() {
let c: Mesh<()> = Mesh::cube(2.0, None);
let plane_x = Plane::from_normal(Vector3::x(), 0.0); let mirror_x = c.mirror(plane_x);
let bb_mx = mirror_x.bounding_box();
assert!(approx_eq(bb_mx.mins.x, -2.0, EPSILON));
assert!(approx_eq(bb_mx.maxs.x, 0.0, EPSILON));
}
#[test]
#[cfg(feature = "chull-io")]
fn test_csg_convex_hull() {
let c1: Mesh<()> = Mesh::sphere(1.0, 16, 8, None);
let hull = c1.convex_hull();
assert!(!hull.polygons.is_empty());
}
#[test]
#[cfg(feature = "chull-io")]
fn test_csg_minkowski_sum() {
let c1: Mesh<()> = Mesh::cube(2.0, None).center();
let c2: Mesh<()> = Mesh::cube(1.0, None).center();
let sum = c1.minkowski_sum(&c2);
let bb_sum = sum.bounding_box();
assert!(approx_eq(bb_sum.mins.x, -1.5, 0.01));
assert!(approx_eq(bb_sum.maxs.x, 1.5, 0.01));
}
#[test]
fn test_csg_subdivide_triangles() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let subdiv = cube.subdivide_triangles(1.try_into().expect("not 0"));
assert_eq!(subdiv.polygons.len(), 6 * 8);
}
#[test]
fn test_csg_renormalize() {
let mut cube: Mesh<()> = Mesh::cube(2.0, None);
for poly in &mut cube.polygons {
for v in &mut poly.vertices {
v.normal = Vector3::x(); }
}
cube.renormalize();
for poly in &cube.polygons {
for v in &poly.vertices {
assert!(approx_eq(v.normal.x, poly.plane.normal().x, EPSILON));
assert!(approx_eq(v.normal.y, poly.plane.normal().y, EPSILON));
assert!(approx_eq(v.normal.z, poly.plane.normal().z, EPSILON));
}
}
}
#[test]
fn test_csg_ray_intersections() {
let cube: Mesh<()> = Mesh::cube(2.0, None).center();
let origin = Point3::new(-2.0, 0.0, 0.0);
let direction = Vector3::new(1.0, 0.0, 0.0);
let hits = cube.ray_intersections(&origin, &direction);
assert_eq!(hits.len(), 2);
assert!(approx_eq(hits[0].1, 1.0, EPSILON));
assert!(approx_eq(hits[1].1, 3.0, EPSILON));
}
#[test]
fn test_csg_square() {
let sq: Sketch<()> = Sketch::square(2.0, None);
let mesh_2d: Mesh<()> = sq.into();
assert_eq!(mesh_2d.polygons.len(), 1);
let poly = &mesh_2d.polygons[0];
assert!(
matches!(poly.vertices.len(), 4 | 5),
"Expected 4 or 5 vertices, got {}",
poly.vertices.len()
);
}
#[test]
fn test_csg_circle() {
let circle: Sketch<()> = Sketch::circle(2.0, 32, None);
let mesh_2d: Mesh<()> = circle.into();
assert_eq!(mesh_2d.polygons.len(), 1);
let poly = &mesh_2d.polygons[0];
assert!(
matches!(poly.vertices.len(), 32 | 33),
"Expected 32 or 33 vertices, got {}",
poly.vertices.len()
);
}
#[test]
fn test_csg_extrude() {
let sq: Sketch<()> = Sketch::square(2.0, None);
let extruded = sq.extrude(5.0);
assert_eq!(extruded.polygons.len(), 8);
let bb = extruded.bounding_box();
assert!(approx_eq(bb.mins.z, 0.0, EPSILON));
assert!(approx_eq(bb.maxs.z, 5.0, EPSILON));
}
#[test]
fn test_csg_revolve() {
let square: Sketch<()> = Sketch::square(2.0, None)
.translate(1.0, 0.0, 0.0)
.rotate(90.0, 0.0, 0.0);
let revolve = square.revolve(360.0, 16).unwrap();
assert!(!revolve.polygons.is_empty());
}
#[test]
fn test_csg_bounding_box() {
let sphere: Mesh<()> = Mesh::sphere(1.0, 16, 8, None);
let bb = sphere.bounding_box();
assert!(approx_eq(bb.mins.x, -1.0, 0.1));
assert!(approx_eq(bb.mins.y, -1.0, 0.1));
assert!(approx_eq(bb.mins.z, -1.0, 0.1));
assert!(approx_eq(bb.maxs.x, 1.0, 0.1));
assert!(approx_eq(bb.maxs.y, 1.0, 0.1));
assert!(approx_eq(bb.maxs.z, 1.0, 0.1));
}
#[test]
fn test_csg_vertices() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let verts = cube.vertices();
assert_eq!(verts.len(), 24);
}
#[test]
#[cfg(feature = "offset")]
fn test_csg_offset_2d() {
let square: Sketch<()> = Sketch::square(2.0, None);
let grown = square.offset(0.5);
let shrunk = square.offset(-0.5);
let bb_square = square.bounding_box();
let bb_grown = grown.bounding_box();
let bb_shrunk = shrunk.bounding_box();
println!("Square bb: {:#?}", bb_square);
println!("Grown bb: {:#?}", bb_grown);
println!("Shrunk bb: {:#?}", bb_shrunk);
assert!(bb_grown.maxs.x > bb_square.maxs.x + 0.4);
assert!(bb_shrunk.maxs.x < bb_square.maxs.x + 0.1);
}
#[cfg(feature = "truetype-text")]
#[test]
fn test_csg_text() {
let font_data = include_bytes!("../asar.ttf");
let text_csg: Sketch<()> = Sketch::text("ABC", font_data, 10.0, None);
assert!(!text_csg.geometry.is_empty());
}
#[test]
fn test_csg_to_trimesh() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let shape = cube.to_trimesh();
if let Some(trimesh) = shape {
assert_eq!(trimesh.indices().len(), 12); } else {
panic!("Expected a TriMesh");
}
}
#[test]
fn test_csg_mass_properties() {
let cube: Mesh<()> = Mesh::cube(2.0, None).center(); let (mass, com, _frame) = cube.mass_properties(1.0);
println!("{:#?}", mass);
assert!(approx_eq(mass, 8.0, 0.1));
assert!(approx_eq(com.x, 0.0, 0.001));
assert!(approx_eq(com.y, 0.0, 0.001));
assert!(approx_eq(com.z, 0.0, 0.001));
}
#[test]
fn test_csg_to_rigid_body() {
use crate::float_types::rapier3d::prelude::*;
let cube: Mesh<()> = Mesh::cube(2.0, None);
let mut rb_set = RigidBodySet::new();
let mut co_set = ColliderSet::new();
let handle = cube.to_rigid_body(
&mut rb_set,
&mut co_set,
Vector3::new(10.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, FRAC_PI_2), 1.0,
);
let rb = rb_set.get(handle).unwrap();
let pos = rb.translation();
assert!(approx_eq(pos.x, 10.0, EPSILON));
}
#[test]
#[cfg(feature = "stl-io")]
fn test_csg_to_stl_and_from_stl_file() -> Result<(), Box<dyn std::error::Error>> {
let tmp_path = "test_csg_output.stl";
let cube: Mesh<()> = Mesh::cube(2.0, None);
let res = cube.to_stl_binary("A cube");
let _ = std::fs::write(tmp_path, res.as_ref().unwrap());
assert!(res.is_ok());
let stl_data: Vec<u8> = std::fs::read(tmp_path)?;
let csg_in: Mesh<()> = Mesh::from_stl(&stl_data, None)?;
assert_eq!(csg_in.polygons.len(), 12);
let _ = std::fs::remove_file(tmp_path);
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
struct MyMetaData {
id: u32,
label: String,
}
#[test]
fn test_polygon_metadata_string() {
let verts = vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
];
let mut poly = Polygon::new(verts, Some("triangle".to_string()));
assert_eq!(poly.metadata(), Some(&"triangle".to_string()));
poly.set_metadata("updated".to_string());
assert_eq!(poly.metadata(), Some(&"updated".to_string()));
if let Some(data) = poly.metadata_mut() {
data.push_str("_appended");
}
assert_eq!(poly.metadata(), Some(&"updated_appended".to_string()));
}
#[test]
fn test_polygon_metadata_integer() {
let verts = vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
];
let poly = Polygon::new(verts, Some(42u32));
assert_eq!(poly.metadata(), Some(&42));
}
#[test]
fn test_polygon_metadata_custom_struct() {
let my_data = MyMetaData {
id: 999,
label: "MyLabel".into(),
};
let verts = vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
];
let poly = Polygon::new(verts, Some(my_data.clone()));
assert_eq!(poly.metadata(), Some(&my_data));
}
#[test]
fn test_csg_construction_with_metadata() {
let poly_a = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 1.0, 0.0), Vector3::z()),
],
Some("PolyA".to_string()),
);
let poly_b = Polygon::new(
vec![
Vertex::new(Point3::new(2.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(3.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(3.0, 1.0, 0.0), Vector3::z()),
],
Some("PolyB".to_string()),
);
let csg = Mesh::from_polygons(&[poly_a.clone(), poly_b.clone()], None);
assert_eq!(csg.polygons.len(), 2);
assert_eq!(csg.polygons[0].metadata(), Some(&"PolyA".to_string()));
assert_eq!(csg.polygons[1].metadata(), Some(&"PolyB".to_string()));
}
#[test]
fn test_union_metadata() {
let cube1 = Mesh::cube(1.0, None); let mut cube1 = cube1; for p in &mut cube1.polygons {
p.set_metadata("Cube1".to_string());
}
let cube2 = Mesh::cube(1.0, None).translate(0.5, 0.0, 0.0);
let mut cube2 = cube2;
for p in &mut cube2.polygons {
p.set_metadata("Cube2".to_string());
}
let union_csg = cube1.union(&cube2);
for poly in &union_csg.polygons {
let data = poly.metadata().unwrap();
assert!(
data == "Cube1" || data == "Cube2",
"Union polygon has unexpected shared data = {:?}",
data
);
}
}
#[test]
fn test_difference_metadata() {
let mut cube1 = Mesh::cube(2.0, Some("Cube1".to_string()));
for p in &mut cube1.polygons {
p.set_metadata("Cube1".to_string());
}
let mut cube2 = Mesh::cube(2.0, Some("Cube2".to_string())).translate(0.5, 0.5, 0.5);
for p in &mut cube2.polygons {
p.set_metadata("Cube2".to_string());
}
let result = cube1.difference(&cube2);
println!("{:#?}", cube1);
println!("{:#?}", cube2);
println!("{:#?}", result);
for poly in &result.polygons {
assert_eq!(poly.metadata(), Some(&"Cube1".to_string()));
}
}
#[test]
fn test_intersect_metadata() {
let mut cube1 = Mesh::cube(2.0, None);
for p in &mut cube1.polygons {
p.set_metadata("Cube1".to_string());
}
let mut cube2 = Mesh::cube(2.0, None).translate(0.5, 0.5, 0.5);
for p in &mut cube2.polygons {
p.set_metadata("Cube2".to_string());
}
let result = cube1.intersection(&cube2);
for poly in &result.polygons {
let data = poly.metadata().unwrap();
assert!(
data == "Cube1" || data == "Cube2",
"Intersection polygon has unexpected shared data = {:?}",
data
);
}
}
#[test]
fn test_flip_invert_metadata() {
let mut csg = Mesh::cube(2.0, None);
for p in &mut csg.polygons {
p.set_metadata("MyCube".to_string());
}
let inverted = csg.inverse();
for poly in &inverted.polygons {
assert_eq!(poly.metadata(), Some(&"MyCube".to_string()));
}
}
#[test]
fn test_subdivide_metadata() {
let poly = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(2.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(2.0, 2.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 2.0, 0.0), Vector3::z()),
],
Some("LargeQuad".to_string()),
);
let csg = Mesh::from_polygons(&[poly], None);
let subdivided = csg.subdivide_triangles(1.try_into().expect("not 0"));
assert!(subdivided.polygons.len() > 1);
for spoly in &subdivided.polygons {
assert_eq!(spoly.metadata(), Some(&"LargeQuad".to_string()));
}
}
#[test]
fn test_transform_metadata() {
let poly = Polygon::new(
vec![
Vertex::new(Point3::origin(), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
],
Some("Tri".to_string()),
);
let csg = Mesh::from_polygons(&[poly], None);
let csg_trans = csg.translate(10.0, 5.0, 0.0);
let csg_scale = csg_trans.scale(2.0, 2.0, 1.0);
let csg_rot = csg_scale.rotate(0.0, 0.0, 45.0);
for poly in &csg_rot.polygons {
assert_eq!(poly.metadata(), Some(&"Tri".to_string()));
}
}
#[test]
fn test_complex_metadata_struct_in_boolean_ops() {
#[derive(Debug, Clone, PartialEq)]
struct Color(u8, u8, u8);
let mut csg1 = Mesh::cube(2.0, None);
for p in &mut csg1.polygons {
p.set_metadata(Color(255, 0, 0));
}
let mut csg2 = Mesh::cube(2.0, None).translate(0.5, 0.5, 0.5);
for p in &mut csg2.polygons {
p.set_metadata(Color(0, 255, 0));
}
let unioned = csg1.union(&csg2);
for poly in &unioned.polygons {
let col = poly.metadata().unwrap();
assert!(
*col == Color(255, 0, 0) || *col == Color(0, 255, 0),
"Unexpected color in union: {:?}",
col
);
}
}
#[test]
fn test_square_ccw_ordering() {
let square = Sketch::<()>::square(2.0, None);
let mp = square.to_multipolygon();
assert_eq!(mp.0.len(), 1);
let poly = &mp.0[0];
let area = poly.signed_area();
assert!(area > 0.0, "Square vertices are not CCW ordered");
}
#[test]
#[cfg(feature = "offset")]
fn test_offset_2d_positive_distance_grows() {
let square = Sketch::<()>::square(2.0, None); let offset = square.offset(0.5);
let mp = offset.to_multipolygon();
assert_eq!(mp.0.len(), 1);
let poly = &mp.0[0];
let area = poly.signed_area();
assert!(
area > 4.0,
"Offset with positive distance did not grow the square"
);
}
#[test]
#[cfg(feature = "offset")]
fn test_offset_2d_negative_distance_shrinks() {
let square = Sketch::<()>::square(2.0, None); let offset = square.offset(-0.5);
let mp = offset.to_multipolygon();
assert_eq!(mp.0.len(), 1);
let poly = &mp.0[0];
let area = poly.signed_area();
assert!(
area < 4.0,
"Offset with negative distance did not shrink the square"
);
}
#[test]
fn test_polygon_2d_enforce_ccw_ordering() {
let points_cw = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
let csg_cw = Sketch::<()>::polygon(&points_cw, None);
csg_cw.renormalize();
let poly = &csg_cw.geometry.0[0];
let area = poly.signed_area();
assert!(area > 0.0, "Polygon ordering was not corrected to CCW");
}
#[test]
#[cfg(feature = "offset")]
fn test_circle_offset_2d() {
let circle = Sketch::<()>::circle(1.0, 32, None);
let offset_grow = circle.offset(0.2); let offset_shrink = circle.offset(-0.2);
let grow = offset_grow.to_multipolygon();
let shrink = offset_shrink.to_multipolygon();
let grow_area = grow.0[0].signed_area();
let shrink_area = shrink.0[0].signed_area();
let original_area = 3.141592653589793;
assert!(
grow_area > original_area,
"Offset with positive distance did not grow the circle"
);
assert!(
shrink_area < original_area,
"Offset with negative distance did not shrink the circle"
);
}
fn make_polygon_3d(points: &[[Real; 3]]) -> Polygon<()> {
let mut verts = Vec::new();
for p in points {
let pos = Point3::new(p[0], p[1], p[2]);
let normal = Vector3::z();
verts.push(Vertex::new(pos, normal));
}
Polygon::new(verts, None)
}
#[test]
fn test_same_number_of_vertices() {
let bottom = make_polygon_3d(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.5, 0.0]]);
let top = make_polygon_3d(&[[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.5, 0.5, 1.0]]);
let csg = Sketch::loft(&bottom, &top, true).unwrap();
assert_eq!(
csg.polygons.len(),
1 + 1 + 3
);
}
#[test]
fn test_different_number_of_vertices_panics() {
let bottom = make_polygon_3d(&[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 1.0, 0.0]]);
let top = make_polygon_3d(&[
[0.0, 0.0, 2.0],
[2.0, 0.0, 2.0],
[2.0, 2.0, 2.0],
[0.0, 2.0, 2.0],
]);
let result = Sketch::loft(&bottom, &top, true);
assert!(matches!(result, Err(ValidationError::MismatchedVertices)));
}
#[test]
fn test_consistent_winding() {
let bottom = make_polygon_3d(&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
]);
let top = make_polygon_3d(&[
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
]);
let csg = Sketch::loft(&bottom, &top, false).unwrap();
assert_eq!(csg.polygons.len(), 6);
for poly in &csg.polygons {
assert!(poly.vertices.len() >= 3);
}
}
#[test]
fn test_inverted_orientation() {
let bottom = make_polygon_3d(&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
]);
let mut top = make_polygon_3d(&[
[0.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
]);
top.flip();
let csg = Sketch::loft(&bottom, &top, false).unwrap();
assert_eq!(csg.polygons.len(), 6);
let bbox = csg.bounding_box();
assert!(
bbox.mins.z < bbox.maxs.z,
"Should have a non-zero height in the Z dimension"
);
}
#[test]
fn test_union_of_extruded_shapes() {
let bottom1 = make_polygon_3d(&[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 1.0, 0.0]]);
let top1 = make_polygon_3d(&[[0.0, 0.0, 1.0], [2.0, 0.0, 1.0], [1.0, 1.0, 1.0]]);
let csg1 = Sketch::loft(&bottom1, &top1, true).unwrap();
let bottom2 = make_polygon_3d(&[
[1.0, -0.2, 0.5],
[2.0, -0.2, 0.5],
[2.0, 0.8, 0.5],
[1.0, 0.8, 0.5],
]);
let top2 = make_polygon_3d(&[
[1.0, -0.2, 1.5],
[2.0, -0.2, 1.5],
[2.0, 0.8, 1.5],
[1.0, 0.8, 1.5],
]);
let csg2 = Sketch::loft(&bottom2, &top2, true).unwrap();
let unioned = csg1.union(&csg2);
assert!(!unioned.polygons.is_empty());
let bbox = unioned.bounding_box();
assert!(bbox.mins.z <= 0.0 + EPSILON);
assert!(bbox.maxs.z >= 1.5 - EPSILON);
}
#[test]
fn test_flatten_cube() {
let cube = Mesh::<()>::cube(2.0, None);
let flattened = cube.flatten();
assert_eq!(
flattened.geometry.len(),
1,
"Flattened cube should have 1 face in z=0"
);
let bbox = flattened.bounding_box();
let thickness = bbox.maxs.z - bbox.mins.z;
assert!(
thickness.abs() < EPSILON,
"Flattened shape should have negligible thickness in z"
);
}
#[test]
fn test_slice_cylinder() {
let cyl = Mesh::<()>::cylinder(1.0, 2.0, 32, None).center();
let cross_section = cyl.slice(Plane::from_normal(Vector3::z(), 0.0));
assert_eq!(
cross_section.geometry.len(),
1,
"Slicing a cylinder at z=0 should yield exactly 1 cross-section polygon"
);
let poly_geom = &cross_section.geometry.0[0];
let poly = match poly_geom {
Geometry::Polygon(p) => p,
_ => panic!("Cross-section geometry is not a polygon"),
};
let vcount = poly.exterior().0.len() - 1;
assert!(
vcount >= 3 && vcount <= 40,
"Expected cross-section circle to have a number of edges ~32, got {}",
vcount
);
}
fn polygon_from_xy_points(xy_points: &[[Real; 2]]) -> Polygon<()> {
assert!(xy_points.len() >= 3, "Need at least 3 points for a polygon.");
let normal = Vector3::z();
let vertices: Vec<Vertex> = xy_points
.iter()
.map(|&[x, y]| Vertex::new(Point3::new(x, y, 0.0), normal))
.collect();
Polygon::new(vertices, None)
}
#[test]
fn test_flatten_and_union_single_polygon() {
let square_poly =
polygon_from_xy_points(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
let csg = Mesh::from_polygons(&[square_poly], None);
let flat_csg = csg.flatten();
assert!(
!flat_csg.geometry.0[0].is_empty(),
"Result should not be empty"
);
let bb = flat_csg.bounding_box();
assert_eq!(bb.mins.x, 0.0);
assert_eq!(bb.mins.y, 0.0);
assert_eq!(bb.maxs.x, 1.0);
assert_eq!(bb.maxs.y, 1.0);
}
#[test]
fn test_flatten_and_union_two_overlapping_squares() {
let square1 = polygon_from_xy_points(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
let square2 = polygon_from_xy_points(&[[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]]);
let csg = Mesh::from_polygons(&[square1, square2], None);
let flat_csg = csg.flatten();
assert!(
!flat_csg.geometry.0[0].is_empty(),
"Union should not be empty"
);
let bb = flat_csg.bounding_box();
assert_eq!(bb.mins.x, 0.0);
assert_eq!(bb.maxs.x, 2.0);
assert_eq!(bb.mins.y, 0.0);
assert_eq!(bb.maxs.y, 1.0);
}
#[test]
fn test_flatten_and_union_two_disjoint_squares() {
let square_a = polygon_from_xy_points(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
let square_b = polygon_from_xy_points(&[[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [2.0, 3.0]]);
let csg = Mesh::from_polygons(&[square_a, square_b], None);
let flat_csg = csg.flatten();
assert!(!flat_csg.geometry.0[0].is_empty());
}
#[test]
fn test_flatten_and_union_near_xy_plane() {
let normal = Vector3::z();
let poly1 = Polygon::<()>::new(
vec![
Vertex::new(Point3::new(0.0, 0.0, 1e-6), normal),
Vertex::new(Point3::new(1.0, 0.0, 1e-6), normal),
Vertex::new(Point3::new(1.0, 1.0, 1e-6), normal),
Vertex::new(Point3::new(0.0, 1.0, 1e-6), normal),
],
None,
);
let csg = Mesh::from_polygons(&[poly1], None);
let flat_csg = csg.flatten();
assert!(
!flat_csg.geometry.0[0].is_empty(),
"Should flatten to a valid polygon"
);
let bb = flat_csg.bounding_box();
assert_eq!(bb.mins.x, 0.0);
assert_eq!(bb.maxs.x, 1.0);
assert_eq!(bb.mins.y, 0.0);
assert_eq!(bb.maxs.y, 1.0);
}
#[test]
fn test_flatten_and_union_collinear_edges() {
let rect1 = polygon_from_xy_points(&[[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]]);
let rect2 = polygon_from_xy_points(&[
[2.0, 0.0],
[4.0, 0.0],
[4.0, 1.001], [2.0, 1.0],
]);
let csg = Mesh::<()>::from_polygons(&[rect1, rect2], None);
let flat_csg = csg.flatten();
assert!(!flat_csg.geometry.0[0].is_empty());
let bb = flat_csg.bounding_box();
assert!((bb.maxs.x - 4.0).abs() < 1e-5, "Should span up to x=4.0");
assert!((bb.maxs.y - 1.001).abs() < 1e-3);
}
#[test]
fn test_flatten_and_union_debug() {
let cube = Mesh::<()>::cube(2.0, None);
let flattened = cube.flatten();
assert!(
!flattened.geometry.0[0].is_empty(),
"Flattened cube should not be empty"
);
let area = flattened.geometry.0[0].signed_area();
assert!(area > 3.9, "Flattened cube too small");
}
#[test]
fn test_contains_vertex() {
let csg_cube = Mesh::<()>::cube(6.0, None);
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 3.0)));
assert!(csg_cube.contains_vertex(&Point3::new(1.0, 2.0, 5.9)));
assert!(!csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 6.0)));
assert!(!csg_cube.contains_vertex(&Point3::new(3.0, 3.0, -6.0)));
assert!(!csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 0.0)));
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 0.01)));
#[cfg(feature = "f64")]
{
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 5.99999999)));
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 6.0 - 1e-11)));
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 6.0 - 1e-14)));
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 5.9 + 9e-9)));
}
#[cfg(feature = "f32")]
{
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 5.999999)));
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 6.0 - 1e-6)));
assert!(csg_cube.contains_vertex(&Point3::new(3.0, 3.0, 5.9 + 9e-9)));
}
assert!(csg_cube.contains_vertex(&Point3::new(3.0, -3.0, 3.0)));
assert!(!csg_cube.contains_vertex(&Point3::new(3.0, -3.01, 3.0)));
assert!(csg_cube.contains_vertex(&Point3::new(0.01, 4.0, 3.0)));
assert!(!csg_cube.contains_vertex(&Point3::new(-0.01, 4.0, 3.0)));
let csg_cube_hole = Mesh::<()>::cube(4.0, None);
let cube_with_hole = csg_cube.difference(&csg_cube_hole);
assert!(!cube_with_hole.contains_vertex(&Point3::new(0.01, 4.0, 3.0)));
assert!(cube_with_hole.contains_vertex(&Point3::new(0.01, 4.01, 3.0)));
assert!(!cube_with_hole.contains_vertex(&Point3::new(-0.01, 4.0, 3.0)));
assert!(cube_with_hole.contains_vertex(&Point3::new(1.0, 2.0, 5.9)));
assert!(!cube_with_hole.contains_vertex(&Point3::new(3.0, 3.0, 6.0)));
let csg_sphere = Mesh::<()>::sphere(6.0, 14, 14, None);
assert!(csg_sphere.contains_vertex(&Point3::new(3.0, 3.0, 3.0)));
assert!(csg_sphere.contains_vertex(&Point3::new(-3.0, -3.0, -3.0)));
assert!(!csg_sphere.contains_vertex(&Point3::new(1.0, 2.0, 5.9)));
assert!(!csg_sphere.contains_vertex(&Point3::new(1.0, 1.0, 5.8)));
assert!(!csg_sphere.contains_vertex(&Point3::new(0.0, 3.0, 5.8)));
assert!(csg_sphere.contains_vertex(&Point3::new(0.0, 0.0, 5.8)));
assert!(!csg_sphere.contains_vertex(&Point3::new(3.0, 3.0, 6.0)));
assert!(!csg_sphere.contains_vertex(&Point3::new(3.0, 3.0, -6.0)));
assert!(csg_sphere.contains_vertex(&Point3::new(3.0, 3.0, 0.0)));
assert!(csg_sphere.contains_vertex(&Point3::new(0.0, 0.0, -5.8)));
assert!(!csg_sphere.contains_vertex(&Point3::new(3.0, 3.0, -5.8)));
assert!(!csg_sphere.contains_vertex(&Point3::new(3.0, 3.0, -6.01)));
assert!(csg_sphere.contains_vertex(&Point3::new(3.0, 3.0, 0.01)));
}
#[test]
fn test_union_crash() {
let items: [Mesh<()>; 2] = [
Mesh::from_polygons(
&[
Polygon::new(
vec![
Vertex {
pos: Point3::new(640.0, 0.0, 640.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
Vertex {
pos: Point3::new(768.0, 0.0, 128.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 0.0, 256.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
Vertex {
pos: Point3::new(1024.0, 0.0, 640.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(1024.0, 256.0, 640.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 256.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
Vertex {
pos: Point3::new(768.0, 256.0, 128.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
Vertex {
pos: Point3::new(640.0, 256.0, 640.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(640.0, 0.0, 640.0),
normal: Vector3::new(
0.9701425433158875,
-0.0,
0.24253563582897186,
),
},
Vertex {
pos: Point3::new(640.0, 256.0, 640.0),
normal: Vector3::new(
0.9701425433158875,
-0.0,
0.24253563582897186,
),
},
Vertex {
pos: Point3::new(768.0, 256.0, 128.0),
normal: Vector3::new(
0.9701425433158875,
-0.0,
0.24253563582897186,
),
},
Vertex {
pos: Point3::new(768.0, 0.0, 128.0),
normal: Vector3::new(
0.9701425433158875,
-0.0,
0.24253563582897186,
),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(768.0, 0.0, 128.0),
normal: Vector3::new(
-0.24253563582897186,
0.0,
0.9701425433158875,
),
},
Vertex {
pos: Point3::new(768.0, 256.0, 128.0),
normal: Vector3::new(
-0.24253563582897186,
0.0,
0.9701425433158875,
),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 256.0),
normal: Vector3::new(
-0.24253563582897186,
0.0,
0.9701425433158875,
),
},
Vertex {
pos: Point3::new(1280.0, 0.0, 256.0),
normal: Vector3::new(
-0.24253563582897186,
0.0,
0.9701425433158875,
),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(1280.0, 0.0, 256.0),
normal: Vector3::new(
-0.8320503234863281,
0.0,
-0.5547001957893372,
),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 256.0),
normal: Vector3::new(
-0.8320503234863281,
0.0,
-0.5547001957893372,
),
},
Vertex {
pos: Point3::new(1024.0, 256.0, 640.0),
normal: Vector3::new(
-0.8320503234863281,
0.0,
-0.5547001957893372,
),
},
Vertex {
pos: Point3::new(1024.0, 0.0, 640.0),
normal: Vector3::new(
-0.8320503234863281,
0.0,
-0.5547001957893372,
),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(1024.0, 0.0, 640.0),
normal: Vector3::new(0.0, 0.0, -1.0),
},
Vertex {
pos: Point3::new(1024.0, 256.0, 640.0),
normal: Vector3::new(0.0, 0.0, -1.0),
},
Vertex {
pos: Point3::new(640.0, 256.0, 640.0),
normal: Vector3::new(0.0, 0.0, -1.0),
},
Vertex {
pos: Point3::new(640.0, 0.0, 640.0),
normal: Vector3::new(0.0, 0.0, -1.0),
},
],
None,
),
],
None,
),
Mesh::from_polygons(
&[
Polygon::new(
vec![
Vertex {
pos: Point3::new(896.0, 0.0, 768.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
Vertex {
pos: Point3::new(768.0, 0.0, 512.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 0.0, 384.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 0.0, 640.0),
normal: Vector3::new(0.0, -1.0, 0.0),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(1280.0, 256.0, 640.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 384.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
Vertex {
pos: Point3::new(768.0, 256.0, 512.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
Vertex {
pos: Point3::new(896.0, 256.0, 768.0),
normal: Vector3::new(0.0, 1.0, 0.0),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(896.0, 0.0, 768.0),
normal: Vector3::new(0.8944271802902222, 0.0, -0.4472135901451111),
},
Vertex {
pos: Point3::new(896.0, 256.0, 768.0),
normal: Vector3::new(0.8944271802902222, 0.0, -0.4472135901451111),
},
Vertex {
pos: Point3::new(768.0, 256.0, 512.0),
normal: Vector3::new(0.8944271802902222, 0.0, -0.4472135901451111),
},
Vertex {
pos: Point3::new(768.0, 0.0, 512.0),
normal: Vector3::new(0.8944271802902222, 0.0, -0.4472135901451111),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(768.0, 0.0, 512.0),
normal: Vector3::new(
0.24253563582897186,
-0.0,
0.9701425433158875,
),
},
Vertex {
pos: Point3::new(768.0, 256.0, 512.0),
normal: Vector3::new(
0.24253563582897186,
-0.0,
0.9701425433158875,
),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 384.0),
normal: Vector3::new(
0.24253563582897186,
-0.0,
0.9701425433158875,
),
},
Vertex {
pos: Point3::new(1280.0, 0.0, 384.0),
normal: Vector3::new(
0.24253563582897186,
-0.0,
0.9701425433158875,
),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(1280.0, 0.0, 384.0),
normal: Vector3::new(-1.0, 0.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 384.0),
normal: Vector3::new(-1.0, 0.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 640.0),
normal: Vector3::new(-1.0, 0.0, 0.0),
},
Vertex {
pos: Point3::new(1280.0, 0.0, 640.0),
normal: Vector3::new(-1.0, 0.0, 0.0),
},
],
None,
),
Polygon::new(
vec![
Vertex {
pos: Point3::new(1280.0, 0.0, 640.0),
normal: Vector3::new(
-0.3162277638912201,
0.0,
-0.9486832618713379,
),
},
Vertex {
pos: Point3::new(1280.0, 256.0, 640.0),
normal: Vector3::new(
-0.3162277638912201,
0.0,
-0.9486832618713379,
),
},
Vertex {
pos: Point3::new(896.0, 256.0, 768.0),
normal: Vector3::new(
-0.3162277638912201,
0.0,
-0.9486832618713379,
),
},
Vertex {
pos: Point3::new(896.0, 0.0, 768.0),
normal: Vector3::new(
-0.3162277638912201,
0.0,
-0.9486832618713379,
),
},
],
None,
),
],
None,
),
];
let combined = items[0].union(&items[1]);
println!("{:?}", combined);
}
#[test]
fn test_mesh_quality_analysis() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let qualities = cube.analyze_triangle_quality();
assert!(
!qualities.is_empty(),
"Should have quality metrics for triangles"
);
for quality in &qualities {
assert!(
quality.quality_score >= 0.0,
"Quality score should be non-negative"
);
assert!(
quality.quality_score <= 1.0,
"Quality score should be at most 1.0"
);
assert!(quality.area > 0.0, "Triangle area should be positive");
assert!(quality.min_angle > 0.0, "Minimum angle should be positive");
assert!(quality.max_angle < PI, "Maximum angle should be less than π");
}
let mesh_metrics = cube.compute_mesh_quality();
assert!(
mesh_metrics.avg_quality >= 0.0,
"Average quality should be non-negative"
);
assert!(
mesh_metrics.min_quality >= 0.0,
"Minimum quality should be non-negative"
);
assert!(
mesh_metrics.high_quality_ratio >= 0.0,
"High quality ratio should be non-negative"
);
assert!(
mesh_metrics.high_quality_ratio <= 1.0,
"High quality ratio should be at most 1.0"
);
assert!(
mesh_metrics.avg_edge_length > 0.0,
"Average edge length should be positive"
);
}
#[test]
fn test_adaptive_mesh_refinement() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let original_polygon_count = cube.polygons.len();
let refined = cube.adaptive_refine(0.3, 2.0, 15.0);
assert!(
refined.polygons.len() >= original_polygon_count,
"Adaptive refinement should maintain or increase polygon count"
);
let aggressive_refined = cube.adaptive_refine(0.8, 1.0, 5.0);
assert!(
aggressive_refined.polygons.len() >= refined.polygons.len(),
"More aggressive refinement should result in equal or more polygons"
);
}
#[test]
fn test_laplacian_mesh_smoothing() {
let sphere: Mesh<()> = Mesh::sphere(1.0, 16, 16, None);
let original_positions: Vec<_> = sphere
.polygons
.iter()
.flat_map(|poly| poly.vertices.iter().map(|v| v.pos))
.collect();
let smoothed = sphere.laplacian_smooth(0.1, 2, false);
assert_eq!(
smoothed.polygons.len(),
sphere.polygons.len(),
"Smoothing should preserve polygon count"
);
let smoothed_positions: Vec<_> = smoothed
.polygons
.iter()
.flat_map(|poly| poly.vertices.iter().map(|v| v.pos))
.collect();
assert_eq!(
original_positions.len(),
smoothed_positions.len(),
"Should preserve vertex count"
);
let mut moved_count = 0;
for (orig, smooth) in original_positions.iter().zip(smoothed_positions.iter()) {
if (orig - smooth).norm() > 1e-10 {
moved_count += 1;
}
}
println!("Moved vertices: {}/{}", moved_count, original_positions.len());
}
#[test]
fn test_remove_poor_triangles() {
let vertices = vec![
Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
Vertex::new(Point3::new(0.5, 1e-8, 0.0), Vector3::z()), ];
let bad_polygon: Polygon<()> = Polygon::new(vertices, None);
let csg_with_bad = Mesh::from_polygons(&[bad_polygon], None);
let filtered = csg_with_bad.remove_poor_triangles(0.1);
assert!(
filtered.polygons.len() <= csg_with_bad.polygons.len(),
"Should remove or maintain triangle count"
);
}
#[test]
fn test_vertex_distance_operations() {
let v1 = Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::x());
let v2 = Vertex::new(Point3::new(3.0, 4.0, 0.0), Vector3::y());
let distance = v1.distance_to(&v2);
assert!(
(distance - 5.0).abs() < 1e-10,
"Distance should be 5.0 (3-4-5 triangle)"
);
let distance_sq = v1.distance_squared_to(&v2);
assert!(
(distance_sq - 25.0).abs() < 1e-10,
"Squared distance should be 25.0"
);
let angle = v1.normal_angle_to(&v2);
assert!(
(angle - PI / 2.0).abs() < 1e-10,
"Angle between x and y normals should be π/2"
);
}
#[test]
fn test_vertex_interpolation_methods() {
let v1 = Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::x());
let v2 = Vertex::new(Point3::new(2.0, 2.0, 2.0), Vector3::y());
let mid_linear = v1.interpolate(&v2, 0.5);
assert!(
(mid_linear.pos - Point3::new(1.0, 1.0, 1.0)).norm() < 1e-10,
"Linear interpolation midpoint should be (1,1,1)"
);
let mid_slerp = v1.slerp_interpolate(&v2, 0.5);
assert!(
(mid_slerp.pos - Point3::new(1.0, 1.0, 1.0)).norm() < 1e-10,
"SLERP position should match linear for positions"
);
assert!(
(mid_slerp.normal.norm() - 1.0).abs() < 1e-10,
"SLERP normal should be unit length"
);
}
#[test]
fn test_barycentric_interpolation() {
let v1 = Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::x());
let v2 = Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::y());
let v3 = Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z());
let centroid =
Vertex::barycentric_interpolate(&v1, &v2, &v3, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
let expected_pos = Point3::new(1.0 / 3.0, 1.0 / 3.0, 0.0);
assert!(
(centroid.pos - expected_pos).norm() < 1e-10,
"Barycentric centroid should be at (1/3, 1/3, 0)"
);
let recovered_v1 = Vertex::barycentric_interpolate(&v1, &v2, &v3, 1.0, 0.0, 0.0);
assert!(
(recovered_v1.pos - v1.pos).norm() < 1e-10,
"Barycentric should recover original vertex"
);
}
#[test]
fn test_vertex_clustering() {
let vertices = vec![
Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::x()),
Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::x()),
Vertex::new(Point3::new(0.5, 0.5, 0.0), Vector3::y()),
];
let cluster = VertexCluster::from_vertices(&vertices).expect("Should create cluster");
assert_eq!(cluster.count, 3, "Cluster should contain 3 vertices");
assert!(cluster.radius > 0.0, "Cluster should have positive radius");
let expected_centroid = Point3::new(0.5, 1.0 / 6.0, 0.0);
assert!(
(cluster.position - expected_centroid).norm() < 1e-10,
"Cluster centroid should be average of vertex positions"
);
let representative = cluster.to_vertex();
assert_eq!(
representative.pos, cluster.position,
"Representative should have cluster position"
);
assert_eq!(
representative.normal, cluster.normal,
"Representative should have cluster normal"
);
}
#[test]
fn test_mesh_connectivity_adjacency_usage() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let (vertex_map, adjacency_map) = cube.build_connectivity();
println!("Mesh connectivity analysis:");
println!(" Total unique vertices: {}", vertex_map.vertex_count());
println!(" Adjacency map entries: {}", adjacency_map.len());
assert!(
vertex_map.vertex_count() > 0,
"Vertex map should not be empty"
);
assert!(adjacency_map.len() > 0, "Adjacency map should not be empty");
let mut total_neighbors = 0;
for (vertex_idx, neighbors) in &adjacency_map {
total_neighbors += neighbors.len();
println!(" Vertex {} has {} neighbors", vertex_idx, neighbors.len());
assert!(
neighbors.len() > 0,
"Each vertex should have at least one neighbor"
);
}
println!(" Total neighbor relationships: {}", total_neighbors);
let smoothed_cube = cube.laplacian_smooth(0.1, 1, false);
assert_eq!(
cube.polygons.len(),
smoothed_cube.polygons.len(),
"Smoothing should preserve polygon count"
);
let original_pos = cube.polygons[0].vertices[0].pos;
let smoothed_pos = smoothed_cube.polygons[0].vertices[0].pos;
let position_change = (original_pos - smoothed_pos).norm();
println!(" Position change from smoothing: {:.6}", position_change);
assert!(
position_change > 1e-10,
"Smoothing should change vertex positions"
);
}
#[test]
fn test_vertex_connectivity_analysis() {
let sphere: Mesh<()> = Mesh::sphere(1.0, 16, 8, None);
let (vertex_map, adjacency_map) = sphere.build_connectivity();
let mut vertex_positions = HashMap::new();
for (pos, idx) in vertex_map.get_vertex_positions() {
vertex_positions.insert(*idx, *pos);
}
let mut total_regularity = 0.0;
let mut vertex_count = 0;
for &vertex_idx in adjacency_map.keys().take(5) {
let (valence, regularity) =
crate::mesh::vertex::Vertex::analyze_connectivity_with_index(
vertex_idx,
&adjacency_map,
);
println!(
"Vertex {}: valence={}, regularity={:.3}",
vertex_idx, valence, regularity
);
assert!(valence > 0, "Vertex should have positive valence");
assert!(
regularity >= 0.0 && regularity <= 1.0,
"Regularity should be in [0,1]"
);
total_regularity += regularity;
vertex_count += 1;
}
let avg_regularity = total_regularity / vertex_count as Real;
println!("Average regularity: {:.3}", avg_regularity);
assert!(
avg_regularity > 0.1,
"Sphere vertices should have decent regularity"
);
}
#[test]
fn test_mesh_quality_with_adjacency() {
let cube: Mesh<()> = Mesh::cube(2.0, None).triangulate();
let qualities = cube.analyze_triangle_quality();
println!("Triangle quality analysis:");
println!(" Number of triangles: {}", qualities.len());
if !qualities.is_empty() {
let avg_quality: Real =
qualities.iter().map(|q| q.quality_score).sum::<Real>() / qualities.len() as Real;
println!(" Average quality score: {:.3}", avg_quality);
let min_quality = qualities
.iter()
.map(|q| q.quality_score)
.fold(Real::INFINITY, |a, b| a.min(b));
println!(" Minimum quality score: {:.3}", min_quality);
assert!(avg_quality > 0.1, "Cube triangles should have decent quality");
assert!(min_quality >= 0.0, "Quality scores should be non-negative");
}
let metrics = cube.compute_mesh_quality();
println!("Mesh quality metrics:");
println!(" Average quality: {:.3}", metrics.avg_quality);
println!(" Minimum quality: {:.3}", metrics.min_quality);
println!(" High quality ratio: {:.3}", metrics.high_quality_ratio);
println!(" Sliver count: {}", metrics.sliver_count);
println!(" Average edge length: {:.3}", metrics.avg_edge_length);
println!(" Edge length std: {:.3}", metrics.edge_length_std);
assert!(
metrics.avg_quality >= 0.0,
"Average quality should be non-negative"
);
assert!(
metrics.min_quality >= 0.0,
"Minimum quality should be non-negative"
);
assert!(
metrics.high_quality_ratio >= 0.0 && metrics.high_quality_ratio <= 1.0,
"High quality ratio should be in [0,1]"
);
}
#[test]
fn test_adjacency_map_actually_used() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
let (vertex_map, adjacency_map) = cube.build_connectivity();
assert!(!adjacency_map.is_empty(), "Adjacency map should not be empty");
let mut has_multiple_neighbors = false;
for neighbors in adjacency_map.values() {
if neighbors.len() > 2 {
has_multiple_neighbors = true;
break;
}
}
assert!(
has_multiple_neighbors,
"Some vertices should have multiple neighbors"
);
let smoothed_0_iterations = cube.laplacian_smooth(0.0, 1, false);
let smoothed_1_iterations = cube.laplacian_smooth(0.1, 1, false);
let original_first_vertex = cube.polygons[0].vertices[0].pos;
let zero_smoothed_first_vertex = smoothed_0_iterations.polygons[0].vertices[0].pos;
let smoothed_first_vertex = smoothed_1_iterations.polygons[0].vertices[0].pos;
let zero_diff = (original_first_vertex - zero_smoothed_first_vertex).norm();
assert!(
zero_diff < 1e-10,
"Zero smoothing should not change positions"
);
let smooth_diff = (original_first_vertex - smoothed_first_vertex).norm();
assert!(
smooth_diff > 1e-10,
"Smoothing should change vertex positions"
);
println!("Adjacency map usage verified:");
println!(" Vertex count: {}", vertex_map.vertex_count());
println!(" Adjacency entries: {}", adjacency_map.len());
println!(" Smoothing effect: {:.6}", smooth_diff);
}
#[test]
fn test_cube_basics() {
let cube: Mesh<()> = Mesh::cube(2.0, None);
assert_eq!(cube.polygons.len(), 6);
for poly in &cube.polygons {
assert_eq!(poly.vertices.len(), 4);
}
let bbox = cube.bounding_box();
let width = bbox.maxs.x - bbox.mins.x;
let height = bbox.maxs.y - bbox.mins.y;
let depth = bbox.maxs.z - bbox.mins.z;
assert!((width - 2.0).abs() < 1e-10, "Width should be 2.0");
assert!((height - 2.0).abs() < 1e-10, "Height should be 2.0");
assert!((depth - 2.0).abs() < 1e-10, "Depth should be 2.0");
}
#[test]
fn test_cube_intersection() {
let cube1: Mesh<()> = Mesh::cube(2.0, None);
let cube2: Mesh<()> = Mesh::cube(2.0, None).translate(1.0, 0.0, 0.0);
let intersection = cube1.intersection(&cube2);
assert!(
intersection.polygons.len() > 0,
"Intersection should produce some polygons"
);
let bbox = intersection.bounding_box();
let width = bbox.maxs.x - bbox.mins.x;
assert!(
width > 0.0 && width < 2.0,
"Intersection width should be between 0 and 2"
);
}
#[test]
fn test_taubin_smoothing() {
let sphere: Mesh<()> = Mesh::sphere(1.0, 16, 16, None);
let original_positions: Vec<_> = sphere
.polygons
.iter()
.flat_map(|poly| poly.vertices.iter().map(|v| v.pos))
.collect();
let smoothed = sphere.taubin_smooth(0.1, -0.105, 2, false);
assert_eq!(
smoothed.polygons.len(),
sphere.polygons.len(),
"Smoothing should preserve polygon count"
);
let smoothed_positions: Vec<_> = smoothed
.polygons
.iter()
.flat_map(|poly| poly.vertices.iter().map(|v| v.pos))
.collect();
let mut moved_count = 0;
for (orig, smooth) in original_positions.iter().zip(smoothed_positions.iter()) {
if (orig - smooth).norm() > 1e-10 {
moved_count += 1;
}
}
assert!(
moved_count > 0,
"Taubin smoothing should change vertex positions"
);
}