use ahash::AHashSet;
use cgar::geometry::util::EPS;
use cgar::geometry::{point::Point, spatial_element::SpatialElement, vector::Vector};
use cgar::mesh::{
basic_types::Mesh,
edge_collapse::{CollapseOpts, CollapsePlan, CollapseReject, Midpoint, Placement},
};
use cgar::numeric::cgar_f64::CgarF64;
type TestMesh = Mesh<CgarF64, 3>;
type TestPoint = Point<CgarF64, 3>;
use cgar::io::obj::write_obj;
fn create_simple_triangle() -> TestMesh {
let mut mesh = TestMesh::new();
let v0 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v1 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(1.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v2 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.5),
CgarF64::from(1.0),
CgarF64::from(0.0),
]));
mesh.add_triangle(v0, v1, v2);
mesh.build_boundary_loops();
mesh
}
fn create_square_mesh() -> TestMesh {
let mut mesh = TestMesh::new();
let v0 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v1 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(1.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v2 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(1.0),
CgarF64::from(1.0),
CgarF64::from(0.0),
]));
let v3 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.0),
CgarF64::from(1.0),
CgarF64::from(0.0),
]));
mesh.add_triangle(v0, v1, v2);
mesh.add_triangle(v0, v2, v3);
mesh.build_boundary_loops();
mesh
}
fn create_grid4x4_mesh() -> TestMesh {
let mut mesh = TestMesh::new();
let id = |x: usize, y: usize| -> usize { y * 4 + x };
for y in 0..4 {
for x in 0..4 {
mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(x as f64),
CgarF64::from(y as f64),
CgarF64::from(0.0),
]));
}
}
for y in 0..3 {
for x in 0..3 {
let v00 = id(x, y);
let v10 = id(x + 1, y);
let v01 = id(x, y + 1);
let v11 = id(x + 1, y + 1);
mesh.add_triangle(v00, v10, v11); mesh.add_triangle(v00, v11, v01);
}
}
mesh.build_boundary_loops();
mesh
}
#[test]
fn test_midpoint_placement() {
let mesh = create_square_mesh();
let placement = Midpoint;
let p_star = placement.place(&mesh, 0, 2);
assert!((p_star[0].0 - 0.5).abs() < EPS);
assert!((p_star[1].0 - 0.5).abs() < EPS);
assert!((p_star[2].0 - 0.0).abs() < EPS);
}
#[test]
fn test_collapse_options_default() {
let opts = CollapseOpts::<CgarF64>::default();
assert!(!opts.forbid_border);
assert!(opts.forbid_normal_flip);
}
#[test]
fn test_link_condition_interior_edge() {
let mesh = create_square_mesh();
let pr = mesh.ring_pair(0, 2).expect("no ring_pair for (0,2)");
dbg!(&pr.opposite_a, &pr.opposite_b);
dbg!(&pr.ring0.neighbors_ccw, &pr.ring1.neighbors_ccw);
assert!(mesh.check_link_condition_triangle(0, 2));
}
#[test]
fn test_link_condition_border_edge() {
let mesh = create_simple_triangle();
assert!(mesh.check_link_condition_triangle(0, 1));
assert!(mesh.check_link_condition_triangle(1, 2));
assert!(mesh.check_link_condition_triangle(2, 0));
}
#[test]
fn test_non_adjacent_vertices_fail_link_condition() {
let mut mesh = TestMesh::new();
let v0 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v1 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(1.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v2 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.5),
CgarF64::from(1.0),
CgarF64::from(0.0),
]));
let v3 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(2.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v4 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(3.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v5 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(2.5),
CgarF64::from(1.0),
CgarF64::from(0.0),
]));
mesh.add_triangle(v0, v1, v2);
mesh.add_triangle(v3, v4, v5);
mesh.build_boundary_loops();
assert!(!mesh.check_link_condition_triangle(v0, v3));
}
#[test]
fn test_duplicate_edge_detection() {
let mut mesh = TestMesh::new();
let v0 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v1 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(1.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v2 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.5),
CgarF64::from(1.0),
CgarF64::from(0.0),
]));
let v3 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.5),
CgarF64::from(-1.0),
CgarF64::from(0.0),
]));
mesh.add_triangle(v0, v1, v2);
mesh.add_triangle(v0, v3, v1);
mesh.build_boundary_loops();
assert!(!mesh.would_create_duplicate_edges(v0, v1));
}
#[test]
fn test_two_gon_detection() {
let mesh = create_simple_triangle();
assert!(!mesh.would_create_2gons(0, 1));
assert!(!mesh.would_create_2gons(1, 2));
assert!(!mesh.would_create_2gons(2, 0));
}
#[test]
fn test_collapse_begin_valid_edge() {
let mut mesh = create_square_mesh();
mesh.build_boundary_loops();
let opts = CollapseOpts::default();
let placement = Midpoint;
let result = mesh.collapse_edge_begin_vertices(0, 2, &placement, &opts);
match result {
Ok(plan) => {
assert!(plan.v_keep == 0 || plan.v_keep == 2);
assert!(plan.v_gone == 0 || plan.v_gone == 2);
assert_ne!(plan.v_keep, plan.v_gone);
}
Err(reason) => {
println!("Collapse rejected: {:?}", reason);
}
}
}
#[test]
fn test_border_edge_forbidden() {
let mut mesh = create_simple_triangle();
mesh.build_boundary_loops();
let mut opts = CollapseOpts::default();
opts.forbid_border = true;
let placement = Midpoint;
let result = mesh.collapse_edge_begin_vertices(0, 1, &placement, &opts);
assert!(matches!(result, Err(CollapseReject::BorderForbidden)));
}
#[test]
fn test_border_edge_allowed() {
let mut mesh = create_simple_triangle();
mesh.build_boundary_loops();
let mut opts = CollapseOpts::default();
opts.forbid_border = false;
opts.forbid_normal_flip = false; let placement = Midpoint;
let result = mesh.collapse_edge_begin_vertices(0, 1, &placement, &opts);
match result {
Err(CollapseReject::BorderForbidden) => {
panic!("Should not reject for border when forbid_border = false");
}
_ => {} }
}
#[test]
fn test_degenerate_area_rejection() {
let mut mesh = TestMesh::new();
let v0 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v1 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(1.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v2 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.5),
CgarF64::from(1e-10),
CgarF64::from(0.0),
]));
mesh.add_triangle(v0, v1, v2);
mesh.build_boundary_loops();
let mut opts = CollapseOpts::default();
opts.area_eps2 = CgarF64::from(1e-15); let placement = Midpoint;
let result = mesh.collapse_edge_begin_vertices(0, 1, &placement, &opts);
match result {
Err(CollapseReject::DegenerateFace) => {
}
_ => {
}
}
}
#[test]
fn test_normal_flip_detection() {
let mut mesh = TestMesh::new();
let v0 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v1 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(1.0),
CgarF64::from(0.0),
CgarF64::from(0.0),
]));
let v2 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.5),
CgarF64::from(1.0),
CgarF64::from(0.0),
]));
let v3 = mesh.add_vertex(TestPoint::from_vals([
CgarF64::from(0.5),
CgarF64::from(-2.0),
CgarF64::from(0.0),
]));
mesh.add_triangle(v0, v1, v2);
mesh.add_triangle(v0, v3, v1);
mesh.build_boundary_loops();
let mut opts = CollapseOpts::default();
opts.forbid_normal_flip = true;
let placement = Midpoint;
let result = mesh.collapse_edge_begin_vertices(0, 1, &placement, &opts);
match result {
Err(CollapseReject::NormalFlip) => {
}
Ok(_) => {
}
Err(other) => {
println!("Rejected for other reason: {:?}", other);
}
}
}
#[test]
fn test_collapse_commit_after_begin() {
let mut mesh = create_square_mesh();
let opts = CollapseOpts::default();
let placement = Midpoint;
let original_vertex_count = mesh.vertices.len();
let original_face_count = mesh.faces.iter().filter(|f| !f.removed).count();
println!("got here 0");
let plan = mesh.collapse_edge_begin_vertices(0, 2, &placement, &opts);
if let Ok(plan) = plan {
println!("got here 1");
let result = mesh.collapse_edge_commit(plan);
if result.is_ok() {
let remaining_faces = mesh.faces.iter().filter(|f| !f.removed).count();
assert!(remaining_faces < original_face_count);
assert_eq!(mesh.vertices.len(), original_vertex_count); }
} else {
panic!("Edge collapse failed to begin, {:?}", plan.err());
}
}
#[test]
fn test_ring_pair_computation() {
let mesh = create_square_mesh();
let ring_pair = mesh.ring_pair(0, 2);
assert!(ring_pair.is_some());
if let Some(pr) = ring_pair {
assert!(!pr.ring0.neighbors_ccw.is_empty());
assert!(!pr.ring1.neighbors_ccw.is_empty());
let set0: AHashSet<_> = pr
.ring0
.neighbors_ccw
.iter()
.copied()
.filter(|&n| n != 2) .collect();
let set1: AHashSet<_> = pr
.ring1
.neighbors_ccw
.iter()
.copied()
.filter(|&n| n != 0) .collect();
let common_neighbors: AHashSet<_> = set0.intersection(&set1).copied().collect();
assert_eq!(common_neighbors.len(), 2); assert!(common_neighbors.contains(&1));
assert!(common_neighbors.contains(&3));
}
}
#[test]
fn test_collapse_on_fully_interior_edge() {
let mut mesh = create_grid4x4_mesh();
let _ = write_obj(&mesh, "/mnt/v/cgar_meshes/grid4x4.obj");
let id = |x: usize, y: usize| -> usize { y * 4 + x };
let v_keep = id(1, 1); let v_gone = id(1, 2);
let pr = mesh.ring_pair(v_keep, v_gone).expect("not adjacent?");
assert!(
pr.opposite_a.is_some() && pr.opposite_b.is_some(),
"edge not interior"
);
let original_faces = mesh.faces.iter().filter(|f| !f.removed).count();
let original_verts = mesh.vertices.len();
let placement = Midpoint;
let opts = CollapseOpts::default();
let plan = mesh
.collapse_edge_begin_vertices(v_keep, v_gone, &placement, &opts)
.expect("begin failed");
mesh.collapse_edge_commit(plan).expect("commit failed");
let remaining_faces = mesh.faces.iter().filter(|f| !f.removed).count();
assert_eq!(remaining_faces, original_faces - 2);
assert_eq!(mesh.vertices.len(), original_verts);
mesh.build_boundary_loops();
let ring = mesh.vertex_ring_ccw(v_keep);
assert_eq!(ring.halfedges_ccw.len(), ring.neighbors_ccw.len());
let _ = write_obj(&mesh, "/mnt/v/cgar_meshes/grid4x4_collapse.obj");
}
#[test]
fn test_tangential_smooth_grid_moves_on_irregular_sampling() {
let mut m = create_grid4x4_mesh();
let id = |x: usize, y: usize| -> usize { y * 4 + x };
m.vertices[id(1, 1)].position.coords[0] += &CgarF64(0.15); m.vertices[id(2, 1)].position.coords[1] -= &CgarF64(0.10); m.vertices[id(1, 2)].position.coords[0] -= &CgarF64(0.07);
let _ = write_obj(&m, "/mnt/v/cgar_meshes/deformed_4x4.obj");
let before = m.faces.iter().filter(|f| !f.removed).count();
let moved = m.smooth_tangential(6, CgarF64(0.2), &CgarF64(10.0));
assert!(
moved,
"with in-plane irregularity, tangential smoothing should move vertices"
);
let after = m.faces.iter().filter(|f| !f.removed).count();
assert_eq!(before, after); let _ = write_obj(&m, "/mnt/v/cgar_meshes/deformed_4x4_tangential_smooth.obj");
}