Files
ViewDesignEngine/tests/core/test_cam_strategies.cpp
T
茂之钳 6bc9db663e
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
feat(v5-M3): large assembly LOD + constraint solver + CAM strategies + direct modeling
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
2026-07-26 21:19:38 +08:00

556 lines
17 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/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);
}