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
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user