Files
ViewDesignEngine/tests/brep/test_trimmed_surface.cpp
T
茂之钳 05b62e8238
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
feat(v5-M1): TrimmedSurface integration + SSI boolean + topology healing + tolerance system
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
2026-07-26 20:35:24 +08:00

394 lines
15 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 "vde/brep/trimmed_surface.h"
#include "vde/brep/brep.h"
#include "vde/brep/modeling.h"
#include "vde/curves/nurbs_surface.h"
#include <cmath>
using namespace vde::brep;
using namespace vde::curves;
using vde::core::Point3D;
namespace {
/// Helper: create a simple 10×10 planar NURBS surface in the XY plane
NurbsSurface make_plane() {
std::vector<std::vector<Point3D>> cp = {
{Point3D(0, 0, 0), Point3D(0, 10, 0)},
{Point3D(10, 0, 0), Point3D(10, 10, 0)}
};
std::vector<double> ku = {0, 0, 1, 1};
std::vector<double> kv = {0, 0, 1, 1};
std::vector<std::vector<double>> w = {{1, 1}, {1, 1}};
return NurbsSurface(cp, ku, kv, w, 1, 1);
}
/// Helper: create a linear p-curve line segment in parameter space
PCurve make_pcurve_line(double ua, double va, double ub, double vb) {
std::vector<Point3D> cps = { Point3D(ua, va, 0.0), Point3D(ub, vb, 0.0) };
std::vector<double> knots = { 0.0, 0.0, 1.0, 1.0 };
std::vector<double> weights = { 1.0, 1.0 };
return PCurve(cps, knots, weights, 1);
}
/// Helper: create a rectangular trim loop
TrimLoop make_rect_loop(double u0, double u1, double v0, double v1) {
TrimLoop loop;
loop.is_outer = true;
loop.p_curves.push_back(make_pcurve_line(u0, v0, u1, v0)); // bottom
loop.p_curves.push_back(make_pcurve_line(u1, v0, u1, v1)); // right
loop.p_curves.push_back(make_pcurve_line(u1, v1, u0, v1)); // top
loop.p_curves.push_back(make_pcurve_line(u0, v1, u0, v0)); // left
return loop;
}
} // anonymous namespace
// ═══════════════════════════════════════════════════════════
// 1-3: Untrimmed surface
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, Untrimmed_EvaluatesAllCorners) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
auto p00 = ts.evaluate(0.0, 0.0);
ASSERT_TRUE(p00.has_value());
EXPECT_NEAR(p00->x(), 0.0, 1e-6);
auto p11 = ts.evaluate(1.0, 1.0);
ASSERT_TRUE(p11.has_value());
EXPECT_NEAR(p11->x(), 10.0, 1e-6);
EXPECT_NEAR(p11->y(), 10.0, 1e-6);
auto pc = ts.evaluate(0.5, 0.5);
ASSERT_TRUE(pc.has_value());
EXPECT_NEAR(pc->x(), 5.0, 1e-6);
}
TEST(TrimmedSurfaceTest, Untrimmed_IsInsideAlwaysTrue) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
EXPECT_TRUE(ts.is_inside(0.0, 0.0));
EXPECT_TRUE(ts.is_inside(0.5, 0.5));
EXPECT_TRUE(ts.is_inside(1.0, 1.0));
}
TEST(TrimmedSurfaceTest, Untrimmed_WindingNumber) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
EXPECT_TRUE(ts.loops.empty());
EXPECT_TRUE(ts.is_inside(0.3, 0.7));
}
// ═══════════════════════════════════════════════════════════
// 4-6: Rectangular trim
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, RectTrim_InsideRegion) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.0, 0.5, 0.0, 0.5);
auto p = ts.evaluate(0.25, 0.25);
ASSERT_TRUE(p.has_value());
EXPECT_NEAR(p->x(), 2.5, 1e-6);
EXPECT_NEAR(p->y(), 2.5, 1e-6);
EXPECT_TRUE(ts.is_inside(0.25, 0.25));
}
TEST(TrimmedSurfaceTest, RectTrim_OutsideRegion) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.0, 0.5, 0.0, 0.5);
EXPECT_FALSE(ts.evaluate(0.75, 0.25).has_value());
EXPECT_FALSE(ts.is_inside(0.75, 0.25));
EXPECT_FALSE(ts.is_inside(0.25, 0.75));
EXPECT_FALSE(ts.is_inside(0.75, 0.75));
}
TEST(TrimmedSurfaceTest, RectTrim_BoundaryIsInside) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.0, 0.5, 0.0, 0.5);
EXPECT_TRUE(ts.is_inside(0.0, 0.0));
EXPECT_TRUE(ts.is_inside(0.5, 0.5));
EXPECT_TRUE(ts.is_inside(0.0, 0.5));
}
// ═══════════════════════════════════════════════════════════
// 7-9: Hole (inner loop)
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, Hole_CenterIsOutside) {
auto surf = make_plane();
TrimmedSurface ts;
ts.base_surface = surf;
ts.loops.push_back(make_rect_loop(0.0, 1.0, 0.0, 1.0));
auto hole = make_rect_loop(0.25, 0.75, 0.25, 0.75);
hole.is_outer = false;
ts.loops.push_back(hole);
EXPECT_FALSE(ts.is_inside(0.5, 0.5));
auto p_center = ts.evaluate(0.5, 0.5);
EXPECT_FALSE(p_center.has_value());
}
TEST(TrimmedSurfaceTest, Hole_CornerIsInside) {
auto surf = make_plane();
TrimmedSurface ts;
ts.base_surface = surf;
ts.loops.push_back(make_rect_loop(0.0, 1.0, 0.0, 1.0));
auto hole = make_rect_loop(0.25, 0.75, 0.25, 0.75);
hole.is_outer = false;
ts.loops.push_back(hole);
EXPECT_TRUE(ts.is_inside(0.1, 0.1));
auto p_corner = ts.evaluate(0.1, 0.1);
ASSERT_TRUE(p_corner.has_value());
EXPECT_NEAR(p_corner->x(), 1.0, 1e-6);
}
TEST(TrimmedSurfaceTest, Hole_BoundaryIsOutside) {
auto surf = make_plane();
TrimmedSurface ts;
ts.base_surface = surf;
ts.loops.push_back(make_rect_loop(0.0, 1.0, 0.0, 1.0));
auto hole = make_rect_loop(0.25, 0.75, 0.25, 0.75);
hole.is_outer = false;
ts.loops.push_back(hole);
// Point on hole boundary → outside
EXPECT_FALSE(ts.is_inside(0.25, 0.25));
}
// ═══════════════════════════════════════════════════════════
// 10-11: Circular hole (from_rect_with_hole)
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, CircularHole_CenterOutside) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect_with_hole(surf, 0.0, 1.0, 0.0, 1.0,
0.5, 0.5, 0.2, 32);
// Center of hole → outside
EXPECT_FALSE(ts.is_inside(0.5, 0.5));
auto p = ts.evaluate(0.5, 0.5);
EXPECT_FALSE(p.has_value());
}
TEST(TrimmedSurfaceTest, CircularHole_CornerInside) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect_with_hole(surf, 0.0, 1.0, 0.0, 1.0,
0.5, 0.5, 0.2, 32);
// Corner → inside outer, outside hole
EXPECT_TRUE(ts.is_inside(0.1, 0.1));
EXPECT_TRUE(ts.is_inside(0.9, 0.9));
}
// ═══════════════════════════════════════════════════════════
// 12-13: Bounds
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, Bounds_Untrimmed) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
auto b = ts.bounds();
EXPECT_NEAR(b.min().x(), 0.0, 1e-6);
EXPECT_NEAR(b.max().x(), 10.0, 1e-6);
EXPECT_NEAR(b.min().y(), 0.0, 1e-6);
EXPECT_NEAR(b.max().y(), 10.0, 1e-6);
}
TEST(TrimmedSurfaceTest, Bounds_RectangularTrim) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.25, 0.75, 0.25, 0.75);
auto b = ts.bounds();
EXPECT_NEAR(b.min().x(), 2.5, 1e-6);
EXPECT_NEAR(b.max().x(), 7.5, 1e-6);
}
// ═══════════════════════════════════════════════════════════
// 14-15: Winding number
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, WindingNumber_RectOuter) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.2, 0.8, 0.2, 0.8);
int w = ts.winding_number(0.5, 0.5, ts.loops[0]);
EXPECT_GT(std::abs(w), 0); // Inside outer loop
int w_out = ts.winding_number(0.0, 0.0, ts.loops[0]);
EXPECT_EQ(w_out, 0); // Outside
}
TEST(TrimmedSurfaceTest, WindingNumber_Hole) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect_with_hole(surf, 0.0, 1.0, 0.0, 1.0,
0.4, 0.4, 0.15, 32);
ASSERT_EQ(ts.loops.size(), 2u);
// Hole loop at center
int w_hole_in = ts.winding_number(0.4, 0.4, ts.loops[1]);
EXPECT_GT(std::abs(w_hole_in), 0);
int w_hole_out = ts.winding_number(0.1, 0.1, ts.loops[1]);
EXPECT_EQ(w_hole_out, 0);
}
// ═══════════════════════════════════════════════════════════
// 16: Closest boundary point
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, ClosestBoundary) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.25, 0.75, 0.25, 0.75);
// Point outside → should project to boundary
auto bp = ts.closest_boundary_point(1.0, 0.5);
// Should be near the right edge of trimmed region in 3D
EXPECT_NEAR(bp.x(), 7.5, 1e-5); // u=0.75 → x=7.5
EXPECT_NEAR(bp.y(), 5.0, 1e-5); // v=0.5 → y=5.0
}
// ═══════════════════════════════════════════════════════════
// 17-18: to_mesh
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, ToMesh_Untrimmed) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
auto mesh = ts.to_mesh(0.1);
EXPECT_GT(mesh.num_vertices(), 0u);
EXPECT_GT(mesh.num_faces(), 0u);
}
TEST(TrimmedSurfaceTest, ToMesh_RectTrimmed) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.25, 0.75, 0.25, 0.75);
auto mesh = ts.to_mesh(0.1);
EXPECT_GT(mesh.num_vertices(), 0u);
EXPECT_GT(mesh.num_faces(), 0u);
// All vertices should be inside the trimmed region bounds
auto b = mesh.bounds();
EXPECT_GE(b.min().x(), 2.0);
EXPECT_LE(b.max().x(), 8.0);
}
// ═══════════════════════════════════════════════════════════
// 19-20: BrepModel integration
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, BrepModel_AddTrimmedSurface) {
BrepModel model;
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.0, 1.0, 0.0, 1.0);
int tid = model.add_trimmed_surface(ts);
EXPECT_EQ(tid, 0);
EXPECT_EQ(model.num_trimmed_surfaces(), 1u);
const auto& stored = model.trimmed_surface(tid);
EXPECT_EQ(stored.loops.size(), 1u);
EXPECT_TRUE(stored.loops[0].is_outer);
}
TEST(TrimmedSurfaceTest, BrepModel_SetFaceTrimmedSurface) {
BrepModel model;
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.0, 1.0, 0.0, 1.0);
int tid = model.add_trimmed_surface(ts);
int v0 = model.add_vertex({0,0,0});
int v1 = model.add_vertex({1,0,0});
int v2 = model.add_vertex({1,1,0});
int v3 = model.add_vertex({0,1,0});
int e1 = model.add_edge(v0, v1);
int e2 = model.add_edge(v1, v2);
int e3 = model.add_edge(v2, v3);
int e4 = model.add_edge(v3, v0);
int lp = model.add_loop({e1,e2,e3,e4}, true);
int sid = model.add_surface(surf);
int fid = model.add_face(sid, {lp});
model.set_face_trimmed_surface(fid, tid);
const auto& face = model.face(fid);
EXPECT_EQ(face.trimmed_surface_id, tid);
}
TEST(TrimmedSurfaceTest, BrepModel_ToMeshWithTrimmedSurface) {
auto box = make_box(10, 10, 10);
// make_box now creates TrimmedSurface-wrapped faces
auto mesh = box.to_mesh(0.1);
EXPECT_GT(mesh.num_vertices(), 0u);
EXPECT_GT(mesh.num_faces(), 0u);
// Verify each face has a trimmed_surface_id set
for (size_t fi = 0; fi < box.num_faces(); ++fi) {
const auto& face = box.face(static_cast<int>(fi));
EXPECT_GE(face.trimmed_surface_id, 0);
}
}
TEST(TrimmedSurfaceTest, BrepModel_SphereTrimmedSurface) {
auto sphere = make_sphere(5.0, 8, 4);
auto mesh = sphere.to_mesh(0.1);
EXPECT_GT(mesh.num_vertices(), 0u);
EXPECT_GT(mesh.num_faces(), 0u);
// All vertices should be approximately on the sphere surface (radius ~5)
auto b = mesh.bounds();
for (size_t vi = 0; vi < mesh.num_vertices(); ++vi) {
auto p = mesh.vertex(vi);
double dist = std::sqrt(p.x()*p.x() + p.y()*p.y() + p.z()*p.z());
EXPECT_NEAR(dist, 5.0, 1.0); // tolerance for approximation
}
}
TEST(TrimmedSurfaceTest, BrepModel_CylinderTrimmedSurface) {
auto cyl = make_cylinder(3.0, 10.0, 16);
auto mesh = cyl.to_mesh(0.1);
EXPECT_GT(mesh.num_vertices(), 0u);
EXPECT_GT(mesh.num_faces(), 0u);
for (size_t fi = 0; fi < cyl.num_faces(); ++fi) {
const auto& face = cyl.face(static_cast<int>(fi));
EXPECT_GE(face.trimmed_surface_id, 0);
}
}
// ═══════════════════════════════════════════════════════════
// 21-22: from_surface / from_rect constructors
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, FromSurface_HasNoLoops) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
EXPECT_TRUE(ts.loops.empty());
EXPECT_TRUE(ts.is_inside(0.5, 0.5));
}
TEST(TrimmedSurfaceTest, FromRect_CreatesOneOuterLoop) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.0, 1.0, 0.0, 1.0);
EXPECT_EQ(ts.loops.size(), 1u);
EXPECT_TRUE(ts.loops[0].is_outer);
EXPECT_EQ(ts.loops[0].p_curves.size(), 4u);
}
// ═══════════════════════════════════════════════════════════
// 23: is_valid with trimmed surfaces
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, BrepModel_IsValidWithTrimmedFaces) {
auto box = make_box(5, 5, 5);
EXPECT_TRUE(box.is_valid());
}