Skip to main content

brep_kernel/io/iges/
export.rs

1//! Kernel `BrepSolid` → IGES trimmed-NURBS-surface export.
2//!
3//! Each B-rep face becomes one IGES **144** Trimmed Surface: a **128** rational
4//! B-spline surface, and one **142** Curve-on-Surface per loop (outer first).
5//! Each 142 references two **102** composite curves — the loop's coedge pcurves
6//! in parameter space (`BPTR`) and the same boundary as 3D edge curves
7//! (`CPTR`) — assembled from per-coedge **126** rational B-spline curves.
8//!
9//! Face orientation (`same_sense`) is intentionally NOT encoded: the importer
10//! recovers a coherent outward orientation with [`crate::sew_solid`], so the
11//! wire format need only carry surfaces and boundary geometry.
12
13use crate::step::edge_subcurve;
14use crate::topology::{BrepSolid, EdgeRecord, FaceRecord, LoopRecord};
15
16use super::entities::{
17    composite_102, curve_on_surface_142, curve_to_126, surface_to_128, trimmed_surface_144,
18};
19use super::writer::{GlobalParams, IgesWriter};
20
21/// Serialize solids as an IGES 5.3 document of trimmed NURBS surfaces.
22///
23/// `unit` is one of `millimeter|meter|centimeter|inch|foot` (default
24/// millimetre); coordinates are written verbatim, so callers should pass the
25/// unit the kernel geometry is expressed in (millimetres in this kernel).
26pub fn export_iges(
27    solids: &[BrepSolid],
28    name: &str,
29    unit: &str,
30    timestamp: &str,
31) -> Result<String, String> {
32    if solids.is_empty() {
33        return Err("iges_export: no solids to export".into());
34    }
35    let (units_flag, units_name) = units_for(unit);
36
37    let mut writer = IgesWriter::new();
38    writer.add_start_line(&format!("IGES export of '{name}' from the BREP kernel."));
39
40    let mut face_count = 0usize;
41    for solid in solids {
42        for shell in &solid.shells {
43            for face in &shell.faces {
44                export_face(&mut writer, solid, face)?;
45                face_count += 1;
46            }
47        }
48    }
49    if face_count == 0 {
50        return Err("iges_export: solids contain no faces".into());
51    }
52
53    let global = GlobalParams {
54        product_id: if name.is_empty() { "PART".into() } else { name.to_string() },
55        file_name: format!("{}.igs", if name.is_empty() { "part" } else { name }),
56        units_flag,
57        units_name,
58        timestamp: normalize_timestamp(timestamp),
59        min_resolution: 1e-7,
60    };
61    Ok(writer.finish(global))
62}
63
64fn export_face(writer: &mut IgesWriter, solid: &BrepSolid, face: &FaceRecord) -> Result<(), String> {
65    if face.loops.is_empty() {
66        return Err(format!("iges_export: face {} has no loops", face.id));
67    }
68    let sptr = writer.add_entity(surface_to_128(&face.surface)?);
69
70    let mut loop_ptrs: Vec<i64> = Vec::with_capacity(face.loops.len());
71    for loop_record in &face.loops {
72        loop_ptrs.push(export_loop(writer, solid, sptr, loop_record)?);
73    }
74    let (outer, holes) = loop_ptrs
75        .split_first()
76        .ok_or_else(|| format!("iges_export: face {} produced no boundaries", face.id))?;
77    writer.add_entity(trimmed_surface_144(sptr, *outer, holes));
78    Ok(())
79}
80
81/// Export one loop as a 142 (returns its DE pointer). Emits per-coedge 126
82/// curves in both parameter and model space, grouped by two 102 composites.
83fn export_loop(
84    writer: &mut IgesWriter,
85    solid: &BrepSolid,
86    sptr: i64,
87    loop_record: &LoopRecord,
88) -> Result<i64, String> {
89    if loop_record.coedges.is_empty() {
90        return Err(format!("iges_export: loop {} has no coedges", loop_record.id));
91    }
92    let mut model_ptrs: Vec<i64> = Vec::with_capacity(loop_record.coedges.len());
93    let mut param_ptrs: Vec<i64> = Vec::with_capacity(loop_record.coedges.len());
94    for coedge in &loop_record.coedges {
95        let edge = edge_for(solid, coedge.edge_id)?;
96        // 3D edge curve, trimmed to the represented interval and oriented in
97        // loop direction.
98        let mut model_curve = edge_subcurve(edge)?;
99        if !coedge.forward {
100            model_curve = model_curve.reversed()?;
101        }
102        model_ptrs.push(writer.add_entity(curve_to_126(&model_curve)?));
103        // Parameter-space pcurve is already directional.
104        param_ptrs.push(writer.add_entity(curve_to_126(&coedge.pcurve)?));
105    }
106    let model_composite = writer.add_entity(composite_102(&model_ptrs));
107    let param_composite = writer.add_entity(composite_102(&param_ptrs));
108    Ok(writer.add_entity(curve_on_surface_142(sptr, param_composite, model_composite)))
109}
110
111fn edge_for<'a>(solid: &'a BrepSolid, id: u64) -> Result<&'a EdgeRecord, String> {
112    solid
113        .edges
114        .iter()
115        .find(|e| e.id == id)
116        .ok_or_else(|| format!("iges_export: dangling edge id {id}"))
117}
118
119fn units_for(unit: &str) -> (i64, String) {
120    match unit.trim().to_ascii_lowercase().as_str() {
121        "inch" | "inches" | "in" => (1, "IN".into()),
122        "foot" | "feet" | "ft" => (4, "FT".into()),
123        "meter" | "metre" | "m" => (6, "M".into()),
124        "centimeter" | "centimetre" | "cm" => (10, "CM".into()),
125        _ => (2, "MM".into()),
126    }
127}
128
129fn normalize_timestamp(timestamp: &str) -> String {
130    if timestamp.trim().is_empty() {
131        "00000000.000000".into()
132    } else {
133        timestamp.to_string()
134    }
135}