Files
ViewDesignEngine/tests/foundation/test_industrial_formats.cpp
T
茂之钳 e469ef75ef
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
feat(v5-M4): industrial formats + GPU acceleration + Python bindings
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
2026-07-26 21:34:44 +08:00

638 lines
20 KiB
C++

#include <gtest/gtest.h>
#include <fstream>
#include <cstdio>
#include <cmath>
#include "vde/foundation/industrial_formats.h"
#include "vde/brep/modeling.h"
using namespace vde::io;
using namespace vde::brep;
using namespace vde::core;
// ═══════════════════════════════════════════════════════════════
// 辅助函数
// ═══════════════════════════════════════════════════════════════
static BrepModel make_test_box() {
return make_box(2.0, 3.0, 4.0);
}
static BrepModel make_test_cylinder() {
return make_cylinder(1.0, 5.0);
}
// 清理临时文件
static void clean(const char* path) {
std::remove(path);
}
// 验证 BrepModel 的基本有效性
static void expect_valid_body(const BrepModel& m) {
EXPECT_GT(m.num_vertices(), 0);
EXPECT_GT(m.num_edges(), 0);
EXPECT_GT(m.num_faces(), 0);
EXPECT_GT(m.num_bodies(), 0);
}
// ═══════════════════════════════════════════════════════════════
// JT 测试 (ISO 14306)
// ═══════════════════════════════════════════════════════════════
TEST(IndustrialJT, ExportBoxToJt) {
auto box = make_test_box();
const char* path = "/tmp/test_box.jt";
clean(path);
ASSERT_NO_THROW(export_jt(box, path));
std::ifstream f(path, std::ios::binary);
ASSERT_TRUE(f.good());
// 检查 JT 版本字符串
char version[80] = {};
f.read(version, 80);
EXPECT_NE(std::string(version).find("JT"), std::string::npos);
// 检查字节序标记
char byte_order;
f.read(&byte_order, 1);
EXPECT_EQ(byte_order, 0); // LE
clean(path);
}
TEST(IndustrialJT, ImportExportedJt) {
auto box = make_test_box();
const char* path = "/tmp/test_roundtrip.jt";
clean(path);
export_jt(box, path);
// 使用默认选项导入(加载网格)
JTOptions opts;
opts.load_mesh = true;
opts.load_brep = false;
auto imported = import_jt(path, opts);
expect_valid_body(imported);
clean(path);
}
TEST(IndustrialJT, ImportJtWithBrepOption) {
auto box = make_test_box();
const char* path = "/tmp/test_jt_brep.jt";
clean(path);
export_jt(box, path);
JTOptions opts;
opts.load_brep = true;
opts.load_mesh = false;
auto imported = import_jt(path, opts);
// 即使 B-Rep 加载失败,也应返回有效(可能为空)模型
EXPECT_NO_THROW(import_jt(path, opts));
clean(path);
}
TEST(IndustrialJT, ImportJtWithCustomLod) {
auto box = make_test_box();
const char* path = "/tmp/test_jt_lod.jt";
clean(path);
export_jt(box, path);
JTOptions opts;
opts.max_lod = 1;
auto imported = import_jt(path, opts);
expect_valid_body(imported);
clean(path);
}
TEST(IndustrialJT, ImportJtWithPmiOption) {
auto box = make_test_box();
const char* path = "/tmp/test_jt_pmi.jt";
clean(path);
export_jt(box, path);
JTOptions opts;
opts.load_pmi = true;
opts.load_meta = true;
auto imported = import_jt(path, opts);
expect_valid_body(imported);
clean(path);
}
TEST(IndustrialJT, ImportJtNonexistentFile) {
EXPECT_THROW(import_jt("/tmp/nonexistent_jt_file.jt"), std::runtime_error);
}
// ═══════════════════════════════════════════════════════════════
// Parasolid XT 测试
// ═══════════════════════════════════════════════════════════════
TEST(IndustrialParasolid, ExportBoxToXt) {
auto box = make_test_box();
const char* path = "/tmp/test_box.x_t";
clean(path);
ASSERT_NO_THROW(export_parasolid_xt(box, path));
std::ifstream f(path);
ASSERT_TRUE(f.good());
std::string first_line;
std::getline(f, first_line);
// Parasolid text 格式以 "**" 开头
EXPECT_EQ(first_line.substr(0, 2), "**");
clean(path);
}
TEST(IndustrialParasolid, ImportExportedXt) {
auto box = make_test_box();
const char* path = "/tmp/test_xt_roundtrip.x_t";
clean(path);
export_parasolid_xt(box, path);
auto imported = import_parasolid_xt(path);
expect_valid_body(imported);
clean(path);
}
TEST(IndustrialParasolid, ImportXtWithVertexData) {
auto box = make_test_box();
const char* path = "/tmp/test_xt_vertex.x_t";
clean(path);
export_parasolid_xt(box, path);
auto imported = import_parasolid_xt(path);
EXPECT_GT(imported.num_vertices(), 0);
EXPECT_GT(imported.num_edges(), 0);
EXPECT_GT(imported.num_faces(), 0);
clean(path);
}
TEST(IndustrialParasolid, ExportCylinderToXt) {
auto cyl = make_test_cylinder();
const char* path = "/tmp/test_cyl.x_t";
clean(path);
export_parasolid_xt(cyl, path);
std::ifstream f(path);
std::string content((std::istreambuf_iterator<char>(f)), {});
EXPECT_NE(content.find("VERTEX"), std::string::npos);
EXPECT_NE(content.find("EDGE"), std::string::npos);
clean(path);
}
TEST(IndustrialParasolid, ImportXtNonexistentFile) {
EXPECT_THROW(import_parasolid_xt("/tmp/nonexistent_xt.x_t"), std::runtime_error);
}
// ═══════════════════════════════════════════════════════════════
// ACIS SAT 测试
// ═══════════════════════════════════════════════════════════════
TEST(IndustrialACIS, ExportBoxToSatV27) {
auto box = make_test_box();
const char* path = "/tmp/test_box.sat";
clean(path);
ASSERT_NO_THROW(export_acis_sat(box, path, ACISVersion::V27));
std::ifstream f(path);
ASSERT_TRUE(f.good());
std::string first_line;
std::getline(f, first_line);
// 第一行应包含版本号
EXPECT_NE(first_line.find("27"), std::string::npos);
clean(path);
}
TEST(IndustrialACIS, ExportBoxToSatV16) {
auto box = make_test_box();
const char* path = "/tmp/test_box_v16.sat";
clean(path);
export_acis_sat(box, path, ACISVersion::V16);
std::ifstream f(path);
std::string first_line;
std::getline(f, first_line);
EXPECT_NE(first_line.find("16"), std::string::npos);
clean(path);
}
TEST(IndustrialACIS, ImportExportedSat) {
auto box = make_test_box();
const char* path = "/tmp/test_sat_roundtrip.sat";
clean(path);
export_acis_sat(box, path);
auto imported = import_acis_sat(path);
expect_valid_body(imported);
clean(path);
}
TEST(IndustrialACIS, ImportSatWithEndMarker) {
auto box = make_test_box();
const char* path = "/tmp/test_sat_end.sat";
clean(path);
export_acis_sat(box, path);
std::ifstream f(path);
std::string content((std::istreambuf_iterator<char>(f)), {});
EXPECT_NE(content.find("End-of-ACIS-data"), std::string::npos);
auto imported = import_acis_sat(path);
expect_valid_body(imported);
clean(path);
}
TEST(IndustrialACIS, ExportCylinderToSat) {
auto cyl = make_test_cylinder();
const char* path = "/tmp/test_cyl.sat";
clean(path);
export_acis_sat(cyl, path);
std::ifstream f(path);
std::string content((std::istreambuf_iterator<char>(f)), {});
EXPECT_NE(content.find("ViewDesignEngine"), std::string::npos);
EXPECT_NE(content.find("body"), std::string::npos);
clean(path);
}
TEST(IndustrialACIS, ImportSatNonexistentFile) {
EXPECT_THROW(import_acis_sat("/tmp/nonexistent.sat"), std::runtime_error);
}
// ═══════════════════════════════════════════════════════════════
// IFC 测试 (ISO 16739)
// ═══════════════════════════════════════════════════════════════
static std::string make_minimal_ifc() {
return R"(ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDesignEngine Test'),'2;1');
FILE_NAME('test.ifc','2025-01-01',('VDE'),(''),'','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('proj',$,'Test Project',$,$,$,$,(#2),#10);
#2=IFCSITE('site',$,$,$,$,$,$,$,$,$,$,$);
#3=IFCWALL('wall1',$,'Wall 300x3000x240',$,$,#20,#30,$);
#10=IFCOWNERHISTORY(#11,#12,$,$,$,$,$,$);
#11=IFCPERSON('user','','',$,$,$,$,$);
#12=IFCORGANIZATION('VDE','',$,$);
#20=IFCLOCALPLACEMENT($,#21);
#21=IFCAXIS2PLACEMENT3D(#22,$,$);
#22=IFCCARTESIANPOINT((0.0,0.0,0.0));
#30=IFCPRODUCTDEFINITIONSHAPE($,$,(#40));
#40=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#50));
#50=IFCEXTRUDEDAREASOLID(#60,$,$,3.0);
#60=IFCRECTANGLEPROFILEDEF(.AREA.,$,#70,5.0,0.24);
#70=IFCAXIS2PLACEMENT2D(#71,$);
#71=IFCCARTESIANPOINT((0.0,0.0));
#80=IFCSLAB('slab1',$,'Slab 4000x300x4000',$,$,#90,#100,$);
#90=IFCLOCALPLACEMENT($,#91);
#91=IFCAXIS2PLACEMENT3D(#92,$,$);
#92=IFCCARTESIANPOINT((0.0,0.0,0.0));
#100=IFCPRODUCTDEFINITIONSHAPE($,$,(#110));
#110=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#120));
#120=IFCEXTRUDEDAREASOLID(#130,$,$,0.3);
#130=IFCRECTANGLEPROFILEDEF(.AREA.,$,#140,4.0,4.0);
#140=IFCAXIS2PLACEMENT2D(#141,$);
#141=IFCCARTESIANPOINT((0.0,0.0));
#150=IFCBEAM('beam1',$,'Beam 4000x300x300',$,$,#160,#170,$);
#160=IFCLOCALPLACEMENT($,#161);
#161=IFCAXIS2PLACEMENT3D(#162,$,$);
#162=IFCCARTESIANPOINT((0.0,0.0,3.0));
#170=IFCPRODUCTDEFINITIONSHAPE($,$,(#180));
#180=IFCSHAPEREPRESENTATION($,'Body','SweptSolid',(#190));
#190=IFCEXTRUDEDAREASOLID(#200,$,$,0.3);
#200=IFCRECTANGLEPROFILEDEF(.AREA.,$,#210,0.3,0.3);
#210=IFCAXIS2PLACEMENT2D(#211,$);
#211=IFCCARTESIANPOINT((0.0,0.0));
ENDSEC;
END-ISO-10303-21;
)";
}
TEST(IndustrialIFC, ImportWall) {
std::string ifc = make_minimal_ifc();
const char* path = "/tmp/test_wall.ifc";
clean(path);
{
std::ofstream f(path);
f << ifc;
}
auto models = import_ifc(path);
EXPECT_GE(models.size(), 1u) << "Should find at least one IFC product";
if (!models.empty()) {
const auto& wall = models[0];
EXPECT_GT(wall.num_vertices(), 0);
EXPECT_GT(wall.num_bodies(), 0);
}
clean(path);
}
TEST(IndustrialIFC, ImportSlab) {
std::string ifc = make_minimal_ifc();
const char* path = "/tmp/test_slab.ifc";
clean(path);
{
std::ofstream f(path);
f << ifc;
}
auto models = import_ifc(path);
// 应找到 wall + slab + beam
EXPECT_GE(models.size(), 2u);
clean(path);
}
TEST(IndustrialIFC, ImportBeam) {
std::string ifc = make_minimal_ifc();
const char* path = "/tmp/test_beam.ifc";
clean(path);
{
std::ofstream f(path);
f << ifc;
}
auto models = import_ifc(path);
EXPECT_GE(models.size(), 3u);
clean(path);
}
TEST(IndustrialIFC, ImportProductCount) {
std::string ifc = make_minimal_ifc();
const char* path = "/tmp/test_count.ifc";
clean(path);
{
std::ofstream f(path);
f << ifc;
}
auto models = import_ifc(path);
// IFC 包含 IfcWall + IfcSlab + IfcBeam
EXPECT_EQ(models.size(), 3u);
clean(path);
}
TEST(IndustrialIFC, WallHasCorrectDimensions) {
std::string ifc = make_minimal_ifc();
const char* path = "/tmp/test_wall_dims.ifc";
clean(path);
{
std::ofstream f(path);
f << ifc;
}
auto models = import_ifc(path);
ASSERT_GE(models.size(), 1u);
// 验证模型有顶点
const auto& wall = models[0];
EXPECT_GT(wall.num_vertices(), 0);
EXPECT_GT(wall.num_faces(), 0);
clean(path);
}
TEST(IndustrialIFC, ImportNonexistentFile) {
EXPECT_THROW(import_ifc("/tmp/nonexistent.ifc"), std::runtime_error);
}
// ═══════════════════════════════════════════════════════════════
// STEP AP242 测试
// ═══════════════════════════════════════════════════════════════
TEST(IndustrialStepAP242, ExportSingleBox) {
auto box = make_test_box();
std::string result = export_step_ap242({box});
EXPECT_NE(result.find("ISO-10303-21"), std::string::npos);
EXPECT_NE(result.find("AP242"), std::string::npos);
EXPECT_NE(result.find("MANIFOLD_SOLID_BREP"), std::string::npos);
EXPECT_NE(result.find("ENDSEC"), std::string::npos);
EXPECT_NE(result.find("END-ISO-10303-21"), std::string::npos);
}
TEST(IndustrialStepAP242, ExportMultipleBodies) {
auto box = make_test_box();
auto cyl = make_test_cylinder();
std::string result = export_step_ap242({box, cyl});
// 应包含两个 PRODUCT
EXPECT_NE(result.find("PRODUCT"), std::string::npos);
EXPECT_NE(result.find("CLOSED_SHELL"), std::string::npos);
}
TEST(IndustrialStepAP242, ExportWithPMIAnnotations) {
auto box = make_test_box();
std::vector<StepPMIAnnotation> annotations;
StepPMIAnnotation ann;
ann.type = "linear_dimension";
ann.id = "DIM001";
ann.description = "Length dimension";
ann.nominal_value = 100.0;
ann.upper_tolerance = 0.1;
ann.lower_tolerance = -0.1;
ann.datum_system = "Datum_A";
annotations.push_back(ann);
std::string result = export_step_ap242({box}, annotations);
EXPECT_NE(result.find("PRODUCT_MANUFACTURING_INFORMATION"), std::string::npos);
EXPECT_NE(result.find("DIMENSIONAL_CHARACTERISTIC_REPRESENTATION"), std::string::npos);
EXPECT_NE(result.find("SHAPE_DIMENSION_REPRESENTATION"), std::string::npos);
}
TEST(IndustrialStepAP242, ExportWithGeometricTolerance) {
auto box = make_test_box();
std::vector<StepPMIAnnotation> annotations;
StepPMIAnnotation ann;
ann.type = "angular_dimension";
ann.id = "TOL001";
ann.description = "Angular tolerance";
ann.nominal_value = 90.0;
ann.upper_tolerance = 0.5;
ann.lower_tolerance = -0.5;
ann.datum_system = "Datum_B";
annotations.push_back(ann);
std::string result = export_step_ap242({box}, annotations);
EXPECT_NE(result.find("GEOMETRIC_TOLERANCE"), std::string::npos);
EXPECT_NE(result.find("DATUM_SYSTEM"), std::string::npos);
}
TEST(IndustrialStepAP242, ExportWithMaterial) {
auto box = make_test_box();
StepAP242Options opts;
opts.include_material = true;
opts.include_pmi = false;
std::string result = export_step_ap242({box}, {}, opts);
EXPECT_NE(result.find("MATERIAL_DESIGNATION"), std::string::npos);
EXPECT_NE(result.find("MATERIAL_PROPERTY"), std::string::npos);
}
TEST(IndustrialStepAP242, ExportWithoutPmi) {
auto box = make_test_box();
StepAP242Options opts;
opts.include_pmi = false;
opts.include_tolerance = false;
std::string result = export_step_ap242({box}, {}, opts);
// 不应包含 PMI 实体
EXPECT_EQ(result.find("PRODUCT_MANUFACTURING_INFORMATION"), std::string::npos);
// 但基本几何信息应存在
EXPECT_NE(result.find("CARTESIAN_POINT"), std::string::npos);
}
TEST(IndustrialStepAP242, ExportValidationProperties) {
auto box = make_test_box();
StepAP242Options opts;
opts.include_validation = true;
std::string result = export_step_ap242({box}, {}, opts);
EXPECT_NE(result.find("VALIDATION"), std::string::npos);
}
TEST(IndustrialStepAP242, ExportToFile) {
auto box = make_test_box();
const char* path = "/tmp/test_ap242.stp";
clean(path);
export_step_ap242_file(path, {box});
std::ifstream f(path);
ASSERT_TRUE(f.good());
std::string content((std::istreambuf_iterator<char>(f)), {});
EXPECT_NE(content.find("AP242"), std::string::npos);
clean(path);
}
TEST(IndustrialStepAP242, ExportToFileWithPMI) {
auto box = make_test_box();
const char* path = "/tmp/test_ap242_pmi.stp";
clean(path);
std::vector<StepPMIAnnotation> anns;
StepPMIAnnotation ann;
ann.type = "linear_dimension";
ann.id = "PMI1";
ann.nominal_value = 50.0;
anns.push_back(ann);
export_step_ap242_file(path, {box}, anns);
std::ifstream f(path);
std::string content((std::istreambuf_iterator<char>(f)), {});
EXPECT_NE(content.find("PRODUCT_MANUFACTURING_INFORMATION"), std::string::npos);
clean(path);
}
// ═══════════════════════════════════════════════════════════════
// PDF 3D 测试
// ═══════════════════════════════════════════════════════════════
TEST(IndustrialPDF3D, ExportBoxToPdf) {
auto box = make_test_box();
auto pdf_data = export_pdf3d(box);
EXPECT_GT(pdf_data.size(), 100u) << "PDF should have meaningful content";
// 检查 PDF 头部
std::string header(pdf_data.begin(), pdf_data.begin() + std::min(size_t(8), pdf_data.size()));
EXPECT_EQ(header, "%PDF-1.7");
}
TEST(IndustrialPDF3D, ExportContainsU3D) {
auto box = make_test_box();
auto pdf_data = export_pdf3d(box);
// 查找 U3D 标记
std::string content(pdf_data.begin(), pdf_data.end());
EXPECT_NE(content.find("U3D"), std::string::npos);
EXPECT_NE(content.find("/Subtype /U3D"), std::string::npos);
}
TEST(IndustrialPDF3D, ExportToFile) {
auto box = make_test_box();
const char* path = "/tmp/test_3d.pdf";
clean(path);
export_pdf3d_file(path, box);
std::ifstream f(path, std::ios::binary);
ASSERT_TRUE(f.good());
char magic[8] = {};
f.read(magic, 8);
EXPECT_EQ(std::string(magic, 8), "%PDF-1.7");
// 读取全部内容检查
f.seekg(0, std::ios::end);
auto size = f.tellg();
EXPECT_GT(size, 200) << "PDF file should be at least 200 bytes";
clean(path);
}
TEST(IndustrialPDF3D, ExportContains3DAnnotation) {
auto box = make_test_box();
auto pdf_data = export_pdf3d(box);
std::string content(pdf_data.begin(), pdf_data.end());
EXPECT_NE(content.find("/Subtype /3D"), std::string::npos);
EXPECT_NE(content.find("/3DA"), std::string::npos);
}
// ═══════════════════════════════════════════════════════════════
// 边界情况测试
// ═══════════════════════════════════════════════════════════════
TEST(IndustrialBoundary, ExportEmptyBrep) {
BrepModel empty;
// 导出空模型不应崩溃
EXPECT_NO_THROW(export_jt(empty, "/tmp/test_empty.jt"));
EXPECT_NO_THROW(export_parasolid_xt(empty, "/tmp/test_empty.x_t"));
EXPECT_NO_THROW(export_acis_sat(empty, "/tmp/test_empty.sat"));
clean("/tmp/test_empty.jt");
clean("/tmp/test_empty.x_t");
clean("/tmp/test_empty.sat");
}
TEST(IndustrialBoundary, StepAp242EmptyAnnotations) {
auto box = make_test_box();
std::vector<StepPMIAnnotation> empty_anns;
std::string result = export_step_ap242({box}, empty_anns);
// 不应崩溃,且应包含基本结构
EXPECT_NE(result.find("ISO-10303-21"), std::string::npos);
}