Files
ViewDesignEngine/tests/brep/test_brep_modeling.cpp
T

374 lines
12 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include "vde/brep/brep.h"
#include "vde/brep/modeling.h"
#include <cmath>
using namespace vde::brep;
using namespace vde::core;
// ═══════════════════════════════════════════════════════════
// Fillet Tests
// ═══════════════════════════════════════════════════════════
TEST(FilletTest, FilletOnBoxEdge) {
auto box = make_box(4.0, 4.0, 4.0);
ASSERT_TRUE(box.is_valid());
ASSERT_GE(box.num_edges(), 12u); // 4 edges × 6 faces / shared? Actually 4*6=24 in current impl
// Fillet edge 0 (first edge of first face)
auto result = fillet(box, 0, 0.5);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
// Should have more faces than original (fillet adds new faces, removes sharper ones)
// The fillet replaces 2 faces with their trimmed versions + N fillet faces
EXPECT_GT(result.num_faces(), 4u);
EXPECT_GT(result.num_vertices(), 8u);
}
TEST(FilletTest, FilletOnBoxEdge_ValidBody) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = fillet(box, 0, 0.3);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
EXPECT_GE(result.num_faces(), 3u);
EXPECT_GE(result.num_vertices(), 8u);
}
TEST(FilletTest, FilletOnBoxEdge_ToMesh) {
auto box = make_box(3.0, 3.0, 3.0);
auto result = fillet(box, 0, 0.3);
auto mesh = result.to_mesh();
EXPECT_GT(mesh.num_faces(), 0u);
EXPECT_GT(mesh.num_vertices(), 0u);
}
TEST(FilletTest, Fillet_ZeroRadius_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = fillet(box, 0, 0.0);
// Zero radius should return original unchanged
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(FilletTest, Fillet_InvalidEdge_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = fillet(box, -1, 0.5);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(FilletTest, Fillet_NonexistentEdge_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
int fake_edge = static_cast<int>(box.num_edges() + 10);
auto result = fillet(box, fake_edge, 0.5);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(FilletTest, Fillet_NonManifoldEdge_ReturnsOriginal) {
// With shared topology (v4.2+), each edge is shared by exactly 2 faces
auto box = make_box(2.0, 2.0, 2.0);
auto faces = box.edge_faces(0);
EXPECT_EQ(faces.size(), 2u);
}
TEST(FilletTest, Fillet_MultipleEdges) {
auto box = make_box(4.0, 4.0, 4.0);
// First verify our model has enough edges
ASSERT_GE(box.num_edges(), 4u);
// Fillet first edge
auto result1 = fillet(box, 0, 0.3);
EXPECT_TRUE(result1.is_valid());
EXPECT_GT(result1.num_faces(), 4u);
// Fillet a different edge
if (box.num_edges() >= 5) {
auto result2 = fillet(box, 4, 0.3);
EXPECT_TRUE(result2.is_valid());
}
}
// ═══════════════════════════════════════════════════════════
// Variable-Radius Fillet Tests
// ═══════════════════════════════════════════════════════════
TEST(FilletVariableTest, FilletVariableBoxEdge) {
auto box = make_box(2.0, 2.0, 2.0);
ASSERT_TRUE(box.is_valid());
auto result = fillet_variable(box, 0, 0.2, 0.5, 8);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
// Should add fillet faces (at least the original 6 + fillet strip faces)
EXPECT_GE(result.num_faces(), 6u);
EXPECT_GE(result.num_vertices(), 8u);
}
TEST(FilletVariableTest, FilletVariableBoxEdge_ToMesh) {
auto box = make_box(3.0, 3.0, 3.0);
auto result = fillet_variable(box, 0, 0.2, 0.6, 10);
EXPECT_TRUE(result.is_valid());
auto mesh = result.to_mesh();
EXPECT_GT(mesh.num_faces(), 0u);
EXPECT_GT(mesh.num_vertices(), 0u);
}
TEST(FilletVariableTest, FilletVariableZeroStartRadius) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = fillet_variable(box, 0, 0.0, 0.3, 8);
// Zero start radius should still produce a valid body
// (fillet fades from 0 at start to 0.3 at end)
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
EXPECT_GE(result.num_faces(), 6u);
}
TEST(FilletVariableTest, FilletVariableBothZero_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = fillet_variable(box, 0, 0.0, 0.0, 8);
// Both radii zero: should return original unchanged
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(FilletVariableTest, FilletVariableInvalidEdge_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = fillet_variable(box, -1, 0.2, 0.5, 8);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(FilletVariableTest, FilletVariableNonexistentEdge_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
int fake_edge = static_cast<int>(box.num_edges() + 10);
auto result = fillet_variable(box, fake_edge, 0.2, 0.5, 8);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(FilletVariableTest, FilletVariableHighSamples) {
auto box = make_box(4.0, 4.0, 4.0);
auto result = fillet_variable(box, 0, 0.3, 0.8, 20);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
// More samples → more fillet strip faces (20 samples = 20 quad faces)
EXPECT_GE(result.num_faces(), 25u); // at least 20 fillet + a few rebuild faces
}
TEST(FilletVariableTest, FilletVariableSymmetric) {
// r_start == r_end should behave like constant fillet
auto box = make_box(3.0, 3.0, 3.0);
auto result = fillet_variable(box, 0, 0.4, 0.4, 8);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
EXPECT_GE(result.num_faces(), 6u);
}
// ═══════════════════════════════════════════════════════════
// Chamfer Tests
// ═══════════════════════════════════════════════════════════
TEST(ChamferTest, ChamferOnBoxEdge) {
auto box = make_box(4.0, 4.0, 4.0);
ASSERT_TRUE(box.is_valid());
auto result = chamfer(box, 0, 0.5);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
// Chamfer adds faces (bevel face replaces sharp corner)
EXPECT_GT(result.num_faces(), 4u);
EXPECT_GT(result.num_vertices(), 8u);
}
TEST(ChamferTest, Chamfer_ToMesh) {
auto box = make_box(3.0, 3.0, 3.0);
auto result = chamfer(box, 0, 0.3);
auto mesh = result.to_mesh();
EXPECT_GT(mesh.num_faces(), 0u);
EXPECT_GT(mesh.num_vertices(), 0u);
}
TEST(ChamferTest, Chamfer_ZeroDistance_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = chamfer(box, 0, 0.0);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(ChamferTest, Chamfer_InvalidEdge_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = chamfer(box, -1, 0.5);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
TEST(ChamferTest, Chamfer_NonexistentEdge_ReturnsOriginal) {
auto box = make_box(2.0, 2.0, 2.0);
int fake_edge = static_cast<int>(box.num_edges() + 5);
auto result = chamfer(box, fake_edge, 0.5);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_faces(), box.num_faces());
}
// ═══════════════════════════════════════════════════════════
// Shell Tests
// ═══════════════════════════════════════════════════════════
TEST(ShellTest, Shell_OpenBox_CreatesThinWall) {
auto box = make_box(2.0, 2.0, 2.0);
// Shell with open face (remove first face, thickness = 0.2)
auto result = shell(box, 0, 0.2);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
// Shell creates both outer and inner faces + side walls for each edge of open face
// Open face has 4 edges → 4 side wall quads
// Remaining 5 faces → 5 outer + 5 inner = 10 faces
// Total: 4 side walls + 10 offset faces = 14 faces
EXPECT_GE(result.num_faces(), 10u);
EXPECT_GE(result.num_vertices(), 16u);
// Bounds should expand outward slightly (both inner and outer vertices exist)
auto b = result.bounds();
EXPECT_NEAR(b.extent().x(), 2.0, 0.3);
EXPECT_NEAR(b.extent().y(), 2.0, 0.3);
EXPECT_NEAR(b.extent().z(), 2.0, 0.3);
}
TEST(ShellTest, Shell_OpenBox_ToMesh) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = shell(box, 0, 0.2);
auto mesh = result.to_mesh();
EXPECT_GT(mesh.num_faces(), 0u);
EXPECT_GT(mesh.num_vertices(), 0u);
}
TEST(ShellTest, Shell_ClosedBody_HollowSphere) {
auto sphere = make_sphere(1.5, 16, 8);
// Closed shell: no opening, just hollow
auto result = shell(sphere, -1, 0.1);
EXPECT_TRUE(result.is_valid());
EXPECT_EQ(result.num_bodies(), 1u);
// With closed shell, we get inner + outer faces for each original face
EXPECT_GE(result.num_faces(), sphere.num_faces() * 2);
auto mesh = result.to_mesh();
EXPECT_GT(mesh.num_faces(), 0u);
}
TEST(ShellTest, Shell_ClosedBody_ToMesh) {
auto sphere = make_sphere(1.0, 12, 6);
auto result = shell(sphere, -1, 0.15);
auto mesh = result.to_mesh();
EXPECT_GT(mesh.num_faces(), 0u);
EXPECT_GT(mesh.num_vertices(), 0u);
}
TEST(ShellTest, Shell_PositiveThickness_OutwardOffset) {
auto box = make_box(2.0, 2.0, 2.0);
// Positive thickness → outward offset
auto result = shell(box, 0, 0.3);
EXPECT_TRUE(result.is_valid());
auto b = result.bounds();
// Outer bounds should expand
EXPECT_GT(b.extent().x(), 2.0 - 1e-6);
}
TEST(ShellTest, Shell_NegativeThickness_InwardOffset) {
auto box = make_box(2.0, 2.0, 2.0);
// Negative thickness → inward offset
// Note: current implementation always uses -abs(thickness) for inward
auto result = shell(box, 0, -0.2);
EXPECT_TRUE(result.is_valid());
// Should still produce valid geometry
EXPECT_GT(result.num_faces(), 4u);
}
TEST(ShellTest, Shell_InvalidOpenFace) {
auto box = make_box(2.0, 2.0, 2.0);
int fake_face = static_cast<int>(box.num_faces() + 10);
auto result = shell(box, fake_face, 0.2);
// Should still work (just no side walls)
EXPECT_TRUE(result.is_valid());
EXPECT_GE(result.num_faces(), 4u);
}
TEST(ShellTest, Shell_SideWallsExist) {
auto box = make_box(2.0, 2.0, 2.0);
auto result = shell(box, 0, 0.2);
// With an open face, the face count should be:
// 5 outer faces + 5 inner faces + (4 edges * 1 side wall each) = 14
// But our box has 6 faces, each with 4 edges, 24 total edges
// Face 0 has 4 edges → 4 side walls
auto open_edges = box.face_edges(0);
size_t expected_walls = open_edges.size();
// Total faces = (num_faces-1)*2 + expected_walls
size_t expected_total = (box.num_faces() - 1) * 2 + expected_walls;
EXPECT_GE(result.num_faces(), expected_total);
}
TEST(ShellTest, Shell_ThinWall_VeryThin) {
auto box = make_box(2.0, 2.0, 2.0);
// Very thin wall should still work
auto result = shell(box, 0, 0.01);
EXPECT_TRUE(result.is_valid());
EXPECT_GT(result.num_faces(), 4u);
}
TEST(ShellTest, Shell_ClosedBox) {
auto box = make_box(2.0, 2.0, 2.0);
// Closed shell (open_face = -1) — no opening, fully hollow box
auto result = shell(box, -1, 0.1);
EXPECT_TRUE(result.is_valid());
// All 6 faces → 6 outer + 6 inner = 12 faces (no side walls)
EXPECT_GE(result.num_faces(), 12u);
}
TEST(ShellTest, Shell_ZeroSized) {
auto box = make_box(1.0, 1.0, 1.0);
// Shell with thickness larger than box — should still produce valid geometry
auto result = shell(box, 0, 0.6);
EXPECT_TRUE(result.is_valid());
// May produce fewer faces if some faces collapse, but shouldn't crash
EXPECT_GE(result.num_faces(), 0u);
}