feat(brep): S13-B IGES export + format utilities
Add IGES v5.3 export for B-Rep models with: - Entity mapping: Point(116), Line(110), Circular Arc(100), B-Spline Curve(126), Plane(108), B-Spline Surface(128), Vertex(502), Edge(504), Loop(508), Face(510), Shell(514), Manifold Solid B-Rep(186) - Auto-detection of circular arcs (degree-2 NURBS with isosceles control triangle) and planes (1x1 degree surfaces) - Fixed 80-column IGES format with S/G/D/P/T sections - Header-only format_utils.h for IGES/STEP number formatting New files: - include/vde/foundation/format_utils.h - include/vde/brep/iges_export.h - src/brep/iges_export.cpp - tests/brep/test_iges_export.cpp
This commit is contained in:
@@ -4,3 +4,5 @@ add_vde_test(test_step_export)
|
||||
add_vde_test(test_step_import)
|
||||
add_vde_test(test_brep_boolean)
|
||||
add_vde_test(test_brep_validate)
|
||||
add_vde_test(test_iges_import)
|
||||
add_vde_test(test_iges_export)
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/brep.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include "vde/brep/iges_export.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// IGES export — section structure tests
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportBox_HasSections) {
|
||||
auto box = make_box(2, 3, 4);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
// Must have all 5 sections
|
||||
EXPECT_NE(iges.find("S0000001"), std::string::npos) << "Missing Start section";
|
||||
EXPECT_NE(iges.find("G0000001"), std::string::npos) << "Missing Global section";
|
||||
EXPECT_NE(iges.find("D0000001"), std::string::npos) << "Missing DE section";
|
||||
EXPECT_NE(iges.find("P0000001"), std::string::npos) << "Missing PD section";
|
||||
EXPECT_NE(iges.find("T0000001"), std::string::npos) << "Missing Terminate section";
|
||||
}
|
||||
|
||||
TEST(IgesExportTest, ExportBox_HasEightyColumns) {
|
||||
auto box = make_box(1, 1, 1);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
std::istringstream iss(iges);
|
||||
std::string line;
|
||||
while (std::getline(iss, line)) {
|
||||
if (line.empty()) continue;
|
||||
EXPECT_EQ(line.size(), 80u) << "Line not 80 chars: " << line.substr(0, 40) << "...";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(IgesExportTest, ExportBox_ContainsPointEntity) {
|
||||
auto box = make_box(1, 1, 1);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
// DE section should have type 116 (Point) entries
|
||||
EXPECT_NE(iges.find(" 116"), std::string::npos) << "Missing Point(116) entities";
|
||||
}
|
||||
|
||||
TEST(IgesExportTest, ExportBox_ContainsLineEntity) {
|
||||
auto box = make_box(1, 1, 1);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
// Should have type 110 (Line) — box edges are straight
|
||||
EXPECT_NE(iges.find(" 110"), std::string::npos) << "Missing Line(110) entities";
|
||||
}
|
||||
|
||||
TEST(IgesExportTest, ExportBox_ContainsPlaneEntity) {
|
||||
auto box = make_box(1, 1, 1);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
// Box faces should be detected as planes (108)
|
||||
EXPECT_NE(iges.find(" 108"), std::string::npos) << "Missing Plane(108) entities";
|
||||
}
|
||||
|
||||
TEST(IgesExportTest, ExportBox_ContainsTopologyEntities) {
|
||||
auto box = make_box(1, 1, 1);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
EXPECT_NE(iges.find(" 502"), std::string::npos) << "Missing Vertex(502)";
|
||||
EXPECT_NE(iges.find(" 504"), std::string::npos) << "Missing Edge(504)";
|
||||
EXPECT_NE(iges.find(" 508"), std::string::npos) << "Missing Loop(508)";
|
||||
EXPECT_NE(iges.find(" 510"), std::string::npos) << "Missing Face(510)";
|
||||
EXPECT_NE(iges.find(" 514"), std::string::npos) << "Missing Shell(514)";
|
||||
EXPECT_NE(iges.find(" 186"), std::string::npos) << "Missing MSBO(186)";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Cylinder export
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportCylinder_HasValidSections) {
|
||||
auto cyl = make_cylinder(1.0, 4.0);
|
||||
std::string iges = export_iges({cyl});
|
||||
|
||||
EXPECT_NE(iges.find("S0000001"), std::string::npos);
|
||||
EXPECT_NE(iges.find("G0000001"), std::string::npos);
|
||||
EXPECT_NE(iges.find("T0000001"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(IgesExportTest, ExportCylinder_ContainsSurfaceEntity) {
|
||||
auto cyl = make_cylinder(1.0, 4.0);
|
||||
std::string iges = export_iges({cyl});
|
||||
|
||||
// Cylinder surface is curved → B-Spline surface (128)
|
||||
EXPECT_NE(iges.find(" 128"), std::string::npos) << "Missing B-Spline Surface(128)";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Sphere export
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportSphere_HasValidSections) {
|
||||
auto sphere = make_sphere(2.0);
|
||||
std::string iges = export_iges({sphere});
|
||||
|
||||
EXPECT_NE(iges.find("S0000001"), std::string::npos);
|
||||
EXPECT_NE(iges.find("G0000001"), std::string::npos);
|
||||
EXPECT_NE(iges.find("T0000001"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(IgesExportTest, ExportSphere_ContainsBsplineSurface) {
|
||||
auto sphere = make_sphere(2.0);
|
||||
std::string iges = export_iges({sphere});
|
||||
|
||||
// Sphere should have B-Spline surfaces (128)
|
||||
EXPECT_NE(iges.find(" 128"), std::string::npos)
|
||||
<< "Sphere should export B-Spline surfaces (type 128)";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Multiple bodies
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportMultipleBodies_HasMultipleSolids) {
|
||||
auto box1 = make_box(1, 1, 1);
|
||||
auto box2 = make_box(2, 2, 2);
|
||||
std::string iges = export_iges({box1, box2});
|
||||
|
||||
// Count MSBO (186) entities — should have at least 2
|
||||
int count = 0;
|
||||
size_t pos = 0;
|
||||
while ((pos = iges.find(" 186", pos)) != std::string::npos) {
|
||||
count++;
|
||||
pos += 8;
|
||||
}
|
||||
EXPECT_GE(count, 2) << "Expected at least 2 MSBO entities";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Empty model
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportEmptyModel_ProducesValidOutput) {
|
||||
BrepModel empty;
|
||||
std::string iges = export_iges({empty});
|
||||
|
||||
// Should still produce all sections
|
||||
EXPECT_NE(iges.find("S0000001"), std::string::npos);
|
||||
EXPECT_NE(iges.find("G0000001"), std::string::npos);
|
||||
EXPECT_NE(iges.find("T0000001"), std::string::npos);
|
||||
|
||||
// Terminate section should exist with counts
|
||||
EXPECT_NE(iges.find("T0000001"), std::string::npos);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// File export
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportFile_WritesToDisk) {
|
||||
auto box = make_box(1, 1, 1);
|
||||
std::string path = "/tmp/test_vde_iges_export.igs";
|
||||
export_iges_file(path, {box});
|
||||
|
||||
std::ifstream in(path);
|
||||
EXPECT_TRUE(in.good());
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(in)),
|
||||
std::istreambuf_iterator<char>());
|
||||
EXPECT_NE(content.find("S0000001"), std::string::npos);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Line count check (every line is 80 chars + \n)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportBox_AllLinesAreEightyColumns) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
std::istringstream iss(iges);
|
||||
std::string line;
|
||||
int line_count = 0;
|
||||
while (std::getline(iss, line)) {
|
||||
line_count++;
|
||||
EXPECT_EQ(line.size(), 80u) << "Line " << line_count << " is not 80 columns";
|
||||
}
|
||||
EXPECT_GT(line_count, 10) << "Should have many lines";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// DE section integrity: every DE line ends with Dnnnnnnn
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(IgesExportTest, ExportBox_DELinesHaveCorrectSuffix) {
|
||||
auto box = make_box(1, 1, 1);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
int de_count = 0;
|
||||
size_t pos = 0;
|
||||
while ((pos = iges.find('D', pos)) != std::string::npos) {
|
||||
// Check it's a DE line suffix (last 8 chars of a line)
|
||||
// 'D' followed by 7 digits and then newline
|
||||
if (pos >= 72 && iges[pos] == 'D') {
|
||||
// Verify it's at position 72 (0-indexed) in its line
|
||||
size_t line_start = iges.rfind('\n', pos);
|
||||
if (line_start == std::string::npos) line_start = -1; // first line
|
||||
if (pos - line_start == 73) { // 0-indexed → col 73
|
||||
de_count++;
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
EXPECT_GT(de_count, 0) << "Should have DE lines with D suffix";
|
||||
}
|
||||
Reference in New Issue
Block a user