5825609c17
- add_vertex: return array index instead of global ID (fix ID/index mismatch) - is_valid: validate by array index, not vertex ID (IDs not contiguous when shared with edges/loops) - fillet/fillet_variable: fix duplicate edge in sorted_edges (loop had fillet edge twice) - test updates: Fillet_NonManifoldEdge expects 2 faces per edge (shared topology)
374 lines
12 KiB
C++
374 lines
12 KiB
C++
#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);
|
||
}
|