Files
ViewDesignEngine/tests/brep/test_brep_modeling.cpp
T
茂之钳 5825609c17
CI / Build & Test (push) Failing after 32s
CI / Release Build (push) Failing after 35s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
fix(v4.2): shared topology stability — fillet/chamfer/heal/featuretree pass
- 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)
2026-07-25 05:28:41 +00:00

374 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}