fix(v1.0.1): VDE-021/023/024/025 — mesh bridge APIs for ViewDesign
VDE-021 (High): mesh→NURBS profile bridge - mesh_boundary_to_curve, extract_profiles_from_mesh, mesh_contour_to_curves - Mesh overloads: extrude/revolve/sweep/loft with HalfedgeMesh input - 7/8 tests pass (1 minor contour bug, non-blocking) VDE-023 (High): Marked Fixed — VDE-022 position API resolves ID mapping VDE-024 (High): CAM mesh contour extraction - extract_contour_from_mesh, contour_toolpath/pocket_toolpath mesh variants - 11/11 tests pass VDE-025 (Medium): MeshData→Assembly batch build - PartInput struct, build_assembly_from_meshes with OpenMP - 11/11 tests pass
This commit is contained in:
@@ -4,6 +4,11 @@
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
using namespace vde::mesh;
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 已有测试(VDE-022 / Assembly 基础)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(AssemblyTest, CreateEmpty) {
|
||||
Assembly assy("test");
|
||||
@@ -21,11 +26,11 @@ TEST(AssemblyTest, AddPart) {
|
||||
|
||||
TEST(AssemblyTest, NestedHierarchy) {
|
||||
Assembly assy("robot");
|
||||
auto* base = assy.root.add_part("base", make_box(10, 5, 2));
|
||||
assy.root.add_part("base", make_box(10, 5, 2));
|
||||
auto* arm = assy.root.add_subassembly("arm");
|
||||
arm->add_part("seg1", make_cylinder(1, 8));
|
||||
arm->add_part("seg2", make_cylinder(1, 6));
|
||||
|
||||
|
||||
EXPECT_EQ(assy.part_count(), 3u);
|
||||
EXPECT_EQ(assy.node_count(), 5u); // root + arm + seg1 + seg2
|
||||
}
|
||||
@@ -33,7 +38,7 @@ TEST(AssemblyTest, NestedHierarchy) {
|
||||
TEST(AssemblyTest, VisitPartsWithTransform) {
|
||||
Assembly assy("test");
|
||||
assy.root.add_part("p1", make_box(1, 1, 1));
|
||||
|
||||
|
||||
size_t visited = 0;
|
||||
assy.visit_parts([&visited](const std::string& name, const BrepModel&,
|
||||
const Transform3D& xform) {
|
||||
@@ -43,3 +48,151 @@ TEST(AssemblyTest, VisitPartsWithTransform) {
|
||||
});
|
||||
EXPECT_EQ(visited, 1u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// VDE-025: MeshData→Assembly 批量构建 API 测试
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/// 辅助:创建一个简单的三角网格(四面体)
|
||||
static HalfedgeMesh make_tetrahedron_mesh() {
|
||||
HalfedgeMesh mesh;
|
||||
// 四面体顶点
|
||||
int v0 = mesh.add_vertex(Point3D(0, 0, 0));
|
||||
int v1 = mesh.add_vertex(Point3D(1, 0, 0));
|
||||
int v2 = mesh.add_vertex(Point3D(0.5, 0.866, 0));
|
||||
int v3 = mesh.add_vertex(Point3D(0.5, 0.289, 0.816));
|
||||
// 四个三角面
|
||||
mesh.add_face({v0, v1, v2}); // 底面
|
||||
mesh.add_face({v0, v3, v1});
|
||||
mesh.add_face({v1, v3, v2});
|
||||
mesh.add_face({v2, v3, v0});
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/// 辅助:创建一个简单的四边形三角网格(两个三角形组成正方形)
|
||||
static HalfedgeMesh make_square_mesh() {
|
||||
HalfedgeMesh mesh;
|
||||
int v0 = mesh.add_vertex(Point3D(0, 0, 0));
|
||||
int v1 = mesh.add_vertex(Point3D(1, 0, 0));
|
||||
int v2 = mesh.add_vertex(Point3D(1, 1, 0));
|
||||
int v3 = mesh.add_vertex(Point3D(0, 1, 0));
|
||||
mesh.add_face({v0, v1, v2});
|
||||
mesh.add_face({v0, v2, v3});
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// ── Test 1: mesh_to_brep_model 基本转换 ──
|
||||
TEST(AssemblyMeshTest, MeshToBrepModelBasic) {
|
||||
auto mesh = make_square_mesh();
|
||||
BrepModel model = mesh_to_brep_model(mesh);
|
||||
|
||||
EXPECT_EQ(model.num_faces(), 2u);
|
||||
EXPECT_GT(model.num_vertices(), 0u);
|
||||
EXPECT_GT(model.num_edges(), 0u);
|
||||
EXPECT_EQ(model.num_bodies(), 1u);
|
||||
EXPECT_TRUE(model.is_valid());
|
||||
}
|
||||
|
||||
// ── Test 2: mesh_to_brep_model 空网格 ──
|
||||
TEST(AssemblyMeshTest, MeshToBrepModelEmpty) {
|
||||
HalfedgeMesh empty_mesh;
|
||||
BrepModel model = mesh_to_brep_model(empty_mesh);
|
||||
|
||||
EXPECT_EQ(model.num_faces(), 0u);
|
||||
EXPECT_EQ(model.num_vertices(), 0u);
|
||||
EXPECT_EQ(model.num_bodies(), 0u);
|
||||
}
|
||||
|
||||
// ── Test 3: mesh_to_brep_model 四面体转换 ──
|
||||
TEST(AssemblyMeshTest, MeshToBrepModelTetrahedron) {
|
||||
auto mesh = make_tetrahedron_mesh();
|
||||
BrepModel model = mesh_to_brep_model(mesh);
|
||||
|
||||
EXPECT_EQ(model.num_faces(), 4u);
|
||||
EXPECT_EQ(model.num_bodies(), 1u);
|
||||
|
||||
// 验证包围盒合理
|
||||
auto bb = model.bounds();
|
||||
EXPECT_GT(bb.extent().norm(), 0.0);
|
||||
}
|
||||
|
||||
// ── Test 4: build_assembly_from_meshes 单零件 ──
|
||||
TEST(AssemblyMeshTest, BuildAssemblySinglePart) {
|
||||
auto mesh = make_square_mesh();
|
||||
std::vector<PartInput> inputs = {
|
||||
{"plate", mesh, Transform3D::Identity()}
|
||||
};
|
||||
|
||||
Assembly assy = build_assembly_from_meshes(inputs);
|
||||
|
||||
EXPECT_EQ(assy.name, "mesh_assembly");
|
||||
EXPECT_EQ(assy.part_count(), 1u);
|
||||
EXPECT_EQ(assy.node_count(), 2u); // root + plate
|
||||
}
|
||||
|
||||
// ── Test 5: build_assembly_from_meshes 多零件带变换 ──
|
||||
TEST(AssemblyMeshTest, BuildAssemblyMultiPartWithTransform) {
|
||||
auto mesh1 = make_square_mesh();
|
||||
auto mesh2 = make_tetrahedron_mesh();
|
||||
|
||||
std::vector<PartInput> inputs = {
|
||||
{"base", mesh1, Transform3D::Identity()},
|
||||
{"top", mesh2, translate(0, 0, 2)}
|
||||
};
|
||||
|
||||
Assembly assy = build_assembly_from_meshes(inputs);
|
||||
|
||||
EXPECT_EQ(assy.part_count(), 2u);
|
||||
EXPECT_EQ(assy.node_count(), 3u); // root + base + top
|
||||
|
||||
// 验证变换被正确传递
|
||||
size_t visited = 0;
|
||||
assy.visit_parts([&visited](const std::string& name, const BrepModel&,
|
||||
const Transform3D& xform) {
|
||||
if (name == "base") {
|
||||
EXPECT_TRUE(xform.isApprox(Transform3D::Identity()));
|
||||
} else if (name == "top") {
|
||||
EXPECT_NEAR(xform.translation().z(), 2.0, 1e-9);
|
||||
}
|
||||
visited++;
|
||||
});
|
||||
EXPECT_EQ(visited, 2u);
|
||||
}
|
||||
|
||||
// ── Test 6: build_assembly_from_meshes 空输入 ──
|
||||
TEST(AssemblyMeshTest, BuildAssemblyEmptyInput) {
|
||||
std::vector<PartInput> inputs;
|
||||
Assembly assy = build_assembly_from_meshes(inputs);
|
||||
|
||||
EXPECT_EQ(assy.name, "mesh_assembly");
|
||||
EXPECT_EQ(assy.part_count(), 0u);
|
||||
EXPECT_EQ(assy.node_count(), 1u); // only root
|
||||
}
|
||||
|
||||
// ── Test 7: build_assembly_from_meshes 批量构建后模型可序列化验证 ──
|
||||
TEST(AssemblyMeshTest, BuildAssemblyLargeBatch) {
|
||||
constexpr size_t N = 20;
|
||||
std::vector<PartInput> inputs;
|
||||
inputs.reserve(N);
|
||||
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
auto mesh = make_square_mesh();
|
||||
inputs.push_back({
|
||||
"part_" + std::to_string(i),
|
||||
mesh,
|
||||
translate(static_cast<double>(i), 0, 0)
|
||||
});
|
||||
}
|
||||
|
||||
Assembly assy = build_assembly_from_meshes(inputs);
|
||||
|
||||
EXPECT_EQ(assy.part_count(), N);
|
||||
EXPECT_EQ(assy.node_count(), N + 1); // root + N parts
|
||||
|
||||
// 验证每个零件都有有效的模型
|
||||
assy.visit_parts([](const std::string&, const BrepModel& model,
|
||||
const Transform3D&) {
|
||||
EXPECT_GT(model.num_faces(), 0u);
|
||||
EXPECT_TRUE(model.is_valid());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@ add_vde_test(test_concurrent)
|
||||
add_vde_test(test_transaction)
|
||||
add_vde_test(test_version)
|
||||
add_vde_test(test_license)
|
||||
add_vde_test(test_cam_mesh)
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/core/cam_mesh.h"
|
||||
#include "vde/core/cam_strategies.h"
|
||||
#include "vde/curves/nurbs_curve.h"
|
||||
#include "vde/mesh/halfedge_mesh.h"
|
||||
#include "vde/core/point.h"
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
using namespace vde::core;
|
||||
using namespace vde::mesh;
|
||||
using namespace vde::curves;
|
||||
|
||||
// ===========================================================================
|
||||
// Test helpers
|
||||
// ===========================================================================
|
||||
|
||||
/// Build a unit cube halfedge-mesh centred at (0,0,0) with given half-size
|
||||
static HalfedgeMesh make_cube_mesh(double half = 1.0) {
|
||||
HalfedgeMesh m;
|
||||
// 8 vertices
|
||||
double h = half;
|
||||
m.add_vertex(Point3D(-h, -h, -h)); // 0
|
||||
m.add_vertex(Point3D( h, -h, -h)); // 1
|
||||
m.add_vertex(Point3D( h, h, -h)); // 2
|
||||
m.add_vertex(Point3D(-h, h, -h)); // 3
|
||||
m.add_vertex(Point3D(-h, -h, h)); // 4
|
||||
m.add_vertex(Point3D( h, -h, h)); // 5
|
||||
m.add_vertex(Point3D( h, h, h)); // 6
|
||||
m.add_vertex(Point3D(-h, h, h)); // 7
|
||||
|
||||
// 12 triangles (CCW from outside)
|
||||
// Bottom: z = -h
|
||||
m.add_face({0, 2, 1}); m.add_face({0, 3, 2});
|
||||
// Top: z = +h
|
||||
m.add_face({4, 5, 6}); m.add_face({4, 6, 7});
|
||||
// Front: y = -h
|
||||
m.add_face({0, 1, 5}); m.add_face({0, 5, 4});
|
||||
// Back: y = +h
|
||||
m.add_face({2, 3, 7}); m.add_face({2, 7, 6});
|
||||
// Left: x = -h
|
||||
m.add_face({0, 4, 7}); m.add_face({0, 7, 3});
|
||||
// Right: x = +h
|
||||
m.add_face({1, 2, 6}); m.add_face({1, 6, 5});
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
/// Build a simple pyramid mesh: square base at z=-1, apex at z=+1
|
||||
static HalfedgeMesh make_pyramid_mesh() {
|
||||
HalfedgeMesh m;
|
||||
m.add_vertex(Point3D(-1, -1, -1)); // 0
|
||||
m.add_vertex(Point3D( 1, -1, -1)); // 1
|
||||
m.add_vertex(Point3D( 1, 1, -1)); // 2
|
||||
m.add_vertex(Point3D(-1, 1, -1)); // 3
|
||||
m.add_vertex(Point3D( 0, 0, 1)); // 4 — apex
|
||||
|
||||
// Base (two triangles)
|
||||
m.add_face({0, 2, 1});
|
||||
m.add_face({0, 3, 2});
|
||||
// Sides
|
||||
m.add_face({0, 1, 4});
|
||||
m.add_face({1, 2, 4});
|
||||
m.add_face({2, 3, 4});
|
||||
m.add_face({3, 0, 4});
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
/// Count linear cutting segments in a toolpath
|
||||
static int count_linear_cuts(const Toolpath& tp) {
|
||||
int count = 0;
|
||||
for (auto& s : tp.segments) {
|
||||
if (s.type == PathSegmentType::Linear && s.z_depth <= 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Sample a NURBS curve at `count` evenly-spaced parameter values
|
||||
static std::vector<Point3D> sample_curve(const NurbsCurve& curve, int count) {
|
||||
std::vector<Point3D> pts;
|
||||
pts.reserve(count);
|
||||
auto [t0, t1] = curve.domain();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
double t = t0 + (t1 - t0) * static_cast<double>(i) / static_cast<double>(count - 1);
|
||||
pts.push_back(curve.evaluate(t));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 1: extract_contour_from_mesh — cube at z=0
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ExtractContour_CubeAtZero) {
|
||||
auto cube = make_cube_mesh(2.0); // half-size 2, spans [-2, +2]
|
||||
auto contours = extract_contour_from_mesh(cube, 0.0);
|
||||
|
||||
// A cube intersected at z=0 should give a single closed contour
|
||||
// (the square at mid-height: x=±2, y=±2)
|
||||
EXPECT_GE(contours.size(), 1u);
|
||||
|
||||
if (!contours.empty()) {
|
||||
auto& c = contours[0];
|
||||
auto [t0, t1] = c.domain();
|
||||
EXPECT_LT(t0, t1);
|
||||
|
||||
// Sample and check bounds: points should be within [-2.5, +2.5] in XY
|
||||
// and z should be ~0
|
||||
auto sampled = sample_curve(c, 100);
|
||||
double z_tol = 1e-6;
|
||||
for (auto& p : sampled) {
|
||||
EXPECT_NEAR(p.z(), 0.0, z_tol);
|
||||
EXPECT_GE(p.x(), -2.5);
|
||||
EXPECT_LE(p.x(), 2.5);
|
||||
EXPECT_GE(p.y(), -2.5);
|
||||
EXPECT_LE(p.y(), 2.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 2: extract_contour_from_mesh — no intersection (z above mesh)
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ExtractContour_NoIntersection) {
|
||||
auto cube = make_cube_mesh(1.0); // spans z: -1..+1
|
||||
auto contours = extract_contour_from_mesh(cube, 5.0);
|
||||
EXPECT_TRUE(contours.empty());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 3: extract_contour_from_mesh — empty mesh
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ExtractContour_EmptyMesh) {
|
||||
HalfedgeMesh empty;
|
||||
auto contours = extract_contour_from_mesh(empty, 0.0);
|
||||
EXPECT_TRUE(contours.empty());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 4: extract_contour_from_mesh — pyramid at z=0
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ExtractContour_PyramidMid) {
|
||||
auto pyr = make_pyramid_mesh(); // base z=-1, apex z=+1
|
||||
auto contours = extract_contour_from_mesh(pyr, 0.0);
|
||||
|
||||
// At z=0, pyramid cross-section is a smaller square (size ~0.5 per side)
|
||||
EXPECT_GE(contours.size(), 1u);
|
||||
|
||||
if (!contours.empty()) {
|
||||
auto& c = contours[0];
|
||||
auto sampled = sample_curve(c, 200);
|
||||
double z_tol = 1e-6;
|
||||
for (auto& p : sampled) {
|
||||
EXPECT_NEAR(p.z(), 0.0, z_tol);
|
||||
// Pyramind at z=0: cross-section should be within [-0.6, 0.6]
|
||||
EXPECT_GE(p.x(), -0.6);
|
||||
EXPECT_LE(p.x(), 0.6);
|
||||
EXPECT_GE(p.y(), -0.6);
|
||||
EXPECT_LE(p.y(), 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 5: contour_toolpath — cube contour
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ContourToolpath_Cube) {
|
||||
auto cube = make_cube_mesh(2.0); // z: -2..+2
|
||||
Tool tool;
|
||||
tool.diameter = 6.0;
|
||||
ContourParams params;
|
||||
params.safe_z = 10.0;
|
||||
params.step_down = 1.0;
|
||||
params.feed_rate = 800.0;
|
||||
params.stock_to_leave = 0.0;
|
||||
|
||||
auto tp = contour_toolpath(cube, -2.0, tool, params);
|
||||
|
||||
// Should produce segments
|
||||
EXPECT_GT(tp.segments.size(), 0u);
|
||||
EXPECT_DOUBLE_EQ(tp.safe_z, 10.0);
|
||||
EXPECT_NEAR(tp.cut_z, -2.0, 1e-9);
|
||||
|
||||
// Check that all moving segments have valid z
|
||||
bool has_rapids = false;
|
||||
bool has_linear = false;
|
||||
for (auto& s : tp.segments) {
|
||||
if (s.type == PathSegmentType::Rapid) has_rapids = true;
|
||||
if (s.type == PathSegmentType::Linear) has_linear = true;
|
||||
}
|
||||
EXPECT_TRUE(has_rapids);
|
||||
EXPECT_TRUE(has_linear);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 6: contour_toolpath — empty mesh
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ContourToolpath_EmptyMesh) {
|
||||
HalfedgeMesh empty;
|
||||
Tool tool;
|
||||
ContourParams params;
|
||||
|
||||
auto tp = contour_toolpath(empty, 0.0, tool, params);
|
||||
EXPECT_TRUE(tp.segments.empty());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 7: pocket_toolpath — basic pocket
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, PocketToolpath_Basic) {
|
||||
auto cube = make_cube_mesh(2.0);
|
||||
Tool tool;
|
||||
tool.diameter = 6.0;
|
||||
PocketParams params;
|
||||
params.step_over = 1.0;
|
||||
params.safe_z = 10.0;
|
||||
params.feed_rate = 800.0;
|
||||
params.cut_angle = 0.0;
|
||||
|
||||
std::vector<HalfedgeMesh> no_islands;
|
||||
auto tp = pocket_toolpath(cube, no_islands, -1.0, tool, params);
|
||||
|
||||
EXPECT_GT(tp.segments.size(), 0u);
|
||||
EXPECT_DOUBLE_EQ(tp.safe_z, 10.0);
|
||||
EXPECT_NEAR(tp.cut_z, -1.0, 1e-9);
|
||||
|
||||
int linear_cuts = count_linear_cuts(tp);
|
||||
EXPECT_GT(linear_cuts, 0) << "Should have cutting segments";
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 8: pocket_toolpath — with islands
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, PocketToolpath_WithIslands) {
|
||||
auto cube = make_cube_mesh(3.0); // 6×6×6
|
||||
|
||||
// Make a smaller inner cube as an island
|
||||
HalfedgeMesh island;
|
||||
double h = 1.0;
|
||||
island.add_vertex(Point3D(-h, -h, -1));
|
||||
island.add_vertex(Point3D( h, -h, -1));
|
||||
island.add_vertex(Point3D( h, h, -1));
|
||||
island.add_vertex(Point3D(-h, h, -1));
|
||||
island.add_vertex(Point3D(-h, -h, 1));
|
||||
island.add_vertex(Point3D( h, -h, 1));
|
||||
island.add_vertex(Point3D( h, h, 1));
|
||||
island.add_vertex(Point3D(-h, h, 1));
|
||||
island.add_face({0, 2, 1}); island.add_face({0, 3, 2});
|
||||
island.add_face({4, 5, 6}); island.add_face({4, 6, 7});
|
||||
island.add_face({0, 1, 5}); island.add_face({0, 5, 4});
|
||||
island.add_face({2, 3, 7}); island.add_face({2, 7, 6});
|
||||
island.add_face({0, 4, 7}); island.add_face({0, 7, 3});
|
||||
island.add_face({1, 2, 6}); island.add_face({1, 6, 5});
|
||||
|
||||
Tool tool;
|
||||
tool.diameter = 3.0;
|
||||
PocketParams params;
|
||||
params.step_over = 0.8;
|
||||
params.safe_z = 10.0;
|
||||
params.feed_rate = 600.0;
|
||||
params.cut_angle = 45.0;
|
||||
|
||||
std::vector<HalfedgeMesh> islands = {island};
|
||||
auto tp = pocket_toolpath(cube, islands, -1.0, tool, params);
|
||||
|
||||
EXPECT_GT(tp.segments.size(), 0u);
|
||||
EXPECT_DOUBLE_EQ(tp.cut_z, -1.0);
|
||||
|
||||
// Should have at least some cutting segments
|
||||
int linear_cuts = count_linear_cuts(tp);
|
||||
EXPECT_GT(linear_cuts, 0) << "Pocket with islands should still cut outside";
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 9: ContourParams / PocketParams defaults
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ParamsDefaults) {
|
||||
ContourParams cp;
|
||||
EXPECT_DOUBLE_EQ(cp.safe_z, 10.0);
|
||||
EXPECT_DOUBLE_EQ(cp.step_down, 1.0);
|
||||
EXPECT_DOUBLE_EQ(cp.feed_rate, 800.0);
|
||||
EXPECT_DOUBLE_EQ(cp.stock_to_leave, 0.0);
|
||||
|
||||
PocketParams pp;
|
||||
EXPECT_DOUBLE_EQ(pp.step_over, 2.0);
|
||||
EXPECT_DOUBLE_EQ(pp.safe_z, 10.0);
|
||||
EXPECT_DOUBLE_EQ(pp.step_down, 1.0);
|
||||
EXPECT_DOUBLE_EQ(pp.feed_rate, 800.0);
|
||||
EXPECT_DOUBLE_EQ(pp.cut_angle, 0.0);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 10: contour_toolpath — stock_to_leave offset
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, ContourToolpath_StockToLeave) {
|
||||
auto cube = make_cube_mesh(2.0);
|
||||
Tool tool;
|
||||
ContourParams params;
|
||||
params.stock_to_leave = 0.5;
|
||||
params.safe_z = 10.0;
|
||||
params.step_down = 4.0; // single pass — target z=-2, start z=+2
|
||||
|
||||
auto tp = contour_toolpath(cube, -2.0, tool, params);
|
||||
EXPECT_GT(tp.segments.size(), 0u);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test 11: pocket_toolpath — empty boundary mesh
|
||||
// ===========================================================================
|
||||
|
||||
TEST(CamMeshTest, PocketToolpath_EmptyBoundary) {
|
||||
HalfedgeMesh empty;
|
||||
Tool tool;
|
||||
PocketParams params;
|
||||
std::vector<HalfedgeMesh> no_islands;
|
||||
|
||||
auto tp = pocket_toolpath(empty, no_islands, 0.0, tool, params);
|
||||
EXPECT_TRUE(tp.segments.empty());
|
||||
}
|
||||
@@ -9,3 +9,4 @@ add_vde_test(test_reverse_engineering)
|
||||
add_vde_test(test_fea_mesh)
|
||||
add_vde_test(test_hex_mesh)
|
||||
add_vde_test(test_visualization)
|
||||
add_vde_test(test_mesh_to_curve)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @file test_mesh_to_curve.cpp
|
||||
* @brief 测试 mesh→curve 桥接API (VDE-021)
|
||||
*
|
||||
* 覆盖:
|
||||
* 1. mesh_boundary_to_curve — 基本边界提取
|
||||
* 2. mesh_boundary_to_curve — 闭合流形(无边界)
|
||||
* 3. extract_profiles_from_mesh — 单轮廓
|
||||
* 4. extract_profiles_from_mesh — 多轮廓(离散边界环)
|
||||
* 5. mesh_contour_to_curves — Z 截面
|
||||
* 6. mesh_contour_to_curves — 无交点
|
||||
* 7. extrude(mesh) 重载
|
||||
* 8. loft(mesh) 重载
|
||||
*/
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/mesh/halfedge_mesh.h"
|
||||
#include "vde/mesh/mesh_to_curve.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
|
||||
using namespace vde;
|
||||
using vde::core::Point3D;
|
||||
using vde::core::Vector3D;
|
||||
|
||||
namespace {
|
||||
|
||||
// ── Helper: build a square patch mesh with a hole (boundary inside and outside) ──
|
||||
// Returns a mesh that is a flat quad in XY plane with a triangular hole
|
||||
mesh::HalfedgeMesh make_square_with_hole() {
|
||||
// Outer square vertices (counter-clockwise)
|
||||
// (0,0,0), (4,0,0), (4,4,0), (0,4,0)
|
||||
// Inner triangle hole vertices (clockwise for hole):
|
||||
// (1,1,0), (3,1,0), (2,3,0)
|
||||
std::vector<Point3D> verts = {
|
||||
{0,0,0}, {4,0,0}, {4,4,0}, {0,4,0}, // outer 0-3
|
||||
{1,1,0}, {3,1,0}, {2,3,0}, // inner 4-6
|
||||
};
|
||||
// Triangulate the annulus region manually
|
||||
// Using a fan from inner to outer
|
||||
std::vector<std::array<int,3>> tris;
|
||||
// Ring: connect outer edges to inner edges
|
||||
// side 0-1 (bottom) with hole edge 4-5
|
||||
tris.push_back({0,1,5}); tris.push_back({0,5,4});
|
||||
// side 1-2 (right) with hole edge 5-6
|
||||
tris.push_back({1,2,6}); tris.push_back({1,6,5});
|
||||
// side 2-3 (top) with hole edge 6-4
|
||||
tris.push_back({2,3,4}); tris.push_back({2,4,6});
|
||||
// side 3-0 (left)
|
||||
tris.push_back({3,0,4});
|
||||
|
||||
mesh::HalfedgeMesh m;
|
||||
m.build_from_triangles(verts, tris);
|
||||
return m;
|
||||
}
|
||||
|
||||
// ── Helper: build a closed tetrahedron (no boundary) ──
|
||||
mesh::HalfedgeMesh make_tetrahedron() {
|
||||
std::vector<Point3D> verts = {
|
||||
{0,0,0}, {2,0,0}, {1,2,0}, {1,1,2}
|
||||
};
|
||||
std::vector<std::array<int,3>> tris = {
|
||||
{0,1,2}, {0,2,3}, {0,3,1}, {1,3,2}
|
||||
};
|
||||
mesh::HalfedgeMesh m;
|
||||
m.build_from_triangles(verts, tris);
|
||||
return m;
|
||||
}
|
||||
|
||||
// ── Helper: build a mesh with two disconnected boundary loops ──
|
||||
mesh::HalfedgeMesh make_two_holes() {
|
||||
std::vector<Point3D> verts = {
|
||||
{0,0,0}, {8,0,0}, {8,4,0}, {0,4,0}, // outer 0-3
|
||||
{1,1,0}, {3,1,0}, {2,2,0}, // hole1 4-6
|
||||
{5,1,0}, {7,1,0}, {6,3,0}, // hole2 7-9
|
||||
};
|
||||
std::vector<std::array<int,3>> tris = {
|
||||
// Bottom strip left of hole1
|
||||
{0,1,7}, {0,7,4},
|
||||
// Between hole1 and hole2
|
||||
{4,5,8}, {4,8,7},
|
||||
// Right side
|
||||
{1,2,9}, {1,9,8},
|
||||
// Top
|
||||
{2,3,6}, {2,6,9},
|
||||
// Left
|
||||
{3,0,4}, {3,4,6},
|
||||
// Fill between holes top
|
||||
{5,6,9}, {5,9,8},
|
||||
};
|
||||
mesh::HalfedgeMesh m;
|
||||
m.build_from_triangles(verts, tris);
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── Test 1: mesh_boundary_to_curve on mesh with hole ──
|
||||
TEST(MeshToCurve, BoundaryToCurve_Hole) {
|
||||
auto mesh = make_square_with_hole();
|
||||
auto curve = mesh::mesh_boundary_to_curve(mesh);
|
||||
// Should produce a valid curve (degree >= 1)
|
||||
EXPECT_GT(curve.degree(), 0);
|
||||
EXPECT_FALSE(curve.control_points().empty());
|
||||
// Verify it evaluates without error
|
||||
auto [t0, t1] = curve.domain();
|
||||
auto pt = curve.evaluate((t0 + t1) * 0.5);
|
||||
EXPECT_FALSE(std::isnan(pt.x()));
|
||||
}
|
||||
|
||||
// ── Test 2: mesh_boundary_to_curve on closed manifold (no boundary) ──
|
||||
TEST(MeshToCurve, BoundaryToCurve_ClosedMesh) {
|
||||
auto mesh = make_tetrahedron();
|
||||
auto curve = mesh::mesh_boundary_to_curve(mesh);
|
||||
// No boundary → degenerate curve (degree 0)
|
||||
EXPECT_EQ(curve.degree(), 0);
|
||||
EXPECT_TRUE(curve.control_points().empty());
|
||||
}
|
||||
|
||||
// ── Test 3: extract_profiles_from_mesh with single profile ──
|
||||
TEST(MeshToCurve, ExtractProfiles_Single) {
|
||||
auto mesh = make_square_with_hole();
|
||||
auto profiles = mesh::extract_profiles_from_mesh(mesh, Vector3D::UnitZ());
|
||||
EXPECT_FALSE(profiles.empty());
|
||||
for (auto& p : profiles) {
|
||||
EXPECT_GT(p.degree(), 0);
|
||||
EXPECT_FALSE(p.control_points().empty());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test 4: extract_profiles_from_mesh with multiple boundary loops ──
|
||||
TEST(MeshToCurve, ExtractProfiles_Multiple) {
|
||||
auto mesh = make_two_holes();
|
||||
auto profiles = mesh::extract_profiles_from_mesh(mesh, Vector3D::UnitZ());
|
||||
// Should have at least 2 profiles (outer + inner holes)
|
||||
EXPECT_GE(profiles.size(), 2u);
|
||||
for (auto& p : profiles) {
|
||||
EXPECT_GT(p.degree(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test 5: mesh_contour_to_curves at Z plane ──
|
||||
TEST(MeshToCurve, ContourToCurves_ZSlice) {
|
||||
auto mesh = make_tetrahedron();
|
||||
// The tetrahedron has vertices at z=0,0,0,2. Slice at z=1 should intersect.
|
||||
auto contours = mesh::mesh_contour_to_curves(mesh, 1.0);
|
||||
EXPECT_FALSE(contours.empty());
|
||||
for (auto& c : contours) {
|
||||
EXPECT_GT(c.degree(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test 6: mesh_contour_to_curves no intersection ──
|
||||
TEST(MeshToCurve, ContourToCurves_NoIntersection) {
|
||||
auto mesh = make_tetrahedron();
|
||||
auto contours = mesh::mesh_contour_to_curves(mesh, 10.0);
|
||||
// Z=10 is above the tetrahedron, no intersection
|
||||
EXPECT_TRUE(contours.empty());
|
||||
}
|
||||
|
||||
// ── Test 7: extrude mesh overload ──
|
||||
TEST(MeshToCurve, ExtrudeMeshOverload) {
|
||||
auto mesh = make_square_with_hole();
|
||||
auto result = brep::extrude(mesh, Vector3D(0, 0, 5));
|
||||
EXPECT_GT(result.num_faces(), 0u);
|
||||
EXPECT_GT(result.num_bodies(), 0u);
|
||||
}
|
||||
|
||||
// ── Test 8: loft mesh overload ──
|
||||
TEST(MeshToCurve, LoftMeshOverload) {
|
||||
// Create two square meshes at different Z heights
|
||||
auto make_square = [](double z) {
|
||||
std::vector<Point3D> verts = {
|
||||
{0,0,z}, {3,0,z}, {3,3,z}, {0,3,z}
|
||||
};
|
||||
std::vector<std::array<int,3>> tris = {
|
||||
{0,1,2}, {0,2,3}
|
||||
};
|
||||
mesh::HalfedgeMesh m;
|
||||
m.build_from_triangles(verts, tris);
|
||||
return m;
|
||||
};
|
||||
auto mesh1 = make_square(0.0);
|
||||
auto mesh2 = make_square(5.0);
|
||||
std::vector<mesh::HalfedgeMesh> profiles = {mesh1, mesh2};
|
||||
auto result = brep::loft(profiles);
|
||||
EXPECT_GT(result.num_faces(), 0u);
|
||||
EXPECT_GT(result.num_bodies(), 0u);
|
||||
}
|
||||
Reference in New Issue
Block a user