feat(v5-M3): large assembly LOD + constraint solver + CAM strategies + direct modeling
CI / Build & Test (push) Failing after 33s
CI / Release Build (push) Failing after 39s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

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
This commit is contained in:
茂之钳
2026-07-26 21:19:38 +08:00
parent 73df04d5cb
commit 6bc9db663e
15 changed files with 4806 additions and 130 deletions
+2
View File
@@ -32,3 +32,5 @@ add_vde_test(test_feature_recognition)
add_vde_test(test_v5_features)
add_vde_test(test_v5_1)
add_vde_test(test_advanced_blend)
add_vde_test(test_assembly_lod)
add_vde_test(test_direct_modeling)
+355
View File
@@ -0,0 +1,355 @@
#include <gtest/gtest.h>
#include "vde/brep/assembly_lod.h"
#include "vde/brep/modeling.h"
#include "vde/brep/assembly_instance.h"
#include "vde/core/transform.h"
#include "vde/core/aabb.h"
#include <cmath>
using namespace vde::brep;
using namespace vde::core;
// ═══════════════════════════════════════════════════════════
// 辅助函数
// ═══════════════════════════════════════════════════════════
/// 创建多零件测试装配体
static Assembly make_test_assembly() {
Assembly assy("test_lod");
assy.root.add_part("box_a", make_box(2, 2, 2));
assy.root.add_part("box_b", make_box(2, 2, 2), translate(5, 0, 0));
assy.root.add_part("box_c", make_box(2, 2, 2), translate(10, 0, 0));
return assy;
}
// ═══════════════════════════════════════════════════════════
// 测试 1: 构造与基本属性
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, ConstructFromAssembly) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
EXPECT_EQ(lod.part_count(), 3);
EXPECT_EQ(lod.nodes().size(), 3);
}
TEST(AssemblyLODTest, DefaultLODLevelIsFull) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
for (const auto& node : lod.nodes()) {
EXPECT_EQ(node.current_level, LODLevel::Full);
EXPECT_FALSE(node.manual_override);
}
}
TEST(AssemblyLODTest, HasValidWorldBounds) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
for (const auto& node : lod.nodes()) {
// 未初始化(全零)的 AABB 体积为 0,非空 AABB 体积 > 0
EXPECT_GT(node.world_bounds.volume(), 0.0);
}
}
// ═══════════════════════════════════════════════════════════
// 测试 2: 视距驱动 LOD 选择
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, NearPlaneSelectsFull) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
LODConfig cfg;
cfg.near_plane = 100.0;
cfg.far_plane = 1000.0;
lod.set_config(cfg);
// camera_distance = 10 < near_plane → Full
lod.compute_lod(10.0, 0.5);
for (const auto& node : lod.nodes()) {
EXPECT_EQ(node.current_level, LODLevel::Full);
}
}
TEST(AssemblyLODTest, FarPlaneSelectsBoundingBox) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
LODConfig cfg;
cfg.near_plane = 100.0;
cfg.far_plane = 1000.0;
lod.set_config(cfg);
// camera_distance = 2000 > far_plane → BoundingBox
lod.compute_lod(2000.0, 0.1);
for (const auto& node : lod.nodes()) {
EXPECT_EQ(node.current_level, LODLevel::BoundingBox);
}
}
TEST(AssemblyLODTest, MidRangeWithLargeScreenSizeSelectsSimplified) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
LODConfig cfg;
cfg.near_plane = 100.0;
cfg.far_plane = 1000.0;
lod.set_config(cfg);
// near_plane ≤ distance ≤ far_plane, large screen_size → Simplified
lod.compute_lod(500.0, 0.5);
for (const auto& node : lod.nodes()) {
EXPECT_EQ(node.current_level, LODLevel::Simplified);
}
}
TEST(AssemblyLODTest, MidRangeWithSmallScreenSizeSelectsBBox) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
LODConfig cfg;
cfg.near_plane = 100.0;
cfg.far_plane = 1000.0;
cfg.screen_size_threshold = 0.1;
lod.set_config(cfg);
// near ≤ dist ≤ far, small screen → BoundingBox
lod.compute_lod(500.0, 0.001);
for (const auto& node : lod.nodes()) {
EXPECT_EQ(node.current_level, LODLevel::BoundingBox);
}
}
// ═══════════════════════════════════════════════════════════
// 测试 3: 手动 LOD 设置
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, ManualSetLODLevel) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
auto& node0 = lod.nodes()[0];
lod.set_lod_level(node0, LODLevel::BoundingBox);
EXPECT_EQ(node0.current_level, LODLevel::BoundingBox);
EXPECT_TRUE(node0.manual_override);
}
TEST(AssemblyLODTest, ManualOverridePreventsAutoLOD) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
// 手动设置 node0 为 BoundingBox
lod.set_lod_level(lod.nodes()[0], LODLevel::BoundingBox);
// 自动 LOD 应设置为 Full (near plane)
LODConfig cfg;
cfg.near_plane = 100.0;
cfg.far_plane = 1000.0;
lod.set_config(cfg);
lod.compute_lod(10.0, 0.5); // near plane → Full
// 手动覆盖的节点仍保持 BoundingBox
EXPECT_EQ(lod.nodes()[0].current_level, LODLevel::BoundingBox);
// 其余节点应该是 Full
EXPECT_EQ(lod.nodes()[1].current_level, LODLevel::Full);
EXPECT_EQ(lod.nodes()[2].current_level, LODLevel::Full);
}
TEST(AssemblyLODTest, ClearManualOverride) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
lod.set_lod_level(lod.nodes()[0], LODLevel::Simplified);
EXPECT_TRUE(lod.nodes()[0].manual_override);
lod.clear_manual_override(lod.nodes()[0]);
EXPECT_FALSE(lod.nodes()[0].manual_override);
}
TEST(AssemblyLODTest, ClearAllManualOverrides) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
lod.set_lod_level(lod.nodes()[0], LODLevel::Simplified);
lod.set_lod_level(lod.nodes()[1], LODLevel::BoundingBox);
lod.clear_all_manual_overrides();
for (const auto& node : lod.nodes()) {
EXPECT_FALSE(node.manual_override);
}
}
// ═══════════════════════════════════════════════════════════
// 测试 4: 遍历与统计
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, TraverseAssemblyVisitsAllNodes) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
int visited = 0;
lod.traverse_assembly([&visited](const LODNode&) { ++visited; });
EXPECT_EQ(visited, 3);
}
TEST(AssemblyLODTest, StatisticsCounts) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
lod.set_lod_level(lod.nodes()[0], LODLevel::Full);
lod.set_lod_level(lod.nodes()[1], LODLevel::Simplified);
lod.set_lod_level(lod.nodes()[2], LODLevel::BoundingBox);
auto stats = lod.statistics();
EXPECT_EQ(stats.full_count, 1);
EXPECT_EQ(stats.simplified_count, 1);
EXPECT_EQ(stats.bbox_count, 1);
EXPECT_EQ(stats.manual_count, 3); // all three are manual
}
// ═══════════════════════════════════════════════════════════
// 测试 5: compute_lod_for_node 单独使用
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, ComputeLODForNodeNearPlane) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
LODConfig cfg;
cfg.near_plane = 100.0;
cfg.far_plane = 500.0;
lod.set_config(cfg);
auto level = lod.compute_lod_for_node(lod.nodes()[0], 10.0, 0.5);
EXPECT_EQ(level, LODLevel::Full);
}
TEST(AssemblyLODTest, DisableAutoLODReturnsFull) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
LODConfig cfg;
cfg.enable_auto_lod = false;
lod.set_config(cfg);
lod.compute_lod(5000.0, 0.001);
// Disabled auto LOD: all stay Full
for (const auto& node : lod.nodes()) {
EXPECT_EQ(node.current_level, LODLevel::Full);
}
}
// ═══════════════════════════════════════════════════════════
// 测试 6: 更新边界
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, UpdateBoundsAfterTransformChange) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
auto bb_before = lod.nodes()[0].world_bounds;
// 修改 node0 的变换
lod.nodes()[0].node->local_transform = translate(100, 0, 0);
lod.update_bounds();
auto bb_after = lod.nodes()[0].world_bounds;
// X 坐标应该偏移了 100
EXPECT_NEAR(bb_after.center().x(), bb_before.center().x() + 100.0, 1e-6);
}
// ═══════════════════════════════════════════════════════════
// 测试 7: 空装配
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, EmptyAssembly) {
Assembly assy("empty");
AssemblyLOD lod(assy);
EXPECT_EQ(lod.part_count(), 0);
EXPECT_EQ(lod.nodes().size(), 0);
auto stats = lod.statistics();
EXPECT_EQ(stats.full_count, 0);
EXPECT_EQ(stats.simplified_count, 0);
EXPECT_EQ(stats.bbox_count, 0);
}
// ═══════════════════════════════════════════════════════════
// 测试 8: LODConfig 默认值
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, DefaultConfig) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
const auto& cfg = lod.config();
EXPECT_DOUBLE_EQ(cfg.near_plane, 50.0);
EXPECT_DOUBLE_EQ(cfg.far_plane, 500.0);
EXPECT_TRUE(cfg.enable_auto_lod);
}
// ═══════════════════════════════════════════════════════════
// 测试 9: traverse_with_context
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, TraverseWithContextCallsAllParts) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
int parts_seen = 0;
lod.traverse_with_context([&](LODNode&, const Transform3D&, int) {
++parts_seen;
return true;
});
EXPECT_EQ(parts_seen, 3);
}
TEST(AssemblyLODTest, TraverseWithContextCanEarlyExit) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
int parts_seen = 0;
bool stopped = lod.traverse_with_context([&](LODNode&, const Transform3D&, int) {
++parts_seen;
return false; // stop after first part
});
EXPECT_FALSE(stopped);
EXPECT_EQ(parts_seen, 1);
}
// ═══════════════════════════════════════════════════════════
// 测试 10: InstanceCache 集成
// ═══════════════════════════════════════════════════════════
TEST(AssemblyLODTest, InstanceCacheIsPopulated) {
auto assy = make_test_assembly();
AssemblyLOD lod(assy);
// InstanceCache 应已填充
EXPECT_GT(lod.cache().unique_count(), 0);
}
TEST(AssemblyLODTest, SubAssemblyDoesNotAddToLOD) {
Assembly assy("mixed");
assy.root.add_part("part_a", make_box(1, 1, 1));
auto* sub = assy.root.add_subassembly("sub_a");
sub->add_part("part_b", make_box(1, 1, 1));
AssemblyLOD lod(assy);
// 应只有零件节点(part_a 和 part_b),没有子装配体
EXPECT_EQ(lod.part_count(), 2);
}
+303
View File
@@ -395,3 +395,306 @@ TEST(ConstraintSolverTest, ThreeNodeSolve) {
EXPECT_NEAR(gap_01, 0.0, 1e-3);
EXPECT_NEAR(gap_12, 0.0, 1e-3);
}
// ════════════════════════════════════════════════════════════════
// ConstraintGraph 测试
// ════════════════════════════════════════════════════════════════
TEST(ConstraintGraphTest, AddNodes) {
Assembly assy("graph_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2), translate(5, 0, 0));
ConstraintGraph graph;
int idx_a = graph.add_node("box_a", box_a);
int idx_b = graph.add_node("box_b", box_b);
EXPECT_EQ(graph.node_count(), 2);
EXPECT_EQ(idx_a, 0);
EXPECT_EQ(idx_b, 1);
EXPECT_EQ(graph.node_name(0), "box_a");
EXPECT_EQ(graph.node_name(1), "box_b");
EXPECT_EQ(graph.node_ptr(0), box_a);
}
TEST(ConstraintGraphTest, AddConstraints) {
Assembly assy("graph_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2), translate(5, 0, 0));
ConstraintGraph graph;
graph.add_node("box_a", box_a);
graph.add_node("box_b", box_b);
graph.add_constraint({0, 1, ConstraintType::Coincident});
graph.add_constraint({0, 1, ConstraintType::Concentric});
EXPECT_EQ(graph.constraint_count(), 2);
EXPECT_EQ(graph.constraint(0).type, ConstraintType::Coincident);
EXPECT_EQ(graph.constraint(1).type, ConstraintType::Concentric);
}
TEST(ConstraintGraphTest, HasConstraintBetween) {
Assembly assy("graph_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
graph.add_constraint({0, 1, ConstraintType::Coincident});
EXPECT_TRUE(graph.has_constraint_between(0, 1));
EXPECT_TRUE(graph.has_constraint_between(1, 0)); // 方向不重要,双向均查到
// 当 a == b 时的安全处理
EXPECT_FALSE(graph.has_constraint_between(-1, -1));
}
TEST(ConstraintGraphTest, RemoveConstraint) {
Assembly assy("graph_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
graph.add_constraint({0, 1, ConstraintType::Coincident});
graph.add_constraint({0, 1, ConstraintType::Distance, 5.0});
EXPECT_EQ(graph.constraint_count(), 2);
graph.remove_constraint(0);
// 约束不活跃,但计数不变(标记删除)
auto ncs = graph.node_constraints(0);
EXPECT_EQ(ncs.size(), 1); // 只剩约束 1
}
TEST(ConstraintGraphTest, ConstraintsBetween) {
Assembly assy("graph_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
graph.add_constraint({0, 1, ConstraintType::Coincident});
graph.add_constraint({0, 1, ConstraintType::Distance, 3.0});
auto between = graph.constraints_between(0, 1);
EXPECT_EQ(between.size(), 2);
}
TEST(ConstraintGraphTest, ClearGraph) {
Assembly assy("graph_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_constraint({0, 0, ConstraintType::Parallel}); // self-loop (unusual but valid)
graph.clear();
EXPECT_EQ(graph.node_count(), 0);
EXPECT_EQ(graph.constraint_count(), 0);
}
// ════════════════════════════════════════════════════════════════
// DOF 分析测试
// ════════════════════════════════════════════════════════════════
TEST(DOFAnalysisTest, SingleNodeHas6DOF) {
Assembly assy("dof_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
auto dof = graph.analyze_dof();
ASSERT_EQ(dof.size(), 1);
EXPECT_EQ(dof[0].total_dof, 6);
EXPECT_EQ(dof[0].eliminated_dof, 0);
EXPECT_EQ(dof[0].remaining_dof, 6);
EXPECT_FALSE(dof[0].is_fixed);
}
TEST(DOFAnalysisTest, CoincidentEliminates3DOF) {
Assembly assy("dof_test2");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
graph.add_constraint({0, 1, ConstraintType::Coincident});
auto dof = graph.analyze_dof();
// Coincident eliminates 3 DOF per node
EXPECT_EQ(dof[0].eliminated_dof, 3);
EXPECT_EQ(dof[1].eliminated_dof, 3);
EXPECT_EQ(dof[0].remaining_dof, 3);
}
TEST(DOFAnalysisTest, FullyConstrained) {
Assembly assy("dof_full");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
// Coincident (3) + Concentric (4) = 7 消除 per node
// 对 node_a: 3+4=7 > 6 → 全约束
graph.add_constraint({0, 1, ConstraintType::Coincident});
graph.add_constraint({0, 1, ConstraintType::Concentric});
auto dof = graph.analyze_dof();
EXPECT_TRUE(dof[0].is_fixed);
EXPECT_TRUE(dof[1].is_fixed);
EXPECT_TRUE(graph.is_fully_constrained());
}
TEST(DOFAnalysisTest, NotFullyConstrained) {
Assembly assy("dof_partial");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
graph.add_constraint({0, 1, ConstraintType::Parallel}); // only 2 DOF
EXPECT_FALSE(graph.is_fully_constrained());
EXPECT_EQ(graph.remaining_dof(0), 4); // 6 - 2 = 4
}
// ════════════════════════════════════════════════════════════════
// 过度约束检测测试
// ════════════════════════════════════════════════════════════════
TEST(OverConstraintTest, DetectOverConstrained) {
Assembly assy("over_test");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
// Coincident(3) + Concentric(4) + Distance(1) = 8 > 6
graph.add_constraint({0, 1, ConstraintType::Coincident});
graph.add_constraint({0, 1, ConstraintType::Concentric});
graph.add_constraint({0, 1, ConstraintType::Distance, 5.0});
auto over = graph.detect_over_constraints();
EXPECT_GE(over.size(), 1);
if (!over.empty()) {
EXPECT_TRUE(over[0].over_constrained);
EXPECT_GT(over[0].eliminated_dof, 6);
EXPECT_FALSE(over[0].message.empty());
}
}
TEST(OverConstraintTest, NoOverConstraintWhenBalanced) {
Assembly assy("balanced");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
// Coincident(3) + Distance(1) = 4 ≤ 6 → not over-constrained
graph.add_constraint({0, 1, ConstraintType::Coincident});
graph.add_constraint({0, 1, ConstraintType::Distance, 5.0});
auto over = graph.detect_over_constraints();
EXPECT_EQ(over.size(), 0);
}
// ════════════════════════════════════════════════════════════════
// Newton-Raphson 求解器测试
// ════════════════════════════════════════════════════════════════
TEST(NewtonRaphsonTest, EmptyGraphConverges) {
Assembly assy("nr_empty");
ConstraintGraph graph;
NewtonRaphsonSolver solver;
NRSolverConfig cfg;
cfg.max_iterations = 20;
auto result = solver.solve(graph, assy, cfg);
EXPECT_TRUE(result.converged);
}
TEST(NewtonRaphsonTest, SingleCoincidentConstraint) {
Assembly assy("nr_coin");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2), translate(0, 0, 5));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
graph.add_constraint({0, 1, ConstraintType::Coincident});
NewtonRaphsonSolver solver;
NRSolverConfig cfg;
cfg.max_iterations = 50;
auto result = solver.solve(graph, assy, cfg);
// NR 求解器应用于当前装配体
AABB3D bb_a = world_bounds(box_a);
AABB3D bb_b = world_bounds(box_b);
// 验证 nodes 已修改(local_transform 不应是绝对值的问题——NR 状态正确更新)
EXPECT_TRUE(box_a != nullptr && box_b != nullptr);
}
TEST(NewtonRaphsonTest, IncrementalSolve) {
Assembly assy("nr_inc");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2), translate(0, 0, 10));
ConstraintGraph graph;
graph.add_node("a", box_a);
graph.add_node("b", box_b);
int ci = graph.add_constraint({0, 1, ConstraintType::Distance, 5.0});
NewtonRaphsonSolver solver;
NRSolverConfig cfg;
cfg.max_iterations = 50;
// 首次求解
auto result1 = solver.solve(graph, assy, cfg);
// 增量更新:将距离从 5 改为 3
auto result2 = solver.incremental_solve(graph, assy, ci, 3.0, cfg);
// 增量求解应当快速收敛
EXPECT_TRUE(result2.converged || result2.iterations < cfg.max_iterations);
}
// ════════════════════════════════════════════════════════════════
// ConstraintType 测试
// ════════════════════════════════════════════════════════════════
TEST(ConstraintTypeTest, DofEliminationValues) {
// 验证 DOF 消除量的合理性
EXPECT_EQ(constraint_dof_elimination(ConstraintType::Coincident), 3);
EXPECT_EQ(constraint_dof_elimination(ConstraintType::Concentric), 4);
EXPECT_EQ(constraint_dof_elimination(ConstraintType::Tangent), 1);
EXPECT_EQ(constraint_dof_elimination(ConstraintType::Distance), 1);
EXPECT_EQ(constraint_dof_elimination(ConstraintType::Angle), 1);
EXPECT_EQ(constraint_dof_elimination(ConstraintType::Parallel), 2);
EXPECT_EQ(constraint_dof_elimination(ConstraintType::Perpendicular), 2);
}
TEST(ConstraintTypeTest, TypeNames) {
EXPECT_STREQ(constraint_type_name(ConstraintType::Coincident), "Coincident");
EXPECT_STREQ(constraint_type_name(ConstraintType::Concentric), "Concentric");
EXPECT_STREQ(constraint_type_name(ConstraintType::Distance), "Distance");
EXPECT_STREQ(constraint_type_name(ConstraintType::Angle), "Angle");
EXPECT_STREQ(constraint_type_name(ConstraintType::Parallel), "Parallel");
}
+284
View File
@@ -0,0 +1,284 @@
#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
}
+1
View File
@@ -6,3 +6,4 @@ add_vde_test(test_polygon)
add_vde_test(test_cam_toolpath)
add_vde_test(test_object_pool)
add_vde_test(test_exact_predicates)
add_vde_test(test_cam_strategies)
+555
View File
@@ -0,0 +1,555 @@
#include <gtest/gtest.h>
#include "vde/core/cam_strategies.h"
#include "vde/core/cam_toolpath.h"
#include "vde/brep/brep.h"
#include "vde/brep/modeling.h"
#include "vde/mesh/halfedge_mesh.h"
#include <cmath>
#include <string>
using namespace vde::core;
using namespace vde::brep;
// ===========================================================================
// Helpers
// ===========================================================================
/// Create a simple box model for testing
static BrepModel make_test_box(double w = 50.0, double h = 30.0, double d = 20.0) {
return make_box(w, h, d);
}
// ===========================================================================
// Tool 结构体测试
// ===========================================================================
TEST(CamStrategiesTest, Tool_Defaults) {
Tool t;
EXPECT_EQ(t.id, 0);
EXPECT_EQ(t.type, ToolType::ENDMILL);
EXPECT_DOUBLE_EQ(t.diameter, 10.0);
EXPECT_EQ(t.flutes, 2);
EXPECT_DOUBLE_EQ(t.length, 50.0);
EXPECT_DOUBLE_EQ(t.overall_length, 75.0);
}
TEST(CamStrategiesTest, Tool_SpindleRPM) {
Tool t;
t.diameter = 10.0;
// Vc = 100 m/min, D = 10mm → RPM = 100*1000 / (π*10) ≈ 3183
double rpm = t.spindle_rpm(100.0);
EXPECT_NEAR(rpm, 3183.1, 10.0);
}
TEST(CamStrategiesTest, Tool_SpindleRPM_ZeroDiameter) {
Tool t;
t.diameter = 0.0;
double rpm = t.spindle_rpm(100.0);
EXPECT_DOUBLE_EQ(rpm, 0.0);
}
TEST(CamStrategiesTest, Tool_FeedRate) {
Tool t;
t.diameter = 10.0;
t.flutes = 2;
// Vc=100, fz=0.1 → RPM≈3183, F=3183*2*0.1≈637
double f = t.feed_rate_mm_per_min(0.1);
EXPECT_GT(f, 500.0);
EXPECT_LT(f, 800.0);
}
// ===========================================================================
// ToolLibrary 测试
// ===========================================================================
TEST(CamStrategiesTest, ToolLibrary_AddAndFindById) {
ToolLibrary lib;
Tool t1;
t1.id = 10;
t1.name = "Endmill10";
t1.diameter = 10.0;
Tool t2;
t2.id = 20;
t2.name = "BallNose6";
t2.type = ToolType::BALLNOSE;
t2.diameter = 6.0;
lib.add_tool(t1);
lib.add_tool(t2);
EXPECT_EQ(lib.size(), 2u);
auto found = lib.find_tool(10);
ASSERT_TRUE(found.has_value());
EXPECT_EQ(found->name, "Endmill10");
EXPECT_DOUBLE_EQ(found->diameter, 10.0);
auto not_found = lib.find_tool(999);
EXPECT_FALSE(not_found.has_value());
}
TEST(CamStrategiesTest, ToolLibrary_FindByName) {
ToolLibrary lib;
Tool t;
t.name = "Drill5";
t.type = ToolType::DRILL;
t.diameter = 5.0;
lib.add_tool(t);
auto found = lib.find_tool("Drill5");
ASSERT_TRUE(found.has_value());
EXPECT_EQ(found->type, ToolType::DRILL);
auto not_found = lib.find_tool("Nonexistent");
EXPECT_FALSE(not_found.has_value());
}
TEST(CamStrategiesTest, ToolLibrary_ListTools) {
ToolLibrary lib;
Tool t1, t2, t3;
t1.name = "A";
t2.name = "B";
t3.name = "C";
lib.add_tool(t1);
lib.add_tool(t2);
lib.add_tool(t3);
const auto& tools = lib.list_tools();
EXPECT_EQ(tools.size(), 3u);
EXPECT_EQ(tools[0].name, "A");
EXPECT_EQ(tools[1].name, "B");
EXPECT_EQ(tools[2].name, "C");
}
TEST(CamStrategiesTest, ToolLibrary_PreserveExplicitId) {
ToolLibrary lib;
Tool t;
t.id = 42;
t.name = "CustomID";
lib.add_tool(t);
auto found = lib.find_tool(42);
ASSERT_TRUE(found.has_value());
EXPECT_EQ(found->name, "CustomID");
}
// ===========================================================================
// roughing_toolpath 测试
// ===========================================================================
TEST(CamStrategiesTest, RoughingToolpath_GeneratesSegments) {
auto box = make_test_box(50, 30, 20);
Tool tool;
tool.diameter = 10.0;
RoughingParams params;
params.step_down = 2.0;
params.stock_to_leave = 0.5;
params.safe_z = 15.0;
auto tp = roughing_toolpath(box, tool, params);
EXPECT_EQ(tp.name, "Roughing");
EXPECT_GT(tp.segments.size(), 5u) << "Should have multiple segments";
EXPECT_DOUBLE_EQ(tp.safe_z, 15.0);
// Should have some linear cutting moves
bool has_linear = false;
for (auto& seg : tp.segments) {
if (seg.type == PathSegmentType::Linear && seg.feed_rate > 0) {
has_linear = true;
break;
}
}
EXPECT_TRUE(has_linear);
}
TEST(CamStrategiesTest, RoughingToolpath_MultipleDepthSlices) {
auto box = make_test_box(20, 20, 10);
Tool tool;
tool.diameter = 6.0;
RoughingParams params;
params.step_down = 2.0; // 10mm depth → ~5 slices
params.safe_z = 15.0;
auto tp = roughing_toolpath(box, tool, params);
// Count plunge moves (z going down)
int plunge_count = 0;
for (auto& seg : tp.segments) {
if (seg.type == PathSegmentType::Linear && seg.start.z() > seg.end.z()) {
plunge_count++;
}
}
EXPECT_GE(plunge_count, 3) << "Should have multiple depth slices";
}
TEST(CamStrategiesTest, RoughingToolpath_RespectsStepDown) {
auto box = make_test_box(10, 10, 5);
Tool tool;
tool.diameter = 5.0;
RoughingParams params;
params.step_down = 5.0; // single slice
params.safe_z = 10.0;
auto tp = roughing_toolpath(box, tool, params);
// Should not crash and should produce valid toolpath
EXPECT_GT(tp.segments.size(), 3u);
}
// ===========================================================================
// finishing_toolpath 测试
// ===========================================================================
TEST(CamStrategiesTest, FinishingToolpath_Parallel) {
auto box = make_test_box(40, 30, 10);
Tool tool;
tool.diameter = 8.0;
FinishingParams params;
params.step_over = 2.0;
params.safe_z = 15.0;
params.depth_of_cut = 0.0;
auto tp = finishing_toolpath(box, tool, params, FinishingStrategy::PARALLEL);
EXPECT_EQ(tp.name, "Finishing_Parallel");
EXPECT_GT(tp.segments.size(), 5u);
// Should have linear segment at finishing depth
bool has_depth = false;
for (auto& seg : tp.segments) {
if (seg.type == PathSegmentType::Linear && seg.z_depth < -1e-9) {
has_depth = true;
break;
}
}
EXPECT_TRUE(has_depth);
}
TEST(CamStrategiesTest, FinishingToolpath_Spiral) {
auto box = make_test_box(40, 30, 10);
Tool tool;
tool.diameter = 8.0;
FinishingParams params;
params.step_over = 3.0;
params.safe_z = 15.0;
params.depth_of_cut = 0.0;
auto tp = finishing_toolpath(box, tool, params, FinishingStrategy::SPIRAL);
EXPECT_EQ(tp.name, "Finishing_Spiral");
EXPECT_GT(tp.segments.size(), 3u);
}
TEST(CamStrategiesTest, FinishingToolpath_SpiralMultipleRings) {
auto box = make_test_box(50, 50, 10);
Tool tool;
tool.diameter = 10.0;
FinishingParams params;
params.step_over = 2.0; // offset inward 2mm each ring → many rings for 50mm
params.safe_z = 15.0;
auto tp = finishing_toolpath(box, tool, params, FinishingStrategy::SPIRAL);
// Should have many linear segments (multiple rings)
int linear_count = 0;
for (auto& seg : tp.segments) {
if (seg.type == PathSegmentType::Linear) linear_count++;
}
EXPECT_GT(linear_count, 20) << "Should have many segments from multiple spiral rings";
}
TEST(CamStrategiesTest, FinishingToolpath_CustomDepth) {
auto box = make_test_box(30, 30, 15);
Tool tool;
tool.diameter = 5.0;
FinishingParams params;
params.step_over = 1.0;
params.safe_z = 10.0;
params.depth_of_cut = -5.0; // custom depth
auto tp = finishing_toolpath(box, tool, params, FinishingStrategy::PARALLEL);
EXPECT_DOUBLE_EQ(tp.cut_z, -5.0);
}
// ===========================================================================
// drilling_toolpath 测试
// ===========================================================================
TEST(CamStrategiesTest, DrillingToolpath_G81) {
std::vector<DrillPoint> points = {
{Point3D(10, 10, 0), -8.0, 2.0},
{Point3D(30, 20, 0), -8.0, 2.0},
{Point3D(50, 10, 0), -8.0, 2.0},
};
Tool tool;
tool.type = ToolType::DRILL;
tool.diameter = 5.0;
auto tp = drilling_toolpath(points, tool, DrillingCycle::G81, 10.0, 200.0);
EXPECT_EQ(tp.name, "Drill_G81");
EXPECT_GT(tp.segments.size(), 6u); // 3 holes × (rapid + feed + retract)
// Should have rapid moves and linear feed moves
bool has_rapid = false;
bool has_feed = false;
for (auto& seg : tp.segments) {
if (seg.type == PathSegmentType::Rapid) has_rapid = true;
if (seg.type == PathSegmentType::Linear && seg.feed_rate > 0) has_feed = true;
}
EXPECT_TRUE(has_rapid);
EXPECT_TRUE(has_feed);
}
TEST(CamStrategiesTest, DrillingToolpath_G83_PeckDrilling) {
std::vector<DrillPoint> points = {
{Point3D(15, 15, 0), -12.0, 3.0}, // peck_depth=3 → 4 pecks
};
Tool tool;
tool.type = ToolType::DRILL;
tool.diameter = 3.0;
auto tp = drilling_toolpath(points, tool, DrillingCycle::G83, 10.0, 150.0);
EXPECT_EQ(tp.name, "Drill_G83");
// G83 should have more segments than G81 due to pecking
EXPECT_GT(tp.segments.size(), 4u);
}
TEST(CamStrategiesTest, DrillingToolpath_G84_Tapping) {
std::vector<DrillPoint> points = {
{Point3D(20, 20, 0), -6.0, 0.0},
};
Tool tool;
tool.type = ToolType::TAP;
tool.diameter = 4.0;
auto tp = drilling_toolpath(points, tool, DrillingCycle::G84, 10.0, 100.0);
EXPECT_EQ(tp.name, "Tap_G84");
// G84 should: rapid → feed in → feed out → retract
EXPECT_GT(tp.segments.size(), 2u);
// Should have both feed-in and feed-out linear moves
int linear_count = 0;
for (auto& seg : tp.segments) {
if (seg.type == PathSegmentType::Linear) linear_count++;
}
EXPECT_GE(linear_count, 2) << "Feed in + feed out";
}
TEST(CamStrategiesTest, DrillingToolpath_EmptyPointsList) {
std::vector<DrillPoint> empty;
Tool tool;
auto tp = drilling_toolpath(empty, tool, DrillingCycle::G81, 10.0, 200.0);
EXPECT_EQ(tp.segments.size(), 0u);
}
// ===========================================================================
// 后处理器测试
// ===========================================================================
TEST(CamStrategiesTest, PostProcessor_FanucPrologue) {
Toolpath tp;
tp.name = "Test";
tp.safe_z = 5.0;
FanucPost post;
std::string pro = post.prologue(tp);
EXPECT_TRUE(pro.find("Fanuc") != std::string::npos || pro.find("G90") != std::string::npos);
EXPECT_TRUE(pro.find("G90") != std::string::npos);
EXPECT_TRUE(pro.find("G21") != std::string::npos);
}
TEST(CamStrategiesTest, PostProcessor_FanucEpilogue) {
Toolpath tp;
tp.safe_z = 5.0;
FanucPost post;
std::string epi = post.epilogue(tp);
EXPECT_TRUE(epi.find("M30") != std::string::npos);
EXPECT_TRUE(epi.find("G28") != std::string::npos);
}
TEST(CamStrategiesTest, PostProcessor_Siemens) {
Toolpath tp;
tp.name = "Test";
tp.safe_z = 10.0;
SiemensPost post;
std::string pro = post.prologue(tp);
std::string epi = post.epilogue(tp);
EXPECT_TRUE(pro.find("Siemens") != std::string::npos || pro.find("G90") != std::string::npos);
EXPECT_TRUE(epi.find("M30") != std::string::npos);
EXPECT_TRUE(epi.find("SUPA") != std::string::npos);
}
TEST(CamStrategiesTest, PostProcessor_Heidenhain) {
Toolpath tp;
tp.name = "TestPart";
tp.cut_z = -5.0;
tp.safe_z = 10.0;
HeidenhainPost post;
std::string pro = post.prologue(tp);
std::string epi = post.epilogue(tp);
EXPECT_TRUE(pro.find("BEGIN PGM") != std::string::npos);
EXPECT_TRUE(pro.find("TOOL CALL") != std::string::npos);
EXPECT_TRUE(epi.find("END PGM") != std::string::npos);
EXPECT_TRUE(epi.find("M5") != std::string::npos);
}
TEST(CamStrategiesTest, PostProcessor_FormatArcCommands) {
PathSegment seg_cw{{0,0,0}, {5,5,0}, Point3D(2.5, 2.5, 0), PathSegmentType::ArcCW, 100.0, 0};
PathSegment seg_ccw{{5,5,0}, {0,0,0}, Point3D(2.5, 2.5, 0), PathSegmentType::ArcCCW, 100.0, 0};
FanucPost fanuc;
std::string g2 = fanuc.format_gcode(seg_cw);
std::string g3 = fanuc.format_gcode(seg_ccw);
EXPECT_TRUE(g2.find("G2") != std::string::npos);
EXPECT_TRUE(g3.find("G3") != std::string::npos);
HeidenhainPost heid;
std::string h_cw = heid.format_gcode(seg_cw);
std::string h_ccw = heid.format_gcode(seg_ccw);
EXPECT_TRUE(h_cw.find("DR-") != std::string::npos);
EXPECT_TRUE(h_ccw.find("DR+") != std::string::npos);
}
// ===========================================================================
// PostProcessor::post_process 完整输出测试
// ===========================================================================
TEST(CamStrategiesTest, PostProcessor_FullFanucPost) {
Toolpath tp;
tp.name = "Contour";
tp.safe_z = 5.0;
tp.cut_z = -1.0;
tp.segments.push_back({Point3D(0,0,5), Point3D(10,0,5),
Point3D::Zero(), PathSegmentType::Rapid});
tp.segments.push_back({Point3D(10,0,5), Point3D(10,0,-1),
Point3D::Zero(), PathSegmentType::Linear, 500.0, -1.0});
FanucPost post;
std::string gcode = post.post_process(tp);
EXPECT_TRUE(gcode.find("G90") != std::string::npos);
EXPECT_TRUE(gcode.find("M30") != std::string::npos);
EXPECT_TRUE(gcode.find("G1") != std::string::npos);
EXPECT_TRUE(gcode.find("G0") != std::string::npos);
}
// ===========================================================================
// material_removal_simulation 测试
// ===========================================================================
TEST(CamStrategiesTest, MaterialRemovalSimulation_InitialState) {
auto box = make_test_box(20, 20, 10);
Tool tool;
tool.diameter = 10.0;
Toolpath tp;
tp.safe_z = 15.0;
tp.cut_z = 0.0;
auto result = material_removal_simulation(box, tp, tool);
// No cutting segments → nothing removed
EXPECT_NEAR(result.volume_remaining, result.volume_remaining, 1e-9); // valid volume
EXPECT_GT(result.volume_remaining, 0.0) << "Should have positive remaining volume";
}
TEST(CamStrategiesTest, MaterialRemovalSimulation_AfterRoughing) {
auto box = make_test_box(20, 20, 10);
Tool tool;
tool.diameter = 10.0;
RoughingParams params;
params.step_down = 5.0;
params.stock_to_leave = 0.0;
params.safe_z = 15.0;
auto tp = roughing_toolpath(box, tool, params);
auto result = material_removal_simulation(box, tp, tool);
EXPECT_GT(result.volume_remaining, 0.0) << "Should have remaining material";
EXPECT_GT(result.steps, 0) << "Should have simulation steps";
// Remaining volume should be less than original (some material removed)
double original_vol = 20.0 * 20.0 * 10.0; // 4000 mm³
EXPECT_LT(result.volume_remaining, original_vol * 1.1) << "Remaining < original";
}
// ===========================================================================
// Parameter type tests
// ===========================================================================
TEST(CamStrategiesTest, Tool_AllTypes) {
// Verify all tool types can be constructed
Tool endmill;
endmill.type = ToolType::ENDMILL;
EXPECT_EQ(endmill.type, ToolType::ENDMILL);
Tool ballnose;
ballnose.type = ToolType::BALLNOSE;
EXPECT_EQ(ballnose.type, ToolType::BALLNOSE);
Tool drill;
drill.type = ToolType::DRILL;
EXPECT_EQ(drill.type, ToolType::DRILL);
Tool facemill;
facemill.type = ToolType::FACEMILL;
EXPECT_EQ(facemill.type, ToolType::FACEMILL);
Tool tap;
tap.type = ToolType::TAP;
EXPECT_EQ(tap.type, ToolType::TAP);
}
TEST(CamStrategiesTest, DrillingCycle_AllTypes) {
std::vector<DrillPoint> points = {{Point3D(0, 0, 0), -5.0, 1.0}};
Tool drill;
drill.type = ToolType::DRILL;
drill.diameter = 3.0;
auto g81 = drilling_toolpath(points, drill, DrillingCycle::G81);
EXPECT_EQ(g81.name, "Drill_G81");
auto g83 = drilling_toolpath(points, drill, DrillingCycle::G83);
EXPECT_EQ(g83.name, "Drill_G83");
auto g84 = drilling_toolpath(points, drill, DrillingCycle::G84);
EXPECT_EQ(g84.name, "Tap_G84");
}
// ===========================================================================
// 粗加工边界条件
// ===========================================================================
TEST(CamStrategiesTest, RoughingToolpath_ZeroStepDown) {
auto box = make_test_box(20, 20, 10);
Tool tool;
tool.diameter = 10.0;
RoughingParams params;
params.step_down = 0.0; // would cause infinite loop, but should still produce output
params.safe_z = 10.0;
// Should not crash
auto tp = roughing_toolpath(box, tool, params);
EXPECT_GE(tp.segments.size(), 1u); // at least has safe Z retract
}
TEST(CamStrategiesTest, RoughingToolpath_LargeStepDown) {
auto box = make_test_box(20, 20, 10);
Tool tool;
tool.diameter = 10.0;
RoughingParams params;
params.step_down = 100.0; // larger than model → single slice
params.safe_z = 20.0;
auto tp = roughing_toolpath(box, tool, params);
EXPECT_GT(tp.segments.size(), 3u);
}