feat(v3.6): TrimmedSurface — B-Rep fundamental primitive
CI / Build & Test (push) Failing after 41s
CI / Release Build (push) Failing after 32s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

This commit is contained in:
茂之钳
2026-07-24 14:25:51 +00:00
parent 605e1aded5
commit ebce6abdd6
5 changed files with 455 additions and 0 deletions
+1
View File
@@ -10,3 +10,4 @@ add_vde_test(test_assembly)
add_vde_test(test_brep_face_split)
add_vde_test(test_measure)
add_vde_test(test_assembly_constraints)
add_vde_test(test_trimmed_surface)
+247
View File
@@ -0,0 +1,247 @@
#include <gtest/gtest.h>
#include "vde/brep/trimmed_surface.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
/// Control grid: 2×2, degree 1×1, knots {0,0,1,1}
/// Domain: u∈[0,1], v∈[0,1] → maps to XY square [0,10]×[0,10] at Z=0
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
// ═══════════════════════════════════════════════════════════
// Untrimmed surface
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, Untrimmed_EvaluatesAllCorners) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
// All points in the parameter domain should evaluate successfully
auto p00 = ts.evaluate(0.0, 0.0);
ASSERT_TRUE(p00.has_value());
EXPECT_NEAR(p00->x(), 0.0, 1e-6);
EXPECT_NEAR(p00->y(), 0.0, 1e-6);
EXPECT_NEAR(p00->z(), 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);
EXPECT_NEAR(p11->z(), 0.0, 1e-6);
auto p10 = ts.evaluate(1.0, 0.0);
ASSERT_TRUE(p10.has_value());
EXPECT_NEAR(p10->x(), 10.0, 1e-6);
EXPECT_NEAR(p10->y(), 0.0, 1e-6);
auto p01 = ts.evaluate(0.0, 1.0);
ASSERT_TRUE(p01.has_value());
EXPECT_NEAR(p01->x(), 0.0, 1e-6);
EXPECT_NEAR(p01->y(), 10.0, 1e-6);
// Center
auto pc = ts.evaluate(0.5, 0.5);
ASSERT_TRUE(pc.has_value());
EXPECT_NEAR(pc->x(), 5.0, 1e-6);
EXPECT_NEAR(pc->y(), 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));
EXPECT_TRUE(ts.is_inside(0.123, 0.789));
}
// ═══════════════════════════════════════════════════════════
// Rectangular trim
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, RectTrim_InsideRegion) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_rect(surf, 0.0, 0.5, 0.0, 0.5);
// Point at (0.25, 0.25) → 3D: (2.5, 2.5, 0) — inside trim
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_NEAR(p->z(), 0.0, 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);
// Point at (0.75, 0.25) — outside trim (u > 0.5)
auto p = ts.evaluate(0.75, 0.25);
EXPECT_FALSE(p.has_value());
EXPECT_FALSE(ts.is_inside(0.75, 0.25));
// Point at (0.25, 0.75) — outside trim (v > 0.5)
EXPECT_FALSE(ts.is_inside(0.25, 0.75));
// Point at (0.75, 0.75) — outside both
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);
// Points on the boundary should be considered inside (convention)
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));
EXPECT_TRUE(ts.is_inside(0.5, 0.0));
}
// ═══════════════════════════════════════════════════════════
// Hole (inner loop)
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, Hole_CenterIsOutside) {
auto surf = make_plane();
TrimmedSurface ts;
ts.base_surface = surf;
// Outer loop: full [0,1]×[0,1] rectangle
ts.loops.push_back(make_rect_loop(0.0, 1.0, 0.0, 1.0));
// Inner loop (hole): [0.25,0.75]×[0.25,0.75] square
auto hole = make_rect_loop(0.25, 0.75, 0.25, 0.75);
hole.is_outer = false;
ts.loops.push_back(hole);
// Center (0.5, 0.5) is inside the hole → outside trimmed surface
EXPECT_FALSE(ts.is_inside(0.5, 0.5));
auto p_center = ts.evaluate(0.5, 0.5);
EXPECT_FALSE(p_center.has_value());
// Corner (0.1, 0.1) is inside outer, outside hole → inside trimmed surface
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);
EXPECT_NEAR(p_corner->y(), 1.0, 1e-6);
// Point inside hole boundary
EXPECT_FALSE(ts.is_inside(0.3, 0.3));
}
// ═══════════════════════════════════════════════════════════
// Bounds
// ═══════════════════════════════════════════════════════════
TEST(TrimmedSurfaceTest, Bounds_Untrimmed) {
auto surf = make_plane();
auto ts = TrimmedSurface::from_surface(surf);
auto b = ts.bounds();
// The plane spans [0,10]×[0,10] at Z=0
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);
EXPECT_NEAR(b.min().z(), 0.0, 1e-6);
EXPECT_NEAR(b.max().z(), 0.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();
// Trimmed to [2.5, 7.5]×[2.5, 7.5] at Z=0
EXPECT_NEAR(b.min().x(), 2.5, 1e-6);
EXPECT_NEAR(b.max().x(), 7.5, 1e-6);
EXPECT_NEAR(b.min().y(), 2.5, 1e-6);
EXPECT_NEAR(b.max().y(), 7.5, 1e-6);
EXPECT_NEAR(b.min().z(), 0.0, 1e-6);
EXPECT_NEAR(b.max().z(), 0.0, 1e-6);
}
TEST(TrimmedSurfaceTest, Bounds_WithHole) {
auto surf = make_plane();
TrimmedSurface ts;
ts.base_surface = surf;
ts.loops.push_back(make_rect_loop(0.2, 0.8, 0.2, 0.8));
auto hole = make_rect_loop(0.3, 0.7, 0.3, 0.7);
hole.is_outer = false;
ts.loops.push_back(hole);
auto b = ts.bounds();
// Bounds are computed from outer loop only, spans [2,8]×[2,8] at Z=0
EXPECT_NEAR(b.min().x(), 2.0, 1e-6);
EXPECT_NEAR(b.max().x(), 8.0, 1e-6);
EXPECT_NEAR(b.min().y(), 2.0, 1e-6);
EXPECT_NEAR(b.max().y(), 8.0, 1e-6);
}
// ═══════════════════════════════════════════════════════════
// from_surface constructor
// ═══════════════════════════════════════════════════════════
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));
}
// ═══════════════════════════════════════════════════════════
// from_rect constructor
// ═══════════════════════════════════════════════════════════
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);
// Rectangular loop has 4 p-curves (one per side)
EXPECT_EQ(ts.loops[0].p_curves.size(), 4u);
}