#include #include "vde/brep/iges_import.h" #include "vde/brep/brep.h" #include "vde/brep/modeling.h" #include #include #include #include #include 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(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(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& 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 s_lines; std::vector g_lines; std::vector d_lines; // pairs: 2 per entity std::vector p_lines; void add_s_line(const std::string& txt) { std::string line = pad_right(txt, 72) + "S"; line += pad_left(static_cast(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(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(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(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(s_lines.size()); int g_cnt = static_cast(g_lines.size()); int d_cnt = static_cast(d_lines.size()); int p_cnt = static_cast(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()); }