use std::collections::HashMap;
use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::Topology;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::{Face, FaceSurface};
use brepkit_topology::shell::Shell;
use brepkit_topology::solid::{Solid, SolidId};
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
use crate::IoError;
pub fn read_iges(input: &str, topo: &mut Topology) -> Result<Vec<SolidId>, IoError> {
let entities = parse_iges_entities(input)?;
build_topology(topo, &entities)
}
#[derive(Debug)]
struct IgesEntity {
entity_type: u32,
params: String,
#[allow(dead_code)]
de_seq: u32,
}
fn parse_iges_entities(input: &str) -> Result<Vec<IgesEntity>, IoError> {
let mut d_lines: Vec<&str> = Vec::new();
let mut p_lines: Vec<&str> = Vec::new();
for line in input.lines() {
if line.len() < 73 {
continue;
}
let section = line.as_bytes().get(72).copied().unwrap_or(b' ');
match section {
b'D' => d_lines.push(line),
b'P' => p_lines.push(line),
_ => {} }
}
let mut dir_entries: Vec<(u32, u32, u32)> = Vec::new();
let mut i = 0;
while i + 1 < d_lines.len() {
let line1 = d_lines[i];
let entity_type = parse_int_field(line1, 0, 8)?;
let pd_start = parse_int_field(line1, 8, 16)?;
let de_seq = parse_int_field(line1, 73, 80)?;
dir_entries.push((entity_type, pd_start, de_seq));
i += 2; }
let mut pd_by_de: HashMap<u32, String> = HashMap::new();
for p_line in &p_lines {
let data_part = if p_line.len() >= 64 {
&p_line[..64]
} else {
p_line
};
let de_ptr = if p_line.len() >= 72 {
parse_int_field(p_line, 64, 72).unwrap_or(0)
} else {
0
};
pd_by_de
.entry(de_ptr)
.or_default()
.push_str(data_part.trim_end());
}
let mut entities = Vec::new();
for (entity_type, _pd_start, de_seq) in &dir_entries {
let params = pd_by_de.get(de_seq).cloned().unwrap_or_default();
let clean_params = strip_entity_prefix(¶ms, *entity_type);
entities.push(IgesEntity {
entity_type: *entity_type,
params: clean_params,
de_seq: *de_seq,
});
}
Ok(entities)
}
fn parse_int_field(line: &str, start: usize, end: usize) -> Result<u32, IoError> {
let end = end.min(line.len());
if start >= end {
return Ok(0);
}
let field = line[start..end].trim();
if field.is_empty() {
return Ok(0);
}
field.parse::<u32>().map_err(|e| IoError::ParseError {
reason: format!("invalid IGES integer field '{field}': {e}"),
})
}
fn strip_entity_prefix(params: &str, entity_type: u32) -> String {
let prefix = format!("{entity_type},");
params.strip_prefix(&prefix).unwrap_or(params).to_string()
}
fn build_topology(topo: &mut Topology, entities: &[IgesEntity]) -> Result<Vec<SolidId>, IoError> {
let mut face_ids = Vec::new();
for entity in entities {
if entity.entity_type == 108
&& let Ok(face_id) = build_plane_face(topo, &entity.params)
{
face_ids.push(face_id);
}
}
if face_ids.is_empty() {
return Ok(Vec::new());
}
let shell = Shell::new(face_ids).map_err(|e| IoError::ParseError {
reason: format!("failed to build shell: {e}"),
})?;
let shell_id = topo.add_shell(shell);
let solid_id = topo.add_solid(Solid::new(shell_id, Vec::new()));
Ok(vec![solid_id])
}
fn build_plane_face(
topo: &mut Topology,
params: &str,
) -> Result<brepkit_topology::face::FaceId, IoError> {
let values = parse_float_params(params);
if values.len() < 4 {
return Err(IoError::ParseError {
reason: format!("IGES plane entity needs 4 params, got {}", values.len()),
});
}
let normal = Vec3::new(values[0], values[1], values[2]);
let d = values[3];
let norm_len = normal.length();
if norm_len < 1e-10 {
return Err(IoError::ParseError {
reason: "IGES plane has zero normal".to_string(),
});
}
let unit_normal = Vec3::new(
normal.x() / norm_len,
normal.y() / norm_len,
normal.z() / norm_len,
);
let origin = Point3::new(
unit_normal.x() * d / norm_len,
unit_normal.y() * d / norm_len,
unit_normal.z() * d / norm_len,
);
let ax = Vec3::new(1.0, 0.0, 0.0);
let ay = Vec3::new(0.0, 1.0, 0.0);
let candidate = if unit_normal.dot(ax).abs() < 0.9 {
ax
} else {
ay
};
let u_dir = unit_normal.cross(candidate);
let u_len = u_dir.length().max(1e-10);
let u_dir = Vec3::new(u_dir.x() / u_len, u_dir.y() / u_len, u_dir.z() / u_len);
let v_dir = unit_normal.cross(u_dir);
let half = 0.5;
let p0 = offset_point(origin, u_dir, -half, v_dir, -half);
let p1 = offset_point(origin, u_dir, half, v_dir, -half);
let p2 = offset_point(origin, u_dir, half, v_dir, half);
let p3 = offset_point(origin, u_dir, -half, v_dir, half);
let v0 = topo.add_vertex(Vertex::new(p0, 1e-7));
let v1 = topo.add_vertex(Vertex::new(p1, 1e-7));
let v2 = topo.add_vertex(Vertex::new(p2, 1e-7));
let v3 = topo.add_vertex(Vertex::new(p3, 1e-7));
let e01 = topo.add_edge(Edge::new(v0, v1, EdgeCurve::Line));
let e12 = topo.add_edge(Edge::new(v1, v2, EdgeCurve::Line));
let e23 = topo.add_edge(Edge::new(v2, v3, EdgeCurve::Line));
let e30 = topo.add_edge(Edge::new(v3, v0, EdgeCurve::Line));
let wire = Wire::new(
vec![
OrientedEdge::new(e01, true),
OrientedEdge::new(e12, true),
OrientedEdge::new(e23, true),
OrientedEdge::new(e30, true),
],
true,
)
.map_err(|e| IoError::ParseError {
reason: format!("failed to build wire: {e}"),
})?;
let wire_id = topo.add_wire(wire);
let surface = FaceSurface::Plane {
normal: unit_normal,
d: d / norm_len,
};
let face_id = topo.add_face(Face::new(wire_id, Vec::new(), surface));
Ok(face_id)
}
fn offset_point(origin: Point3, u: Vec3, a: f64, v: Vec3, b: f64) -> Point3 {
Point3::new(
u.x().mul_add(a, v.x().mul_add(b, origin.x())),
u.y().mul_add(a, v.y().mul_add(b, origin.y())),
u.z().mul_add(a, v.z().mul_add(b, origin.z())),
)
}
fn parse_float_params(params: &str) -> Vec<f64> {
let clean = params.trim_end_matches(';');
clean
.split(',')
.filter_map(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
trimmed.parse::<f64>().ok()
}
})
.collect()
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use brepkit_topology::Topology;
use brepkit_topology::test_utils::make_unit_cube_non_manifold;
use super::*;
use crate::iges::writer;
#[test]
fn roundtrip_unit_cube() {
let mut write_topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut write_topo);
let iges_str = writer::write_iges(&write_topo, &[solid]).unwrap();
let mut read_topo = Topology::new();
let solids = read_iges(&iges_str, &mut read_topo).unwrap();
assert_eq!(solids.len(), 1);
let read_solid = read_topo.solid(solids[0]).unwrap();
let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
assert_eq!(shell.faces().len(), 6);
}
#[test]
fn roundtrip_box_primitive() {
let mut write_topo = Topology::new();
let solid =
brepkit_operations::primitives::make_box(&mut write_topo, 2.0, 3.0, 4.0).unwrap();
let iges_str = writer::write_iges(&write_topo, &[solid]).unwrap();
let mut read_topo = Topology::new();
let solids = read_iges(&iges_str, &mut read_topo).unwrap();
assert_eq!(solids.len(), 1);
}
#[test]
fn empty_file_returns_empty() {
let mut topo = Topology::new();
let solids = read_iges("", &mut topo).unwrap();
assert!(solids.is_empty());
}
#[test]
fn parse_float_params_basic() {
let floats = parse_float_params("1.0,2.5,-3.0,0.;");
assert_eq!(floats.len(), 4);
assert!((floats[0] - 1.0).abs() < 1e-10);
assert!((floats[1] - 2.5).abs() < 1e-10);
assert!((floats[2] - (-3.0)).abs() < 1e-10);
assert!((floats[3]).abs() < 1e-10);
}
#[test]
fn parse_int_field_basic() {
let val = parse_int_field(" 108 1", 0, 8).unwrap();
assert_eq!(val, 108);
}
}