feat(v3.6): TrimmedSurface — B-Rep fundamental primitive
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user