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
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "vde/brep/brep.h"
#include <string>
#include <vector>
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<BrepModel>& bodies);
/// Export to IGES file
void export_iges_file(const std::string& filepath, const std::vector<BrepModel>& bodies);
} // namespace vde::brep
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include "vde/core/point.h"
#include <string>
#include <sstream>
#include <iomanip>
#include <cmath>
#include <cstdio>
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
+2
View File
@@ -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
)
+647
View File
@@ -0,0 +1,647 @@
#include "vde/brep/iges_export.h"
#include "vde/brep/modeling.h"
#include "vde/foundation/format_utils.h"
#include <cmath>
#include <fstream>
#include <map>
#include <sstream>
#include <vector>
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<BrepModel>& bodies);
private:
// ── Section line buffers ──
std::vector<std::string> start_lines_;
std::vector<std::string> global_lines_;
std::vector<std::string> de_lines_; // each is a formatted 80-col DE line
std::vector<std::string> 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<int, int> vert_de_; // TopoVertex.id → DE
std::map<int, int> edge_de_; // TopoEdge.id → DE
std::map<int, int> loop_de_; // TopoLoop.id → DE
std::map<int, int> face_de_; // TopoFace.id → DE
std::map<int, int> shell_de_; // TopoShell.id → DE
std::map<int, int> body_de_; // TopoBody.id → DE
std::map<int, int> curve_de_; // curve pointer → DE
std::map<int, int> surf_de_; // surface id → DE
std::map<int, int> 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<int>(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<int>(cp.size());
int nv = cp.empty() ? 0 : static_cast<int>(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<int>& edge_des, const std::vector<int>& 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<int>& 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<int>& 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<int>(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<int, int> curve_des; // edge index → curve DE
for (size_t ei = 0; ei < body.num_edges(); ++ei) {
auto& e = body.edge(static_cast<int>(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<int, int> surf_des; // surface id → surface DE
for (size_t fi = 0; fi < body.num_faces(); ++fi) {
auto& f = body.face(static_cast<int>(fi));
int sid = f.surface_id;
if (surf_des.find(sid) == surf_des.end()) {
if (sid >= 0 && static_cast<size_t>(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<int, int> vert_topo_de; // vertex id → vertex entity DE
for (size_t vi = 0; vi < body.num_vertices(); ++vi) {
auto& v = body.vertex(static_cast<int>(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<int, int> edge_topo_de; // edge id → edge entity DE
for (size_t ei = 0; ei < body.num_edges(); ++ei) {
auto& e = body.edge(static_cast<int>(ei));
int cde = curve_des.count(static_cast<int>(ei)) ? curve_des[static_cast<int>(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<int, int> loop_topo_de; // loop id → loop entity DE
for (const auto& loop : body.all_loops()) {
std::vector<int> e_des;
std::vector<int> 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<int>(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<int, int> face_topo_de; // face id → face entity DE
for (size_t fi = 0; fi < body.num_faces(); ++fi) {
auto& f = body.face(static_cast<int>(fi));
int sd = surf_des.count(f.surface_id) ? surf_des[f.surface_id] : 0;
std::vector<int> 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<int> face_list;
for (size_t fi = 0; fi < body.num_faces(); ++fi) {
auto& f = body.face(static_cast<int>(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<BrepModel>& 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<int>(i + 1));
// Global section
for (size_t i = 0; i < global_lines_.size(); ++i)
out << format_iges_line(global_lines_[i], 'G', static_cast<int>(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<BrepModel>& bodies) {
IgesWriter writer;
return writer.write(bodies);
}
void export_iges_file(const std::string& filepath, const std::vector<BrepModel>& bodies) {
std::ofstream out(filepath);
if (!out) return;
out << export_iges(bodies);
}
} // namespace vde::brep
+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";
}