1aa753ba50
P0 — Boolean Robustness (Parasolid): - 7-class degenerate taxonomy: surface_overlap, curve_overlap, high_order, boundary_contact, vertex_contact, non_manifold_contact - Multi-point sampling: N=sqrt(area/tol²) points, >75% voting, exact_orient3d fallback on boundary cases - PrecisionTracker: accumulated loss, threshold warning P0 — Auto-Heal Pipeline (ACIS): - 5-step pipeline: Stitch→Simplify→Regularize→Orient→Check - AutoHealReport with per-step status, element counts, overall score P1 — Direct Modeling (ACIS): - fill_hole: edge loop → plane fit → NURBS insert - pull_up_to_face: push_pull to target face alignment Total: +~1500 lines across 6 files
408 lines
14 KiB
C++
408 lines
14 KiB
C++
#include <gtest/gtest.h>
|
|
#include "vde/brep/brep.h"
|
|
#include "vde/brep/modeling.h"
|
|
#include "vde/brep/direct_modeling.h"
|
|
#include "vde/brep/brep_validate.h"
|
|
#include "vde/core/transform.h"
|
|
#include <cmath>
|
|
|
|
using namespace vde::brep;
|
|
using namespace vde::core;
|
|
using namespace vde::curves;
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// tweak_face — 面偏移测试
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, TweakFace_BoxTopOffset) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = tweak_face(box, 0, 0.5);
|
|
|
|
EXPECT_TRUE(result.success) << "tweak_face should succeed for box top face";
|
|
EXPECT_GT(result.body.num_faces(), 0u);
|
|
|
|
// After tweak, model should still be valid
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid) << "Offset model should still be valid";
|
|
}
|
|
|
|
TEST(DirectModelingTest, TweakFace_BoxInwardOffset) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = tweak_face(box, 0, -0.3);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, TweakFace_ZeroDelta) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = tweak_face(box, 0, 0.0);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
// Zero delta should produce valid model (essentially unchanged)
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
EXPECT_EQ(result.body.num_faces(), box.num_faces());
|
|
}
|
|
|
|
TEST(DirectModelingTest, TweakFace_InvalidFaceId) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = tweak_face(box, 99, 0.5);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|
|
|
|
TEST(DirectModelingTest, TweakFace_CylinderCap) {
|
|
auto cyl = make_cylinder(2, 4, 32);
|
|
auto result = tweak_face(cyl, 0, 0.5);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// move_face — 面移动测试
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, MoveFace_Translate) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto T = translate(Vector3D(0.5, 0, 0));
|
|
auto result = move_face(box, 0, T);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, MoveFace_Rotate) {
|
|
auto box = make_box(2, 2, 2);
|
|
// Rotate around Z axis by small angle
|
|
auto T = rotate_z(M_PI / 12);
|
|
auto result = move_face(box, 0, T);
|
|
|
|
// Small rotation should succeed
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, MoveFace_TranslateAndRotate) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto T = translate(Vector3D(0.2, 0.1, 0)) * rotate_z(0.1);
|
|
auto result = move_face(box, 0, T);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, MoveFace_LargeRotationFails) {
|
|
auto box = make_box(2, 2, 2);
|
|
// Large rotation should cause normal flip and be rejected
|
|
auto T = rotate_z(2.0); // ~115 degrees
|
|
auto result = move_face(box, 0, T);
|
|
|
|
// May succeed or fail depending on detection, but should not crash
|
|
// At minimum the result body should exist
|
|
EXPECT_GE(result.body.num_faces(), 0u);
|
|
}
|
|
|
|
TEST(DirectModelingTest, MoveFace_InvalidFaceId) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto T = translate(Vector3D(0.5, 0, 0));
|
|
auto result = move_face(box, 99, T);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// replace_face — 面替换测试
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, ReplaceFace_WithSameSurface) {
|
|
auto box = make_box(2, 2, 2);
|
|
// Replace face 0 with its own surface (should succeed)
|
|
const auto& orig_face = box.face(0);
|
|
const auto& orig_surf = box.surface(orig_face.surface_id);
|
|
|
|
auto result = replace_face(box, 0, orig_surf);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, ReplaceFace_WithCurvedSurface) {
|
|
auto box = make_box(2, 2, 2);
|
|
|
|
// Create a slightly curved surface matching the top face boundary
|
|
// Top face of box(2,2,2) has corners: (-1,-1,1), (1,-1,1), (1,1,1), (-1,1,1)
|
|
std::vector<std::vector<Point3D>> grid = {
|
|
{Point3D(-1, -1, 1.0), Point3D(-1, 1, 1.0)},
|
|
{Point3D( 1, -1, 1.2), Point3D( 1, 1, 1.2)} // curved up in middle
|
|
};
|
|
NurbsSurface curved(grid, {0,0,1,1}, {0,0,1,1}, {}, 1, 1);
|
|
|
|
auto result = replace_face(box, 0, curved);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, ReplaceFace_InvalidFaceId) {
|
|
auto box = make_box(2, 2, 2);
|
|
const auto& orig_face = box.face(0);
|
|
const auto& orig_surf = box.surface(orig_face.surface_id);
|
|
|
|
auto result = replace_face(box, 99, orig_surf);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// push_pull — 推拉测试
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, PushPull_ExtrudePositive) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = push_pull(box, 0, 1.0);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
// Extrusion should add side faces
|
|
EXPECT_GT(result.new_faces, 0);
|
|
EXPECT_GT(result.body.num_faces(), box.num_faces());
|
|
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, PushPull_CutNegative) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = push_pull(box, 0, -0.3);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
// Cut should not add new faces, but reshape existing ones
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, PushPull_ZeroDistance) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = push_pull(box, 0, 0.0);
|
|
|
|
// Zero distance push-pull: model unchanged (side faces degenerate)
|
|
EXPECT_GE(result.body.num_faces(), box.num_faces());
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, PushPull_InvalidFaceId) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = push_pull(box, 99, 1.0);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// offset_face — 面等距偏移测试
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, OffsetFace_Basic) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = offset_face(box, 0, 0.3);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, OffsetFace_Inward) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = offset_face(box, 0, -0.2);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Validation after operations — 操作后模型验证
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, AllOps_PreserveValidation) {
|
|
auto box = make_box(2, 2, 2);
|
|
|
|
// tweak_face
|
|
auto r1 = tweak_face(box, 0, 0.3);
|
|
EXPECT_TRUE(validate(r1.body).valid);
|
|
|
|
// move_face
|
|
auto r2 = move_face(box, 0, translate(Vector3D(0.1, 0, 0)));
|
|
EXPECT_TRUE(validate(r2.body).valid);
|
|
|
|
// push_pull
|
|
auto r3 = push_pull(box, 0, 0.5);
|
|
EXPECT_TRUE(validate(r3.body).valid);
|
|
|
|
// offset_face
|
|
auto r4 = offset_face(box, 0, 0.2);
|
|
EXPECT_TRUE(validate(r4.body).valid);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 退化情况 — 非法位置测试
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, Degenerate_NegativeFaceId) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = tweak_face(box, -1, 0.5);
|
|
EXPECT_FALSE(result.success);
|
|
}
|
|
|
|
TEST(DirectModelingTest, Degenerate_MoveIntoSelfIntersection) {
|
|
auto box = make_box(2, 2, 2);
|
|
// Move a face far enough to flip the normal → should be detected
|
|
auto T = rotate_x(M_PI); // 180° rotation
|
|
auto result = move_face(box, 0, T);
|
|
|
|
// Should at minimum produce a body (even if invalid)
|
|
EXPECT_GE(result.body.num_faces(), 0u);
|
|
}
|
|
|
|
TEST(DirectModelingTest, Degenerate_ExcessivePushPull) {
|
|
auto box = make_box(2, 2, 2);
|
|
// Push-pull with very large negative distance (cut through)
|
|
auto result = push_pull(box, 0, -5.0);
|
|
|
|
EXPECT_TRUE(result.success || !result.errors.empty());
|
|
// Should not crash even with excessive distance
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// fill_hole — 填孔测试 (v5.5)
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, FillHole_BoxFaceEdges) {
|
|
// 使用 box 已有面的边作为 hole 边界来测试 fill_hole
|
|
auto box = make_box(2, 2, 2);
|
|
auto face_edges = box.face_edges(0);
|
|
|
|
auto result = fill_hole(box, face_edges);
|
|
|
|
EXPECT_TRUE(result.success) << "fill_hole should succeed with valid edge loop";
|
|
EXPECT_GT(result.affected_faces, 0); // 应返回新增面的 face_id
|
|
EXPECT_GT(result.body.num_faces(), box.num_faces()); // 新增了一个面
|
|
}
|
|
|
|
TEST(DirectModelingTest, FillHole_TooFewEdges) {
|
|
auto box = make_box(2, 2, 2);
|
|
// 少于 3 条边不能构成面
|
|
std::vector<int> two_edges = {0, 1};
|
|
|
|
auto result = fill_hole(box, two_edges);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|
|
|
|
TEST(DirectModelingTest, FillHole_NonClosedLoop) {
|
|
auto box = make_box(2, 2, 2);
|
|
// 取不连续的边,无法形成封闭环
|
|
std::vector<int> non_closed = {0, 5, 10};
|
|
|
|
auto result = fill_hole(box, non_closed);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|
|
|
|
TEST(DirectModelingTest, FillHole_InvalidEdgeId) {
|
|
auto box = make_box(2, 2, 2);
|
|
// 包含不存在的边 ID
|
|
std::vector<int> bad_edges = {0, 999, 1, 2};
|
|
|
|
auto result = fill_hole(box, bad_edges);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|
|
|
|
TEST(DirectModelingTest, FillHole_TriangleHole) {
|
|
// 用三条边(三角形 hole)测试
|
|
auto box = make_box(2, 2, 2);
|
|
// 取某个面的前 3 条边组成三角形环
|
|
auto face_edges = box.face_edges(0);
|
|
ASSERT_GE(face_edges.size(), 3u);
|
|
|
|
std::vector<int> tri_loop = {face_edges[0], face_edges[1], face_edges[2]};
|
|
// 检查这 3 条边是否首尾相连
|
|
const auto& b = box;
|
|
int v00 = b.edge(tri_loop[0]).v_start;
|
|
int v01 = b.edge(tri_loop[0]).v_end;
|
|
int v10 = b.edge(tri_loop[1]).v_start;
|
|
int v11 = b.edge(tri_loop[1]).v_end;
|
|
int v20 = b.edge(tri_loop[2]).v_start;
|
|
int v21 = b.edge(tri_loop[2]).v_end;
|
|
|
|
if (v01 == v10 && v11 == v20 && v21 == v00) {
|
|
auto result = fill_hole(box, tri_loop);
|
|
// 即使可能不完全封闭整个面,也不应崩溃
|
|
EXPECT_GE(result.body.num_faces(), 0u);
|
|
}
|
|
// 如果不能构成环则跳过
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// pull_up_to_face — 拉伸面到目标面测试 (v5.5)
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(DirectModelingTest, PullUpFace_Basic) {
|
|
// Box: 面 0 (顶面, z=1) → 面 2 (底面, z=-1),拉到底面
|
|
auto box = make_box(2, 2, 2);
|
|
// 面 0 和面 2 是相对面
|
|
auto result = pull_up_to_face(box, 0, 2);
|
|
|
|
EXPECT_TRUE(result.success) << "pull_up_to_face should succeed";
|
|
EXPECT_GT(result.body.num_faces(), 0u);
|
|
}
|
|
|
|
TEST(DirectModelingTest, PullUpFace_OppositeFace) {
|
|
// 将 box 的一个侧面拉到对面
|
|
auto box = make_box(2, 2, 2);
|
|
// 面 3 和面 4 是相对的侧面 (x 方向)
|
|
auto result = pull_up_to_face(box, 3, 4);
|
|
|
|
EXPECT_TRUE(result.success);
|
|
auto val = validate(result.body);
|
|
EXPECT_TRUE(val.valid);
|
|
}
|
|
|
|
TEST(DirectModelingTest, PullUpFace_InvalidFaceId) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = pull_up_to_face(box, 99, 0);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|
|
|
|
TEST(DirectModelingTest, PullUpFace_SameFace) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = pull_up_to_face(box, 0, 0);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|
|
|
|
TEST(DirectModelingTest, PullUpFace_InvalidTarget) {
|
|
auto box = make_box(2, 2, 2);
|
|
auto result = pull_up_to_face(box, 0, 999);
|
|
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_FALSE(result.errors.empty());
|
|
}
|