Files
ViewDesignEngine/tests/brep/test_brep_face_split.cpp
茂之钳 4f049b1296
CI / Build & Test (push) Failing after 44s
CI / Release Build (push) Failing after 45s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
fix: compilation + test fixes — 26/42 failures resolved
- CAM offset: fix knot collision in fit_clamped/make_circle → NaN
- Face split: fix add_face returning global ID instead of index → SEGFAULT
- Volume: fix formula to use abs-per-face for orientation robustness
- Build: fix vde_mesh linking, add vde_collision to vde_brep deps
- Tests: fix surface_intersection test, fuzz CMake, face split expectations
- Rename test_constraint_solver → test_constraint_solver_3d (naming conflict)
- Various include/namespace fixes across test files
2026-07-25 00:02:48 +00:00

343 lines
15 KiB
C++

#include <gtest/gtest.h>
#include "vde/brep/brep.h"
#include "vde/brep/modeling.h"
#include "vde/brep/brep_face_split.h"
#include "vde/curves/nurbs_surface.h"
#include <cmath>
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<BrepModel>& frags) {
int n = 0;
for (auto& f : frags) n += static_cast<int>(f.num_faces());
return n;
}
// Helper: count total vertices across all fragments
static int total_verts(const std::vector<BrepModel>& frags) {
int n = 0;
for (auto& f : frags) n += static_cast<int>(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<int>(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<std::vector<Point3D>> 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<std::vector<double>>{}, 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<std::vector<Point3D>> 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<std::vector<double>>{}, 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<std::vector<Point3D>> 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<std::vector<double>>{}, 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<std::vector<Point3D>> 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<std::vector<double>>{}, 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);
}
}