Files
ViewDesignEngine/tests/brep/test_direct_modeling.cpp
T
茂之钳 1aa753ba50
CI / Build & Test (push) Failing after 29s
CI / Release Build (push) Failing after 36s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
feat(v1.0.1): Parasolid + ACIS boolean robustness improvements
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
2026-07-27 20:14:52 +08:00

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());
}