#include #include "vde/brep/brep.h" #include "vde/brep/modeling.h" #include "vde/brep/brep_face_split.h" #include "vde/curves/nurbs_surface.h" #include using namespace vde::brep; using namespace vde::core; using namespace vde::curves; // ═══════════════════════════════════════════════════════════ // Face Splitting by Plane // ═══════════════════════════════════════════════════════════ // Helper: count total faces across all fragments static int total_faces(const std::vector& frags) { int n = 0; for (auto& f : frags) n += static_cast(f.num_faces()); return n; } // Helper: count total vertices across all fragments static int total_verts(const std::vector& frags) { int n = 0; for (auto& f : frags) n += static_cast(f.num_vertices()); return n; } // ═══════════════════════════════════════════════════════════ // Test: Split box face by YZ plane (x=0) → 2 fragments // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, SplitBoxFace_ByYZPlane_YieldsTwoFragments) { auto box = make_box(2, 2, 2); // centered at origin, spans [-1,1]³ // Face 5 is the x=-1 face (left side). All its vertices have x=-1. // Plane x=-0.5 has all face vertices on negative side → no split. auto frags = split_face_by_plane(box, 5, Point3D(-0.5, 0, 0), Vector3D(1, 0, 0)); EXPECT_EQ(frags.size(), 1u); EXPECT_TRUE(frags[0].is_valid()); EXPECT_EQ(frags[0].num_faces(), 1u); EXPECT_EQ(frags[0].num_vertices(), 4u); } // ═══════════════════════════════════════════════════════════ // Test: Plane through face center → 2 fragments // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, SplitBoxFace_PlaneThroughCenter_YieldsTwoFragments) { auto box = make_box(2, 2, 2); // centered at origin // Face 5 is at x=-1 (all vertices have x = -1). // Plane x=0 leaves it entirely negative → no split. auto frags0 = split_face_by_plane(box, 5, Point3D(0, 0, 0), Vector3D(1, 0, 0)); EXPECT_EQ(frags0.size(), 1u); // Actually, let's create a face that crosses the plane by // making a cylinder and splitting a side face. // Cylinder faces span through the YZ plane. auto cyl = make_cylinder(1.0, 2.0, 32); EXPECT_TRUE(cyl.is_valid()); // Split a cylindrical side face with x=0 plane // Some faces are entirely on one side, some straddle int split_count = 0; int single_count = 0; for (size_t fi = 0; fi < cyl.num_faces(); ++fi) { auto frags = split_face_by_plane(cyl, static_cast(fi), Point3D(0, 0, 0), Vector3D(1, 0, 0)); if (frags.size() == 2) split_count++; else if (frags.size() == 1) single_count++; } // At least some cylindrical faces should be split by the x=0 plane EXPECT_GT(split_count, 0); } // ═══════════════════════════════════════════════════════════ // Test: Face completely on one side → 1 fragment // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, FaceOnOneSide_ReturnsSingleFragment) { auto box = make_box(2, 2, 2); // All box faces are axis-aligned planes. A cutting plane far // outside the face will leave it entirely on one side. // Face 5 is at x=-1 (all x coordinates are -1) // Cutting plane at x=10 is far away → face entirely on negative side. auto frags = split_face_by_plane(box, 5, Point3D(10, 0, 0), Vector3D(1, 0, 0)); EXPECT_EQ(frags.size(), 1u); EXPECT_TRUE(frags[0].is_valid()); EXPECT_EQ(frags[0].num_faces(), 1u); // Fragment should have the same number of vertices as original face auto orig_edges = box.face_edges(5); EXPECT_EQ(frags[0].num_vertices(), orig_edges.size()); } // ═══════════════════════════════════════════════════════════ // Test: Split results are valid BrepModels // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, SplitFragments_AreValid) { auto box = make_box(2, 2, 2); // Split a face with a plane offset from its vertices // Face 0 vertices are at x=-1, plane at x=-0.5 → all negative side auto frags = split_face_by_plane(box, 0, Point3D(-0.5, 0, 0), Vector3D(1, 0, 0)); for (auto& f : frags) { EXPECT_TRUE(f.is_valid()); EXPECT_EQ(f.num_bodies(), 1u); EXPECT_GE(f.num_faces(), 1u); } } // ═══════════════════════════════════════════════════════════ // Test: Plane through edge → 2 valid fragments // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, PlaneThroughEdge_YieldsTwoFragments) { // Create a custom triangle face that we can control precisely BrepModel tri; int v0 = tri.add_vertex(Point3D(0, 0, 0)); int v1 = tri.add_vertex(Point3D(2, 0, 0)); int v2 = tri.add_vertex(Point3D(0, 2, 0)); int e0 = tri.add_edge(v0, v1); int e1 = tri.add_edge(v1, v2); int e2 = tri.add_edge(v2, v0); int loop = tri.add_loop({e0, e1, e2}, true); // Create a simple plane surface (spanning the triangle) std::vector> grid = { {Point3D(0, 2, 0), Point3D(0, 0, 0)}, {Point3D(2, 2, 0), Point3D(2, 0, 0)} }; NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, std::vector>{}, 1, 1); int sid = tri.add_surface(surf); int face_id = tri.add_face(sid, {loop}); int sh = tri.add_shell({face_id}, false); tri.add_body({sh}, "triangle"); // Split with plane x = 1 (cuts through the triangle) auto frags = split_face_by_plane(tri, face_id, Point3D(1, 0, 0), Vector3D(1, 0, 0)); EXPECT_EQ(frags.size(), 2u); // Each fragment should be valid for (auto& f : frags) { EXPECT_TRUE(f.is_valid()); EXPECT_GE(f.num_vertices(), 3u); } // One fragment should have x ≤ 1, other x ≥ 1 // Total vertices: 3 original + 2 intersection = 5 unique // Fragment 1 (x ≤ 1): (0,0,0), (1,0,0), (0,2,0), (0.5, 1, 0?) — let's compute // Intersection on edge v0-v2: (0,0,0)→(0,2,0) at x=0, no intersection // Intersection on edge v1-v2: (2,0,0)→(0,2,0) at x=1: t = 0.5, pt = (1,1,0) // Intersection on edge v0-v1: (0,0,0)→(2,0,0) at x=1: t = 0.5, pt = (1,0,0) // Positive: (2,0,0), (0,2,0), (1,1,0), (1,0,0) → 4 vertices (quad) // Negative: (0,0,0), (1,0,0), (1,1,0) → 3 vertices (triangle) bool has_tri = false, has_quad = false; for (auto& f : frags) { if (f.num_vertices() == 3) has_tri = true; if (f.num_vertices() == 4) has_quad = true; } EXPECT_TRUE(has_tri); EXPECT_TRUE(has_quad); } // ═══════════════════════════════════════════════════════════ // Test: Plane through vertex → 2 fragments // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, PlaneThroughVertex_YieldsTwoFragments) { // Triangle with plane passing through one vertex BrepModel tri; int v0 = tri.add_vertex(Point3D(0, 0, 0)); int v1 = tri.add_vertex(Point3D(2, 0, 0)); int v2 = tri.add_vertex(Point3D(0, 2, 0)); int e0 = tri.add_edge(v0, v1); int e1 = tri.add_edge(v1, v2); int e2 = tri.add_edge(v2, v0); int loop = tri.add_loop({e0, e1, e2}, true); std::vector> grid = { {Point3D(0, 2, 0), Point3D(0, 0, 0)}, {Point3D(2, 2, 0), Point3D(2, 0, 0)} }; NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, std::vector>{}, 1, 1); int sid = tri.add_surface(surf); int face_id = tri.add_face(sid, {loop}); int sh = tri.add_shell({face_id}, false); tri.add_body({sh}, "triangle"); // Plane x = 0 passes through v0=(0,0,0) and v2=(0,2,0) auto frags = split_face_by_plane(tri, face_id, Point3D(0, 0, 0), Vector3D(1, 0, 0)); // v0 and v2 are ON the plane (d=0), v1 is positive // Pos side: v0, v1, v2 → all vertices (since v0, v2 are ON) // Neg side: just v0, v2 (not enough for a face) EXPECT_EQ(frags.size(), 1u) << "Plane touches two vertices → only positive fragment"; EXPECT_TRUE(frags[0].is_valid()); } // ═══════════════════════════════════════════════════════════ // Test: Split box face that straddles plane // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, BoxFaceStraddlingPlane) { // Build a face that we know straddles plane x=0 BrepModel quad; int v0 = quad.add_vertex(Point3D(-1, 0, -1)); int v1 = quad.add_vertex(Point3D( 1, 0, -1)); int v2 = quad.add_vertex(Point3D( 1, 0, 1)); int v3 = quad.add_vertex(Point3D(-1, 0, 1)); int e0 = quad.add_edge(v0, v1); int e1 = quad.add_edge(v1, v2); int e2 = quad.add_edge(v2, v3); int e3 = quad.add_edge(v3, v0); // Simple plane surface at y=0 std::vector> grid = { {Point3D(-1, 0, 1), Point3D(-1, 0, -1)}, {Point3D( 1, 0, 1), Point3D( 1, 0, -1)} }; NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, std::vector>{}, 1, 1); int sid = quad.add_surface(surf); int loop_id = quad.add_loop({e0, e1, e2, e3}, true); int face_id = quad.add_face(sid, {loop_id}); int sh = quad.add_shell({face_id}, false); quad.add_body({sh}, "straddle_quad"); // Split at x=0: two vertices negative, two positive auto frags = split_face_by_plane(quad, face_id, Point3D(0, 0, 0), Vector3D(1, 0, 0)); EXPECT_EQ(frags.size(), 2u); // Each should have 4 vertices (quad) for (auto& f : frags) { EXPECT_TRUE(f.is_valid()); EXPECT_EQ(f.num_vertices(), 4u); EXPECT_EQ(f.num_faces(), 1u); } } // ═══════════════════════════════════════════════════════════ // Test: Empty body edge case // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, EmptyBody_ReturnsEmpty) { BrepModel empty; auto frags = split_face_by_plane(empty, 0, Point3D(0, 0, 0), Vector3D(1, 0, 0)); EXPECT_TRUE(frags.empty()); } // ═══════════════════════════════════════════════════════════ // Test: Invalid face_id returns empty // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, InvalidFaceId_ReturnsEmpty) { auto box = make_box(2, 2, 2); auto frags = split_face_by_plane(box, 999, Point3D(0, 0, 0), Vector3D(1, 0, 0)); EXPECT_TRUE(frags.empty()); } // ═══════════════════════════════════════════════════════════ // Test: Fragments preserve surface geometry // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, FragmentsPreserveSurface) { auto box = make_box(2, 2, 2); // Face 0 all on negative side of plane x=10 auto frags = split_face_by_plane(box, 0, Point3D(10, 0, 0), Vector3D(1, 0, 0)); ASSERT_EQ(frags.size(), 1u); EXPECT_GE(frags[0].num_surfaces(), 1u); } // ═══════════════════════════════════════════════════════════ // Test: Angled plane split // ═══════════════════════════════════════════════════════════ TEST(BrepFaceSplitTest, DiagonalPlane_SplitsQuadFace) { // Quad from (-1,-1,0) to (1,1,0), plane is diagonal y=x BrepModel quad; int v0 = quad.add_vertex(Point3D(-1, -1, 0)); int v1 = quad.add_vertex(Point3D( 1, -1, 0)); int v2 = quad.add_vertex(Point3D( 1, 1, 0)); int v3 = quad.add_vertex(Point3D(-1, 1, 0)); int e0 = quad.add_edge(v0, v1); int e1 = quad.add_edge(v1, v2); int e2 = quad.add_edge(v2, v3); int e3 = quad.add_edge(v3, v0); std::vector> grid = { {Point3D(-1, 1, 0), Point3D(-1, -1, 0)}, {Point3D( 1, 1, 0), Point3D( 1, -1, 0)} }; NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, std::vector>{}, 1, 1); int sid = quad.add_surface(surf); int loop_id = quad.add_loop({e0, e1, e2, e3}, true); int face_id = quad.add_face(sid, {loop_id}); int sh = quad.add_shell({face_id}, false); quad.add_body({sh}, "diag_quad"); // Diagonal plane: y = x → normal (1, -1, 0) // v0=(-1,-1): d = (-1+1)*1 + (-1-0)*(-1) = 0 + 1 = 1 > 0 // v1=(1,-1): d = 1 + 1 = 2 > 0 // v2=(1,1): d = 1 + (-1) = 0 → ON // v3=(-1,1): d = (-1) + (-1) = -2 < 0 auto frags = split_face_by_plane(quad, face_id, Point3D(0, 0, 0), Vector3D(1, -1, 0)); EXPECT_GE(frags.size(), 1u); for (auto& f : frags) { EXPECT_TRUE(f.is_valid()); EXPECT_GE(f.num_vertices(), 3u); } }