feat(brep): S13-B IGES export + format utilities
CI / Build & Test (push) Failing after 33s
CI / Release Build (push) Failing after 39s

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:
茂之钳
2026-07-24 07:34:27 +00:00
parent 092f35c3af
commit 4f75bb8b07
6 changed files with 951 additions and 0 deletions
+2
View File
@@ -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)
+214
View File
@@ -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";
}