From 4f75bb8b07157ad66ba0f647dd60763dfe4d607a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=82=E4=B9=8B=E9=92=B3?= Date: Fri, 24 Jul 2026 07:34:27 +0000 Subject: [PATCH] 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 --- include/vde/brep/iges_export.h | 27 ++ include/vde/foundation/format_utils.h | 59 +++ src/CMakeLists.txt | 2 + src/brep/iges_export.cpp | 647 ++++++++++++++++++++++++++ tests/brep/CMakeLists.txt | 2 + tests/brep/test_iges_export.cpp | 214 +++++++++ 6 files changed, 951 insertions(+) create mode 100644 include/vde/brep/iges_export.h create mode 100644 include/vde/foundation/format_utils.h create mode 100644 src/brep/iges_export.cpp create mode 100644 tests/brep/test_iges_export.cpp diff --git a/include/vde/brep/iges_export.h b/include/vde/brep/iges_export.h new file mode 100644 index 0000000..bc488a9 --- /dev/null +++ b/include/vde/brep/iges_export.h @@ -0,0 +1,27 @@ +#pragma once +#include "vde/brep/brep.h" +#include +#include + +namespace vde::brep { + +/// Export B-Rep bodies to IGES format string (version 5.3) +/// Supports common entity types for maximum compatibility: +/// - Type 116: Point +/// - Type 110: Line +/// - Type 100: Circular Arc +/// - Type 126: Rational B-Spline Curve +/// - Type 108: Plane +/// - Type 128: Rational B-Spline Surface +/// - Type 502: Vertex (B-Rep topology) +/// - Type 504: Edge +/// - Type 508: Loop +/// - Type 510: Face +/// - Type 514: Shell +/// - Type 186: Manifold Solid B-Rep Object +[[nodiscard]] std::string export_iges(const std::vector& bodies); + +/// Export to IGES file +void export_iges_file(const std::string& filepath, const std::vector& bodies); + +} // namespace vde::brep diff --git a/include/vde/foundation/format_utils.h b/include/vde/foundation/format_utils.h new file mode 100644 index 0000000..4d5996e --- /dev/null +++ b/include/vde/foundation/format_utils.h @@ -0,0 +1,59 @@ +#pragma once +#include "vde/core/point.h" +#include +#include +#include +#include +#include + +namespace vde::foundation { + +/// Format double with adequate precision for CAD (12 significant digits) +inline std::string fmt_double(double v, int precision = 12) { + if (std::isnan(v)) return "0.0"; + if (std::isinf(v)) return v > 0 ? "1e30" : "-1e30"; + std::ostringstream ss; + ss << std::setprecision(precision) << std::scientific << v; + std::string s = ss.str(); + // Ensure always has exponent sign (e.g., "1.0e+00" not "1.0e00") + auto epos = s.find('e'); + if (epos != std::string::npos) { + if (epos + 1 < s.size() && s[epos + 1] != '-' && s[epos + 1] != '+') { + s.insert(epos + 1, "+"); + } + } + return s; +} + +/// Format for IGES: uses D exponent +inline std::string fmt_iges_real(double v) { + auto s = fmt_double(v, 12); + auto pos = s.find('e'); + if (pos != std::string::npos) s[pos] = 'D'; + return s; +} + +/// Format a Point3D as comma-separated reals +inline std::string fmt_point(const core::Point3D& p, bool use_d = false) { + auto fmt = use_d ? fmt_iges_real : fmt_double; + return fmt(p.x()) + "," + fmt(p.y()) + "," + fmt(p.z()); +} + +/// Pad a string to exactly 72 characters with spaces +inline std::string pad72(const std::string& s) { + std::string result = s; + if (result.size() > 72) result.resize(72); + result.resize(72, ' '); + return result; +} + +/// Format an IGES section line: data + section marker (80 columns total) +inline std::string format_iges_line(const std::string& data, char section_char, int seq) { + std::string line = pad72(data); + char seq_buf[9]; + std::snprintf(seq_buf, sizeof(seq_buf), "%c%07d", section_char, seq); + line += std::string(seq_buf); + return line + "\n"; +} + +} // namespace vde::foundation diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 71ccd81..2c4d7b6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -143,6 +143,8 @@ add_library(vde_brep STATIC brep/modeling.cpp brep/step_export.cpp brep/step_import.cpp + brep/iges_import.cpp + brep/iges_export.cpp brep/brep_boolean.cpp brep/brep_validate.cpp ) diff --git a/src/brep/iges_export.cpp b/src/brep/iges_export.cpp new file mode 100644 index 0000000..a8396f9 --- /dev/null +++ b/src/brep/iges_export.cpp @@ -0,0 +1,647 @@ +#include "vde/brep/iges_export.h" +#include "vde/brep/modeling.h" +#include "vde/foundation/format_utils.h" +#include +#include +#include +#include +#include + +namespace vde::brep { + +using namespace vde::foundation; +using curves::NurbsCurve; +using curves::NurbsSurface; +using core::Point3D; +using core::Vector3D; + +namespace { + +// ═══════════════════════════════════════════════════════════ +// IgesWriter — internal IGES file builder +// ═══════════════════════════════════════════════════════════ + +class IgesWriter { +public: + std::string write(const std::vector& bodies); + +private: + // ── Section line buffers ── + std::vector start_lines_; + std::vector global_lines_; + std::vector de_lines_; // each is a formatted 80-col DE line + std::vector pd_lines_; // each is a formatted 80-col PD line + + int de_seq_ = 0; // next DE sequence number + int pd_seq_ = 0; // next PD line sequence number + + // ── Map entity identity → DE sequence number ── + std::map vert_de_; // TopoVertex.id → DE + std::map edge_de_; // TopoEdge.id → DE + std::map loop_de_; // TopoLoop.id → DE + std::map face_de_; // TopoFace.id → DE + std::map shell_de_; // TopoShell.id → DE + std::map body_de_; // TopoBody.id → DE + std::map curve_de_; // curve pointer → DE + std::map surf_de_; // surface id → DE + std::map point_de_; // vertex index → point DE + + // ── Helpers ────────────────────────────────────────── + int next_de() { return ++de_seq_; } + int next_pd() { return ++pd_seq_; } + + // Append an 8-char right-justified integer field + static std::string field_i(int v, int width = 8) { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%*d", width, v); + return std::string(buf); + } + + // Append a DE line + int append_de(int entity_type, int pd_start, int pd_count, + int line_font = 1, int level = 1) { + int seq = next_de(); + std::string line; + line += field_i(entity_type); // 1-8 + line += field_i(pd_start); // 9-16 PD start line number + line += field_i(0); // 17-24 structure + line += field_i(line_font); // 25-32 line font + line += field_i(level); // 33-40 level + line += field_i(0); // 41-48 view + line += field_i(0); // 49-56 transform + line += field_i(0); // 57-64 label + line += field_i(0); // 65-72 status + // 73-80: sequence + 'D' + char rhs[9]; + std::snprintf(rhs, sizeof(rhs), "%c%07d", 'D', seq); + line += std::string(rhs); + de_lines_.push_back(line); + return seq; + } + + // Append parameter data for one entity; return PD start line number. + // Splits long parameter strings across multiple 64-char lines. + int append_pd(const std::string& params, int de_sequence) { + int start = pd_seq_ + 1; + std::string remaining = params; + // Remove trailing comma if present + if (!remaining.empty() && remaining.back() == ',') + remaining.pop_back(); + + while (!remaining.empty()) { + int seq = next_pd(); + std::string chunk; + if (remaining.size() <= 64) { + chunk = remaining; + remaining.clear(); + } else { + // Split at 64 or at last comma within 64 + size_t split = 64; + size_t comma = remaining.rfind(',', 63); + if (comma != std::string::npos && comma > 0) + split = comma + 1; // include the comma + chunk = remaining.substr(0, split); + remaining = remaining.substr(split); + // If continued line starts with comma, trim it + if (!remaining.empty() && remaining[0] == ',') + remaining = remaining.substr(1); + } + + // Chunk → cols 1-64, DE pointer → cols 65-72, P-sequence → cols 73-80 + if (chunk.size() < 64) chunk.resize(64, ' '); + // Append DE pointer (right-justified, 8 chars) + char de_ptr[9]; + std::snprintf(de_ptr, sizeof(de_ptr), "%8d", de_sequence); + chunk += std::string(de_ptr); + // Append PXXXXXXX (8 chars) + char rhs[9]; + std::snprintf(rhs, sizeof(rhs), "P%07d", seq); + chunk += std::string(rhs); + + pd_lines_.push_back(chunk); + } + return start; + } + + // ── Geometry entity writers ────────────────────────── + + /// Type 116: Point + int write_point(const Point3D& p) { + std::string params = fmt_iges_real(p.x()) + "," + + fmt_iges_real(p.y()) + "," + + fmt_iges_real(p.z()); + int pd_start = append_pd(params, de_seq_ + 1); + return append_de(116, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 110: Line + int write_line(const Point3D& p1, const Point3D& p2) { + std::string params = fmt_iges_real(p1.x()) + "," + fmt_iges_real(p1.y()) + + "," + fmt_iges_real(p1.z()) + "," + + fmt_iges_real(p2.x()) + "," + fmt_iges_real(p2.y()) + + "," + fmt_iges_real(p2.z()); + int pd_start = append_pd(params, de_seq_ + 1); + return append_de(110, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 100: Circular Arc + /// IGES places arc in Z=ZT plane; ZT → Z offset, then 6 params for X,Y of center/start/end + int write_arc(double zt, const Point3D& center_2d, + const Point3D& start_2d, const Point3D& end_2d) { + std::string params = fmt_iges_real(zt) + "," + + fmt_iges_real(center_2d.x()) + "," + fmt_iges_real(center_2d.y()) + "," + + fmt_iges_real(start_2d.x()) + "," + fmt_iges_real(start_2d.y()) + "," + + fmt_iges_real(end_2d.x()) + "," + fmt_iges_real(end_2d.y()); + int pd_start = append_pd(params, de_seq_ + 1); + return append_de(100, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 126: Rational B-Spline Curve + int write_bspline_curve(const NurbsCurve& curve) { + auto& knots = curve.knots(); + auto& cp = curve.control_points(); + auto& w = curve.weights(); + int K = static_cast(cp.size()) - 1; // upper index + int M = curve.degree(); + int N = K + 1; // number of CPs (= order for clamped) + + // Properties: planar, open, rational/polynomial, nonperiodic + int prop1 = 0; // nonplanar (conservative) + int prop2 = 0; // open + int prop3 = (!w.empty() && + std::any_of(w.begin(), w.end(), [](double wt){ return std::abs(wt - 1.0) > 1e-10; })) + ? 1 : 0; // 0=polynomial, 1=rational + int prop4 = 0; // nonperiodic + + // Domain + double tmin = knots.empty() ? 0.0 : knots.front(); + double tmax = knots.empty() ? 1.0 : knots.back(); + + std::ostringstream ss; + ss << K << "," << M << "," << prop1 << "," << prop2 << "," << prop3 << "," << prop4 << ","; + + // Knot sequence: M + K + 1 values + for (const auto& k : knots) + ss << fmt_iges_real(k) << ","; + + // Weights: all N values (rational or not) + if (!w.empty()) { + for (const auto& wt : w) + ss << fmt_iges_real(wt) << ","; + } else { + for (int i = 0; i < N; ++i) + ss << "1.0D+00,"; + } + + // Control points: N * 3 values (X,Y,Z triples) + for (const auto& pt : cp) + ss << fmt_iges_real(pt.x()) << "," << fmt_iges_real(pt.y()) << "," << fmt_iges_real(pt.z()) << ","; + + // Parameter range + ss << fmt_iges_real(tmin) << "," << fmt_iges_real(tmax) << ","; + + // Unit normal (0,0,0 for non-planar) + ss << "0.0D+00,0.0D+00,0.0D+00"; + + int pd_start = append_pd(ss.str(), de_seq_ + 1); + return append_de(126, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 108: Plane (bounded) + int write_plane(const NurbsSurface& surf) { + // Get 3 points on the surface to compute plane equation + Point3D p00 = surf.evaluate(0.0, 0.0); + Point3D p10 = surf.evaluate(1.0, 0.0); + Point3D p01 = surf.evaluate(0.0, 1.0); + Vector3D n = (p10 - p00).cross(p01 - p00); + double len = n.norm(); + if (len < 1e-12) n = Vector3D::UnitZ(); + else n /= len; + + double A = n.x(), B = n.y(), C = n.z(); + double D = -(A * p00.x() + B * p00.y() + C * p00.z()); + + std::string params = fmt_iges_real(A) + "," + fmt_iges_real(B) + "," + + fmt_iges_real(C) + "," + fmt_iges_real(D) + ",0"; + int pd_start = append_pd(params, de_seq_ + 1); + return append_de(108, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 128: Rational B-Spline Surface + int write_bspline_surface(const NurbsSurface& surf) { + auto& cp = surf.control_points(); + auto& w = surf.weights(); + const auto& ku = surf.knots_u(); + const auto& kv = surf.knots_v(); + int du = surf.degree_u(), dv = surf.degree_v(); + int nu = static_cast(cp.size()); + int nv = cp.empty() ? 0 : static_cast(cp[0].size()); + + int K1 = nu - 1, K2 = nv - 1; + int M1 = du, M2 = dv; + + // Properties + int prop1 = 0, prop2 = 0; // open in both u and v + int prop3 = (!w.empty() && + std::any_of(w.begin(), w.end(), [](const auto& row) { + return std::any_of(row.begin(), row.end(), + [](double wt){ return std::abs(wt - 1.0) > 1e-10; }); + })) ? 1 : 0; + int prop4 = 0; + + double umin = ku.empty() ? 0.0 : ku.front(); + double umax = ku.empty() ? 1.0 : ku.back(); + double vmin = kv.empty() ? 0.0 : kv.front(); + double vmax = kv.empty() ? 1.0 : kv.back(); + + std::ostringstream ss; + ss << K1 << "," << K2 << "," << M1 << "," << M2 << "," + << prop1 << "," << prop2 << "," << prop3 << "," << prop4 << ","; + + // U knots: M1 + K1 + 1 + for (const auto& k : ku) + ss << fmt_iges_real(k) << ","; + + // V knots: M2 + K2 + 1 + for (const auto& k : kv) + ss << fmt_iges_real(k) << ","; + + // Weights: (K1+1)*(K2+1) values, u-major + if (!w.empty()) { + for (int i = 0; i < nu; ++i) + for (int j = 0; j < nv; ++j) + ss << fmt_iges_real(w[i][j]) << ","; + } else { + for (int i = 0; i < nu * nv; ++i) + ss << "1.0D+00,"; + } + + // Control points: (K1+1)*(K2+1)*3 values, u-major + for (int i = 0; i < nu; ++i) + for (int j = 0; j < nv; ++j) + ss << fmt_iges_real(cp[i][j].x()) << "," + << fmt_iges_real(cp[i][j].y()) << "," + << fmt_iges_real(cp[i][j].z()) << ","; + + // Parameter ranges + ss << fmt_iges_real(umin) << "," << fmt_iges_real(umax) << "," + << fmt_iges_real(vmin) << "," << fmt_iges_real(vmax); + + int pd_start = append_pd(ss.str(), de_seq_ + 1); + return append_de(128, pd_start, pd_seq_ - pd_start + 1); + } + + // ── Detection helpers ──────────────────────────────── + + bool is_circular_arc(const NurbsCurve& curve, + Point3D& center, double& radius, + Vector3D& normal) { + if (curve.degree() != 2) return false; + auto& cp = curve.control_points(); + if (cp.size() != 3) return false; + + // Check weights: [1, w, 1] where w = cos(theta/2) + auto& w = curve.weights(); + if (!w.empty()) { + if (std::abs(w[0] - 1.0) > 1e-8) return false; + if (std::abs(w[2] - 1.0) > 1e-8) return false; + } + + // Control triangle: P0-P1 and P2-P1 should be equal length (isosceles) + Vector3D v0 = cp[1] - cp[0]; + Vector3D v1 = cp[1] - cp[2]; + double d0 = v0.norm(); + double d1 = v1.norm(); + if (std::abs(d0 - d1) > std::max(d0, d1) * 1e-6) return false; + + // Compute center as intersection of perpendicular bisectors + Vector3D mid0 = (cp[0] + cp[1]) * 0.5; + Vector3D mid1 = (cp[2] + cp[1]) * 0.5; + + // Plane normal + normal = (cp[1] - cp[0]).cross(cp[2] - cp[0]); + double nl = normal.norm(); + if (nl < 1e-12) return false; + normal /= nl; + + // Perpendicular bisectors (in plane) + Vector3D perp0 = normal.cross(v0).normalized(); + Vector3D perp1 = normal.cross(v1).normalized(); + + // Intersection of lines: mid0 + t*perp0 = mid1 + s*perp1 + // Solve 2D projection onto plane basis + Vector3D bx = v0.normalized(); + Vector3D by = normal.cross(bx).normalized(); + + auto proj2d = [&](const Point3D& p) { + return Point3D(p.dot(bx), p.dot(by), 0); + }; + auto unproj = [&](const Point3D& p) { + return Point3D(p.x() * bx + p.y() * by); + }; + + Point3D m0_2d = proj2d(mid0); + Point3D m1_2d = proj2d(mid1); + Point3D d0_2d = proj2d(perp0); + Point3D d1_2d = proj2d(perp1); + + // Solve: m0_2d + t*d0_2d.x = m1_2d + s*d1_2d.x (in x) + double denom = d0_2d.x() * d1_2d.y() - d0_2d.y() * d1_2d.x(); + if (std::abs(denom) < 1e-14) return false; + double t = ((m1_2d.x() - m0_2d.x()) * d1_2d.y() - + (m1_2d.y() - m0_2d.y()) * d1_2d.x()) / denom; + + center = unproj(Point3D(m0_2d.x() + t * d0_2d.x(), m0_2d.y() + t * d0_2d.y(), 0)); + radius = (cp[0] - center).norm(); + return true; + } + + bool is_plane(const NurbsSurface& surf) const { + if (surf.degree_u() != 1 || surf.degree_v() != 1) return false; + auto [nu, nv] = surf.num_control_points(); + return (nu == 2 && nv == 2); + } + + // ── Topology entity writers ────────────────────────── + + /// Type 502: Vertex + int write_vertex(int point_de) { + std::string params = std::to_string(point_de); + int pd_start = append_pd(params, de_seq_ + 1); + return append_de(502, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 504: Edge + int write_edge(int curve_de, int v_start_de, int v_end_de) { + std::string params = std::to_string(curve_de) + "," + + std::to_string(v_start_de) + "," + + std::to_string(v_end_de); + int pd_start = append_pd(params, de_seq_ + 1); + return append_de(504, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 508: Loop + int write_loop(const std::vector& edge_des, const std::vector& orientations) { + std::ostringstream ss; + ss << edge_des.size(); + // For each edge: type(1=edge), edge DE, orientation(0=forward), param_count(0) + for (size_t i = 0; i < edge_des.size(); ++i) { + int orient = (i < orientations.size()) ? orientations[i] : 0; + ss << "," << "1," << edge_des[i] << "," << orient << ",0"; + } + // Number of index groups (same as edge count) + ss << "," << edge_des.size(); + int pd_start = append_pd(ss.str(), de_seq_ + 1); + return append_de(508, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 510: Face + int write_face(int surface_de, const std::vector& loop_des, bool sense) { + std::ostringstream ss; + ss << surface_de << "," << (sense ? 1 : 0) << "," << loop_des.size(); + for (int ld : loop_des) + ss << "," << "1," << ld << "," << (ld == loop_des[0] ? 1 : 0) << ",0"; + int pd_start = append_pd(ss.str(), de_seq_ + 1); + return append_de(510, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 514: Shell + int write_shell(const std::vector& face_des) { + std::ostringstream ss; + ss << face_des.size(); + for (int fd : face_des) + ss << "," << fd; + int pd_start = append_pd(ss.str(), de_seq_ + 1); + return append_de(514, pd_start, pd_seq_ - pd_start + 1); + } + + /// Type 186: Manifold Solid B-Rep Object + int write_manifold_solid_brep(int shell_de) { + std::string params = std::to_string(shell_de) + ",0HNAME,0"; + int pd_start = append_pd(params, de_seq_ + 1); + return append_de(186, pd_start, pd_seq_ - pd_start + 1); + } + + // ── Curve curve writing (line / arc / bspline dispatch) ── + + int write_curve(const NurbsCurve& curve) { + // Line detection: degree 1, 2 CPs + if (curve.degree() == 1 && curve.control_points().size() == 2) { + auto& cp = curve.control_points(); + return write_line(cp[0], cp[1]); + } + + // Circular arc detection + Point3D center; double radius; Vector3D normal; + if (is_circular_arc(curve, center, radius, normal)) { + auto& cp = curve.control_points(); + Point3D start_pt = cp[0]; + Point3D end_pt = cp[2]; + + // Build local coordinate frame to get 2D projections + Vector3D nx = (start_pt - center).normalized(); + Vector3D ny = normal.cross(nx).normalized(); + + auto to_2d = [&](const Point3D& p) -> Point3D { + Vector3D d = p - center; + return Point3D(d.dot(nx), d.dot(ny), 0); + }; + + Point3D c2d = to_2d(center); // should be (0,0,0) + Point3D s2d = to_2d(start_pt); + Point3D e2d = to_2d(end_pt); + + // ZT = projection of center onto normal + double zt = center.dot(normal); + + return write_arc(zt, c2d, s2d, e2d); + } + + return write_bspline_curve(curve); + } + + int write_surface(const NurbsSurface& surf) { + if (is_plane(surf)) + return write_plane(surf); + return write_bspline_surface(surf); + } + + // ── Process a single body ──────────────────────────── + + void process_body(const BrepModel& body) { + // Phase 1: Write point entities for all vertices + // Map vertex id → point DE sequence + for (size_t vi = 0; vi < body.num_vertices(); ++vi) { + auto& v = body.vertex(static_cast(vi)); + int pt_de = write_point(v.point); + point_de_[v.id] = pt_de; + } + + // Phase 2: Write curve entities for edges + // Map edge index → curve DE sequence + std::map curve_des; // edge index → curve DE + for (size_t ei = 0; ei < body.num_edges(); ++ei) { + auto& e = body.edge(static_cast(ei)); + if (e.curve) { + int cde = write_curve(*e.curve); + curve_des[ei] = cde; + } + } + + // Phase 3: Write surface entities for faces + // Map surface id → surface DE sequence + std::map surf_des; // surface id → surface DE + for (size_t fi = 0; fi < body.num_faces(); ++fi) { + auto& f = body.face(static_cast(fi)); + int sid = f.surface_id; + if (surf_des.find(sid) == surf_des.end()) { + if (sid >= 0 && static_cast(sid) < body.num_surfaces()) { + surf_des[sid] = write_surface(body.surface(sid)); + } + } + } + + // Phase 4: Write topology — vertex, edge, loop, face, shell, MSBO + // Vertex topology entities + std::map vert_topo_de; // vertex id → vertex entity DE + for (size_t vi = 0; vi < body.num_vertices(); ++vi) { + auto& v = body.vertex(static_cast(vi)); + int pt_de = point_de_.count(v.id) ? point_de_[v.id] : 0; + vert_topo_de[v.id] = write_vertex(pt_de); + } + + // Edge topology entities + std::map edge_topo_de; // edge id → edge entity DE + for (size_t ei = 0; ei < body.num_edges(); ++ei) { + auto& e = body.edge(static_cast(ei)); + int cde = curve_des.count(static_cast(ei)) ? curve_des[static_cast(ei)] : 0; + int vs = vert_topo_de.count(e.v_start) ? vert_topo_de[e.v_start] : 0; + int ve = vert_topo_de.count(e.v_end) ? vert_topo_de[e.v_end] : 0; + edge_topo_de[e.id] = write_edge(cde, vs, ve); + } + + // Loop entities + std::map loop_topo_de; // loop id → loop entity DE + for (const auto& loop : body.all_loops()) { + std::vector e_des; + std::vector orientations; + for (int ei : loop.edges) { + if (edge_topo_de.count(ei)) { + e_des.push_back(edge_topo_de[ei]); + // Check edge orientation + for (size_t eidx = 0; eidx < body.num_edges(); ++eidx) { + auto& e = body.edge(static_cast(eidx)); + if (e.id == ei) { + orientations.push_back(e.reversed ? 1 : 0); + break; + } + } + } + } + if (!e_des.empty()) + loop_topo_de[loop.id] = write_loop(e_des, orientations); + } + + // Face entities + std::map face_topo_de; // face id → face entity DE + for (size_t fi = 0; fi < body.num_faces(); ++fi) { + auto& f = body.face(static_cast(fi)); + int sd = surf_des.count(f.surface_id) ? surf_des[f.surface_id] : 0; + std::vector ld_list; + for (int li : f.loops) { + if (loop_topo_de.count(li)) + ld_list.push_back(loop_topo_de[li]); + } + if (!ld_list.empty()) { + face_topo_de[f.id] = write_face(sd, ld_list, !f.reversed); + } + } + + // Shell entity + std::vector face_list; + for (size_t fi = 0; fi < body.num_faces(); ++fi) { + auto& f = body.face(static_cast(fi)); + if (face_topo_de.count(f.id)) + face_list.push_back(face_topo_de[f.id]); + } + + if (!face_list.empty()) { + int sh_de = write_shell(face_list); + write_manifold_solid_brep(sh_de); + } + } + +public: + // ── Build sections from collected lines ────────────── + void build_sections() { + // Start section + start_lines_.push_back("ViewDesignEngine IGES Export"); + start_lines_.push_back("Generated by VDE B-Rep → IGES v5.3"); + + // Global section + global_lines_.push_back("1H,,1H;,12HVDENGINE.IGS,53HViewDesignEngine B-Rep IGES Export," + "32,38,6,308,15,,1.0,1,2HIN,32768,0.016," + "13H2026-07-24.001,0.01,10000.0,,,11,0,12HUNKNOWN"); + } +}; + +// ── Writer::write — entry point ───────────────────────── + +std::string IgesWriter::write(const std::vector& bodies) { + // Build header sections + build_sections(); + + // Process each body + for (const auto& body : bodies) { + if (body.num_vertices() > 0) + process_body(body); + } + + // Assembly + std::ostringstream out; + + // Start section + for (size_t i = 0; i < start_lines_.size(); ++i) + out << format_iges_line(start_lines_[i], 'S', static_cast(i + 1)); + + // Global section + for (size_t i = 0; i < global_lines_.size(); ++i) + out << format_iges_line(global_lines_[i], 'G', static_cast(i + 1)); + + // Directory Entry section + for (const auto& line : de_lines_) + out << line << "\n"; + + // Parameter Data section + for (const auto& line : pd_lines_) + out << line << "\n"; + + // Terminate section — S count, G count, D count, P count, padded to 72 + std::ostringstream term_ss; + term_ss << 'S' << std::setw(7) << std::setfill('0') << start_lines_.size() + << 'G' << std::setw(7) << std::setfill('0') << global_lines_.size() + << 'D' << std::setw(7) << std::setfill('0') << de_lines_.size() + << 'P' << std::setw(7) << std::setfill('0') << pd_lines_.size(); + out << format_iges_line(term_ss.str(), 'T', 1); + + return out.str(); +} + +} // anonymous namespace + +// ═══════════════════════════════════════════════════════════ +// Public API +// ═══════════════════════════════════════════════════════════ + +std::string export_iges(const std::vector& bodies) { + IgesWriter writer; + return writer.write(bodies); +} + +void export_iges_file(const std::string& filepath, const std::vector& bodies) { + std::ofstream out(filepath); + if (!out) return; + out << export_iges(bodies); +} + +} // namespace vde::brep diff --git a/tests/brep/CMakeLists.txt b/tests/brep/CMakeLists.txt index fbe9359..0d9726d 100644 --- a/tests/brep/CMakeLists.txt +++ b/tests/brep/CMakeLists.txt @@ -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) diff --git a/tests/brep/test_iges_export.cpp b/tests/brep/test_iges_export.cpp new file mode 100644 index 0000000..03047c7 --- /dev/null +++ b/tests/brep/test_iges_export.cpp @@ -0,0 +1,214 @@ +#include +#include "vde/brep/brep.h" +#include "vde/brep/modeling.h" +#include "vde/brep/iges_export.h" +#include +#include +#include + +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(in)), + std::istreambuf_iterator()); + 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"; +}