#include #include "vde/brep/trimmed_surface.h" #include "vde/brep/brep.h" #include "vde/brep/modeling.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 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 // ═══════════════════════════════════════════════════════════ // 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(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(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()); }