Files
ViewDesignEngine/tests/brep/test_ssi_boolean.cpp
T
茂之钳 1aa753ba50
CI / Build & Test (push) Failing after 29s
CI / Release Build (push) Failing after 36s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
feat(v1.0.1): Parasolid + ACIS boolean robustness improvements
P0 — Boolean Robustness (Parasolid):
- 7-class degenerate taxonomy: surface_overlap, curve_overlap, high_order,
  boundary_contact, vertex_contact, non_manifold_contact
- Multi-point sampling: N=sqrt(area/tol²) points, >75% voting,
  exact_orient3d fallback on boundary cases
- PrecisionTracker: accumulated loss, threshold warning

P0 — Auto-Heal Pipeline (ACIS):
- 5-step pipeline: Stitch→Simplify→Regularize→Orient→Check
- AutoHealReport with per-step status, element counts, overall score

P1 — Direct Modeling (ACIS):
- fill_hole: edge loop → plane fit → NURBS insert
- pull_up_to_face: push_pull to target face alignment

Total: +~1500 lines across 6 files
2026-07-27 20:14:52 +08:00

495 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
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);
}