Files
ViewDesignEngine/tests/brep/test_ssi_boolean.cpp
T

340 lines
13 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include <chrono>
#include "vde/brep/brep.h"
#include "vde/brep/modeling.h"
#include "vde/brep/ssi_boolean.h"
#include "vde/brep/brep_validate.h"
#include "vde/core/exact_predicates.h"
using namespace vde::brep;
using namespace vde::core;
// ═══════════════════════════════════════════════════════════
// Helper: extract BrepModel from SSIBooleanResult
// ═══════════════════════════════════════════════════════════
namespace {
BrepModel u(const BrepModel& a, const BrepModel& b) { return ssi_boolean_union(a, b).result; }
BrepModel i(const BrepModel& a, const BrepModel& b) { return ssi_boolean_intersection(a, b).result; }
BrepModel d(const BrepModel& a, const BrepModel& b) { return ssi_boolean_difference(a, b).result; }
/// Verify result is valid and has expected face count range
void expect_valid_with_faces(const BrepModel& r, size_t min_faces, size_t max_faces) {
EXPECT_TRUE(r.is_valid());
EXPECT_GE(r.num_faces(), min_faces);
EXPECT_LE(r.num_faces(), max_faces);
}
}
// ═══════════════════════════════════════════════════════════
// Suite 1: Union — basic scenarios
// ═══════════════════════════════════════════════════════════
TEST(SSIBooleanUnionTest, Disjoint_Boxes) {
// Two non-overlapping boxes → union should produce both
auto box1 = make_box(2, 2, 2);
auto box2 = make_box(2, 2, 2);
auto result = u(box1, box2);
EXPECT_TRUE(result.is_valid());
}
TEST(SSIBooleanUnionTest, Overlapping_IdenticalBoxes) {
auto box = make_box(2, 2, 2);
auto result = u(box, box);
EXPECT_TRUE(result.is_valid());
}
TEST(SSIBooleanUnionTest, Contained_BoxInLargerBox) {
// Small box fully inside larger box → union = larger box
auto big = make_box(4, 4, 4);
auto small = make_box(2, 2, 2);
auto result = u(big, small);
EXPECT_TRUE(result.is_valid());
EXPECT_GT(result.num_faces(), 0u);
}
TEST(SSIBooleanUnionTest, BoxWithSphere) {
auto box = make_box(3, 3, 3);
auto sphere = make_sphere(1.5);
auto r = ssi_boolean_union(box, sphere);
EXPECT_TRUE(r.result.is_valid());
// SSI should produce intersection curves
EXPECT_GT(r.num_ssi_curves, 0);
}
TEST(SSIBooleanUnionTest, BoxWithCylinder) {
auto box = make_box(4, 4, 4);
auto cyl = make_cylinder(1.0, 6.0);
auto r = ssi_boolean_union(box, cyl);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIBooleanUnionTest, Commutative) {
auto box1 = make_box(2, 2, 2);
auto box2 = make_box(2, 2, 2);
auto r1 = ssi_boolean_union(box1, box2);
auto r2 = ssi_boolean_union(box2, box1);
EXPECT_TRUE(r1.result.is_valid());
EXPECT_TRUE(r2.result.is_valid());
// Both should have diagnostic info
EXPECT_GE(r1.num_fragments, 0);
EXPECT_GE(r2.num_fragments, 0);
}
TEST(SSIBooleanUnionTest, WithEmpty) {
auto box = make_box(1, 1, 1);
BrepModel empty;
auto r = ssi_boolean_union(box, empty);
EXPECT_TRUE(r.result.is_valid());
EXPECT_GT(r.result.num_faces(), 0u);
}
// ═══════════════════════════════════════════════════════════
// Suite 2: Intersection — basic scenarios
// ═══════════════════════════════════════════════════════════
TEST(SSIBooleanIntersectionTest, Overlapping_IdenticalBoxes) {
auto box = make_box(2, 2, 2);
auto r = ssi_boolean_intersection(box, box);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIBooleanIntersectionTest, Disjoint_ProducesEmpty) {
// Two equal boxes → intersection is the box itself
auto box = make_box(2, 2, 2);
auto r = ssi_boolean_intersection(box, box);
EXPECT_TRUE(r.result.is_valid());
// Self-intersection: all faces are IN → result should have faces
EXPECT_GT(r.result.num_faces(), 0u);
}
TEST(SSIBooleanIntersectionTest, Contained_SmallInBig) {
auto big = make_box(4, 4, 4);
auto small = make_box(2, 2, 2);
auto r = ssi_boolean_intersection(big, small);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIBooleanIntersectionTest, BoxWithSphere) {
auto box = make_box(3, 3, 3);
auto sphere = make_sphere(1.5);
auto r = ssi_boolean_intersection(box, sphere);
EXPECT_TRUE(r.result.is_valid());
EXPECT_GT(r.num_ssi_curves, 0);
}
TEST(SSIBooleanIntersectionTest, WithEmpty) {
auto box = make_box(1, 1, 1);
BrepModel empty;
auto r = ssi_boolean_intersection(box, empty);
EXPECT_TRUE(r.result.is_valid());
EXPECT_EQ(r.result.num_faces(), 0u);
}
// ═══════════════════════════════════════════════════════════
// Suite 3: Difference — basic scenarios
// ═══════════════════════════════════════════════════════════
TEST(SSIBooleanDifferenceTest, SelfDifference_Empty) {
auto box = make_box(2, 2, 2);
auto r = ssi_boolean_difference(box, box);
EXPECT_TRUE(r.result.is_valid());
// A \ A should have no fragments (all IN, none kept as OUT for diff)
}
TEST(SSIBooleanDifferenceTest, Disjoint_Boxes) {
auto box1 = make_box(2, 2, 2);
auto box2 = make_box(2, 2, 2);
auto r = ssi_boolean_difference(box1, box2);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIBooleanDifferenceTest, LargeMinusSmall) {
auto big = make_box(4, 4, 4);
auto small = make_box(2, 2, 2);
auto r = ssi_boolean_difference(big, small);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIBooleanDifferenceTest, BoxMinusSphere) {
auto box = make_box(4, 4, 4);
auto sphere = make_sphere(1.5);
auto r = ssi_boolean_difference(box, sphere);
EXPECT_TRUE(r.result.is_valid());
EXPECT_GT(r.num_ssi_curves, 0);
}
TEST(SSIBooleanDifferenceTest, WithEmpty) {
auto box = make_box(1, 1, 1);
BrepModel empty;
auto r = ssi_boolean_difference(box, empty);
EXPECT_TRUE(r.result.is_valid());
// A \ ∅ = A
EXPECT_GT(r.result.num_faces(), 0u);
}
// ═══════════════════════════════════════════════════════════
// Suite 4: Degenerate scenarios
// ═══════════════════════════════════════════════════════════
TEST(SSIDegenerateTest, Coplanar_Faces_Union) {
// Two identical boxes → coplanar faces everywhere
auto box1 = make_box(2, 2, 2);
auto box2 = make_box(2, 2, 2);
auto r = ssi_boolean_union(box1, box2);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIDegenerateTest, Coplanar_Faces_Intersection) {
auto box1 = make_box(2, 2, 2);
auto box2 = make_box(2, 2, 2);
auto r = ssi_boolean_intersection(box1, box2);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIDegenerateTest, Coplanar_Faces_Difference) {
auto box1 = make_box(2, 2, 2);
auto box2 = make_box(2, 2, 2);
auto r = ssi_boolean_difference(box1, box2);
EXPECT_TRUE(r.result.is_valid());
}
TEST(SSIDegenerateTest, SharedVertices_SphereAndSphere) {
auto s1 = make_sphere(1.0);
auto s2 = make_sphere(1.0);
auto r = ssi_boolean_union(s1, s2);
EXPECT_TRUE(r.result.is_valid());
EXPECT_GT(r.num_ssi_curves, 0) << "Two identical spheres should have SSI curves";
}
TEST(SSIDegenerateTest, ZeroVolume_FlatBody) {
auto box = make_box(2, 2, 2);
BrepModel empty;
// Boolean with empty body should not crash
auto r1 = ssi_boolean_union(box, empty);
auto r2 = ssi_boolean_intersection(box, empty);
auto r3 = ssi_boolean_difference(box, empty);
EXPECT_TRUE(r1.result.is_valid());
EXPECT_TRUE(r2.result.is_valid());
EXPECT_TRUE(r3.result.is_valid());
}
// ═══════════════════════════════════════════════════════════
// Suite 5: Exact predicate tests
// ═══════════════════════════════════════════════════════════
TEST(SSIExactPredicateTest, NearBoundary_Classification) {
// Same box union → all faces are ON or IN the other, exact predicates matter
auto box = make_box(2, 2, 2);
auto r = ssi_boolean_union(box, box);
EXPECT_TRUE(r.result.is_valid());
// Diagnostic info should be populated
EXPECT_GE(r.num_fragments, 0);
// num_exact_upgrades shows how many times we fell back to GMP
EXPECT_GE(r.num_exact_upgrades, 0);
}
TEST(SSIExactPredicateTest, Adaptive_Escalation_Counts) {
auto box = make_box(3, 3, 3);
auto sphere = make_sphere(1.5);
auto r = ssi_boolean_intersection(box, sphere);
// The diagnostic should track exact upgrades
EXPECT_GE(r.num_exact_upgrades, 0);
EXPECT_GT(r.total_time_ms(), 0.0);
EXPECT_GT(r.ssi_time_ms, 0.0);
}
TEST(SSIExactPredicateTest, Orient3D_Consistency) {
// Verify exact_orient3d produces consistent results
double r1 = exact_orient3d(0,0,0, 1,0,0, 0,1,0, 0,0,1);
double r2 = exact_orient3d(0,0,0, 1,0,0, 0,1,0, 0,0,1);
// Same input should give same output
EXPECT_DOUBLE_EQ(r1, r2);
EXPECT_GT(r1, 0.0) << "Point (0,0,1) should be above plane z=0 with CCW (0,0,0)-(1,0,0)-(0,1,0)";
}
TEST(SSIExactPredicateTest, PointInPolyhedron_Basic) {
// Build a simple tetrahedron
auto box = make_box(2, 2, 2);
auto r = ssi_boolean_union(box, box);
// The result should be valid regardless
EXPECT_TRUE(r.result.is_valid());
}
// ═══════════════════════════════════════════════════════════
// Suite 6: Diagnostic information
// ═══════════════════════════════════════════════════════════
TEST(SSIDiagnosticTest, Result_ContainsTiming) {
auto box = make_box(2, 2, 2);
auto r = ssi_boolean_union(box, box);
EXPECT_GE(r.num_ssi_curves, 0);
EXPECT_GE(r.num_fragments, 0);
EXPECT_GE(r.num_kept, 0);
EXPECT_GT(r.total_time_ms(), 0.0);
}
TEST(SSIDiagnosticTest, Union_ClassificationBalance) {
auto box = make_box(2, 2, 2);
auto r = ssi_boolean_union(box, box);
// Sum of classified should match num_fragments
EXPECT_EQ(r.num_classified_in + r.num_classified_out + r.num_classified_on,
r.num_fragments);
}
TEST(SSIDiagnosticTest, ErrorMessage_EmptyOnSuccess) {
auto box = make_box(1, 1, 1);
auto r = ssi_boolean_union(box, box);
EXPECT_TRUE(r.error_message.empty());
}
// ═══════════════════════════════════════════════════════════
// Suite 7: Performance benchmarks (stubs)
// ═══════════════════════════════════════════════════════════
TEST(SSIPerformanceTest, Union_Under100ms) {
auto box1 = make_box(2, 2, 2);
auto box2 = make_box(2, 2, 2);
auto start = std::chrono::steady_clock::now();
auto r = ssi_boolean_union(box1, box2);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
EXPECT_TRUE(r.result.is_valid());
EXPECT_LT(elapsed, 2000) << "SSI union of two boxes should be fast";
}
TEST(SSIPerformanceTest, Intersection_Under100ms) {
auto box = make_box(2, 2, 2);
auto start = std::chrono::steady_clock::now();
auto r = ssi_boolean_intersection(box, box);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
EXPECT_TRUE(r.result.is_valid());
EXPECT_LT(elapsed, 2000) << "SSI intersection should be fast";
}
TEST(SSIPerformanceTest, Difference_Under100ms) {
auto box = make_box(2, 2, 2);
auto start = std::chrono::steady_clock::now();
auto r = ssi_boolean_difference(box, box);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
EXPECT_TRUE(r.result.is_valid());
EXPECT_LT(elapsed, 2000) << "SSI difference should be fast";
}
TEST(SSIPerformanceTest, SSI_Time_Recorded) {
auto box = make_box(3, 3, 3);
auto sphere = make_sphere(1.5);
auto r = ssi_boolean_union(box, sphere);
// SSI time should be a significant portion of total time
EXPECT_GT(r.ssi_time_ms, 0.0);
EXPECT_LE(r.ssi_time_ms, r.total_time_ms() + 1.0); // allow tiny rounding
}