feat(brep): S13-A — IGES import support
CI / Build & Test (push) Failing after 33s
CI / Release Build (push) Failing after 32s

Implement IGES (Initial Graphics Exchange Specification) file reader that
converts IGES entities to B-Rep models.

Key features:
- 80-character fixed-width card image parser (S/G/D/P/T sections)
- Directory Entry parsing with all 18 DE fields per entity
- Parameter Data parsing with DE pointer tracking
- D exponent notation support (e.g., 1.5D+2 = 150)

Supported IGES entity types:
  Curve entities (→ NurbsCurve):
    Type 100 - Circular Arc (rational degree-2)
    Type 102 - Composite Curve
    Type 104 - Conic Arc (ellipse/hyperbola/parabola)
    Type 106 - Copious Data (polyline)
    Type 110 - Line (degree-1)
    Type 126 - Rational B-Spline Curve

  Surface entities (→ NurbsSurface):
    Type 108 - Plane
    Type 114 - Parametric Spline Surface
    Type 118 - Ruled Surface
    Type 120 - Surface of Revolution
    Type 122 - Tabulated Cylinder (extrude)
    Type 128 - Rational B-Spline Surface
    Type 140 - Offset Surface
    Type 192 - Right Circular Cylindrical Surface
    Type 194 - Right Circular Conical Surface
    Type 196 - Spherical Surface
    Type 198 - Toroidal Surface

  Topology entities (→ BrepModel):
    Type 186 - Manifold Solid B-Rep Object
    Type 502 - Vertex
    Type 504 - Edge
    Type 508 - Loop
    Type 510 - Face
    Type 514 - Shell

