use rustc_hash::FxHashMap as HashMap;
#[derive(Clone, Debug)]
pub(crate) struct DirectoryEntry {
pub entity_type: i64,
#[allow(dead_code)]
pub form: i64,
}
#[derive(Clone, Debug)]
pub(crate) struct ParsedEntity {
pub de: DirectoryEntry,
pub params: Vec<String>,
}
impl ParsedEntity {
pub fn real(&self, index: usize) -> Result<f64, String> {
let token = self
.params
.get(index)
.ok_or_else(|| format!("iges: entity {} missing param {index}", self.de.entity_type))?;
parse_real(token)
}
pub fn int(&self, index: usize) -> Result<i64, String> {
let token = self
.params
.get(index)
.ok_or_else(|| format!("iges: entity {} missing param {index}", self.de.entity_type))?;
token
.trim()
.parse::<i64>()
.map_err(|_| format!("iges: entity {} param {index} not an integer: {token:?}", self.de.entity_type))
}
}
#[derive(Clone, Debug)]
pub(crate) struct GlobalSection {
pub units_flag: i64,
pub units_name: String,
pub model_scale: f64,
}
impl GlobalSection {
pub fn length_scale_mm(&self) -> Result<f64, String> {
let per_unit = match self.units_flag {
1 => 25.4, 2 => 1.0, 4 => 304.8, 5 => 1_609_344.0, 6 => 1000.0, 7 => 1_000_000.0, 8 => 0.0254, 9 => 0.001, 10 => 10.0, 11 => 25.4e-6, 3 | 0 => match self.units_name.trim().to_ascii_uppercase().as_str() {
"MM" => 1.0,
"IN" | "INCH" | "INCHES" => 25.4,
"M" | "METER" | "METERS" | "METRE" => 1000.0,
"CM" => 10.0,
"FT" | "FEET" => 304.8,
other => {
return Err(format!(
"iges_import: unsupported units name {other:?} (flag {})",
self.units_flag
))
}
},
other => return Err(format!("iges_import: unsupported units flag {other}")),
};
let scale = if self.model_scale.abs() > 1e-12 {
per_unit / self.model_scale
} else {
per_unit
};
Ok(scale)
}
}
pub(crate) struct IgesFile {
pub global: GlobalSection,
pub entities: Vec<ParsedEntity>,
by_pointer: HashMap<i64, usize>,
}
impl IgesFile {
pub fn entity(&self, de_pointer: i64) -> Result<&ParsedEntity, String> {
self.by_pointer
.get(&de_pointer)
.and_then(|&i| self.entities.get(i))
.ok_or_else(|| format!("iges: dangling DE pointer {de_pointer}"))
}
pub fn entities_of_type(&self, entity_type: i64) -> impl Iterator<Item = &ParsedEntity> {
self.entities
.iter()
.filter(move |e| e.de.entity_type == entity_type)
}
}
pub(crate) fn parse_iges(text: &str) -> Result<IgesFile, String> {
let mut global_raw = String::new();
let mut de_lines: Vec<String> = Vec::new();
let mut pd_by_pointer: HashMap<i64, String> = HashMap::default();
let mut pd_order: Vec<i64> = Vec::new();
for raw_line in text.split('\n') {
let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
if line.len() < 73 {
if line.trim().is_empty() {
continue;
}
}
let chars: Vec<char> = line.chars().collect();
if chars.len() < 73 {
continue;
}
let section = chars[72];
let cols_1_72: String = chars[..72.min(chars.len())].iter().collect();
match section {
'S' => { }
'G' => {
global_raw.push_str(&cols_1_72);
}
'D' => {
de_lines.push(cols_1_72);
}
'P' => {
let data: String = chars[..64.min(chars.len())].iter().collect();
let ptr_field: String = if chars.len() >= 72 {
chars[64..72].iter().collect()
} else if chars.len() > 64 {
chars[64..].iter().collect()
} else {
String::new()
};
let de_pointer: i64 = ptr_field
.trim()
.parse()
.map_err(|_| format!("iges: bad PD back-pointer {ptr_field:?}"))?;
if !pd_by_pointer.contains_key(&de_pointer) {
pd_order.push(de_pointer);
}
pd_by_pointer.entry(de_pointer).or_default().push_str(&data);
}
'T' => { }
_ => { }
}
}
let global = parse_global(&global_raw)?;
if de_lines.len() % 2 != 0 {
return Err(format!(
"iges: directory-entry section has an odd line count ({})",
de_lines.len()
));
}
let mut directory: HashMap<i64, DirectoryEntry> = HashMap::default();
for pair_index in 0..de_lines.len() / 2 {
let line1 = &de_lines[pair_index * 2];
let line2 = &de_lines[pair_index * 2 + 1];
let entity_type = de_field(line1, 0)?;
let form = de_field(line2, 4)?;
let de_pointer = (2 * pair_index + 1) as i64;
directory.insert(de_pointer, DirectoryEntry { entity_type, form });
}
let mut entities: Vec<ParsedEntity> = Vec::new();
let mut by_pointer: HashMap<i64, usize> = HashMap::default();
let mut de_pointers: Vec<i64> = directory.keys().copied().collect();
de_pointers.sort_unstable();
for de_pointer in de_pointers {
let de = directory[&de_pointer].clone();
let raw = pd_by_pointer
.get(&de_pointer)
.ok_or_else(|| format!("iges: entity DE {de_pointer} has no parameter data"))?;
let tokens = tokenize_parameters(raw, global.param_delim(), global.record_delim())?;
if tokens.is_empty() {
return Err(format!("iges: entity DE {de_pointer} has empty parameter data"));
}
let pd_type: i64 = tokens[0]
.trim()
.parse()
.map_err(|_| format!("iges: entity DE {de_pointer} has a non-numeric type token {:?}", tokens[0]))?;
if pd_type != de.entity_type {
return Err(format!(
"iges: entity DE {de_pointer} type {} disagrees with its parameter-data type {pd_type}",
de.entity_type
));
}
let params = tokens[1..].to_vec();
by_pointer.insert(de_pointer, entities.len());
entities.push(ParsedEntity { de, params });
}
Ok(IgesFile {
global,
entities,
by_pointer,
})
}
impl GlobalSection {
fn param_delim(&self) -> char {
','
}
fn record_delim(&self) -> char {
';'
}
}
fn de_field(line: &str, index: usize) -> Result<i64, String> {
let chars: Vec<char> = line.chars().collect();
let start = index * 8;
let end = (start + 8).min(chars.len());
if start >= chars.len() {
return Ok(0);
}
let field: String = chars[start..end].iter().collect();
let trimmed = field.trim();
if trimmed.is_empty() {
return Ok(0);
}
trimmed
.parse::<i64>()
.map_err(|_| format!("iges: directory field {index} not an integer: {field:?}"))
}
fn parse_global(raw: &str) -> Result<GlobalSection, String> {
let tokens = tokenize_parameters(raw, ',', ';')?;
let get = |i: usize| tokens.get(i).map(|s| s.as_str()).unwrap_or("");
let model_scale = parse_real(get(12)).unwrap_or(1.0);
let units_flag = get(13).trim().parse::<i64>().unwrap_or(2);
let units_name = get(14).trim().to_string();
Ok(GlobalSection {
units_flag,
units_name: if units_name.is_empty() {
"MM".to_string()
} else {
units_name
},
model_scale,
})
}
fn tokenize_parameters(raw: &str, param_delim: char, record_delim: char) -> Result<Vec<String>, String> {
let chars: Vec<char> = raw.chars().collect();
let mut tokens: Vec<String> = Vec::new();
let mut current = String::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == param_delim {
tokens.push(current.trim().to_string());
current.clear();
i += 1;
continue;
}
if c == record_delim {
tokens.push(current.trim().to_string());
current.clear();
return Ok(tokens);
}
if c == 'H' && !current.trim().is_empty() && current.trim().chars().all(|d| d.is_ascii_digit()) {
let length: usize = current.trim().parse().unwrap_or(0);
current.clear();
let mut literal = String::new();
for _ in 0..length {
i += 1;
if i < chars.len() {
literal.push(chars[i]);
}
}
tokens.push(literal);
i += 1;
if i < chars.len() && (chars[i] == param_delim) {
i += 1;
} else if i < chars.len() && chars[i] == record_delim {
return Ok(tokens);
}
continue;
}
current.push(c);
i += 1;
}
let last = current.trim();
if !last.is_empty() {
tokens.push(last.to_string());
}
Ok(tokens)
}
pub(crate) fn parse_real(token: &str) -> Result<f64, String> {
let t = token.trim();
if t.is_empty() {
return Ok(0.0);
}
let normalized = t.replace(['D', 'd'], "E");
normalized
.parse::<f64>()
.map_err(|_| format!("iges: not a real number: {token:?}"))
}