#include #include #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::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::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::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); 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); }