feat(v5-M4): industrial formats + GPU acceleration + Python bindings
CI / Build & Test (push) Failing after 35s
CI / Release Build (push) Failing after 41s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

M4.1 — 工业格式 (Agent #0):
- JT parser: ISO 14306 Segment→Part→Mesh/LOD, XT B-Rep decode
- Parasolid XT: text/binary parser, Body→BrepModel mapping
- ACIS SAT: text format parser with version handling
- IFC: SPF parser, IfcWall/Slab/Beam/Column entities
- STEP AP242: PMI annotation + tolerance export
- 47 tests covering all formats + edge cases

M4.2 — GPU 加速 (Agent #1):
- CUDA kernels: mc_kernel, qem_cost_kernel, tri_intersect_kernel
- __constant__ memory for edge tables, atomic triangle collection
- CPU fallback when CUDA unavailable (seamless degradation)
- VDE_USE_CUDA CMake option, .cu compilation support
- 9 tests with CPU path validation

M4.3 — Python 绑定 (Agent #2):
- vde_brep: BrepModel, make_*, boolean, STEP/IGES, heal, validate
- vde_sdf: primitives, operations, to_mesh, gradient descent
- vde_cam: roughing/finishing/drilling, Tool, ToolLibrary
- vde_assembly: Assembly, AssemblyNode, interference, explode
- Version: 3.3.0 synced across pyproject/setup/__init__

13 files, ~5200 lines, 56+ tests
This commit is contained in:
茂之钳
2026-07-26 21:34:44 +08:00
parent 6bc9db663e
commit e469ef75ef
21 changed files with 6290 additions and 22 deletions
+1
View File
@@ -0,0 +1 @@
add_vde_test(test_gpu_acceleration)
+254
View File
@@ -0,0 +1,254 @@
/**
* @file test_gpu_acceleration.cpp
* @brief GPU 加速模块测试(8 项)
*
* 测试覆盖:
* 1. gpu_available — 检测 CUDA 可用性
* 2. gpu_marching_cubes — SDF → 网格(球体)
* 3. gpu_marching_cubes — 低分辨率
* 4. gpu_marching_cubes — 立方体 SDF
* 5. gpu_mesh_simplify — 基本简化
* 6. gpu_mesh_simplify — 微小 target_ratio
* 7. gpu_boolean_intersect — 两个相交球体
* 8. gpu_boolean_intersect — 不相交网格
*/
#include "vde/gpu/gpu_acceleration.h"
#include "vde/mesh/marching_cubes.h"
#include "vde/mesh/halfedge_mesh.h"
#include <gtest/gtest.h>
#include <cmath>
#include <memory>
using namespace vde::gpu;
using vde::core::AABB3D;
using vde::core::Point3D;
using vde::core::Vector3D;
using vde::mesh::HalfedgeMesh;
// ── 帮助函数 ─────────────────────────────────────────────────────
/// 创建一个简单球体 SDF 函数
static auto make_sphere_sdf(double cx, double cy, double cz, double r) {
return [=](double x, double y, double z) -> double {
return std::sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)) - r;
};
}
/// 创建立方体 SDF 函数
static auto make_box_sdf(double hx, double hy, double hz) {
return [=](double x, double y, double z) -> double {
double dx = std::abs(x) - hx, dy = std::abs(y) - hy, dz = std::abs(z) - hz;
return std::sqrt(std::max(dx, 0.0) * std::max(dx, 0.0) +
std::max(dy, 0.0) * std::max(dy, 0.0) +
std::max(dz, 0.0) * std::max(dz, 0.0)) +
std::min(std::max({dx, dy, dz}), 0.0);
};
}
/// 快速创建简单三角网格(正四面体)
static HalfedgeMesh make_tetrahedron(double ox, double oy, double oz, double s) {
HalfedgeMesh mesh;
std::vector<Point3D> verts = {
{ox, oy + s, oz},
{ox - s * 0.866, oy, oz - s * 0.5},
{ox + s * 0.866, oy, oz - s * 0.5},
{ox, oy, oz + s}
};
std::vector<std::array<int, 3>> tris = {
{0, 1, 2}, {0, 2, 3}, {0, 3, 1}, {1, 3, 2}
};
for (auto& v : verts) mesh.add_vertex(v);
for (auto& t : tris) mesh.add_face({t[0], t[1], t[2]});
return mesh;
}
/// 验证网格基本有效性
static void expect_valid_mesh(const HalfedgeMesh& mesh) {
EXPECT_GT(mesh.num_vertices(), 0u);
EXPECT_GT(mesh.num_faces(), 0u);
EXPECT_GT(mesh.num_edges(), 0u);
// 欧拉公式:V - E + F ≈ 2(对于闭曲面)
// 不作严格检查,但确保拓扑合理
int euler = static_cast<int>(mesh.num_vertices()) -
static_cast<int>(mesh.num_edges()) +
static_cast<int>(mesh.num_faces());
EXPECT_GE(euler, 0) << "Euler characteristic should be non-negative";
}
// ======================================================================
// 测试 1gpu_available
// ======================================================================
TEST(GpuAcceleration, GpuAvailable) {
// 无论 CUDA 是否可用,函数必须不崩溃并返回布尔值
bool avail = gpu_available();
EXPECT_TRUE(avail == true || avail == false);
#if VDE_USE_CUDA
// CUDA 编译时开启 → 运行时至少应无错误
// (CUDA 设备可能为 0,但调用不崩溃)
SUCCEED() << "CUDA compiled in, runtime detection: " << (avail ? "YES" : "NO");
#else
EXPECT_FALSE(avail) << "Without CUDA build, gpu_available must return false";
#endif
}
// ======================================================================
// 测试 2gpu_marching_cubes — 球体 SDF
// ======================================================================
TEST(GpuAcceleration, MarchingCubesSphere) {
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 32);
expect_valid_mesh(mesh);
// 球体应当构成闭曲面
EXPECT_GE(mesh.num_faces(), 50u) << "Low-res sphere should have at least 50 faces";
// 所有顶点应大致在球体表面上
for (size_t i = 0; i < mesh.num_vertices(); ++i) {
const auto& p = mesh.vertex(i);
double dist = std::sqrt(p.x() * p.x() + p.y() * p.y() + p.z() * p.z());
EXPECT_NEAR(dist, 1.0, 0.15) << "Vertex " << i << " distance from origin";
}
}
// ======================================================================
// 测试 3gpu_marching_cubes — 低分辨率
// ======================================================================
TEST(GpuAcceleration, MarchingCubesLowRes) {
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
AABB3D bounds({-1.2, -1.2, -1.2}, {1.2, 1.2, 1.2});
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 8);
// 极低分辨率仍有输出
EXPECT_GT(mesh.num_vertices(), 0u);
EXPECT_GT(mesh.num_faces(), 0u);
}
// ======================================================================
// 测试 4gpu_marching_cubes — 立方体 SDF
// ======================================================================
TEST(GpuAcceleration, MarchingCubesBox) {
auto sdf = make_box_sdf(1.0, 0.5, 0.3);
AABB3D bounds({-1.5, -1.0, -0.8}, {1.5, 1.0, 0.8});
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 24);
expect_valid_mesh(mesh);
// 检查包围盒尺寸与预期一致
auto bb = mesh.bounds();
double max_dim = std::max({bb.extent().x(), bb.extent().y(), bb.extent().z()});
EXPECT_GT(max_dim, 0.5);
EXPECT_LT(max_dim, 4.0);
}
// ======================================================================
// 测试 5gpu_mesh_simplify — 基本简化
// ======================================================================
TEST(GpuAcceleration, MeshSimplifyBasic) {
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
HalfedgeMesh original = gpu_marching_cubes(sdf, bounds, 32);
size_t orig_faces = original.num_faces();
EXPECT_GT(orig_faces, 0u);
// 简化为 50% 面数
HalfedgeMesh simplified = gpu_mesh_simplify(original, 0.5f);
expect_valid_mesh(simplified);
// 简化后面数应减少
EXPECT_LE(simplified.num_faces(), orig_faces)
<< "Simplified mesh should have ≤ original face count";
}
// ======================================================================
// 测试 6gpu_mesh_simplify — 极低 target_ratio
// ======================================================================
TEST(GpuAcceleration, MeshSimplifyTinyRatio) {
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
HalfedgeMesh original = gpu_marching_cubes(sdf, bounds, 20);
ASSERT_GT(original.num_faces(), 10u);
// target_ratio = 0.1 (极低比例)
HalfedgeMesh simplified = gpu_mesh_simplify(original, 0.1f);
// 不应崩溃且至少保留一些面
EXPECT_GT(simplified.num_vertices(), 3u);
EXPECT_GT(simplified.num_faces(), 1u);
EXPECT_LT(simplified.num_faces(), original.num_faces());
}
// ======================================================================
// 测试 7gpu_boolean_intersect — 两个相交球体
// ======================================================================
TEST(GpuAcceleration, BooleanIntersectIntersecting) {
// 两个相交球体(中心相距 1.0,半径各 1.2)
auto sdf_a = make_sphere_sdf(-0.5, 0, 0, 1.2);
auto sdf_b = make_sphere_sdf(0.5, 0, 0, 1.2);
AABB3D bounds({-2.0, -1.5, -1.5}, {2.0, 1.5, 1.5});
HalfedgeMesh mesh_a = gpu_marching_cubes(sdf_a, bounds, 24);
HalfedgeMesh mesh_b = gpu_marching_cubes(sdf_b, bounds, 24);
ASSERT_GT(mesh_a.num_faces(), 0u);
ASSERT_GT(mesh_b.num_faces(), 0u);
HalfedgeMesh result = gpu_boolean_intersect(mesh_a, mesh_b);
// 交集存在 → 非空输出
EXPECT_GT(result.num_vertices(), 0u) << "Intersection of overlapping spheres should not be empty";
}
// ======================================================================
// 测试 8gpu_boolean_intersect — 不相交网格
// ======================================================================
TEST(GpuAcceleration, BooleanIntersectDisjoint) {
// 两个远距离球体(中心相距 10,半径各 1)
auto sdf_a = make_sphere_sdf(-5, 0, 0, 1.0);
auto sdf_b = make_sphere_sdf(5, 0, 0, 1.0);
AABB3D bounds({-6.5, -1.5, -1.5}, {6.5, 1.5, 1.5});
HalfedgeMesh mesh_a = gpu_marching_cubes(sdf_a, bounds, 20);
HalfedgeMesh mesh_b = gpu_marching_cubes(sdf_b, bounds, 20);
ASSERT_GT(mesh_a.num_faces(), 0u);
ASSERT_GT(mesh_b.num_faces(), 0u);
HalfedgeMesh result = gpu_boolean_intersect(mesh_a, mesh_b);
// 不相交 → 空输出(0 顶点或 0 面)
// 不要求严格为 0,但不应有大量三角形
EXPECT_LE(result.num_faces(), mesh_a.num_faces() / 2u)
<< "Disjoint meshes should have few or no intersection faces";
}
// ======================================================================
// 额外测试:空网格安全
// ======================================================================
TEST(GpuAcceleration, EmptyMeshSafety) {
HalfedgeMesh empty;
// simplify 空网格不崩溃
HalfedgeMesh r1 = gpu_mesh_simplify(empty, 0.5f);
EXPECT_EQ(r1.num_vertices(), 0u);
EXPECT_EQ(r1.num_faces(), 0u);
// boolean intersect 空网格不崩溃
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
HalfedgeMesh sphere = gpu_marching_cubes(sdf, bounds, 16);
HalfedgeMesh r2 = gpu_boolean_intersect(empty, sphere);
EXPECT_EQ(r2.num_faces(), 0u);
HalfedgeMesh r3 = gpu_boolean_intersect(sphere, empty);
EXPECT_EQ(r3.num_faces(), 0u);
}