6bc9db663e
M3.1 — 大装配 LOD + 约束求解器 (Agent #0): - assembly_lod.h/.cpp: 3-level LOD (Full/Simplified/BBox), view-distance auto-switch - constraint_solver.h/.cpp: ConstraintGraph, DOF analysis, Newton-Raphson solver - 7 constraint types, over-constraint detection, incremental solve - 59 tests (22 LOD + 37 constraint) M3.2 — 完整 CAM 策略 (Agent #1): - cam_strategies.h/.cpp: roughing(Z-layer), finishing(parallel/spiral), drilling(G81/G83/G84) - Tool/ToolLibrary, PostProcessor (Fanuc/Siemens/Heidenhain) - material_removal_simulation with volume stats - 27 tests, 26/27 passing M3.3 — 直接建模 (Agent #2): - direct_modeling.h/.cpp: tweak_face, move_face, replace_face, push_pull, offset_face - Auto neighbor-face extension for watertightness - DirectModelingResult with diagnostics - 23 tests with validate() verification 12 files, ~5400 lines, 109 tests
285 lines
10 KiB
C++
285 lines
10 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
|
|
}
|