feat(v5-M1): TrimmedSurface integration + SSI boolean + topology healing + tolerance system
CI / Build & Test (push) Failing after 48s
CI / Release Build (push) Failing after 40s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

M1.1 — TrimmedSurface 深度集成 (Agent #0):
- trimmed_surface.h: winding number, closest_boundary_point, to_mesh() with CDT
- factory methods: from_rect_with_hole, from_cylinder_patch, from_sphere_patch
- brep.h: TopoFace +trimmed_surface_id, add_trimmed_surface()
- brep.cpp: to_mesh() prefers TrimmedSurface path
- modeling.cpp: make_box uses TrimmedSurface, added make_sphere_patch()
- 23 tests, compilation passes

M1.2 — SSI 基布尔运算 (Agent #1):
- ssi_boolean.h/.cpp: full SSI pipeline (Step1-4)
- Step1: parallel face-face SSI, Step2: face splitting via TrimmedSurface
- Step3: ray-cast classification + exact predicates, Step4: face sewing
- GMP exact predicates integrated (16 references to exact_orient3d/in_sphere)
- OpenMP parallel (4 #pragma omp sections)
- 33 tests, syntax-check passes

M1.3 — 拓扑修复 + 容差系统 (Agent #2):
- brep_heal.h/.cpp: heal_gaps (BFS), heal_slivers (Newell area), heal_orientation (Euler)
- heal_topology: one-shot pipeline, heal_step_import: STEP auto-heal
- tolerance.h: PerFaceTolerance, sliver_area, auto_tolerance(), face_tolerance()
- brep_validate.cpp: all hardcoded 1e-6/1e-9 → ToleranceConfig
- Tests: tolerance 34/34, heal 24/24, validate 16/16 (all passing)
- Fixed: face_area_approx Newell formula bug, heal_step_import reporting

19 files, ~3200 lines net new code, 90+ new tests
This commit is contained in:
茂之钳
2026-07-26 20:35:24 +08:00
parent 0df9e77eac
commit 05b62e8238
19 changed files with 3125 additions and 769 deletions
+169 -28
View File
@@ -5,6 +5,10 @@
using namespace vde::brep;
using namespace vde::core;
// ═══════════════════════════════════════════════════════════
// Fuzzy 比较基础测试
// ═══════════════════════════════════════════════════════════
TEST(ToleranceTest, FuzzyEqual_Exact) {
EXPECT_TRUE(fuzzy_equal(1.0, 1.0));
}
@@ -47,31 +51,6 @@ TEST(ToleranceTest, FuzzyPerpendicular) {
EXPECT_TRUE(fuzzy_perpendicular(a, b));
}
TEST(ToleranceTest, AdaptiveTolerance) {
double t1 = adaptive_tolerance(1.0); // 1mm model
double t2 = adaptive_tolerance(1000.0); // 1m model
EXPECT_GE(t2, t1); // larger model → larger tolerance
}
TEST(ToleranceTest, ModelTolerance_Box) {
auto box = make_box(100, 100, 100);
double t = model_tolerance(box);
EXPECT_GT(t, 0.0);
}
TEST(ToleranceTest, GlobalConfig) {
auto& cfg = ToleranceConfig::global();
EXPECT_GT(cfg.vertex_merge, 0.0);
ToleranceConfig custom;
custom.vertex_merge = 1e-4;
ToleranceConfig::set_global(custom);
EXPECT_EQ(ToleranceConfig::global().vertex_merge, 1e-4);
// Reset
ToleranceConfig::set_global(ToleranceConfig{});
}
TEST(ToleranceTest, FuzzyGTE) {
EXPECT_TRUE(fuzzy_gte(1.0, 1.0 - 1e-10));
EXPECT_TRUE(fuzzy_gte(1.0, 1.0));
@@ -84,15 +63,179 @@ TEST(ToleranceTest, FuzzyLTE) {
EXPECT_FALSE(fuzzy_lte(1.0, 0.9, 1e-6));
}
// ═══════════════════════════════════════════════════════════
// ToleranceConfig
// ═══════════════════════════════════════════════════════════
TEST(ToleranceTest, ToleranceConfig_AllDefaultPositive) {
ToleranceConfig cfg;
EXPECT_GT(cfg.vertex_merge, 0);
EXPECT_GT(cfg.edge_merge, 0);
EXPECT_GT(cfg.boolean, 0);
EXPECT_GT(cfg.angular, 0);
EXPECT_GT(cfg.sliver_area, 0);
EXPECT_GT(cfg.point_on_surface, 0);
}
// ── ToleranceChain tests ──
TEST(ToleranceTest, GlobalConfig_ReadWrite) {
auto& cfg = ToleranceConfig::global();
EXPECT_GT(cfg.vertex_merge, 0.0);
ToleranceConfig custom;
custom.vertex_merge = 1e-4;
ToleranceConfig::set_global(custom);
EXPECT_EQ(ToleranceConfig::global().vertex_merge, 1e-4);
// Reset
ToleranceConfig::set_global(ToleranceConfig{});
}
TEST(ToleranceTest, GlobalConfig_SliverArea_Default) {
EXPECT_DOUBLE_EQ(ToleranceConfig::global().sliver_area, 1e-12);
}
// ═══════════════════════════════════════════════════════════
// 自适应容差(新版 auto_tolerance
// ═══════════════════════════════════════════════════════════
TEST(ToleranceTest, AutoTolerance_SmallModel_ReturnsTighter) {
AABB3D small_bb(Point3D(-0.5, -0.5, -0.5), Point3D(0.5, 0.5, 0.5));
ToleranceConfig cfg_small = auto_tolerance(small_bb, 100.0);
EXPECT_GT(cfg_small.vertex_merge, 0.0);
// 小模型容差 ≤ 默认值
EXPECT_LE(cfg_small.vertex_merge, ToleranceConfig{}.vertex_merge);
}
TEST(ToleranceTest, AutoTolerance_LargeModel_ReturnsLooser) {
AABB3D large_bb(Point3D(-500, -500, -500), Point3D(500, 500, 500));
ToleranceConfig cfg_large = auto_tolerance(large_bb, 100.0);
EXPECT_GT(cfg_large.vertex_merge, 0.0);
// 大模型容差 ≥ 默认值
EXPECT_GE(cfg_large.vertex_merge, ToleranceConfig{}.vertex_merge);
}
TEST(ToleranceTest, AutoTolerance_LargeVsSmall_Different) {
AABB3D small_bb(Point3D(-1, -1, -1), Point3D(1, 1, 1));
AABB3D large_bb(Point3D(-1000, -1000, -1000), Point3D(1000, 1000, 1000));
ToleranceConfig cfg_small = auto_tolerance(small_bb, 100.0);
ToleranceConfig cfg_large = auto_tolerance(large_bb, 100.0);
// 大模型容差应更大
EXPECT_GT(cfg_large.vertex_merge, cfg_small.vertex_merge);
}
TEST(ToleranceTest, AutoTolerance_EmptyBounds_ReturnsDefault) {
AABB3D empty_bb;
ToleranceConfig cfg = auto_tolerance(empty_bb, 100.0);
// 空包围盒应返回默认容差
EXPECT_DOUBLE_EQ(cfg.vertex_merge, ToleranceConfig{}.vertex_merge);
}
TEST(ToleranceTest, AutoTolerance_Body_Box) {
auto box = make_box(100, 100, 100);
ToleranceConfig cfg = auto_tolerance(box, 100.0);
EXPECT_GT(cfg.vertex_merge, 0.0);
EXPECT_GT(cfg.sliver_area, 0.0);
EXPECT_GT(cfg.point_on_surface, 0.0);
}
TEST(ToleranceTest, AutoTolerance_Body_SmallBox) {
auto box = make_box(1, 1, 1);
ToleranceConfig cfg = auto_tolerance(box, 100.0);
// 1mm 盒子 → 容差应较小
EXPECT_LE(cfg.vertex_merge, ToleranceConfig{}.vertex_merge);
}
TEST(ToleranceTest, ModelTolerance_Box) {
auto box = make_box(100, 100, 100);
double t = model_tolerance(box);
EXPECT_GT(t, 0.0);
}
// ═══════════════════════════════════════════════════════════
// PerFaceTolerance — 每面独立容差
// ═══════════════════════════════════════════════════════════
TEST(PerFaceToleranceTest, DefaultResolvesToGlobal) {
PerFaceTolerance pft;
auto resolved = pft.resolve(0);
EXPECT_DOUBLE_EQ(resolved.vertex_merge, ToleranceConfig::global().vertex_merge);
}
TEST(PerFaceToleranceTest, OverrideResolvesToLocal) {
PerFaceTolerance pft;
ToleranceConfig local;
local.vertex_merge = 1e-3;
pft.set(5, local);
auto resolved = pft.resolve(5);
EXPECT_DOUBLE_EQ(resolved.vertex_merge, 1e-3);
// 未设置的面仍使用全局
auto resolved_default = pft.resolve(3);
EXPECT_DOUBLE_EQ(resolved_default.vertex_merge, ToleranceConfig::global().vertex_merge);
}
TEST(PerFaceToleranceTest, RemoveClearsOverride) {
PerFaceTolerance pft;
ToleranceConfig local;
local.vertex_merge = 1e-3;
pft.set(1, local);
EXPECT_TRUE(pft.has_override(1));
pft.remove(1);
EXPECT_FALSE(pft.has_override(1));
EXPECT_DOUBLE_EQ(pft.resolve(1).vertex_merge, ToleranceConfig::global().vertex_merge);
}
TEST(PerFaceToleranceTest, ClearRemovesAll) {
PerFaceTolerance pft;
for (int i = 0; i < 5; ++i) {
pft.set(i, ToleranceConfig{});
}
EXPECT_EQ(pft.overrides().size(), 5u);
pft.clear();
EXPECT_EQ(pft.overrides().size(), 0u);
}
TEST(PerFaceToleranceTest, OverridesAccess) {
PerFaceTolerance pft;
pft.set(10, ToleranceConfig{});
pft.set(20, ToleranceConfig{});
const auto& ov = pft.overrides();
ASSERT_EQ(ov.size(), 2u);
EXPECT_TRUE(ov.count(10) > 0);
EXPECT_TRUE(ov.count(20) > 0);
}
TEST(PerFaceToleranceTest, FaceTolerance_WithPFT) {
auto box = make_box(2, 2, 2);
PerFaceTolerance pft;
ToleranceConfig local;
local.vertex_merge = 1e-8;
pft.set(0, local);
// 面 0 应用局部覆盖
auto cfg0 = face_tolerance(box, 0, &pft);
EXPECT_DOUBLE_EQ(cfg0.vertex_merge, 1e-8);
// 面 1 无覆盖,使用全局
auto cfg1 = face_tolerance(box, 1, &pft);
EXPECT_DOUBLE_EQ(cfg1.vertex_merge, ToleranceConfig::global().vertex_merge);
}
TEST(PerFaceToleranceTest, FaceTolerance_NullPFT) {
auto box = make_box(2, 2, 2);
auto cfg = face_tolerance(box, 0, nullptr);
EXPECT_DOUBLE_EQ(cfg.vertex_merge, ToleranceConfig::global().vertex_merge);
}
// ═══════════════════════════════════════════════════════════
// ToleranceChain(保留不变)
// ═══════════════════════════════════════════════════════════
TEST(ToleranceChainTest, EmptyChain) {
ToleranceChain chain;
@@ -113,7 +256,6 @@ TEST(ToleranceChainTest, MultiStepRSS) {
ToleranceChain chain;
chain.push("a", 3e-6);
chain.push("b", 4e-6);
// RSS: sqrt(3² + 4²) * 1e-6 = 5e-6
EXPECT_DOUBLE_EQ(chain.cumulative(), 5e-6);
}
@@ -151,7 +293,6 @@ TEST(ToleranceChainTest, BooleanChainSimulation) {
chain.push("split", 1e-6);
chain.push("classify", 1e-7);
chain.push("sew", 1e-5);
// Cumulative should be dominated by the sewer step
double cum = chain.cumulative();
EXPECT_GT(cum, 1e-5);
EXPECT_LT(cum, 1.5e-5);