Tests cover: line, arc, cylinder, sphere, cone, torus, plane,
B-spline curve/surface, ruled surface, revolution surface,
tabulated cylinder, copious data polyline, manifold solid B-Rep,
D exponent notation, empty/truncated file handling, and file errors.
This commit is contained in:
茂之钳
2026-07-24 07:37:03 +00:00
parent 4f75bb8b07
commit 6cfd186780
3 changed files with 1962 additions and 0 deletions
+548
View File
@@ -0,0 +1,548 @@
#include <gtest/gtest.h>
#include "vde/brep/iges_import.h"
#include "vde/brep/brep.h"
#include "vde/brep/modeling.h"
#include <cmath>
#include <sstream>
#include <iomanip>
#include <vector>
#include <string>
using namespace vde::brep;
using namespace vde::core;
// ─────────────────────────────────────────────────────────────
// Helper: Build IGES 80-character fixed-width records
// ─────────────────────────────────────────────────────────────
namespace {
/// Pad a string to exactly `width` characters (right-justified for integers, left for strings)
std::string pad_right(const std::string& s, int width) {
std::string r = s;
if (static_cast<int>(r.size()) > width) r = r.substr(0, width);
else r.append(width - r.size(), ' ');
return r;
}
std::string pad_left(int val, int width) {
std::string s = std::to_string(val);
if (static_cast<int>(s.size()) > width) s = s.substr(0, width);
else s.insert(0, width - s.size(), ' ');
return s;
}
/// Build one IGES Directory Entry record (72 chars of fields + section char + 7-char seq)
std::string de_line(const std::vector<int>& fields, char section, int seq) {
// fields: 9 values, each right-justified in 8 chars
std::string line;
for (size_t i = 0; i < fields.size() && i < 9; ++i) {
std::string val = std::to_string(fields[i]);
// Right-justify integer in 8-char field
if (val.size() < 8) val.insert(0, 8 - val.size(), ' ');
line += val;
}
// pad remaining to 72
while (line.size() < 72) line += ' ';
line = line.substr(0, 72);
line += section;
line += pad_left(seq, 7);
return line;
}
/// Build one IGES Parameter Data record
std::string pd_line(const std::string& data, int de_ptr, int seq) {
std::string line = data;
// Data occupies cols 1-64 max
while (line.size() < 65) line += ' ';
line = line.substr(0, 64);
// Pad to col 65 (char index 64)
// Col 65 is blank
line += ' ';
// DE pointer in cols 66-72 (right justified in 7 chars)
std::string deplabel = std::to_string(de_ptr);
if (deplabel.size() < 7) deplabel = std::string(7 - deplabel.size(), ' ') + deplabel;
line += deplabel;
line += 'P';
line += pad_left(seq, 7);
return line;
}
/// Build a full IGES file string
struct IgesBuilder {
std::vector<std::string> s_lines;
std::vector<std::string> g_lines;
std::vector<std::string> d_lines; // pairs: 2 per entity
std::vector<std::string> p_lines;
void add_s_line(const std::string& txt) {
std::string line = pad_right(txt, 72) + "S";
line += pad_left(static_cast<int>(s_lines.size() + 1), 7);
s_lines.push_back(line);
}
void add_g_line(const std::string& txt) {
std::string line = pad_right(txt, 72) + "G";
line += pad_left(static_cast<int>(g_lines.size() + 1), 7);
g_lines.push_back(line);
}
// Add a directory entry (two 80-char lines)
void add_de(int type_num, int pd_ptr, int struct_val, int line_font, int level,
int view, int transform, int label, int status,
int line_weight, int color, int param_count, int form_num) {
int seq = static_cast<int>(d_lines.size() + 1);
// Line 1
d_lines.push_back(de_line({type_num, pd_ptr, struct_val, line_font, level,
view, transform, label, status}, 'D', seq));
// Line 2: type, line_weight, color, param_count, form_num, 0, 0, label, subscript
d_lines.push_back(de_line({type_num, line_weight, color, param_count, form_num,
0, 0, label, 0}, 'D', seq + 1));
}
// Add parameter data
void add_pd(const std::string& data, int de_ptr) {
int seq = static_cast<int>(p_lines.size() + 1);
p_lines.push_back(pd_line(data, de_ptr, seq));
}
std::string build() {
std::string result;
for (const auto& l : s_lines) result += l + "\n";
for (const auto& l : g_lines) result += l + "\n";
for (const auto& l : d_lines) result += l + "\n";
for (const auto& l : p_lines) result += l + "\n";
// Terminate section
int s_cnt = static_cast<int>(s_lines.size());
int g_cnt = static_cast<int>(g_lines.size());
int d_cnt = static_cast<int>(d_lines.size());
int p_cnt = static_cast<int>(p_lines.size());
std::string t_line = pad_right(
pad_left(s_cnt, 8) + pad_left(g_cnt, 8) + pad_left(d_cnt, 8) + pad_left(p_cnt, 8),
72) + "T" + pad_left(1, 7);
result += t_line + "\n";
return result;
}
};
/// Standard global section for minimal IGES files
std::string standard_global() {
return "1H,,1H;,4HSLOT,1,,,13,0.016,0.1,,1.0,1.0,1.0,2,1,1,8,,1.0,1,1,0,0;";
}
} // anonymous namespace
// ─────────────────────────────────────────────────────────────
TEST(IgesImport, EmptyData) {
auto bodies = import_iges_from_string("");
EXPECT_TRUE(bodies.empty());
EXPECT_EQ(iges_last_error(), IgesError::ParseError);
}
TEST(IgesImport, TruncatedFile) {
// Missing sections — just a start section
IgesBuilder b;
b.add_s_line("Truncated test file");
auto bodies = import_iges_from_string(b.build());
EXPECT_TRUE(bodies.empty());
EXPECT_NE(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, MinimalLine) {
IgesBuilder b;
b.add_s_line("Test IGES file with a single line");
b.add_g_line(standard_global());
// Single line entity: type 110
// DE entry: PD pointer 1, param count 1
b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
// Line: X1,Y1,Z1,X2,Y2,Z2 = (0,0,0) to (10,0,0)
b.add_pd("0.0,0.0,0.0,10.0,0.0,0.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
// Should produce at least one body (fallback mode with a surface)
ASSERT_FALSE(bodies.empty());
EXPECT_GT(bodies[0].num_surfaces(), 0u);
}
TEST(IgesImport, SingleArc) {
IgesBuilder b;
b.add_s_line("Test IGES file with a circular arc");
b.add_g_line(standard_global());
// Type 100: Circular Arc — 7 params: ZT, Xc, Yc, Xs, Ys, Xe, Ye
// Full circle: center (0,0), radius 5, start (5,0), end (5,0) → full circle
b.add_de(100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,5.0,0.0,5.0,0.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
ASSERT_FALSE(bodies.empty());
}
TEST(IgesImport, SingleLine110) {
// Test line (type 110): basic endpoint geometry
IgesBuilder b;
b.add_s_line("Line test");
b.add_g_line(standard_global());
b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1.0,2.0,3.0,4.0,5.0,6.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, CylinderSurface) {
IgesBuilder b;
b.add_s_line("Cylinder test");
b.add_g_line(standard_global());
// Type 192: Right Circular Cylindrical Surface
// location(0,0,0), axis(0,0,1), radius=5
b.add_de(192, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, SphereSurface) {
IgesBuilder b;
b.add_s_line("Sphere test");
b.add_g_line(standard_global());
// Type 196: Spherical Surface — center(0,0,0), radius=10
b.add_de(196, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,10.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, ConeSurface) {
IgesBuilder b;
b.add_s_line("Cone test");
b.add_g_line(standard_global());
// Type 194: Right Circular Conical Surface
// location(0,0,0), axis(0,0,1), radius=5, semi_angle=0.5 rad
b.add_de(194, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0,0.5;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, TorusSurface) {
IgesBuilder b;
b.add_s_line("Torus test");
b.add_g_line(standard_global());
// Type 198: Toroidal Surface
// center(0,0,0), axis(0,0,1), R=10, r=3
b.add_de(198, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,10.0,3.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, PlaneSurface) {
IgesBuilder b;
b.add_s_line("Plane test");
b.add_g_line(standard_global());
// Type 108: Plane — equation Ax+By+Cz+D=0
// XY plane: 0x+0y+1z+0=0
b.add_de(108, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,1.0,0.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, BSplineCurve) {
IgesBuilder b;
b.add_s_line("B-Spline curve test");
b.add_g_line(standard_global());
// Type 126: Rational B-Spline Curve
// K=5, M=2 (degree 2), PROP1=0, PROP2=0 (non-rational), PROP3=0, N=3 (4 CPs)
// Knot sequence: A(0..K) = K+1=6 values: 0,0,0,0.5,1,1
// CPs: (0,0,0), (2,3,0), (5,1,0), (7,4,0)
b.add_de(126, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("5,2,0,0,0,3,0.0,0.0,0.0,0.5,1.0,1.0,0.0,0.0,0.0,2.0,3.0,0.0,5.0,1.0,0.0,7.0,4.0,0.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, BSplineSurface) {
IgesBuilder b;
b.add_s_line("B-Spline surface test");
b.add_g_line(standard_global());
// Type 128: Rational B-Spline Surface
// K1=1, K2=1, M1=1, M2=1, PROP1-4=0, PROP5=0, N1=0, N2=0 → 1×1 CP, deg 1×1
// U knots: 0.0, 1.0 (K1+1=2 values)
// V knots: 0.0, 1.0 (K2+1=2 values)
// CP: (0,0,0)
b.add_de(128, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,1,1,1,0,0,0,0,0,0,0,0.0,1.0,0.0,1.0,0.0,0.0,0.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, RuledSurface) {
IgesBuilder b;
b.add_s_line("Ruled surface test");
b.add_g_line(standard_global());
// Need two curves as DE entries first, then a ruled surface referencing them
// Line 1: (0,0,0) → (10,0,0), DE index 1
b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,10.0,0.0,0.0;", 1);
// Line 2: (0,5,0) → (10,5,0), DE index 2
b.add_de(110, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,5.0,0.0,10.0,5.0,0.0;", 2);
// Ruled surface 118 referencing lines 1 and 2
b.add_de(118, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,2,0,0;", 3);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, RevolutionSurface) {
IgesBuilder b;
b.add_s_line("Revolution surface test");
b.add_g_line(standard_global());
// Profile line: (5,0,0) → (5,0,10), DE index 1
b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("5.0,0.0,0.0,5.0,0.0,10.0;", 1);
// Surface of revolution 120: profile DE=1, axis origin=(0,0,0), axis dir=(0,0,1)
b.add_de(120, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,0.0,0.0,0.0,0.0,0.0,1.0,0.0,6.283185307;", 2);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, TabulatedCylinder) {
IgesBuilder b;
b.add_s_line("Tabulated cylinder test");
b.add_g_line(standard_global());
// Profile line: (5,0,0) → (5,0,0)... actually a single point doesn't work
// Let's use a simple line: (0,0,0) → (5,0,0), DE 1
b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,5.0,0.0,0.0;", 1);
// Tabulated cylinder 122: curve DE=1, direction (0,0,10)
b.add_de(122, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,0.0,0.0,10.0;", 2);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, CopiousDataPolyline) {
IgesBuilder b;
b.add_s_line("Polyline test");
b.add_g_line(standard_global());
// Type 106: Copious Data, IP=1 (polyline), N=4 points
// Points: (0,0,0), (2,0,0), (2,2,0), (0,2,0)
b.add_de(106, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,4,0.0,0.0,0.0,2.0,0.0,0.0,2.0,2.0,0.0,0.0,2.0,0.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, ManifoldSolidBox) {
// Build a minimal B-Rep box via IGES topology entities
IgesBuilder b;
b.add_s_line("Manifold solid box test");
b.add_g_line(standard_global());
// Entities:
// 1: Vertex (0,0,0) type 502
// 2: Vertex (10,0,0)
// 3: Vertex (10,10,0)
// 4: Vertex (0,10,0)
// 5: Vertex (0,0,10)
// 6: Vertex (10,0,10)
// 7: Vertex (10,10,10)
// 8: Vertex (0,10,10)
// 9: Line (0,0,0)-(10,0,0) type 110 → edge uses curve
// ... and so on. Let's do a simpler subset.
// Vertex 1: (0,0,0)
b.add_de(502, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0;", 1);
// Vertex 2: (10,0,0)
b.add_de(502, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("10.0,0.0,0.0;", 2);
// Vertex 3: (10,10,0)
b.add_de(502, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("10.0,10.0,0.0;", 3);
// Vertex 4: (0,10,0)
b.add_de(502, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,10.0,0.0;", 4);
// Line for edge: (0,0,0)-(10,0,0)
b.add_de(110, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,10.0,0.0,0.0;", 5);
// Edge 1: curve type=1 (line), curve DE=5, start vx DE=1, sv_index=0, end vx DE=2, ev_index=1
b.add_de(504, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,5,1,0,2,1,0,0.0,1.0;", 6);
// Plane for bottom face: z=0 plane
b.add_de(108, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,1.0,0.0;", 7);
// Loop: N=1 edge, type=0, edge_DE=6, vx_count=2, start=1, end=2, cur_srf=0, sparams=0, eparams=0
b.add_de(508, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,0,6,2,1,2,0,0.0,1.0;", 8);
// Face: surface_DE=7, N_loops=1, loop_flag=0, loop_DE=8
b.add_de(510, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("7,1,0,8;", 9);
// Shell: N=1 face, face_DE=9
b.add_de(514, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1,9;", 10);
// Manifold Solid B-Rep 186: shell DE=10
b.add_de(186, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("10;", 11);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
ASSERT_FALSE(bodies.empty());
EXPECT_GT(bodies[0].num_faces(), 0u);
}
TEST(IgesImport, RoundtripBox) {
// Create a box via modeling, then verify IGES import infrastructure works
auto box = make_box(2.0, 1.0, 3.0);
EXPECT_GT(box.num_faces(), 0u);
// Verify we can build and parse IGES with a cylinder
IgesBuilder b;
b.add_s_line("Roundtrip test - cylinder");
b.add_g_line(standard_global());
b.add_de(192, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, InvalidSectionOrder) {
// Put sections in wrong order — only S and G with no D
IgesBuilder b;
b.add_s_line("Bad file");
b.add_g_line(standard_global());
// No D or P sections — just T
std::string result;
for (const auto& l : b.s_lines) result += l + "\n";
for (const auto& l : b.g_lines) result += l + "\n";
// Manually add T with wrong section
std::string t_line = " T 1\n";
result += t_line;
auto bodies = import_iges_from_string(result);
EXPECT_TRUE(bodies.empty());
EXPECT_NE(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, DEExponentNotation) {
// IGES uses D exponent notation (e.g., 1.0D+2 = 100)
IgesBuilder b;
b.add_s_line("D exponent notation test");
b.add_g_line(standard_global());
b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("1.0D+2,2.0D+1,3.5D+0,4.0D-1,5.0D-2,6.0D+3;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, MultipleSurfaces) {
IgesBuilder b;
b.add_s_line("Multiple surface types test");
b.add_g_line(standard_global());
// Plane
b.add_de(108, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,1.0,0.0;", 1);
// Cylinder
b.add_de(192, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0;", 2);
// Sphere
b.add_de(196, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("10.0,0.0,0.0,3.0;", 3);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
}
TEST(IgesImport, FileNotFound) {
auto bodies = import_iges("/nonexistent/path/file.iges");
EXPECT_TRUE(bodies.empty());
EXPECT_EQ(iges_last_error(), IgesError::FileNotFound);
}
TEST(IgesImport, VerifyArcGeometry) {
// A 90-degree arc: center(0,0), start(5,0), end(0,5)
IgesBuilder b;
b.add_s_line("Arc geometry test");
b.add_g_line(standard_global());
b.add_de(100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,5.0,0.0,0.0,5.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
ASSERT_FALSE(bodies.empty());
}
TEST(IgesImport, FullCircleArc) {
// Full circle: center(0,0), start(10,0), end(10,0) → 360°
IgesBuilder b;
b.add_s_line("Full circle arc test");
b.add_g_line(standard_global());
b.add_de(100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
b.add_pd("0.0,0.0,0.0,10.0,0.0,10.0,0.0;", 1);
auto bodies = import_iges_from_string(b.build());
EXPECT_EQ(iges_last_error(), IgesError::Ok);
ASSERT_FALSE(bodies.empty());
}
} // namespace vde::brep