#include #include "vde/boolean/boolean_mesh.h" #include "vde/mesh/halfedge_mesh.h" #include using namespace vde::boolean; using namespace vde::mesh; using namespace vde::core; // ── Helper: build a box mesh (12 triangles, 8 vertices) ── // Face normals point outward (CCW winding from outside) static HalfedgeMesh make_box(double w, double h, double d) { double hw = w * 0.5, hh = h * 0.5, hd = d * 0.5; std::vector verts = { {-hw, -hh, -hd}, { hw, -hh, -hd}, { hw, hh, -hd}, {-hw, hh, -hd}, // 0-3: front {-hw, -hh, hd}, { hw, -hh, hd}, { hw, hh, hd}, {-hw, hh, hd}, // 4-7: back }; // CCW winding viewed from outside std::vector> tris = { {0, 1, 2}, {0, 2, 3}, // front (-z) {5, 4, 6}, {6, 4, 7}, // back (+z) {1, 5, 6}, {1, 6, 2}, // right (+x) {4, 0, 3}, {4, 3, 7}, // left (-x) {0, 4, 5}, {0, 5, 1}, // bottom (-y) {3, 2, 6}, {3, 6, 7}, // top (+y) }; HalfedgeMesh mesh; mesh.build_from_triangles(verts, tris); return mesh; } // ── Helper: build a translated box ── static HalfedgeMesh make_box_at(double cx, double cy, double cz, double w, double h, double d) { double hw = w * 0.5, hh = h * 0.5, hd = d * 0.5; std::vector verts = { {cx-hw, cy-hh, cz-hd}, {cx+hw, cy-hh, cz-hd}, {cx+hw, cy+hh, cz-hd}, {cx-hw, cy+hh, cz-hd}, {cx-hw, cy-hh, cz+hd}, {cx+hw, cy-hh, cz+hd}, {cx+hw, cy+hh, cz+hd}, {cx-hw, cy+hh, cz+hd}, }; std::vector> tris = { {0, 1, 2}, {0, 2, 3}, // front (-z) {5, 4, 6}, {6, 4, 7}, // back (+z) {1, 5, 6}, {1, 6, 2}, // right (+x) {4, 0, 3}, {4, 3, 7}, // left (-x) {0, 4, 5}, {0, 5, 1}, // bottom (-y) {3, 2, 6}, {3, 6, 7}, // top (+y) }; HalfedgeMesh mesh; mesh.build_from_triangles(verts, tris); return mesh; } // ═══════════════════════════════════════════════════════════ // Union tests // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, Union_IntersectingBoxes_HasResult) { auto a = make_box(2, 2, 2); // centered at origin auto b = make_box_at(1, 0, 0, 2, 2, 2); // shifted +x by 1 auto result = mesh_union(a, b); EXPECT_GT(result.num_faces(), 0u); // Union should have at least as many faces as the individual boxes // (since they partially overlap, the exterior faces are kept) EXPECT_GE(result.num_faces(), a.num_faces() / 2); } TEST(BooleanMeshTest, Union_DisjointBoxes_HasAllFaces) { auto a = make_box(1, 1, 1); // centered at origin auto b = make_box_at(3, 0, 0, 1, 1, 1); // far away auto result = mesh_union(a, b); // Disjoint: both meshes should be fully retained EXPECT_EQ(result.num_faces(), a.num_faces() + b.num_faces()); } // ═══════════════════════════════════════════════════════════ // Intersection tests // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, Intersection_OverlappingBoxes_HasResult) { auto a = make_box(2, 2, 2); // [-1,1]³ auto b = make_box_at(1, 0, 0, 2, 2, 2); // [0,2]×[-1,1]×[-1,1] auto result = mesh_intersection(a, b); // Overlap region = [0,1]×[-1,1]×[-1,1] — should have faces EXPECT_GT(result.num_faces(), 0u); } TEST(BooleanMeshTest, Intersection_Disjoint_ReturnsEmpty) { auto a = make_box(1, 1, 1); auto b = make_box_at(3, 0, 0, 1, 1, 1); auto result = mesh_intersection(a, b); EXPECT_EQ(result.num_faces(), 0u); } TEST(BooleanMeshTest, Intersection_BContainsA_ReturnsA) { auto a = make_box(1, 1, 1); // small cube at origin auto b = make_box(4, 4, 4); // large cube containing a auto result = mesh_intersection(a, b); // All faces of A are inside B → intersection should have 12 faces EXPECT_EQ(result.num_faces(), a.num_faces()); } // ═══════════════════════════════════════════════════════════ // Difference tests // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, Difference_Overlapping_HasResult) { auto a = make_box(2, 2, 2); auto b = make_box_at(1, 0, 0, 2, 2, 2); auto result = mesh_difference(a, b); // A-b: faces of A whose centroids are outside B EXPECT_GT(result.num_faces(), 0u); // Should be fewer faces than original A (some faces are clipped) EXPECT_LT(result.num_faces(), a.num_faces()); } TEST(BooleanMeshTest, Difference_Disjoint_ReturnsAllA) { auto a = make_box(1, 1, 1); auto b = make_box_at(3, 0, 0, 1, 1, 1); auto result = mesh_difference(a, b); EXPECT_EQ(result.num_faces(), a.num_faces()); } TEST(BooleanMeshTest, Difference_BContainsA_ReturnsEmpty) { auto a = make_box(1, 1, 1); // small inside auto b = make_box(4, 4, 4); // large container auto result = mesh_difference(a, b); // A is fully inside B → all faces are clipped EXPECT_EQ(result.num_faces(), 0u); } // ═══════════════════════════════════════════════════════════ // Symmetric difference tests // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, SymDiff_Overlapping_HasResult) { auto a = make_box(2, 2, 2); auto b = make_box_at(1, 0, 0, 2, 2, 2); auto result = mesh_sym_diff(a, b); EXPECT_GT(result.num_faces(), 0u); } // ═══════════════════════════════════════════════════════════ // Empty mesh input tests // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, Union_EmptyA_ReturnsB) { HalfedgeMesh empty; auto b = make_box(2, 2, 2); auto result = mesh_union(empty, b); EXPECT_EQ(result.num_faces(), b.num_faces()); } TEST(BooleanMeshTest, Union_EmptyB_ReturnsA) { auto a = make_box(2, 2, 2); HalfedgeMesh empty; auto result = mesh_union(a, empty); EXPECT_EQ(result.num_faces(), a.num_faces()); } TEST(BooleanMeshTest, Intersection_EmptyA_ReturnsEmpty) { HalfedgeMesh empty; auto b = make_box(2, 2, 2); auto result = mesh_intersection(empty, b); EXPECT_EQ(result.num_faces(), 0u); } TEST(BooleanMeshTest, Difference_EmptyA_ReturnsEmpty) { HalfedgeMesh empty; auto b = make_box(2, 2, 2); auto result = mesh_difference(empty, b); EXPECT_EQ(result.num_faces(), 0u); } TEST(BooleanMeshTest, Difference_EmptyB_ReturnsA) { auto a = make_box(2, 2, 2); HalfedgeMesh empty; auto result = mesh_difference(a, empty); EXPECT_EQ(result.num_faces(), a.num_faces()); } // ═══════════════════════════════════════════════════════════ // BooleanOp dispatcher // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, Dispatch_Union) { auto a = make_box(2, 2, 2); auto b = make_box_at(3, 0, 0, 2, 2, 2); auto result = mesh_boolean(a, b, BooleanOp::Union); EXPECT_EQ(result.num_faces(), a.num_faces() + b.num_faces()); } TEST(BooleanMeshTest, Dispatch_Intersection_Disjoint) { auto a = make_box(1, 1, 1); auto b = make_box_at(3, 0, 0, 1, 1, 1); auto result = mesh_boolean(a, b, BooleanOp::Intersection); EXPECT_EQ(result.num_faces(), 0u); } // ═══════════════════════════════════════════════════════════ // Point-in-mesh classification // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, PointInside_OriginInCenteredBox) { auto box = make_box(2, 2, 2); Point3D origin(0, 0, 0); bool inside = is_point_inside_mesh(origin, box); EXPECT_TRUE(inside); } TEST(BooleanMeshTest, PointInside_FarPointOutsideBox) { auto box = make_box(2, 2, 2); Point3D far(10, 0, 0); bool inside = is_point_inside_mesh(far, box); EXPECT_FALSE(inside); } // ═══════════════════════════════════════════════════════════ // Invert / merge helpers // ═══════════════════════════════════════════════════════════ TEST(BooleanMeshTest, Invert_PreservesFaceCount) { auto box = make_box(2, 2, 2); auto inverted = invert_mesh(box); EXPECT_EQ(inverted.num_faces(), box.num_faces()); EXPECT_EQ(inverted.num_vertices(), box.num_vertices()); } TEST(BooleanMeshTest, Merge_TwoBoxes) { auto a = make_box(1, 1, 1); auto b = make_box_at(3, 0, 0, 1, 1, 1); auto merged = merge_meshes(a, b); EXPECT_EQ(merged.num_faces(), a.num_faces() + b.num_faces()); EXPECT_EQ(merged.num_vertices(), a.num_vertices() + b.num_vertices()); }