feat: Sprint 9 — 3D Delaunay/B-Rep/Boolean 测试补全 + 文档更新
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
#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 core::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());
|
||||
}
|
||||
Reference in New Issue
Block a user