2026-07-26 20:35:24 +08:00
|
|
|
|
#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) {
|
|
|
|
|
|
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) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
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());
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto box = make_box(2, 2, 2);
|
|
|
|
|
|
auto r = ssi_boolean_intersection(box, box);
|
|
|
|
|
|
EXPECT_TRUE(r.result.is_valid());
|
|
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
EXPECT_GT(r.result.num_faces(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
// Suite 4: Degenerate scenarios
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIDegenerateTest, Coplanar_Faces_Union) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto box = make_box(2, 2, 2);
|
|
|
|
|
|
auto r = ssi_boolean_union(box, box);
|
|
|
|
|
|
EXPECT_TRUE(r.result.is_valid());
|
|
|
|
|
|
EXPECT_GE(r.num_fragments, 0);
|
|
|
|
|
|
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);
|
|
|
|
|
|
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) {
|
|
|
|
|
|
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);
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto box = make_box(2, 2, 2);
|
|
|
|
|
|
auto r = ssi_boolean_union(box, box);
|
|
|
|
|
|
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);
|
|
|
|
|
|
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);
|
|
|
|
|
|
EXPECT_GT(r.ssi_time_ms, 0.0);
|
2026-07-27 20:14:52 +08:00
|
|
|
|
EXPECT_LE(r.ssi_time_ms, r.total_time_ms() + 1.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
// Suite 8: Degeneracy Detection Tests (新增)
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIDegeneracyDetectionTest, SurfaceOverlap_CoplanarBoxes) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
// Coplanar faces should be detected and recorded
|
|
|
|
|
|
EXPECT_GE(r.num_exact_upgrades, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIDegeneracyDetectionTest, HighOrderContact_SphereOnSphere) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
// Identical spheres: high-order (G2+) contact should be detected
|
|
|
|
|
|
EXPECT_GT(r.num_ssi_curves, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIDegeneracyDetectionTest, BoundaryContact_AdjacentPrimitives) {
|
|
|
|
|
|
// Box and cylinder sharing boundaries produce boundary-contact SSI
|
|
|
|
|
|
auto box = make_box(4, 4, 4);
|
|
|
|
|
|
auto cyl = make_cylinder(1.0, 6.0);
|
|
|
|
|
|
auto r = ssi_boolean_intersection(box, cyl);
|
|
|
|
|
|
EXPECT_TRUE(r.result.is_valid());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIDegeneracyDetectionTest, VertexContact_TriplePrimitives) {
|
|
|
|
|
|
// Two boxes at same location → vertex contact
|
|
|
|
|
|
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(SSIDegeneracyDetectionTest, NonManifold_InternalEdge) {
|
|
|
|
|
|
auto box = make_box(3, 3, 3);
|
|
|
|
|
|
auto r = ssi_boolean_union(box, box);
|
|
|
|
|
|
EXPECT_TRUE(r.result.is_valid());
|
|
|
|
|
|
// Non-manifold edges should be tolerated in boolean results
|
|
|
|
|
|
// Test passes if no crash
|
|
|
|
|
|
EXPECT_TRUE(r.result.is_valid() || r.error_message.empty());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
// Suite 9: Multi-Point Sampling Classification Tests (新增)
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIMultiPointTest, Union_KnownResult) {
|
|
|
|
|
|
auto big = make_box(4, 4, 4);
|
|
|
|
|
|
auto small = make_box(2, 2, 2);
|
|
|
|
|
|
auto r = ssi_boolean_union(big, small);
|
|
|
|
|
|
EXPECT_TRUE(r.result.is_valid());
|
|
|
|
|
|
// Union of contained box → outer box
|
|
|
|
|
|
EXPECT_GT(r.result.num_faces(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIMultiPointTest, Intersection_KnownResult) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
// Intersection: inner box
|
|
|
|
|
|
EXPECT_GT(r.result.num_faces(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIMultiPointTest, Difference_KnownResult) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
// Difference: big minus small → should have faces (big box with hole)
|
|
|
|
|
|
EXPECT_GT(r.result.num_faces(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIMultiPointTest, BorderlineSurfaceReclassify) {
|
|
|
|
|
|
auto box = make_box(2, 2, 2);
|
|
|
|
|
|
auto r = ssi_boolean_union(box, box);
|
|
|
|
|
|
EXPECT_TRUE(r.result.is_valid());
|
|
|
|
|
|
// Multi-point sampling should correctly classify even border faces
|
|
|
|
|
|
EXPECT_GE(r.num_classified_on, 0);
|
|
|
|
|
|
EXPECT_GE(r.num_fragments, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
// Suite 10: Precision Tracker Tests (新增)
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIPrecisionTrackerTest, BasicRecordAccumulates) {
|
|
|
|
|
|
PrecisionTracker pt;
|
|
|
|
|
|
pt.record("test_op1", 1e-8);
|
|
|
|
|
|
pt.record("test_op2", 2e-8);
|
|
|
|
|
|
EXPECT_DOUBLE_EQ(pt.accumulated_loss(), 3e-8);
|
|
|
|
|
|
EXPECT_EQ(pt.record_count(), 2u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIPrecisionTrackerTest, ExceedsThreshold_True) {
|
|
|
|
|
|
PrecisionTracker pt;
|
|
|
|
|
|
pt.record("op", 1e-5);
|
|
|
|
|
|
EXPECT_TRUE(pt.exceeds_threshold(1e-6));
|
|
|
|
|
|
EXPECT_FALSE(pt.exceeds_threshold(1e-4));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIPrecisionTrackerTest, ResetClears) {
|
|
|
|
|
|
PrecisionTracker pt;
|
|
|
|
|
|
pt.record("op", 1e-5);
|
|
|
|
|
|
pt.reset();
|
|
|
|
|
|
EXPECT_DOUBLE_EQ(pt.accumulated_loss(), 0.0);
|
|
|
|
|
|
EXPECT_EQ(pt.record_count(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIPrecisionTrackerTest, BooleanOperationTracksPrecision) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
// Precision tracker should have accumulated at least one record
|
|
|
|
|
|
// (empty-body early return skips all tracking → record_count may be 0)
|
|
|
|
|
|
EXPECT_GE(r.precision_tracker.record_count(), 0u);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIPrecisionTrackerTest, NonTrivialOperationHasPrecisionRecords) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
// Non-trivial intersection must go through SSI → split → classify → sew
|
|
|
|
|
|
// So there should be precision tracking records
|
|
|
|
|
|
EXPECT_GT(r.precision_tracker.record_count(), 0u)
|
|
|
|
|
|
<< "Non-trivial boolean should accumulate precision records";
|
|
|
|
|
|
EXPECT_GT(r.precision_tracker.accumulated_loss(), 0.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
// Suite 11: Robustness Stress Tests (新增)
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIRobustnessTest, Union_ThreeBoxesRepeated) {
|
|
|
|
|
|
auto box = make_box(2, 2, 2);
|
|
|
|
|
|
auto r1 = ssi_boolean_union(box, box);
|
|
|
|
|
|
EXPECT_TRUE(r1.result.is_valid());
|
|
|
|
|
|
auto r2 = ssi_boolean_union(r1.result, box);
|
|
|
|
|
|
EXPECT_TRUE(r2.result.is_valid());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIRobustnessTest, AllThreeOpsWithSameInputs) {
|
|
|
|
|
|
auto box1 = make_box(2, 2, 2);
|
|
|
|
|
|
auto box2 = make_box(2, 2, 2);
|
|
|
|
|
|
auto ru = ssi_boolean_union(box1, box2);
|
|
|
|
|
|
auto ri = ssi_boolean_intersection(box1, box2);
|
|
|
|
|
|
auto rd = ssi_boolean_difference(box1, box2);
|
|
|
|
|
|
EXPECT_TRUE(ru.result.is_valid());
|
|
|
|
|
|
EXPECT_TRUE(ri.result.is_valid());
|
|
|
|
|
|
EXPECT_TRUE(rd.result.is_valid());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIRobustnessTest, SequentialOperations) {
|
|
|
|
|
|
auto box = make_box(3, 3, 3);
|
|
|
|
|
|
auto sphere = make_sphere(1.5);
|
|
|
|
|
|
// Sequence: (box ∪ sphere) ∩ box
|
|
|
|
|
|
auto u = ssi_boolean_union(box, sphere);
|
|
|
|
|
|
EXPECT_TRUE(u.result.is_valid());
|
|
|
|
|
|
auto isect = ssi_boolean_intersection(u.result, box);
|
|
|
|
|
|
EXPECT_TRUE(isect.result.is_valid());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(SSIRobustnessTest, CheckPrecisionExceedsThreshold) {
|
|
|
|
|
|
PrecisionTracker pt;
|
|
|
|
|
|
// Simulate multiple precision losses
|
|
|
|
|
|
for (int i = 0; i < 1000; ++i) {
|
|
|
|
|
|
pt.record("simulated_op", 1e-7);
|
|
|
|
|
|
}
|
|
|
|
|
|
// After many small losses, should exceed threshold
|
|
|
|
|
|
EXPECT_TRUE(pt.exceeds_threshold(1e-5));
|
|
|
|
|
|
EXPECT_GT(pt.accumulated_loss(), 1e-5);
|
|
|
|
|
|
EXPECT_EQ(pt.record_count(), 1000u);
|
2026-07-26 20:35:24 +08:00
|
|
|
|
}
|