256 lines
10 KiB
C++
256 lines
10 KiB
C++
#include <gtest/gtest.h>
|
||
#include "vde/boolean/boolean_mesh.h"
|
||
#include "vde/mesh/halfedge_mesh.h"
|
||
#include <cmath>
|
||
|
||
using namespace vde::boolean;
|
||
using namespace vde::mesh;
|
||
using vde::Point3D;
|
||
|
||
// ── 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<Point3D> 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<std::array<int,3>> 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<Point3D> 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<std::array<int,3>> 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());
|
||
}
|