#include #include "vde/brep/trimmed_surface.h" #include "vde/curves/nurbs_surface.h" #include 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> cp = { {Point3D(0, 0, 0), Point3D(0, 10, 0)}, {Point3D(10, 0, 0), Point3D(10, 10, 0)} }; std::vector ku = {0, 0, 1, 1}; std::vector kv = {0, 0, 1, 1}; std::vector> 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 cps = { Point3D(ua, va, 0.0), Point3D(ub, vb, 0.0) }; std::vector knots = { 0.0, 0.0, 1.0, 1.0 }; std::vector 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); }