use crate::curve::curve_to_plane_parameters;
use crate::step::edge_subcurve;
use crate::step::pcurve::{build_pcurve, EmittedCurve, EmittedFrame, EmittedSurface, Pcurve2d};
use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord};
use crate::{AnalyticSurface, KernelTolerances, NurbsCurve, NurbsSurface, Vec3, Vec4};
use rustc_hash::FxHashMap as HashMap;
use super::entities::{curve_to_126, surface_to_128};
use super::writer::{
hollerith, real_field, GlobalParams, IgesEntity, IgesWriter, STATUS_DEFINITION, STATUS_DEPENDENT,
STATUS_INDEPENDENT,
};
use crate::{StepAssemblyExport, StepColors};
pub struct IgesBody<'a> {
pub name: String,
pub solid: &'a BrepSolid,
pub color: Option<[u8; 3]>,
}
pub fn export_iges(
solids: &[BrepSolid],
name: &str,
unit: &str,
timestamp: &str,
) -> Result<String, String> {
let bodies: Vec<IgesBody<'_>> = solids
.iter()
.map(|solid| IgesBody { name: name.to_string(), solid, color: None })
.collect();
export_iges_bodies(&bodies, name, unit, timestamp)
}
pub fn export_iges_bodies(
bodies: &[IgesBody<'_>],
name: &str,
unit: &str,
timestamp: &str,
) -> Result<String, String> {
if bodies.is_empty() {
return Err("iges_export: no solids to export".into());
}
let mut writer = IgesWriter::new();
writer.add_start_line(&format!("IGES export of '{name}' from the BREP kernel."));
for body in bodies {
write_solid(&mut writer, body, STATUS_INDEPENDENT)?;
}
Ok(finish(writer, name, unit, timestamp))
}
pub fn export_iges_assembly(
assembly: &StepAssemblyExport,
colors: &StepColors,
name: &str,
unit: &str,
timestamp: &str,
) -> Result<String, String> {
let root = assembly.root()?;
let mut writer = IgesWriter::new();
writer.add_start_line(&format!("IGES export of '{name}' from the BREP kernel."));
let mut subfigures: Vec<Option<(i64, usize)>> = vec![None; assembly.products.len()];
let prefixes = assembly.occurrence_prefixes(root);
let context = AssemblyContext { assembly, colors, root, prefixes: &prefixes };
write_product(&mut writer, &context, root, &mut subfigures)?;
Ok(finish(writer, name, unit, timestamp))
}
struct AssemblyContext<'a> {
assembly: &'a StepAssemblyExport,
colors: &'a StepColors,
root: usize,
prefixes: &'a [Vec<String>],
}
fn write_product(
writer: &mut IgesWriter,
context: &AssemblyContext<'_>,
index: usize,
subfigures: &mut Vec<Option<(i64, usize)>>,
) -> Result<(), String> {
let (assembly, root) = (context.assembly, context.root);
if subfigures[index].is_some() {
return Ok(());
}
let is_root = index == root;
let status = if is_root { STATUS_INDEPENDENT } else { STATUS_DEPENDENT };
let mut members = Vec::new();
let mut depth = 1;
let product = &assembly.products[index];
for (body_name, solid) in &product.bodies {
let color = context.colors.resolve(&context.prefixes[index], body_name);
let body = IgesBody { name: body_name.clone(), solid, color };
members.push(write_solid(writer, &body, status)?);
}
for occurrence in assembly.occurrences.iter().filter(|occurrence| occurrence.parent == index) {
write_product(writer, context, occurrence.child, subfigures)?;
let (definition, child_depth) =
subfigures[occurrence.child].ok_or("iges_export: an occurrence places the root product")?;
depth = depth.max(child_depth + 1);
let mut matrix = IgesEntity::new(
124,
occurrence.placement[..12].iter().map(|&value| real_field(value)).collect(),
);
matrix.status = STATUS_DEPENDENT;
let matrix = writer.add_entity(matrix);
let mut params = vec![definition.to_string(), real_field(0.0), real_field(0.0), real_field(0.0), real_field(1.0)];
if !occurrence.designator.is_empty() {
let mut label = IgesEntity::new(406, vec!["1".into(), hollerith(&occurrence.designator)]);
label.form = 15;
let property = writer.add_entity(label);
params.extend(["0".into(), "1".into(), property.to_string()]);
}
let mut instance = IgesEntity::new(408, params);
instance.transform = matrix;
instance.status = status;
instance.label = occurrence.designator.chars().filter(|c| !c.is_whitespace()).take(8).collect();
members.push(writer.add_entity(instance));
}
if is_root {
subfigures[index] = Some((0, 0));
return Ok(());
}
let mut params = vec![depth.to_string(), hollerith(&product.name), members.len().to_string()];
params.extend(members.iter().map(i64::to_string));
let mut definition = IgesEntity::new(308, params);
definition.status = STATUS_DEFINITION;
definition.label = product.name.chars().filter(|c| !c.is_whitespace()).take(8).collect();
subfigures[index] = Some((writer.add_entity(definition), depth));
Ok(())
}
fn finish(writer: IgesWriter, name: &str, unit: &str, timestamp: &str) -> String {
let (units_flag, units_name) = units_for(unit);
let global = GlobalParams {
product_id: if name.is_empty() { "PART".into() } else { name.to_string() },
file_name: format!("{}.igs", if name.is_empty() { "part" } else { name }),
units_flag,
units_name,
timestamp: normalize_timestamp(timestamp),
min_resolution: 1e-7,
};
writer.finish(global)
}
struct Lists {
vertex_list: i64,
edge_list: i64,
vertex_index: HashMap<u64, usize>,
edge_index: HashMap<u64, usize>,
band: f64,
}
pub(super) fn write_solid(writer: &mut IgesWriter, body: &IgesBody<'_>, status: &'static str) -> Result<i64, String> {
let solid = body.solid;
if solid.shells.is_empty() {
return Err(format!("iges_export: solid '{}' has no shells", body.name));
}
let shells = write_shells(writer, solid)?;
let mut params = vec![shells[0].to_string(), "1".into(), (shells.len() - 1).to_string()];
for void in &shells[1..] {
params.push(void.to_string());
params.push("1".into());
}
let label: String = body.name.chars().filter(|c| !c.is_whitespace()).take(8).collect();
let property = (!body.name.is_empty()).then(|| {
let mut name = IgesEntity::new(406, vec!["1".into(), hollerith(&body.name)]);
name.form = 15;
writer.add_entity(name)
});
let color = body.color.map(|[r, g, b]| {
let percent = |c: u8| real_field(f64::from(c) * 100.0 / 255.0);
writer.add_entity(IgesEntity::new(
314,
vec![percent(r), percent(g), percent(b), hollerith(&body.name)],
))
});
if let Some(property) = property {
params.extend(["0".into(), "1".into(), property.to_string()]);
}
let mut entity = IgesEntity::new(186, params);
entity.label = label;
entity.color = color.map_or(0, |pointer| -pointer);
entity.status = status;
Ok(writer.add_entity(entity))
}
pub(super) fn write_shells(writer: &mut IgesWriter, solid: &BrepSolid) -> Result<Vec<i64>, String> {
let lists = write_lists(writer, solid)?;
let mut shells = Vec::with_capacity(solid.shells.len());
for shell in &solid.shells {
let mut faces = Vec::with_capacity(shell.faces.len() * 2);
for face in &shell.faces {
let (pointer, agrees) = write_face(writer, solid, &lists, face)?;
faces.push(pointer.to_string());
faces.push(flag(agrees));
}
let mut params = vec![shell.faces.len().to_string()];
params.extend(faces);
let mut entity = IgesEntity::new(514, params);
entity.form = 1;
shells.push(writer.add_entity(entity));
}
Ok(shells)
}
fn flag(value: bool) -> String {
if value { "1" } else { "0" }.into()
}
fn write_lists(writer: &mut IgesWriter, solid: &BrepSolid) -> Result<Lists, String> {
let mut vertex_index = HashMap::default();
let mut vertex_params = vec![solid.vertices.len().to_string()];
for (index, vertex) in solid.vertices.iter().enumerate() {
vertex_index.insert(vertex.id, index + 1);
vertex_params.extend([real_field(vertex.point.x), real_field(vertex.point.y), real_field(vertex.point.z)]);
}
let mut vertex_entity = IgesEntity::new(502, vertex_params);
vertex_entity.form = 1;
let vertex_list = writer.add_entity(vertex_entity);
let mut edge_index = HashMap::default();
let edges: Vec<&EdgeRecord> = solid.edges.iter().filter(|edge| !edge.degenerate).collect();
let mut edge_params = vec![edges.len().to_string()];
for (index, edge) in edges.iter().enumerate() {
edge_index.insert(edge.id, index + 1);
let curve = writer.add_entity(curve_to_126(&edge_subcurve(edge)?)?);
let start = vertex_of(&vertex_index, edge.start_vertex_id)?;
let end = vertex_of(&vertex_index, edge.end_vertex_id)?;
edge_params.extend([
curve.to_string(),
vertex_list.to_string(),
start.to_string(),
vertex_list.to_string(),
end.to_string(),
]);
}
let mut edge_entity = IgesEntity::new(504, edge_params);
edge_entity.form = 1;
let edge_list = writer.add_entity(edge_entity);
let band = KernelTolerances::for_solid(solid, 1e-7).export_knit;
Ok(Lists { vertex_list, edge_list, vertex_index, edge_index, band })
}
fn vertex_of(index: &HashMap<u64, usize>, id: u64) -> Result<usize, String> {
index.get(&id).copied().ok_or_else(|| format!("iges_export: dangling vertex id {id}"))
}
fn write_face(
writer: &mut IgesWriter,
solid: &BrepSolid,
lists: &Lists,
face: &FaceRecord,
) -> Result<(i64, bool), String> {
if face.loops.is_empty() {
return Err(format!("iges_export: face {} has no loops", face.id));
}
let (surface, reversed, carrier) = write_surface(writer, &face.surface)?;
let agrees = face.same_sense != reversed;
let mut loops = Vec::with_capacity(face.loops.len());
for loop_record in &face.loops {
loops.push(write_loop(writer, solid, lists, face, loop_record, agrees, &carrier)?);
}
let mut params = vec![surface.to_string(), face.loops.len().to_string(), "1".into()];
params.extend(loops.iter().map(i64::to_string));
let mut entity = IgesEntity::new(510, params);
entity.form = 1;
Ok((writer.add_entity(entity), agrees))
}
fn write_loop(
writer: &mut IgesWriter,
solid: &BrepSolid,
lists: &Lists,
face: &FaceRecord,
loop_record: &LoopRecord,
agrees: bool,
carrier: &Carrier,
) -> Result<i64, String> {
let mut uses: Vec<(&CoedgeRecord, bool)> = loop_record.coedges.iter().map(|c| (c, c.forward)).collect();
if !agrees {
uses.reverse();
for (_, forward) in &mut uses {
*forward = !*forward;
}
}
let mut entries = Vec::with_capacity(uses.len());
for (coedge, forward) in uses {
let edge_id = coedge.edge_id;
let edge = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| format!("iges_export: dangling edge id {edge_id}"))?;
let entry: Vec<String> = if edge.degenerate {
let vertex = vertex_of(&lists.vertex_index, edge.start_vertex_id)?;
vec!["1".into(), lists.vertex_list.to_string(), vertex.to_string(), "1".into(), "0".into()]
} else {
let index = lists.edge_index.get(&edge_id).copied().ok_or("iges_export: edge missing from its list")?;
let mut entry = vec!["0".into(), lists.edge_list.to_string(), index.to_string(), flag(forward)];
match parameter_curve(carrier, &face.surface, lists.band, edge, coedge)? {
Some(curve) => {
let curve = if forward { curve } else { curve.reversed()? };
let pointer = writer.add_entity(curve_to_126(&curve)?);
entry.extend(["1".into(), "0".into(), pointer.to_string()]);
}
None => entry.push("0".into()),
}
entry
};
entries.push(entry);
}
let mut params = vec![entries.len().to_string()];
for entry in entries {
params.extend(entry);
}
let mut entity = IgesEntity::new(508, params);
entity.form = 1;
Ok(writer.add_entity(entity))
}
enum Carrier {
Plane { origin: Vec3, x_axis: Vec3, y_axis: Vec3 },
Spline,
Quadric { surface: EmittedSurface, angular_v: bool },
Unparameterized,
}
const DEGREES: f64 = 180.0 / std::f64::consts::PI;
fn parameter_curve(
carrier: &Carrier,
stored: &NurbsSurface,
band: f64,
edge: &EdgeRecord,
coedge: &CoedgeRecord,
) -> Result<Option<NurbsCurve>, String> {
match carrier {
Carrier::Unparameterized => Ok(None),
Carrier::Quadric { surface, angular_v } => {
let curve = edge_subcurve(edge)?;
let [t0, t1] = curve.domain()?;
let oriented = if coedge.forward { coedge.pcurve.clone() } else { coedge.pcurve.reversed()? };
let outcome = build_pcurve(stored, surface, &EmittedCurve::Spline { curve }, &oriented, band)?;
let v_scale = if *angular_v { DEGREES } else { 1.0 };
let scaled = |c: Vec4| Vec4 { x: c.x * DEGREES, y: c.y * v_scale, z: 0.0, w: c.w };
Ok(match outcome.curve {
Some(Pcurve2d::Line { point, vector }) => {
let at = |s: f64| Vec4 { x: point[0] + s * vector[0], y: point[1] + s * vector[1], z: 0.0, w: 1.0 };
Some(NurbsCurve::new(1, vec![t0, t0, t1, t1], vec![scaled(at(t0)), scaled(at(t1))])?)
}
Some(Pcurve2d::Spline(curve)) => {
let points = curve.control_points.iter().map(|&c| scaled(c)).collect();
Some(NurbsCurve::new(curve.degree, curve.knots.clone(), points)?)
}
Some(Pcurve2d::Circle { .. }) | None => None,
})
}
Carrier::Plane { origin, x_axis, y_axis } => {
Ok(Some(curve_to_plane_parameters(&edge_subcurve(edge)?, *origin, *x_axis, *y_axis)?))
}
Carrier::Spline => {
let mut curve = if coedge.forward { coedge.pcurve.clone() } else { coedge.pcurve.reversed()? };
let [q0, q1] = curve.domain()?;
let [t0, t1] = edge_subcurve(edge)?.domain()?;
if q1 <= q0 || t1 <= t0 {
return Ok(None);
}
let scale = (t1 - t0) / (q1 - q0);
for knot in &mut curve.knots {
*knot = t0 + (*knot - q0) * scale;
}
Ok(Some(curve))
}
}
}
fn write_surface(writer: &mut IgesWriter, surface: &NurbsSurface) -> Result<(i64, bool, Carrier), String> {
if let Some(analytic) = surface.analytic() {
if let Some((pointer, reversed)) = write_analytic(writer, surface, analytic)? {
let emitted = |frame: &crate::analytic_surface::RevolutionFrame| EmittedFrame {
origin: frame.origin,
x_axis: frame.x_axis,
y_axis: frame.y_axis,
axis: frame.axis,
azimuth_sign: 1.0,
};
let carrier = match analytic {
AnalyticSurface::Plane { origin, u_dir, v_dir, .. } => {
let normal = u_dir.cross(*v_dir).normalized()?;
let x_axis = u_dir.normalized()?;
Carrier::Plane { origin: *origin, x_axis, y_axis: normal.cross(x_axis) }
}
AnalyticSurface::RuledRevolution { frame, rho0, rho1, .. }
if (rho1 - rho0).abs() <= 1e-9 * (1.0 + rho0.abs().max(rho1.abs())) =>
{
Carrier::Quadric {
surface: EmittedSurface::Cylinder { frame: emitted(frame), radius: *rho0 },
angular_v: false,
}
}
AnalyticSurface::RuledRevolution { .. } => Carrier::Unparameterized,
AnalyticSurface::Sphere { frame, radius } => Carrier::Quadric {
surface: EmittedSurface::Sphere { frame: emitted(frame), radius: *radius },
angular_v: true,
},
AnalyticSurface::Torus { major_radius, minor_radius, .. }
if ((major_radius - minor_radius) - minor_radius).abs() <= 1e-6 * major_radius.abs().max(1.0) =>
{
Carrier::Unparameterized
}
AnalyticSurface::Torus { frame, major_radius, minor_radius } => Carrier::Quadric {
surface: EmittedSurface::Torus {
frame: emitted(frame),
major_radius: *major_radius,
minor_radius: *minor_radius,
},
angular_v: true,
},
AnalyticSurface::Revolution { .. } => Carrier::Unparameterized,
};
return Ok((pointer, reversed, carrier));
}
}
Ok((writer.add_entity(surface_to_128(surface)?), false, Carrier::Spline))
}
fn point_116(writer: &mut IgesWriter, p: Vec3) -> i64 {
writer.add_entity(IgesEntity::new(116, vec![real_field(p.x), real_field(p.y), real_field(p.z)]))
}
fn direction_123(writer: &mut IgesWriter, d: Vec3) -> i64 {
writer.add_entity(IgesEntity::new(123, vec![real_field(d.x), real_field(d.y), real_field(d.z)]))
}
fn sampled_normal(surface: &NurbsSurface) -> Result<(Vec3, Vec3), String> {
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
let (u, v) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
Ok((surface.evaluate(u, v)?, surface.normal(u, v)?))
}
fn write_analytic(
writer: &mut IgesWriter,
surface: &NurbsSurface,
analytic: &AnalyticSurface,
) -> Result<Option<(i64, bool)>, String> {
let (point, normal) = sampled_normal(surface)?;
match analytic {
AnalyticSurface::Plane { origin, u_dir, v_dir, .. } => {
let (Ok(n), Ok(x)) = (u_dir.cross(*v_dir).normalized(), u_dir.normalized()) else {
return Ok(None);
};
let location = point_116(writer, *origin);
let normal = direction_123(writer, n);
let reference = direction_123(writer, x);
let mut entity = IgesEntity::new(190, vec![location.to_string(), normal.to_string(), reference.to_string()]);
entity.form = 1;
Ok(Some((writer.add_entity(entity), false)))
}
AnalyticSurface::RuledRevolution { frame, rho0, rho1, height } => {
let outward = {
let d = point.sub(frame.origin);
d.sub(frame.axis.scale(d.dot(frame.axis)))
};
let reversed = normal.dot(outward) < 0.0;
let radius_scale = 1.0 + rho0.abs().max(rho1.abs());
if (rho1 - rho0).abs() <= 1e-9 * radius_scale {
if *rho0 <= 0.0 {
return Ok(None);
}
let location = point_116(writer, frame.origin);
let axis = direction_123(writer, frame.axis);
let reference = direction_123(writer, frame.x_axis);
let mut entity = IgesEntity::new(
192,
vec![location.to_string(), axis.to_string(), real_field(*rho0), reference.to_string()],
);
entity.form = 1;
return Ok(Some((writer.add_entity(entity), reversed)));
}
let slope = (rho1 - rho0) / height;
let apex_margin = 2.0 * (1e-4 * height.abs().max(1.0) + 1e-9) * slope.abs();
if rho0.min(*rho1) <= apex_margin {
return Ok(None);
}
let direction = if slope >= 0.0 { frame.axis } else { frame.axis.scale(-1.0) };
let location = point_116(writer, frame.origin);
let axis = direction_123(writer, direction);
let reference = direction_123(writer, frame.x_axis);
let mut entity = IgesEntity::new(
194,
vec![
location.to_string(),
axis.to_string(),
real_field(*rho0),
real_field(slope.abs().atan().to_degrees()),
reference.to_string(),
],
);
entity.form = 1;
Ok(Some((writer.add_entity(entity), reversed)))
}
AnalyticSurface::Sphere { frame, radius } => {
let reversed = normal.dot(point.sub(frame.origin)) < 0.0;
let location = point_116(writer, frame.origin);
let axis = direction_123(writer, frame.axis);
let reference = direction_123(writer, frame.x_axis);
let mut entity = IgesEntity::new(
196,
vec![location.to_string(), real_field(*radius), axis.to_string(), reference.to_string()],
);
entity.form = 1;
Ok(Some((writer.add_entity(entity), reversed)))
}
AnalyticSurface::Torus { frame, major_radius, minor_radius } => {
let tube_centre = {
let d = point.sub(frame.origin);
let radial = d.sub(frame.axis.scale(d.dot(frame.axis)));
let Ok(direction) = radial.normalized() else {
return Ok(None);
};
frame.origin.add(direction.scale(*major_radius))
};
let reversed = normal.dot(point.sub(tube_centre)) < 0.0;
let location = point_116(writer, frame.origin);
let axis = direction_123(writer, frame.axis);
let reference = direction_123(writer, frame.x_axis);
let mut entity = IgesEntity::new(
198,
vec![
location.to_string(),
axis.to_string(),
real_field(*major_radius),
real_field(*minor_radius),
reference.to_string(),
],
);
entity.form = 1;
Ok(Some((writer.add_entity(entity), reversed)))
}
AnalyticSurface::Revolution { .. } => Ok(None),
}
}
fn units_for(unit: &str) -> (i64, String) {
match unit.trim().to_ascii_lowercase().as_str() {
"inch" | "inches" | "in" => (1, "IN".into()),
"foot" | "feet" | "ft" => (4, "FT".into()),
"meter" | "metre" | "m" => (6, "M".into()),
"centimeter" | "centimetre" | "cm" => (10, "CM".into()),
_ => (2, "MM".into()),
}
}
fn normalize_timestamp(timestamp: &str) -> String {
if timestamp.trim().is_empty() {
"00000000.000000".into()
} else {
timestamp.to_string()
}
}