diff --git a/include/vde/brep/iges_import.h b/include/vde/brep/iges_import.h new file mode 100644 index 0000000..4e12d3e --- /dev/null +++ b/include/vde/brep/iges_import.h @@ -0,0 +1,33 @@ +#pragma once +#include "vde/brep/brep.h" +#include +#include + +namespace vde::brep { + +/// IGES import error codes +enum class IgesError { + Ok = 0, + FileNotFound, + ParseError, + InvalidSection, + UnsupportedEntity, + MissingEntity, + EntityTypeError, +}; + +/// Parse an IGES file (.igs/.iges) and return B-Rep bodies +/// IGES format: 80-character fixed-width records (ANSI Y14.26M) +/// Supports the most common entity types found in CAD files +[[nodiscard]] std::vector import_iges(const std::string& filepath); + +/// Parse IGES data from a string (for testing) +[[nodiscard]] std::vector import_iges_from_string(const std::string& data); + +/// Get the last error from import_iges / import_iges_from_string +[[nodiscard]] IgesError iges_last_error(); + +/// Get a human-readable description of the last error +[[nodiscard]] const std::string& iges_last_error_message(); + +} // namespace vde::brep diff --git a/src/brep/iges_import.cpp b/src/brep/iges_import.cpp new file mode 100644 index 0000000..8f096d2 --- /dev/null +++ b/src/brep/iges_import.cpp @@ -0,0 +1,1381 @@ +#include "vde/brep/iges_import.h" +#include "vde/curves/nurbs_curve.h" +#include "vde/curves/nurbs_surface.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace vde::brep { + +using core::Point3D; +using core::Vector3D; + +// ───────────────────────────────────────────────────────────── +// Error state +// ───────────────────────────────────────────────────────────── +static IgesError g_last_iges_error = IgesError::Ok; +static std::string g_last_iges_error_msg; + +IgesError iges_last_error() { return g_last_iges_error; } +const std::string& iges_last_error_message() { return g_last_iges_error_msg; } + +static void set_error(IgesError e, const std::string& msg) { + g_last_iges_error = e; + g_last_iges_error_msg = msg; +} +static void clear_error() { g_last_iges_error = IgesError::Ok; g_last_iges_error_msg.clear(); } + +// ───────────────────────────────────────────────────────────── +// Parsed IGES entity +// ───────────────────────────────────────────────────────────── +struct IgesEntity { + int type_number = 0; // Entity type number + int pd_pointer = 0; // Line number in P section where params start + int structure = 0; // Structure (hierarchy info) + int line_font = 0; // Line font pattern number + int level = 0; // Level number + int view = 0; // View pointer + int transform_matrix = 0; // Transformation matrix pointer (0 = identity) + int label_display = 0; // Label display associativity + int status_number = 0; // Status number + + int line_weight = 0; // Line weight number + int color = 0; // Color number + int param_line_count = 0; // Number of parameter data lines + int form_number = 0; // Form number (for parameterized entities) + int entity_label = 0; // Entity label pointer + int entity_subscript = 0; // Entity subscript number + + std::string parameters; // Raw parameter string (split from lines) + int index = 0; // Our sequential index (DE entry position) +}; + +// ───────────────────────────────────────────────────────────── +// IGES Parser — 80-column fixed-width records +// ───────────────────────────────────────────────────────────── +class IgesParser { +public: + explicit IgesParser(const std::string& data); + bool parse(); + const std::vector& entities() const { return entities_; } + const std::string& global_section() const { return global_section_; } + const std::string& start_section() const { return start_section_; } + +private: + struct CardRecord { + char section = '\0'; + int sequence = 0; + std::string fields; // columns 1-72 + std::string raw; // full 80 chars + }; + + const std::string& data_; + std::vector records_; + + std::string start_section_; + std::string global_section_; + + std::vector entities_; + + bool split_records(); + bool parse_section(char section_type, std::string& out_content); + bool parse_directory_entries(const std::vector& records); + bool parse_parameter_data(const std::vector& records); + + static std::string strip(const std::string& s); + static double parse_real(const std::string& s); + static int parse_int(const std::string& s); + static std::vector split_params(const std::string& s); + static std::string field(const std::string& line, int col); // 1-indexed, 8-char field +}; + +IgesParser::IgesParser(const std::string& data) : data_(data) {} + +bool IgesParser::parse() { + if (!split_records()) return false; + + // Find section boundaries + std::vector s_bounds, g_bounds, d_bounds, p_bounds, t_bounds; + for (size_t i = 0; i < records_.size(); ++i) { + switch (records_[i].section) { + case 'S': s_bounds.push_back(i); break; + case 'G': g_bounds.push_back(i); break; + case 'D': d_bounds.push_back(i); break; + case 'P': p_bounds.push_back(i); break; + case 'T': t_bounds.push_back(i); break; + default: break; + } + } + + if (s_bounds.empty() || g_bounds.empty() || d_bounds.empty() || p_bounds.empty()) { + set_error(IgesError::InvalidSection, "Missing required IGES sections"); + return false; + } + + // Start section + for (size_t i = s_bounds.front(); i < g_bounds.front(); ++i) + start_section_ += records_[i].fields + "\n"; + + // Global section + for (size_t i = g_bounds.front(); i < d_bounds.front(); ++i) + global_section_ += records_[i].fields; + + // Directory entry section + std::vector d_records; + for (size_t i = d_bounds.front(); (p_bounds.empty() || i < p_bounds.front()); ++i) + d_records.push_back(records_[i]); + if (!parse_directory_entries(d_records)) return false; + + // Parameter data section + std::vector p_records; + for (size_t i = p_bounds.front(); (t_bounds.empty() || i < t_bounds.front()); ++i) + p_records.push_back(records_[i]); + if (!parse_parameter_data(p_records)) return false; + + return true; +} + +bool IgesParser::split_records() { + size_t pos = 0; + int line_no = 0; + while (pos < data_.size()) { + size_t nl = data_.find('\n', pos); + if (nl == std::string::npos) nl = data_.size(); + + std::string line = data_.substr(pos, nl - pos); + // Strip CR + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + ++line_no; + pos = nl + 1; + + // Skip blank lines + if (line.empty()) continue; + + CardRecord cr; + cr.raw = line; + + // Pad to 80 columns + if (line.size() < 80) line.append(80 - line.size(), ' '); + else if (line.size() > 80) line = line.substr(0, 80); + + cr.section = line[72]; + cr.fields = line.substr(0, 72); + + // Parse sequence number (columns 74-80) + std::string seq_str = line.substr(73, 7); + cr.sequence = parse_int(seq_str); + + records_.push_back(std::move(cr)); + } + return true; +} + +bool IgesParser::parse_directory_entries(const std::vector& d_records) { + if (d_records.size() % 2 != 0) { + set_error(IgesError::ParseError, "Odd number of directory entry records"); + return false; + } + + for (size_t i = 0; i < d_records.size(); i += 2) { + const auto& line1 = d_records[i].fields; + const auto& line2 = d_records[i + 1].fields; + + IgesEntity ent; + ent.index = static_cast(i / 2); + + // Line 1 fields (each 8 chars, 1-indexed columns) + ent.type_number = parse_int(field(line1, 1)); + ent.pd_pointer = parse_int(field(line1, 9)); + ent.structure = parse_int(field(line1, 17)); + ent.line_font = parse_int(field(line1, 25)); + ent.level = parse_int(field(line1, 33)); + ent.view = parse_int(field(line1, 41)); + ent.transform_matrix = parse_int(field(line1, 49)); + ent.label_display = parse_int(field(line1, 57)); + ent.status_number = parse_int(field(line1, 65)); + + // Line 2 fields + // Skip 1-8 (same type_number) + ent.line_weight = parse_int(field(line2, 9)); + ent.color = parse_int(field(line2, 17)); + ent.param_line_count = parse_int(field(line2, 25)); + ent.form_number = parse_int(field(line2, 33)); + // columns 41-56 are reserved + ent.entity_label = parse_int(field(line2, 57)); + ent.entity_subscript = parse_int(field(line2, 65)); + + entities_.push_back(std::move(ent)); + } + return true; +} + +bool IgesParser::parse_parameter_data(const std::vector& p_records) { + // Group P records by DE pointer (cols 66-72 of each line) + std::unordered_map pd_by_de; + + for (const auto& rec : p_records) { + // DE pointer is in cols 66-72 (1-indexed), which is position 65-71 in 0-indexed + std::string de_str = rec.fields.substr(65, 7); + int de_ptr = parse_int(de_str); + // Data is cols 1-64 + pd_by_de[de_ptr] += rec.fields.substr(0, 64); + } + + // Assign parameter strings to entities based on pd_pointer + for (auto& ent : entities_) { + auto it = pd_by_de.find(ent.pd_pointer); + if (it != pd_by_de.end()) { + std::string& raw = it->second; + // Strip trailing spaces and the record delimiter semicolon + while (!raw.empty() && (raw.back() == ' ' || raw.back() == '\t')) + raw.pop_back(); + // The IGES P section lines end with a semicolon followed by spaces + // Find the last semicolon + size_t semi = raw.rfind(';'); + if (semi != std::string::npos) { + // Also strip spaces before semicolon isn't needed — the data ends at ; + // But we want to keep text up to the semicolon or past it + // Actually params can have embedded ; in strings — but typically the + // last semicolon is the record delimiter. We'll strip trailing spaces+';' + while (!raw.empty() && (raw.back() == ' ' || raw.back() == '\t' || raw.back() == ';')) + raw.pop_back(); + } + ent.parameters = raw; + } + } + return true; +} + +// ───────────────────────────────────────────────────────────── +// Static helpers +// ───────────────────────────────────────────────────────────── + +std::string IgesParser::strip(const std::string& s) { + size_t start = 0, end = s.size(); + while (start < end && (s[start] == ' ' || s[start] == '\t')) ++start; + while (end > start && (s[end-1] == ' ' || s[end-1] == '\t')) --end; + return s.substr(start, end - start); +} + +double IgesParser::parse_real(const std::string& s) { + std::string t = strip(s); + if (t.empty()) return 0.0; + + // Handle D exponent notation (e.g., 1.5D+2 → 1.5e+2) + size_t dpos = t.find_first_of("Dd"); + if (dpos != std::string::npos) { + t[dpos] = 'e'; + } + + try { + return std::stod(t); + } catch (...) { + return 0.0; + } +} + +int IgesParser::parse_int(const std::string& s) { + std::string t = strip(s); + if (t.empty()) return 0; + try { + return std::stoi(t); + } catch (...) { + return 0; + } +} + +std::vector IgesParser::split_params(const std::string& s) { + std::vector result; + std::string current; + int paren_depth = 0; + + for (size_t i = 0; i < s.size(); ++i) { + char c = s[i]; + if (c == ',' && paren_depth == 0) { + result.push_back(current); + current.clear(); + } else { + if (c == '(' || c == '[' || c == '{') paren_depth++; + else if (c == ')' || c == ']' || c == '}') paren_depth--; + current += c; + } + } + result.push_back(current); + return result; +} + +std::string IgesParser::field(const std::string& line, int col) { + // col is 1-indexed (1..64), each field is 8 chars + int start = col - 1; + if (start < 0 || start >= static_cast(line.size())) return ""; + return line.substr(start, 8); +} + +// ───────────────────────────────────────────────────────────── +// IGES to B-Rep converter +// ───────────────────────────────────────────────────────────── +class IgesToBrep { +public: + IgesToBrep(const std::vector& entities) : entities_(entities) {} + + std::vector convert() { + std::vector result; + + // First pass: register all entities by index for easy lookup + // Second pass: find manifold solid B-Rep objects (type 186) + std::vector solid_indices; + for (size_t i = 0; i < entities_.size(); ++i) { + entity_by_index_[entities_[i].index] = static_cast(i); + if (entities_[i].type_number == 186) { + solid_indices.push_back(entities_[i].index); + } + } + + if (!solid_indices.empty()) { + for (int si : solid_indices) { + auto body = build_manifold_solid(si); + if (body.num_faces() > 0) + result.push_back(std::move(body)); + } + } + + // Fallback: if no solids found, try collecting all faces/surfaces into one model + if (result.empty()) { + BrepModel model; + bool has_any = false; + + // Collect all faces (type 510) + for (const auto& ent : entities_) { + if (ent.type_number == 510) { + int face_id = convert_face(ent.index, model); + if (face_id >= 0) has_any = true; + } + } + + // If still no faces, collect standalone surfaces + if (!has_any) { + for (const auto& ent : entities_) { + int sid = convert_or_get_surface(ent.index, model); + if (sid >= 0) has_any = true; + } + } + + if (has_any && model.num_faces() > 0) { + std::vector face_ids; + for (size_t i = 0; i < model.num_faces(); ++i) + face_ids.push_back(static_cast(i)); + int shell_id = model.add_shell(face_ids, model.num_faces() >= 4); + model.add_body({shell_id}, "IGES_Model"); + result.push_back(std::move(model)); + } + } + + return result; + } + +private: + const std::vector& entities_; + + // Index maps + std::unordered_map entity_by_index_; // index → position in entities_ + std::unordered_map vp_cache_; // IGES DE index → BrepModel vertex index + std::unordered_map surf_cache_; // IGES DE index → BrepModel surface index + std::unordered_map curve_cache_; + std::unordered_map point_cache_; + + const IgesEntity* get_entity(int index) const { + auto it = entity_by_index_.find(index); + if (it == entity_by_index_.end()) return nullptr; + return &entities_[it->second]; + } + + // ── Helper: parse parameters ── + + static std::vector params(const IgesEntity& ent) { + return IgesParser::split_params(ent.parameters); + } + + static double get_real(const std::vector& p, size_t idx) { + if (idx < p.size()) return IgesParser::parse_real(p[idx]); + return 0.0; + } + + static int get_int(const std::vector& p, size_t idx) { + if (idx < p.size()) return IgesParser::parse_int(p[idx]); + return 0; + } + + static Point3D get_point(const std::vector& p, size_t idx) { + if (idx + 2 < p.size()) + return Point3D( + IgesParser::parse_real(p[idx]), + IgesParser::parse_real(p[idx+1]), + IgesParser::parse_real(p[idx+2])); + return Point3D(0, 0, 0); + } + + // ── Manifold Solid B-Rep (type 186) ── + + BrepModel build_manifold_solid(int de_index) { + BrepModel model; + const auto* ent = get_entity(de_index); + if (!ent) return model; + + auto p = params(*ent); + // 186 params: SHELL DE pointer (1 value) + if (p.empty()) return model; + + int shell_de = IgesParser::parse_int(p[0]); + + // Build shell + auto face_indices = convert_shell(shell_de, model); + + if (!face_indices.empty()) { + int shell_id = model.add_shell(face_indices, true); + model.add_body({shell_id}, "IGES_Solid"); + } + + return model; + } + + // ── Shell (type 514) ── + + std::vector convert_shell(int de_index, BrepModel& model) { + const auto* ent = get_entity(de_index); + if (!ent) return {}; + + auto p = params(*ent); + // 514 params: N, face_DE_1, face_DE_2, ..., face_DE_N + if (p.empty()) return {}; + + int n = IgesParser::parse_int(p[0]); + std::vector face_ids; + for (int i = 0; i < n && static_cast(i + 1) < p.size(); ++i) { + int face_de = IgesParser::parse_int(p[i + 1]); + int fid = convert_face(face_de, model); + if (fid >= 0) face_ids.push_back(fid); + } + return face_ids; + } + + // ── Face (type 510) ── + + int convert_face(int de_index, BrepModel& model) { + const auto* ent = get_entity(de_index); + if (!ent) return -1; + + auto p = params(*ent); + // 510 params: surface_DE, N_loops, loop_flag_1, loop_DE_1, loop_flag_2, loop_DE_2, ... + if (p.size() < 2) return -1; + + int surf_de = IgesParser::parse_int(p[0]); + int surf_id = convert_or_get_surface(surf_de, model); + + int n_loops = IgesParser::parse_int(p[1]); + std::vector loop_ids; + for (int i = 0; i < n_loops; ++i) { + size_t idx = 2 + static_cast(i) * 2; + if (idx + 1 >= p.size()) break; + int loop_flag = IgesParser::parse_int(p[idx]); // 0=outer, 1=inner + int loop_de = IgesParser::parse_int(p[idx+1]); + int lid = convert_loop(loop_de, model, (loop_flag == 0)); + if (lid >= 0) loop_ids.push_back(lid); + } + + if (loop_ids.empty()) return -1; + return model.add_face(surf_id, loop_ids); + } + + // ── Loop (type 508) ── + + int convert_loop(int de_index, BrepModel& model, bool is_outer) { + const auto* ent = get_entity(de_index); + if (!ent) return -1; + + auto p = params(*ent); + // 508 params: N, type_1, edge_DE_1, vertex_count_1, start_vertex_DE_1, end_vertex_DE_1, + // cur_srf_DE_1, start_param_1, end_param_1, ... + if (p.empty()) return -1; + + int n = IgesParser::parse_int(p[0]); + std::vector edge_indices; + int offset = 1; // skip N + + for (int i = 0; i < n; ++i) { + if (offset + 2 >= static_cast(p.size())) break; + int etype = IgesParser::parse_int(p[offset]); + int edge_de = IgesParser::parse_int(p[offset + 1]); + + // Parse edge-only entries (TYPE=0): TYPE, EDGE_DE, COUNT, VX1..VX_COUNT, CS_DE, SP, EP + // Total fields per entry: 3 + COUNT + 3 = 6 + COUNT + int count = (offset + 2 < static_cast(p.size())) ? IgesParser::parse_int(p[offset + 2]) : 2; + int skip = 6 + count; // TYPE + EDGE_DE + COUNT + COUNT_VX + CS_DE + SP + EP + + int ei = convert_edge(edge_de, model); + if (ei >= 0) edge_indices.push_back(ei); + + offset += skip; + if (offset > static_cast(p.size())) break; + } + + if (edge_indices.empty()) return -1; + return model.add_loop(edge_indices, is_outer); + } + + // ── Edge (type 504) ── + + int convert_edge(int de_index, BrepModel& model) { + const auto* ent = get_entity(de_index); + if (!ent) return -1; + + auto p = params(*ent); + // 504 params: CURType, CURDEF, SVDE, SVINDEX, EVDE, EVINDEX, CSDE, SP, EP + // curve_type: 0=unspecified, 1=line, 2=arc, 3=conic, etc. + if (p.size() < 5) return -1; + + int curve_de = IgesParser::parse_int(p[1]); + int v_start_de = IgesParser::parse_int(p[2]); + int v_end_de = IgesParser::parse_int(p[4]); + + int vs = convert_vertex(v_start_de, model); + int ve = convert_vertex(v_end_de, model); + + // Get the curve geometry + curves::NurbsCurve nc = get_or_build_curve(curve_de); + + return model.add_edge(vs, ve, nc); + } + + // ── Vertex (type 502) ── + + int convert_vertex(int de_index, BrepModel& model) { + auto it = vp_cache_.find(de_index); + if (it != vp_cache_.end()) return it->second; + + const auto* ent = get_entity(de_index); + if (!ent) return -1; + + auto p = params(*ent); + // 502 params: X, Y, Z + Point3D pt = get_point(p, 0); + int vi = model.add_vertex(pt); + vp_cache_[de_index] = vi; + return vi; + } + + // ── Curves ── + + curves::NurbsCurve get_or_build_curve(int de_index) { + auto it = curve_cache_.find(de_index); + if (it != curve_cache_.end()) return it->second; + + const auto* ent = get_entity(de_index); + if (!ent) { + // Return default line + auto nc = curves::NurbsCurve({Point3D(0,0,0), Point3D(1,0,0)}, {0,0,1,1}, {1,1}, 1); + curve_cache_[de_index] = nc; + return nc; + } + + curves::NurbsCurve nc({Point3D(0,0,0), Point3D(1,0,0)}, {0,0,1,1}, {1,1}, 1); + try { + switch (ent->type_number) { + case 100: nc = convert_arc(*ent); break; + case 102: nc = convert_composite_curve(*ent); break; + case 104: nc = convert_conic_arc(*ent); break; + case 106: nc = convert_copious_data(*ent); break; + case 110: nc = convert_line(*ent); break; + case 126: nc = convert_bspline_curve(*ent); break; + default: break; + } + } catch (const std::exception&) { + // Keep default line on error + } + curve_cache_[de_index] = nc; + return nc; + } + + // Type 100: Circular Arc + curves::NurbsCurve convert_arc(const IgesEntity& ent) { + auto p = params(ent); + // 100 params: ZT, X1, Y1, X2, Y2, X3, Y3 + // Center = (X1, Y1), start_pt = (X2, Y2), end_pt = (X3, Y3) + // ZT is the plane depth (Z offset from definition plane) + if (p.size() < 7) throw std::runtime_error("Arc requires 7 parameters"); + + double zt = get_real(p, 0); + Point3D center(get_real(p, 1), get_real(p, 2), zt); + Point3D start_pt(get_real(p, 3), get_real(p, 4), zt); + Point3D end_pt(get_real(p, 5), get_real(p, 6), zt); + + Vector3D vs = start_pt - center; + Vector3D ve = end_pt - center; + double R = vs.norm(); + + if (R < 1e-12) throw std::runtime_error("Zero radius arc"); + + Vector3D x_axis = vs.normalized(); + // Compute the arc angle from start to end (counterclockwise) + double dot = vs.dot(ve) / (R * R); + dot = std::clamp(dot, -1.0, 1.0); + double angle = std::acos(dot); + + // Determine sign: cross product Z component + double cross_z = vs.x() * ve.y() - vs.y() * ve.x(); + if (cross_z < 0) angle = 2.0 * M_PI - angle; + + if (angle < 1e-8) angle = 2.0 * M_PI; // full circle + + // For arcs ≤ 180°, use 3 control points (degree 2) + // For larger arcs, split into multiple segments + // Simple approach: split into segments ≤ 180° + return make_arc_nurbs(center, x_axis, R, angle); + } + + // Type 102: Composite Curve + curves::NurbsCurve convert_composite_curve(const IgesEntity& ent) { + auto p = params(ent); + // 102 params: N, entity_DE_1, entity_DE_2, ..., entity_DE_N + if (p.empty()) throw std::runtime_error("Empty composite curve"); + + int n = get_int(p, 0); + if (n <= 0) throw std::runtime_error("Composite curve has 0 entities"); + + // Return the first sub-curve as a fallback — full composite curve stitching + // would be complex; for now we return the first curve + int first_de = get_int(p, 1); + return get_or_build_curve(first_de); + } + + // Type 104: Conic Arc + curves::NurbsCurve convert_conic_arc(const IgesEntity& ent) { + auto p = params(ent); + // 104 params: A, B, C, D, E, F, ZT, X1, Y1, X2, Y2 + // General conic: Ax² + Bxy + Cy² + Dx + Ey + F = 0 + // ZT: plane offset, (X1,Y1) start, (X2,Y2) end + // Form number determines type: 0=unspecified, 1=ellipse, 2=hyperbola, 3=parabola + + if (ent.form_number == 1) { + // Ellipse + if (p.size() < 11) throw std::runtime_error("Conic ellipse requires 11 parameters"); + double a = get_real(p, 0), b = get_real(p, 1), c = get_real(p, 2); + double d = get_real(p, 3), e_val = get_real(p, 4), f = get_real(p, 5); + double zt = get_real(p, 6); + + // For a standard ellipse centered at origin with axes aligned: + // Center: (h, k) from completing the square + // Simple case: A=C and B=0 → circle, else approximate + // Extract center and semi-axes from the general conic + double denom = b*b - 4*a*c; + if (std::abs(denom) < 1e-12) throw std::runtime_error("Degenerate conic"); + + double h = (2*c*d - b*e_val) / denom; + double k = (2*a*e_val - b*d) / denom; + // Semi-axes computation simplified + double rx = std::sqrt(std::abs((a + c + std::sqrt((a-c)*(a-c) + b*b)) / (-2 * (a*h*h + b*h*k + c*k*k - f)))); + double ry = std::sqrt(std::abs((a + c - std::sqrt((a-c)*(a-c) + b*b)) / (-2 * (a*h*h + b*h*k + c*k*k - f)))); + + (void)rx; (void)ry; + (void)h; (void)k; + (void)zt; + + // Use the start/end points and construct a NURBS ellipse + Point3D start(get_real(p, 8), get_real(p, 9), zt); + // For now return an approximation as a circular arc + Point3D center(0, 0, zt); + double R = (start - center).norm(); + return make_arc_nurbs(center, Vector3D(1, 0, 0), R, 2.0 * M_PI); + } + + // Fallback: treat as arc + if (p.size() >= 11) { + double zt = get_real(p, 6); + Point3D start(get_real(p, 7), get_real(p, 8), zt); + Point3D end(get_real(p, 9), get_real(p, 10), zt); + Point3D center((start.x()+end.x())/2, (start.y()+end.y())/2, zt); + double R = (start - center).norm(); + return make_arc_nurbs(center, Vector3D(1, 0, 0), R, M_PI); + } + throw std::runtime_error("Insufficient conic parameters"); + } + + // Type 106: Copious Data + curves::NurbsCurve convert_copious_data(const IgesEntity& ent) { + auto p = params(ent); + // 106 params: IP, N, X1, Y1, Z1, X2, Y2, Z2, ... + // IP: data interpretation (1=points, 2=form 1, 3=form n) + // For polyline (form 0, IP=1): N points + + if (p.size() < 3) throw std::runtime_error("Copious data has too few parameters"); + + int ip = get_int(p, 0); + if (ip == 1) { + // Simple polyline + int n = get_int(p, 1); + size_t idx = 2; + std::vector pts; + for (int i = 0; i < n && idx + 2 < p.size(); ++i) { + pts.push_back(Point3D(get_real(p, idx), get_real(p, idx+1), get_real(p, idx+2))); + idx += 3; + } + + if (pts.size() < 2) throw std::runtime_error("Polyline needs at least 2 points"); + + // Build a degree-1 NURBS polyline + std::vector knots; + std::vector wg(pts.size(), 1.0); + for (int i = 0; i < static_cast(pts.size()) + 2; ++i) + knots.push_back(static_cast(std::min(i, static_cast(pts.size()) - 1))); + + return curves::NurbsCurve(pts, knots, wg, 1); + } + + throw std::runtime_error("Unsupported copious data IP type"); + } + + // Type 110: Line + curves::NurbsCurve convert_line(const IgesEntity& ent) { + auto p = params(ent); + // 110 params: X1, Y1, Z1, X2, Y2, Z2 + if (p.size() < 6) throw std::runtime_error("Line requires 6 parameters"); + + Point3D p1(get_real(p, 0), get_real(p, 1), get_real(p, 2)); + Point3D p2(get_real(p, 3), get_real(p, 4), get_real(p, 5)); + + return curves::NurbsCurve({p1, p2}, {0, 0, 1, 1}, {1, 1}, 1); + } + + // Type 126: Rational B-Spline Curve + curves::NurbsCurve convert_bspline_curve(const IgesEntity& ent) { + auto p = params(ent); + // 126 params: K, M, PROP1, PROP2, PROP3, N, A(N+1), knot sequence, weights, control points + // K: upper index of sum (N = number of control points - 1, K = N+M where M=order) + // M: degree (= order - 1) + // PROP1: planar flag (0=open, 1=closed) + // PROP2: curve type (0=polynomial, 1=rational) + // PROP3: periodic flag (0=non-periodic, 1=periodic) + // N: number of control points - 1 + // A: knot sequence (N+M+1 values) + // W: weights (K-N+1 values, if PROP2=1) + // Control points: X0,Y0,Z0, X1,Y1,Z1, ... + + if (p.size() < 6) throw std::runtime_error("B-Spline curve requires at least 6 parameters"); + + int K = get_int(p, 0); // upper index + int M = get_int(p, 1); // degree (order M = degree + 1... wait, M in IGES is the basis function order?) + // Actually: M = order = degree + 1. So degree = M - 1 + // Re-reading IGES spec: K is upper index (N+M-1 traditionally in IGES 126), + // M is the degree of the basis functions. + // N+1 is the number of control points (N = K - M) + int prop1 = get_int(p, 2); + int prop2 = get_int(p, 3); + int prop3 = get_int(p, 4); + int N_val = get_int(p, 5); // N+1 control points + + int degree = M; + int num_cps = N_val + 1; + int num_knots = K + 1; // A(N+1) means N+1 values stored, but IGES here means K+1 values + + // Parse knot sequence + size_t idx = 6; + std::vector knots; + for (int i = 0; i <= K && idx < p.size(); ++i) { + knots.push_back(get_real(p, idx++)); + } + + // Parse weights if rational + std::vector weights; + if (prop2 == 1) { + // weights: K - N + 1 values (= K - N_val + 1) + int num_weights = K - N_val + 1; + for (int i = 0; i < num_weights && idx < p.size(); ++i) { + weights.push_back(get_real(p, idx++)); + } + } else { + weights.assign(num_cps, 1.0); + } + + // Parse control points + std::vector cps; + for (int i = 0; i < num_cps && idx + 2 < p.size(); ++i) { + cps.push_back(Point3D(get_real(p, idx), get_real(p, idx+1), get_real(p, idx+2))); + idx += 3; + } + + if (cps.empty()) throw std::runtime_error("No control points in B-Spline"); + + // Normalize knot vector for open-uniform if needed + if (knots.size() < static_cast(cps.size() + degree + 1)) { + // Generate open-uniform knots + knots.clear(); + for (int i = 0; i < static_cast(cps.size()) + degree + 1; ++i) { + knots.push_back(static_cast(std::clamp(i - degree, 0, + static_cast(cps.size()) - degree))); + } + } + + if (weights.empty()) + weights.assign(cps.size(), 1.0); + + return curves::NurbsCurve(cps, knots, weights, degree); + } + + // ── Surfaces ── + + int convert_or_get_surface(int de_index, BrepModel& model) { + auto it = surf_cache_.find(de_index); + if (it != surf_cache_.end()) return it->second; + + curves::NurbsSurface surf = get_or_build_surface(de_index); + int sid = model.add_surface(surf); + surf_cache_[de_index] = sid; + return sid; + } + + curves::NurbsSurface get_or_build_surface(int de_index) { + auto it = surface_cache_.find(de_index); + if (it != surface_cache_.end()) return it->second; + + const auto* ent = get_entity(de_index); + if (!ent) { + std::vector> g = {{Point3D(0,0,0), Point3D(1,0,0)}, + {Point3D(0,1,0), Point3D(1,1,0)}}; + auto ns = curves::NurbsSurface(g, {0,0,1,1}, {0,0,1,1}, {}, 1, 1); + surface_cache_[de_index] = ns; + return ns; + } + + std::vector> g0 = {{Point3D(0,0,0), Point3D(1,0,0)}, + {Point3D(0,1,0), Point3D(1,1,0)}}; + curves::NurbsSurface ns(g0, {0,0,1,1}, {0,0,1,1}, {}, 1, 1); + + try { + switch (ent->type_number) { + case 108: ns = convert_plane(*ent); break; + case 114: ns = convert_parametric_spline_surface(*ent); break; + case 118: ns = convert_ruled_surface(*ent); break; + case 120: ns = convert_revolution_surface(*ent); break; + case 122: ns = convert_tabulated_cylinder(*ent); break; + case 128: ns = convert_bspline_surface(*ent); break; + case 140: ns = convert_offset_surface(*ent); break; + case 192: ns = convert_cylinder_surface(*ent); break; + case 194: ns = convert_cone_surface(*ent); break; + case 196: ns = convert_sphere_surface(*ent); break; + case 198: ns = convert_torus_surface(*ent); break; + default: break; + } + } catch (const std::exception&) { + // Keep default surface + } + + surface_cache_[de_index] = ns; + return ns; + } + + // Type 108: Plane + curves::NurbsSurface convert_plane(const IgesEntity& ent) { + auto p = params(ent); + // 108 params: A, B, C, D (plane equation: Ax + By + Cz + D = 0) + // Plus an optional pointer to a closed boundary curve + if (p.size() < 4) throw std::runtime_error("Plane requires 4 parameters"); + + double a = get_real(p, 0), b = get_real(p, 1); + double c = get_real(p, 2), d = get_real(p, 3); + + Vector3D normal(a, b, c); + double len = normal.norm(); + if (len < 1e-12) throw std::runtime_error("Zero-length plane normal"); + normal = normal / len; + d = d / len; + + // Point on plane: project origin + double t = -d; // distance from origin along normal + Point3D origin = t * normal; + + // Build two tangent directions + Vector3D u = std::abs(normal.x()) < 0.9 ? Vector3D(1, 0, 0).cross(normal).normalized() + : Vector3D(0, 1, 0).cross(normal).normalized(); + Vector3D v = normal.cross(u).normalized(); + + double s = 1e6; + std::vector> grid = { + {origin - s*u - s*v, origin - s*u + s*v}, + {origin + s*u - s*v, origin + s*u + s*v} + }; + + return curves::NurbsSurface(grid, {0,0,1,1}, {0,0,1,1}, {}, 1, 1); + } + + // Type 114: Parametric Spline Surface + curves::NurbsSurface convert_parametric_spline_surface(const IgesEntity& ent) { + (void)ent; + // Simplified: return a default plane + throw std::runtime_error("Parametric spline surface not fully implemented"); + } + + // Type 118: Ruled Surface + curves::NurbsSurface convert_ruled_surface(const IgesEntity& ent) { + auto p = params(ent); + // 118 params: curve_DE_1, curve_DE_2, direction_flag, developable_flag + if (p.size() < 2) throw std::runtime_error("Ruled surface requires at least 2 parameters"); + + int c1_de = get_int(p, 0); + int c2_de = get_int(p, 1); + + auto c1 = get_or_build_curve(c1_de); + auto c2 = get_or_build_curve(c2_de); + + return curves::NurbsSurface::ruled(c1, c2); + } + + // Type 120: Surface of Revolution + curves::NurbsSurface convert_revolution_surface(const IgesEntity& ent) { + auto p = params(ent); + // 120 params: curve_DE, axis_origin_X, axis_origin_Y, axis_origin_Z, + // axis_dir_X, axis_dir_Y, axis_dir_Z, start_angle, end_angle + if (p.size() < 8) throw std::runtime_error("Surface of revolution requires at least 8 parameters"); + + int curve_de = get_int(p, 0); + Point3D axis_origin(get_real(p, 1), get_real(p, 2), get_real(p, 3)); + Vector3D axis_dir(get_real(p, 4), get_real(p, 5), get_real(p, 6)); + double start_angle = get_real(p, 7); + double end_angle = (p.size() >= 9) ? get_real(p, 8) : 2.0 * M_PI; + + auto profile = get_or_build_curve(curve_de); + double sweep = end_angle - start_angle; + + return curves::NurbsSurface::revolve(profile, axis_origin, axis_dir, sweep); + } + + // Type 122: Tabulated Cylinder + curves::NurbsSurface convert_tabulated_cylinder(const IgesEntity& ent) { + auto p = params(ent); + // 122 params: curve_DE, dir_X, dir_Y, dir_Z + if (p.size() < 4) throw std::runtime_error("Tabulated cylinder requires at least 4 parameters"); + + int curve_de = get_int(p, 0); + Vector3D dir(get_real(p, 1), get_real(p, 2), get_real(p, 3)); + + auto directrix = get_or_build_curve(curve_de); + + // Build a ruled surface by offsetting the curve along dir + const auto& cps = directrix.control_points(); + const auto& wg = directrix.weights(); + const auto& kts = directrix.knots(); + int deg = directrix.degree(); + + std::vector cps2; + for (const auto& cp : cps) + cps2.push_back(cp + dir); + + auto c2 = curves::NurbsCurve(cps2, kts, wg, deg); + + return curves::NurbsSurface::ruled(directrix, c2); + } + + // Type 128: Rational B-Spline Surface + curves::NurbsSurface convert_bspline_surface(const IgesEntity& ent) { + auto p = params(ent); + // 128 params: + // K1, K2, M1, M2, PROP1, PROP2, PROP3, PROP4, PROP5, + // N1, N2, + // S(N1+1): u-knot sequence (N1+M1+1 values = K1+1 values) + // T(N2+1): v-knot sequence (N2+M2+1 values = K2+1 values) + // W: weights (if PROP5=1) → ((K1-N1)+1) * ((K2-N2)+1) values row-major + // Control points: (N1+1)*(N2+1) points, row-major in v then u? or u then v? + // Actually: X11,Y11,Z11, X12,Y12,Z12, ... (varying first index fastest) + + if (p.size() < 11) throw std::runtime_error("B-Spline surface requires at least 11 parameters"); + + int K1 = get_int(p, 0); + int K2 = get_int(p, 1); + int M1 = get_int(p, 2); + int M2 = get_int(p, 3); + // PROP1-PROP4: planar/closed u/closed v/polynomial flags (skip) + int prop5 = get_int(p, 8); // rational flag + int N1 = get_int(p, 9); + int N2 = get_int(p, 10); + + int deg_u = M1; + int deg_v = M2; + int nu = N1 + 1; + int nv = N2 + 1; + + size_t idx = 11; + + // U knot sequence: K1+1 values + std::vector ku; + for (int i = 0; i <= K1 && idx < p.size(); ++i) + ku.push_back(get_real(p, idx++)); + + // V knot sequence: K2+1 values + std::vector kv; + for (int i = 0; i <= K2 && idx < p.size(); ++i) + kv.push_back(get_real(p, idx++)); + + // Weights (if rational) + std::vector> weights; + if (prop5 == 1) { + int nwu = K1 - N1 + 1; + int nwv = K2 - N2 + 1; + weights.resize(nwu); + for (int i = 0; i < nwu; ++i) { + weights[i].resize(nwv); + for (int j = 0; j < nwv && idx < p.size(); ++j) { + weights[i][j] = get_real(p, idx++); + } + } + } + + // Control points: nu × nv, row-major + std::vector> grid(nu); + for (int i = 0; i < nu; ++i) { + grid[i].resize(nv); + for (int j = 0; j < nv && idx + 2 < p.size(); ++j) { + grid[i][j] = Point3D(get_real(p, idx), get_real(p, idx+1), get_real(p, idx+2)); + idx += 3; + } + } + + if (grid.empty() || grid[0].empty()) throw std::runtime_error("Empty B-Spline surface grid"); + + // Normalize knots if needed + if (ku.size() < static_cast(nu + deg_u + 1)) { + ku.clear(); + for (int i = 0; i < nu + deg_u + 1; ++i) + ku.push_back(static_cast(std::clamp(i - deg_u, 0, nu - deg_u))); + } + if (kv.size() < static_cast(nv + deg_v + 1)) { + kv.clear(); + for (int i = 0; i < nv + deg_v + 1; ++i) + kv.push_back(static_cast(std::clamp(i - deg_v, 0, nv - deg_v))); + } + + return curves::NurbsSurface(grid, ku, kv, weights, deg_u, deg_v); + } + + // Type 140: Offset Surface + curves::NurbsSurface convert_offset_surface(const IgesEntity& ent) { + auto p = params(ent); + // 140 params: base_surface_DE, offset_distance + if (p.size() < 2) throw std::runtime_error("Offset surface requires at least 2 parameters"); + + int base_de = get_int(p, 0); + double offset = get_real(p, 1); + + // Simple offset: return the base surface with a note — full offset + // implementation would require offsetting the NURBS, which is complex. + // For now return the base surface (geometric offset approximation) + (void)offset; + return get_or_build_surface(base_de); + } + + // Type 192: Right Circular Cylindrical Surface + curves::NurbsSurface convert_cylinder_surface(const IgesEntity& ent) { + auto p = params(ent); + // 192 params: location_X, location_Y, location_Z, axis_X, axis_Y, axis_Z, radius + if (p.size() < 7) throw std::runtime_error("Cylinder requires 7 parameters"); + + Point3D loc(get_real(p, 0), get_real(p, 1), get_real(p, 2)); + Vector3D axis(get_real(p, 3), get_real(p, 4), get_real(p, 5)); + double R = get_real(p, 6); + + return make_cylinder_surface(loc, axis, R); + } + + // Type 194: Right Circular Conical Surface + curves::NurbsSurface convert_cone_surface(const IgesEntity& ent) { + auto p = params(ent); + // 194 params: location_X, location_Y, location_Z, axis_X, axis_Y, axis_Z, radius, semi_angle + if (p.size() < 8) throw std::runtime_error("Cone requires 8 parameters"); + + Point3D loc(get_real(p, 0), get_real(p, 1), get_real(p, 2)); + Vector3D axis(get_real(p, 3), get_real(p, 4), get_real(p, 5)); + double R = get_real(p, 6); + double angle = get_real(p, 7); + + return make_cone_surface(loc, axis, R, angle); + } + + // Type 196: Spherical Surface + curves::NurbsSurface convert_sphere_surface(const IgesEntity& ent) { + auto p = params(ent); + // 196 params: center_X, center_Y, center_Z, radius + if (p.size() < 4) throw std::runtime_error("Sphere requires 4 parameters"); + + Point3D center(get_real(p, 0), get_real(p, 1), get_real(p, 2)); + double R = get_real(p, 3); + + return make_sphere_surface(center, R); + } + + // Type 198: Toroidal Surface + curves::NurbsSurface convert_torus_surface(const IgesEntity& ent) { + auto p = params(ent); + // 198 params: center_X, center_Y, center_Z, axis_X, axis_Y, axis_Z, + // major_radius, minor_radius + if (p.size() < 8) throw std::runtime_error("Torus requires 8 parameters"); + + Point3D center(get_real(p, 0), get_real(p, 1), get_real(p, 2)); + Vector3D axis(get_real(p, 3), get_real(p, 4), get_real(p, 5)); + double R = get_real(p, 6); + double r = get_real(p, 7); + + return make_torus_surface(center, axis, R, r); + } + + // ── Geometry factory helpers ── + + static curves::NurbsCurve make_arc_nurbs(const Point3D& C, const Vector3D& X, + double R, double angle) { + // Build a circular arc from angle 0 to `angle` in the XY plane defined by X and Y + // (Y = Z × X, where Z is (0,0,1)) + // Returns degree-2 rational NURBS + + Vector3D Y(0, 0, 1); + if (std::abs(X.z()) > 0.99) Y = Vector3D(0, 1, 0); + Y = Y.cross(X).normalized(); + + // For full circle and large arcs, use multiples of 90° segments + int num_segments = std::max(1, static_cast(std::ceil(angle / (M_PI_2)))); + double seg_angle = angle / num_segments; + + std::vector cps_all; + std::vector wgs_all; + int n_seg_cps = 3; // each 90° segment: 3 control points + int total_segments = num_segments; + + for (int s = 0; s < total_segments; ++s) { + double a0 = s * seg_angle; + double a2 = (s + 1) * seg_angle; + double a1 = (a0 + a2) / 2.0; + + double w = std::cos(seg_angle / 2.0); // = cos(half segment angle) + + Point3D p0 = C + R * (std::cos(a0) * X + std::sin(a0) * Y); + Point3D p1 = C + (R / w) * (std::cos(a1) * X + std::sin(a1) * Y); + Point3D p2 = C + R * (std::cos(a2) * X + std::sin(a2) * Y); + + if (s == 0) { + cps_all.push_back(p0); + wgs_all.push_back(1.0); + } + cps_all.push_back(p1); + wgs_all.push_back(w); + cps_all.push_back(p2); + wgs_all.push_back(1.0); + } + + // Build knots for degree-2 rational curve + int n_cps = static_cast(cps_all.size()); + std::vector knots; + int degree = 2; + // Open-uniform: degree duplicate at each end + int num_segs = (n_cps - 1) / 2; + for (int i = 0; i < n_cps + degree + 1; ++i) { + int k = i - degree; + if (k < 0) k = 0; + if (k > num_segs) k = num_segs; + knots.push_back(static_cast(k)); + } + + return curves::NurbsCurve(cps_all, knots, wgs_all, degree); + } + + static curves::NurbsSurface make_cylinder_surface(const Point3D& C, const Vector3D& axis, + double R) { + // Build two orthogonal directions perpendicular to axis + Vector3D u = std::abs(axis.x()) < 0.9 ? Vector3D(1, 0, 0).cross(axis).normalized() + : Vector3D(0, 1, 0).cross(axis).normalized(); + Vector3D v = axis.cross(u).normalized(); + + double h = 1e6; + double w = std::sqrt(2.0) / 2.0; + + // Circle profile in uv plane (9 CPs for 360°) + std::vector circ; + for (int i = 0; i < 9; ++i) { + double theta = i * M_PI_4; + double cp = std::cos(theta), sp = std::sin(theta); + circ.push_back(C + R * (cp * u + sp * v)); + } + + std::vector> grid = {circ}; + for (const auto& pt : circ) + grid.push_back({pt + h * axis}); + + // Actually we need 2 rows + grid.clear(); + grid.push_back(circ); + std::vector top; + for (const auto& pt : circ) + top.push_back(pt + h * axis); + grid.push_back(top); + + std::vector ku; + for (int i = 0; i < 12; ++i) ku.push_back(i / 4.0); + std::vector kv = {0, 0, 1, 1}; + + std::vector cw = {1, w, 1, w, 1, w, 1, w, 1}; + return curves::NurbsSurface(grid, ku, kv, {cw, cw}, 2, 1); + } + + static curves::NurbsSurface make_cone_surface(const Point3D& C, const Vector3D& axis, + double R, double angle) { + Vector3D u = std::abs(axis.x()) < 0.9 ? Vector3D(1, 0, 0).cross(axis).normalized() + : Vector3D(0, 1, 0).cross(axis).normalized(); + Vector3D v = axis.cross(u).normalized(); + + double h = 1e6; + double R_top = R + h * std::tan(angle); + double w = std::sqrt(2.0) / 2.0; + + std::vector bottom, top_circ; + for (int i = 0; i < 9; ++i) { + double theta = i * M_PI_4; + double cp = std::cos(theta), sp = std::sin(theta); + bottom.push_back(C + R * (cp * u + sp * v)); + top_circ.push_back(C + h * axis + R_top * (cp * u + sp * v)); + } + + std::vector ku; + for (int i = 0; i < 12; ++i) ku.push_back(i / 4.0); + std::vector kv = {0, 0, 1, 1}; + std::vector cw = {1, w, 1, w, 1, w, 1, w, 1}; + + return curves::NurbsSurface({bottom, top_circ}, ku, kv, {cw, cw}, 2, 1); + } + + static curves::NurbsSurface make_sphere_surface(const Point3D& C, double R) { + // Sphere as surface of revolution of a half-circle + double w = std::sqrt(2.0) / 2.0; + + // Revolve half-circle profile about Z axis + // 3 rows (v direction: top pole, equator, bottom pole) × 9 cols (u direction: full circle) + + // The profile: half circle from +Z to -Z in XZ plane: + // P0=(0,0,R), P1=(R,0,0), P2=(0,0,-R) with weights [1, w, 1] → degree 2 + + std::vector> grid(3); + std::vector cw = {1, w, 1, w, 1, w, 1, w, 1}; + + for (int i = 0; i < 9; ++i) { + double theta = i * M_PI_4; + double ct = std::cos(theta), st = std::sin(theta); + + // Top pole (all collapsed to one point) + grid[0].push_back(C + Point3D(0, 0, R)); + + // Equator ring + grid[1].push_back(C + Point3D(R * ct, R * st, 0)); + + // Bottom pole + grid[2].push_back(C + Point3D(0, 0, -R)); + } + + std::vector ku; + for (int i = 0; i < 12; ++i) ku.push_back(i / 4.0); + std::vector kv = {0, 0, 0, 1, 1, 1}; + + std::vector> weights = { + {1, w, 1, w, 1, w, 1, w, 1}, + {w, w*w, w, w*w, w, w*w, w, w*w, w}, + {1, w, 1, w, 1, w, 1, w, 1}, + }; + + return curves::NurbsSurface(grid, ku, kv, weights, 2, 2); + } + + static curves::NurbsSurface make_torus_surface(const Point3D& C, const Vector3D& axis, + double R, double r) { + // Torus: circle of radius r swept around circle of radius R + Vector3D u = std::abs(axis.x()) < 0.9 ? Vector3D(1, 0, 0).cross(axis).normalized() + : Vector3D(0, 1, 0).cross(axis).normalized(); + Vector3D v = axis.cross(u).normalized(); + + double w = std::sqrt(2.0) / 2.0; + std::vector> grid(9); + std::vector> wg(9); + + for (int i = 0; i < 9; ++i) { + double theta = i * M_PI_4; + double ct = std::cos(theta), st = std::sin(theta); + // Major circle center + Point3D mc = C + R * (ct * u + st * v); + + // Minor circle in plane (radial, axis) + Vector3D rad = ct * u + st * v; + for (int j = 0; j < 9; ++j) { + double phi = j * M_PI_4; + double cp = std::cos(phi), sp = std::sin(phi); + grid[i].push_back(mc + r * (cp * rad + sp * axis)); + } + + // Weights + std::vector row_w; + for (int j = 0; j < 9; ++j) { + double wu = (i % 2 == 0) ? 1.0 : w; + double wv_val = (j % 2 == 0) ? 1.0 : w; + row_w.push_back(wu * wv_val); + } + wg[i] = row_w; + } + + std::vector ku, kv; + for (int i = 0; i < 12; ++i) { ku.push_back(i / 4.0); kv.push_back(i / 4.0); } + + return curves::NurbsSurface(grid, ku, kv, wg, 2, 2); + } + + std::unordered_map surface_cache_; +}; + +// ───────────────────────────────────────────────────────────── +// Public API +// ───────────────────────────────────────────────────────────── + +std::vector import_iges_from_string(const std::string& data) { + clear_error(); + + if (data.empty()) { + set_error(IgesError::ParseError, "Empty IGES data"); + return {}; + } + + IgesParser parser(data); + if (!parser.parse()) + return {}; + + IgesToBrep converter(parser.entities()); + try { + return converter.convert(); + } catch (const std::exception& e) { + set_error(IgesError::ParseError, std::string("Conversion error: ") + e.what()); + return {}; + } +} + +std::vector import_iges(const std::string& filepath) { + clear_error(); + + std::ifstream file(filepath); + if (!file.is_open()) { + set_error(IgesError::FileNotFound, "Cannot open file: " + filepath); + return {}; + } + + std::ostringstream buf; + buf << file.rdbuf(); + return import_iges_from_string(buf.str()); +} + +} // namespace vde::brep diff --git a/tests/brep/test_iges_import.cpp b/tests/brep/test_iges_import.cpp new file mode 100644 index 0000000..efe54e5 --- /dev/null +++ b/tests/brep/test_iges_import.cpp @@ -0,0 +1,548 @@ +#include +#include "vde/brep/iges_import.h" +#include "vde/brep/brep.h" +#include "vde/brep/modeling.h" +#include +#include +#include +#include +#include + +using namespace vde::brep; +using namespace vde::core; + +// ───────────────────────────────────────────────────────────── +// Helper: Build IGES 80-character fixed-width records +// ───────────────────────────────────────────────────────────── + +namespace { + +/// Pad a string to exactly `width` characters (right-justified for integers, left for strings) +std::string pad_right(const std::string& s, int width) { + std::string r = s; + if (static_cast(r.size()) > width) r = r.substr(0, width); + else r.append(width - r.size(), ' '); + return r; +} + +std::string pad_left(int val, int width) { + std::string s = std::to_string(val); + if (static_cast(s.size()) > width) s = s.substr(0, width); + else s.insert(0, width - s.size(), ' '); + return s; +} + +/// Build one IGES Directory Entry record (72 chars of fields + section char + 7-char seq) +std::string de_line(const std::vector& fields, char section, int seq) { + // fields: 9 values, each right-justified in 8 chars + std::string line; + for (size_t i = 0; i < fields.size() && i < 9; ++i) { + std::string val = std::to_string(fields[i]); + // Right-justify integer in 8-char field + if (val.size() < 8) val.insert(0, 8 - val.size(), ' '); + line += val; + } + // pad remaining to 72 + while (line.size() < 72) line += ' '; + line = line.substr(0, 72); + line += section; + line += pad_left(seq, 7); + return line; +} + +/// Build one IGES Parameter Data record +std::string pd_line(const std::string& data, int de_ptr, int seq) { + std::string line = data; + // Data occupies cols 1-64 max + while (line.size() < 65) line += ' '; + line = line.substr(0, 64); + // Pad to col 65 (char index 64) + // Col 65 is blank + line += ' '; + // DE pointer in cols 66-72 (right justified in 7 chars) + std::string deplabel = std::to_string(de_ptr); + if (deplabel.size() < 7) deplabel = std::string(7 - deplabel.size(), ' ') + deplabel; + line += deplabel; + line += 'P'; + line += pad_left(seq, 7); + return line; +} + +/// Build a full IGES file string +struct IgesBuilder { + std::vector s_lines; + std::vector g_lines; + std::vector d_lines; // pairs: 2 per entity + std::vector p_lines; + + void add_s_line(const std::string& txt) { + std::string line = pad_right(txt, 72) + "S"; + line += pad_left(static_cast(s_lines.size() + 1), 7); + s_lines.push_back(line); + } + + void add_g_line(const std::string& txt) { + std::string line = pad_right(txt, 72) + "G"; + line += pad_left(static_cast(g_lines.size() + 1), 7); + g_lines.push_back(line); + } + + // Add a directory entry (two 80-char lines) + void add_de(int type_num, int pd_ptr, int struct_val, int line_font, int level, + int view, int transform, int label, int status, + int line_weight, int color, int param_count, int form_num) { + int seq = static_cast(d_lines.size() + 1); + // Line 1 + d_lines.push_back(de_line({type_num, pd_ptr, struct_val, line_font, level, + view, transform, label, status}, 'D', seq)); + // Line 2: type, line_weight, color, param_count, form_num, 0, 0, label, subscript + d_lines.push_back(de_line({type_num, line_weight, color, param_count, form_num, + 0, 0, label, 0}, 'D', seq + 1)); + } + + // Add parameter data + void add_pd(const std::string& data, int de_ptr) { + int seq = static_cast(p_lines.size() + 1); + p_lines.push_back(pd_line(data, de_ptr, seq)); + } + + std::string build() { + std::string result; + for (const auto& l : s_lines) result += l + "\n"; + for (const auto& l : g_lines) result += l + "\n"; + for (const auto& l : d_lines) result += l + "\n"; + for (const auto& l : p_lines) result += l + "\n"; + + // Terminate section + int s_cnt = static_cast(s_lines.size()); + int g_cnt = static_cast(g_lines.size()); + int d_cnt = static_cast(d_lines.size()); + int p_cnt = static_cast(p_lines.size()); + + std::string t_line = pad_right( + pad_left(s_cnt, 8) + pad_left(g_cnt, 8) + pad_left(d_cnt, 8) + pad_left(p_cnt, 8), + 72) + "T" + pad_left(1, 7); + result += t_line + "\n"; + + return result; + } +}; + +/// Standard global section for minimal IGES files +std::string standard_global() { + return "1H,,1H;,4HSLOT,1,,,13,0.016,0.1,,1.0,1.0,1.0,2,1,1,8,,1.0,1,1,0,0;"; +} + +} // anonymous namespace + +// ───────────────────────────────────────────────────────────── + +TEST(IgesImport, EmptyData) { + auto bodies = import_iges_from_string(""); + EXPECT_TRUE(bodies.empty()); + EXPECT_EQ(iges_last_error(), IgesError::ParseError); +} + +TEST(IgesImport, TruncatedFile) { + // Missing sections — just a start section + IgesBuilder b; + b.add_s_line("Truncated test file"); + auto bodies = import_iges_from_string(b.build()); + EXPECT_TRUE(bodies.empty()); + EXPECT_NE(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, MinimalLine) { + IgesBuilder b; + b.add_s_line("Test IGES file with a single line"); + b.add_g_line(standard_global()); + + // Single line entity: type 110 + // DE entry: PD pointer 1, param count 1 + b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + + // Line: X1,Y1,Z1,X2,Y2,Z2 = (0,0,0) to (10,0,0) + b.add_pd("0.0,0.0,0.0,10.0,0.0,0.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); + + // Should produce at least one body (fallback mode with a surface) + ASSERT_FALSE(bodies.empty()); + EXPECT_GT(bodies[0].num_surfaces(), 0u); +} + +TEST(IgesImport, SingleArc) { + IgesBuilder b; + b.add_s_line("Test IGES file with a circular arc"); + b.add_g_line(standard_global()); + + // Type 100: Circular Arc — 7 params: ZT, Xc, Yc, Xs, Ys, Xe, Ye + // Full circle: center (0,0), radius 5, start (5,0), end (5,0) → full circle + b.add_de(100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,5.0,0.0,5.0,0.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); + ASSERT_FALSE(bodies.empty()); +} + +TEST(IgesImport, SingleLine110) { + // Test line (type 110): basic endpoint geometry + IgesBuilder b; + b.add_s_line("Line test"); + b.add_g_line(standard_global()); + + b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1.0,2.0,3.0,4.0,5.0,6.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, CylinderSurface) { + IgesBuilder b; + b.add_s_line("Cylinder test"); + b.add_g_line(standard_global()); + + // Type 192: Right Circular Cylindrical Surface + // location(0,0,0), axis(0,0,1), radius=5 + b.add_de(192, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, SphereSurface) { + IgesBuilder b; + b.add_s_line("Sphere test"); + b.add_g_line(standard_global()); + + // Type 196: Spherical Surface — center(0,0,0), radius=10 + b.add_de(196, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,10.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, ConeSurface) { + IgesBuilder b; + b.add_s_line("Cone test"); + b.add_g_line(standard_global()); + + // Type 194: Right Circular Conical Surface + // location(0,0,0), axis(0,0,1), radius=5, semi_angle=0.5 rad + b.add_de(194, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0,0.5;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, TorusSurface) { + IgesBuilder b; + b.add_s_line("Torus test"); + b.add_g_line(standard_global()); + + // Type 198: Toroidal Surface + // center(0,0,0), axis(0,0,1), R=10, r=3 + b.add_de(198, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,10.0,3.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, PlaneSurface) { + IgesBuilder b; + b.add_s_line("Plane test"); + b.add_g_line(standard_global()); + + // Type 108: Plane — equation Ax+By+Cz+D=0 + // XY plane: 0x+0y+1z+0=0 + b.add_de(108, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,1.0,0.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, BSplineCurve) { + IgesBuilder b; + b.add_s_line("B-Spline curve test"); + b.add_g_line(standard_global()); + + // Type 126: Rational B-Spline Curve + // K=5, M=2 (degree 2), PROP1=0, PROP2=0 (non-rational), PROP3=0, N=3 (4 CPs) + // Knot sequence: A(0..K) = K+1=6 values: 0,0,0,0.5,1,1 + // CPs: (0,0,0), (2,3,0), (5,1,0), (7,4,0) + b.add_de(126, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("5,2,0,0,0,3,0.0,0.0,0.0,0.5,1.0,1.0,0.0,0.0,0.0,2.0,3.0,0.0,5.0,1.0,0.0,7.0,4.0,0.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, BSplineSurface) { + IgesBuilder b; + b.add_s_line("B-Spline surface test"); + b.add_g_line(standard_global()); + + // Type 128: Rational B-Spline Surface + // K1=1, K2=1, M1=1, M2=1, PROP1-4=0, PROP5=0, N1=0, N2=0 → 1×1 CP, deg 1×1 + // U knots: 0.0, 1.0 (K1+1=2 values) + // V knots: 0.0, 1.0 (K2+1=2 values) + // CP: (0,0,0) + b.add_de(128, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,1,1,1,0,0,0,0,0,0,0,0.0,1.0,0.0,1.0,0.0,0.0,0.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, RuledSurface) { + IgesBuilder b; + b.add_s_line("Ruled surface test"); + b.add_g_line(standard_global()); + + // Need two curves as DE entries first, then a ruled surface referencing them + // Line 1: (0,0,0) → (10,0,0), DE index 1 + b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,10.0,0.0,0.0;", 1); + + // Line 2: (0,5,0) → (10,5,0), DE index 2 + b.add_de(110, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,5.0,0.0,10.0,5.0,0.0;", 2); + + // Ruled surface 118 referencing lines 1 and 2 + b.add_de(118, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,2,0,0;", 3); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, RevolutionSurface) { + IgesBuilder b; + b.add_s_line("Revolution surface test"); + b.add_g_line(standard_global()); + + // Profile line: (5,0,0) → (5,0,10), DE index 1 + b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("5.0,0.0,0.0,5.0,0.0,10.0;", 1); + + // Surface of revolution 120: profile DE=1, axis origin=(0,0,0), axis dir=(0,0,1) + b.add_de(120, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,0.0,0.0,0.0,0.0,0.0,1.0,0.0,6.283185307;", 2); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, TabulatedCylinder) { + IgesBuilder b; + b.add_s_line("Tabulated cylinder test"); + b.add_g_line(standard_global()); + + // Profile line: (5,0,0) → (5,0,0)... actually a single point doesn't work + // Let's use a simple line: (0,0,0) → (5,0,0), DE 1 + b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,5.0,0.0,0.0;", 1); + + // Tabulated cylinder 122: curve DE=1, direction (0,0,10) + b.add_de(122, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,0.0,0.0,10.0;", 2); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, CopiousDataPolyline) { + IgesBuilder b; + b.add_s_line("Polyline test"); + b.add_g_line(standard_global()); + + // Type 106: Copious Data, IP=1 (polyline), N=4 points + // Points: (0,0,0), (2,0,0), (2,2,0), (0,2,0) + b.add_de(106, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,4,0.0,0.0,0.0,2.0,0.0,0.0,2.0,2.0,0.0,0.0,2.0,0.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, ManifoldSolidBox) { + // Build a minimal B-Rep box via IGES topology entities + IgesBuilder b; + b.add_s_line("Manifold solid box test"); + b.add_g_line(standard_global()); + + // Entities: + // 1: Vertex (0,0,0) type 502 + // 2: Vertex (10,0,0) + // 3: Vertex (10,10,0) + // 4: Vertex (0,10,0) + // 5: Vertex (0,0,10) + // 6: Vertex (10,0,10) + // 7: Vertex (10,10,10) + // 8: Vertex (0,10,10) + // 9: Line (0,0,0)-(10,0,0) type 110 → edge uses curve + // ... and so on. Let's do a simpler subset. + + // Vertex 1: (0,0,0) + b.add_de(502, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0;", 1); + + // Vertex 2: (10,0,0) + b.add_de(502, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("10.0,0.0,0.0;", 2); + + // Vertex 3: (10,10,0) + b.add_de(502, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("10.0,10.0,0.0;", 3); + + // Vertex 4: (0,10,0) + b.add_de(502, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,10.0,0.0;", 4); + + // Line for edge: (0,0,0)-(10,0,0) + b.add_de(110, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,10.0,0.0,0.0;", 5); + + // Edge 1: curve type=1 (line), curve DE=5, start vx DE=1, sv_index=0, end vx DE=2, ev_index=1 + b.add_de(504, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,5,1,0,2,1,0,0.0,1.0;", 6); + + // Plane for bottom face: z=0 plane + b.add_de(108, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,1.0,0.0;", 7); + + // Loop: N=1 edge, type=0, edge_DE=6, vx_count=2, start=1, end=2, cur_srf=0, sparams=0, eparams=0 + b.add_de(508, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,0,6,2,1,2,0,0.0,1.0;", 8); + + // Face: surface_DE=7, N_loops=1, loop_flag=0, loop_DE=8 + b.add_de(510, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("7,1,0,8;", 9); + + // Shell: N=1 face, face_DE=9 + b.add_de(514, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1,9;", 10); + + // Manifold Solid B-Rep 186: shell DE=10 + b.add_de(186, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("10;", 11); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); + ASSERT_FALSE(bodies.empty()); + EXPECT_GT(bodies[0].num_faces(), 0u); +} + +TEST(IgesImport, RoundtripBox) { + // Create a box via modeling, then verify IGES import infrastructure works + auto box = make_box(2.0, 1.0, 3.0); + EXPECT_GT(box.num_faces(), 0u); + + // Verify we can build and parse IGES with a cylinder + IgesBuilder b; + b.add_s_line("Roundtrip test - cylinder"); + b.add_g_line(standard_global()); + + b.add_de(192, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, InvalidSectionOrder) { + // Put sections in wrong order — only S and G with no D + IgesBuilder b; + b.add_s_line("Bad file"); + b.add_g_line(standard_global()); + // No D or P sections — just T + std::string result; + for (const auto& l : b.s_lines) result += l + "\n"; + for (const auto& l : b.g_lines) result += l + "\n"; + + // Manually add T with wrong section + std::string t_line = " T 1\n"; + result += t_line; + + auto bodies = import_iges_from_string(result); + EXPECT_TRUE(bodies.empty()); + EXPECT_NE(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, DEExponentNotation) { + // IGES uses D exponent notation (e.g., 1.0D+2 = 100) + IgesBuilder b; + b.add_s_line("D exponent notation test"); + b.add_g_line(standard_global()); + + b.add_de(110, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("1.0D+2,2.0D+1,3.5D+0,4.0D-1,5.0D-2,6.0D+3;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, MultipleSurfaces) { + IgesBuilder b; + b.add_s_line("Multiple surface types test"); + b.add_g_line(standard_global()); + + // Plane + b.add_de(108, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,1.0,0.0;", 1); + + // Cylinder + b.add_de(192, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,0.0,0.0,1.0,5.0;", 2); + + // Sphere + b.add_de(196, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("10.0,0.0,0.0,3.0;", 3); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); +} + +TEST(IgesImport, FileNotFound) { + auto bodies = import_iges("/nonexistent/path/file.iges"); + EXPECT_TRUE(bodies.empty()); + EXPECT_EQ(iges_last_error(), IgesError::FileNotFound); +} + +TEST(IgesImport, VerifyArcGeometry) { + // A 90-degree arc: center(0,0), start(5,0), end(0,5) + IgesBuilder b; + b.add_s_line("Arc geometry test"); + b.add_g_line(standard_global()); + + b.add_de(100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,5.0,0.0,0.0,5.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); + ASSERT_FALSE(bodies.empty()); +} + +TEST(IgesImport, FullCircleArc) { + // Full circle: center(0,0), start(10,0), end(10,0) → 360° + IgesBuilder b; + b.add_s_line("Full circle arc test"); + b.add_g_line(standard_global()); + + b.add_de(100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + b.add_pd("0.0,0.0,0.0,10.0,0.0,10.0,0.0;", 1); + + auto bodies = import_iges_from_string(b.build()); + EXPECT_EQ(iges_last_error(), IgesError::Ok); + ASSERT_FALSE(bodies.empty()); +} + +} // namespace vde::brep