1use crate::topology::BrepSolid;
9use crate::{BodyAppearance, StepAssembly, StepOccurrence, StepProduct};
10use rustc_hash::FxHashMap as HashMap;
11
12use super::reader::{parse_iges, IgesFile};
13use super::structure::{self, Matrix, Placed};
14use super::to_step::{analyse_face_sets, free_shell_to_step, manifold_solid_to_step, FaceSets};
15
16pub fn import_iges(text: &str) -> Result<Vec<BrepSolid>, String> {
18 Ok(import_iges_report(text)?.solids)
19}
20
21#[derive(Clone, Debug)]
24pub struct IgesImportReport {
25 pub solids: Vec<BrepSolid>,
28 pub names: Vec<String>,
32 pub appearances: Vec<BodyAppearance>,
35 pub levels: Vec<i64>,
38 pub stated_resolution_mm: Option<f64>,
41 pub diagnostics: crate::KernelDiagnostics,
44}
45
46pub fn import_iges_report(text: &str) -> Result<IgesImportReport, String> {
49 let file = parse_iges(text)?;
50 let scale = file.global.length_scale_mm()?;
51 let stated = file.global.min_resolution * scale;
52 let mut diagnostics = crate::KernelDiagnostics::default();
53 let bodies = solids_with_diagnostics(&file, scale, &mut diagnostics)?;
54 let mut report = IgesImportReport {
55 solids: Vec::with_capacity(bodies.len()),
56 names: Vec::with_capacity(bodies.len()),
57 appearances: Vec::with_capacity(bodies.len()),
58 levels: Vec::with_capacity(bodies.len()),
59 stated_resolution_mm: (stated > 0.0 && stated.is_finite()).then_some(stated),
60 diagnostics,
61 };
62 for body in bodies {
63 report.solids.push(body.solid);
64 report.names.push(body.name.unwrap_or_default());
65 report.appearances.push(BodyAppearance { body: body.color, faces: Vec::new() });
66 report.levels.push(body.level);
67 }
68 Ok(report)
69}
70
71struct Body {
73 solid: BrepSolid,
74 name: Option<String>,
75 color: Option<crate::ImportedColor>,
76 level: i64,
77}
78
79impl Body {
80 fn bare(solid: BrepSolid) -> Self {
81 Self { solid, name: None, color: None, level: 0 }
82 }
83}
84
85type SolidCache = HashMap<i64, Vec<BrepSolid>>;
87
88fn built_solid<'c>(
89 cache: &'c mut SolidCache,
90 file: &IgesFile,
91 pointer: i64,
92 scale: f64,
93 diagnostics: &mut crate::KernelDiagnostics,
94) -> Result<&'c [BrepSolid], String> {
95 if !cache.contains_key(&pointer) {
96 let (built, measured) = manifold_solid(file, pointer, scale)?;
97 merge_diagnostics(diagnostics, measured);
98 cache.insert(pointer, built);
99 }
100 Ok(&cache[&pointer])
101}
102
103fn place(solid: &BrepSolid, placement: &Matrix) -> Result<BrepSolid, String> {
106 if structure::is_identity(placement) {
107 return Ok(solid.clone());
108 }
109 let transform = crate::AffineTransform::new(*placement)?;
110 let mirrored = transform.determinant3() < 0.0;
111 crate::transform_brep(solid, transform, mirrored)
112}
113
114fn solids_with_diagnostics(
115 file: &IgesFile,
116 scale: f64,
117 diagnostics: &mut crate::KernelDiagnostics,
118) -> Result<Vec<Body>, String> {
119 if file.entities_of_type(186).next().is_some() || file.entities_of_type(514).next().is_some() {
124 refuse_uncovered_geometry(file)?;
125 let mut cache = SolidCache::default();
126 let mut bodies = Vec::new();
127 for Placed { solid, placement, name, color, level } in structure::placed_solids(file, scale)? {
128 for built in built_solid(&mut cache, file, solid, scale, diagnostics)? {
129 bodies.push(Body { solid: place(built, &placement)?, name: name.clone(), color, level });
130 }
131 }
132 bodies.extend(free_shell_bodies(file, scale, diagnostics)?);
133 if bodies.is_empty() {
134 let sheets = diagnostics.counters.get(crate::step_import::SHEETS_SKIPPED).copied().unwrap_or(0);
135 return Err(format!(
136 "iges_import: the file has no solids: its {sheets} shell(s) are sheets (open, or a single untrimmed face), not solids"
137 ));
138 }
139 return Ok(bodies);
140 }
141 if file.entities_of_type(408).next().is_some() {
142 return Err(
143 "iges_import: the file places subfigures (408) of trimmed surfaces, which this import cannot build yet"
144 .into(),
145 );
146 }
147
148 if file.entities_of_type(144).next().is_none() && file.entities_of_type(143).next().is_none() {
149 if file.entities_of_type(128).next().is_some() {
150 return Err(
151 "iges_import: file has untrimmed B-spline surfaces (entity 128) but no trimmed surfaces (entity 144) to form faces"
152 .into(),
153 );
154 }
155 return Err("iges_import: the file has no solids (186) or trimmed surfaces (144, 143) to import".into());
156 }
157 let (solids, measured) = face_set_solids(file, scale)?;
158 for each in measured {
159 merge_diagnostics(diagnostics, each);
160 }
161 Ok(solids.into_iter().map(Body::bare).collect())
162}
163
164pub fn read_iges_assembly(text: &str) -> Result<Option<StepAssembly>, String> {
175 let file = parse_iges(text)?;
176 let members = structure::subfigure_member_set(&file)?;
177 let top_instances: Vec<i64> = file
178 .pointers_of_type(408)
179 .into_iter()
180 .map(|(pointer, _)| pointer)
181 .filter(|pointer| !members.contains(pointer))
182 .collect();
183 if top_instances.is_empty() {
184 return Ok(None);
185 }
186 let scale = file.global.length_scale_mm()?;
188 if file.entities_of_type(186).next().is_none() {
189 return Err(
190 "iges_import: the file places subfigures (408) of trimmed surfaces, which this import cannot build yet"
191 .into(),
192 );
193 }
194 refuse_uncovered_geometry(&file)?;
195 structure::placed_solids(&file, scale)?;
196
197 let mut cache = SolidCache::default();
198 let mut diagnostics = crate::KernelDiagnostics::default();
199 let root_name = {
200 let product = file.global.product_id.trim();
201 if product.is_empty() { "IGES".to_string() } else { product.to_string() }
202 };
203 let mut assembly = StepAssembly {
204 products: vec![StepProduct {
205 pd_ref: 0,
206 name: root_name,
207 id: String::new(),
208 bodies: Vec::new(),
209 appearances: Vec::new(),
210 failed_bodies: 0,
211 pmi: None,
212 }],
213 occurrences: Vec::new(),
214 roots: vec![0],
215 first_error: None,
216 pmi: None,
217 pmi_diagnostics: Vec::new(),
218 sheets_skipped: 0,
219 };
220 for (pointer, solid) in file.pointers_of_type(186) {
221 if !members.contains(&pointer) {
222 let color = structure::entity_color(&file, solid);
223 for built in built_solid(&mut cache, &file, pointer, scale, &mut diagnostics)? {
224 assembly.products[0].bodies.push(built.clone());
225 assembly.products[0].appearances.push(BodyAppearance { body: color, faces: Vec::new() });
226 }
227 }
228 }
229 for body in free_shell_bodies(&file, scale, &mut diagnostics)? {
230 assembly.products[0].bodies.push(body.solid);
231 assembly.products[0].appearances.push(BodyAppearance { body: body.color, faces: Vec::new() });
232 }
233 assembly.sheets_skipped = diagnostics.counters.get(crate::step_import::SHEETS_SKIPPED).copied().unwrap_or(0) as usize;
234 let mut products: HashMap<i64, usize> = HashMap::default();
235 for pointer in top_instances {
236 add_instance(&file, scale, pointer, 0, &mut assembly, &mut products, &mut cache, &mut diagnostics, 0)?;
237 }
238 Ok(Some(assembly))
239}
240
241#[allow(clippy::too_many_arguments)]
245fn add_instance(
246 file: &IgesFile,
247 scale: f64,
248 pointer: i64,
249 parent: usize,
250 assembly: &mut StepAssembly,
251 products: &mut HashMap<i64, usize>,
252 cache: &mut SolidCache,
253 diagnostics: &mut crate::KernelDiagnostics,
254 depth: usize,
255) -> Result<(), String> {
256 if depth > 64 {
257 return Err(format!("iges_import: instance DE {pointer} nests deeper than 64 levels"));
258 }
259 let instance = file.entity(pointer)?;
260 let (definition_pointer, definition) = structure::instanced_subfigure(file, pointer)?;
261 let child = match products.get(&definition_pointer) {
262 Some(&index) => index,
263 None => {
264 let index = assembly.products.len();
265 products.insert(definition_pointer, index);
266 assembly.products.push(StepProduct {
267 pd_ref: definition_pointer as usize,
268 name: structure::entity_name(file, definition).unwrap_or_else(|| format!("subfigure#{definition_pointer}")),
269 id: String::new(),
270 bodies: Vec::new(),
271 appearances: Vec::new(),
272 failed_bodies: 0,
273 pmi: None,
274 });
275 for member in structure::subfigure_members(definition)? {
276 let entity = file.entity(member)?;
277 match entity.de.entity_type {
278 186 => {
279 let color = structure::entity_color(file, entity);
280 let built = built_solid(cache, file, member, scale, diagnostics)?.to_vec();
281 let product = &mut assembly.products[index];
282 for solid in built {
283 product.bodies.push(solid);
284 product.appearances.push(BodyAppearance { body: color, faces: Vec::new() });
285 }
286 }
287 408 => add_instance(file, scale, member, index, assembly, products, cache, diagnostics, depth + 1)?,
288 _ => {}
289 }
290 }
291 index
292 }
293 };
294 let placement = structure::instance_matrix(file, instance, scale)?;
295 let rigid = crate::AffineTransform::new(placement).is_ok_and(|transform| transform.is_rigid(1e-9));
296 assembly.occurrences.push(StepOccurrence {
297 edge_ref: pointer as usize,
298 parent,
299 child,
300 designator: structure::entity_name(file, instance).unwrap_or_else(|| format!("instance#{pointer}")),
301 placement,
302 rigid,
303 });
304 Ok(())
305}
306
307fn merge_diagnostics(into: &mut crate::KernelDiagnostics, from: crate::KernelDiagnostics) {
310 into.events.extend(from.events);
311 for (key, count) in from.counters {
312 *into.counters.entry(key).or_insert(0) += count;
313 }
314 for (key, value) in from.measurements {
315 let slot = into.measurements.entry(key).or_insert(value);
316 if value > *slot {
317 *slot = value;
318 }
319 }
320}
321
322fn face_set_solids(
325 file: &IgesFile,
326 scale: f64,
327) -> Result<(Vec<BrepSolid>, Vec<crate::KernelDiagnostics>), String> {
328 let sets = analyse_face_sets(file, scale)?;
329 for body in &sets.bodies {
330 if let Some(reason) = sets.open_reason(body) {
331 return Err(reason);
332 }
333 }
334 let mut built = Vec::with_capacity(sets.bodies.len());
335 for body in &sets.bodies {
336 built.push(face_set_body(&sets, file, scale, body, &[])?);
337 }
338 let mut order: Vec<usize> = (0..built.len()).collect();
340 order.sort_by(|&a, &b| built[b].volume.total_cmp(&built[a].volume));
341 let mut cavities: Vec<Vec<usize>> = vec![Vec::new(); built.len()];
342 let mut is_cavity = vec![false; built.len()];
343 for (rank, &inner) in order.iter().enumerate() {
344 for &outer in &order[..rank] {
345 if !is_cavity[outer] && inside(&built[inner].solid, &built[outer].solid)? {
346 cavities[outer].push(inner);
347 is_cavity[inner] = true;
348 break;
349 }
350 }
351 }
352 let mut solids = Vec::with_capacity(built.len());
353 let mut measured = Vec::with_capacity(built.len());
354 for index in 0..built.len() {
355 if is_cavity[index] {
356 continue;
357 }
358 if cavities[index].is_empty() {
359 solids.push(built[index].solid.clone());
360 measured.push(built[index].diagnostics.clone());
361 continue;
362 }
363 let voids: Vec<(&[usize], bool)> = cavities[index]
364 .iter()
365 .map(|&cavity| (sets.bodies[cavity].as_slice(), !built[cavity].flip))
366 .collect();
367 let with_cavities = face_set_body(&sets, file, scale, &sets.bodies[index], &voids)?;
368 solids.push(with_cavities.solid);
369 measured.push(with_cavities.diagnostics);
370 }
371 Ok((solids, measured))
372}
373
374struct BuiltBody {
375 solid: BrepSolid,
376 volume: f64,
377 flip: bool,
378 diagnostics: crate::KernelDiagnostics,
379}
380
381fn face_set_body(
384 sets: &FaceSets,
385 file: &IgesFile,
386 scale: f64,
387 body: &[usize],
388 voids: &[(&[usize], bool)],
389) -> Result<BuiltBody, String> {
390 let mut first_failure = None;
391 for split in [false, true] {
392 for flip in [false, true] {
393 let text = sets.to_step(file, scale, body, voids, flip, split)?;
394 match solids_or_refusal(&text) {
395 Ok((mut solids, diagnostics)) if solids.len() == 1 => {
396 let solid = solids.remove(0);
397 let volume = crate::solid_signed_volume(&solid)?;
398 if volume > 0.0 {
399 return Ok(BuiltBody { solid, volume, flip, diagnostics });
400 }
401 }
402 Ok(_) => {}
403 Err(error) => {
404 first_failure.get_or_insert(error);
405 }
406 }
407 }
408 }
409 Err(first_failure.unwrap_or_else(|| {
410 format!("iges_import: a set of {} trimmed surfaces built no solid with positive volume", body.len())
411 }))
412}
413
414fn inside(inner: &BrepSolid, outer: &BrepSolid) -> Result<bool, String> {
416 let Some(vertex) = inner.vertices.first() else {
417 return Ok(false);
418 };
419 let reading = crate::classify_point(vertex.point, outer, 1e-6)?;
420 Ok(reading.class == crate::PointClass::In)
421}
422
423fn free_shells(file: &IgesFile) -> Result<Vec<i64>, String> {
426 let mut used_shells: rustc_hash::FxHashSet<i64> = Default::default();
427 for (_, solid) in file.pointers_of_type(186) {
428 used_shells.insert(solid.int(0)?);
429 for index in 0..solid.int(2)?.max(0) as usize {
430 used_shells.insert(solid.int(3 + index * 2)?);
431 }
432 }
433 Ok(file
434 .pointers_of_type(514)
435 .into_iter()
436 .map(|(pointer, _)| pointer)
437 .filter(|pointer| !used_shells.contains(pointer))
438 .collect())
439}
440
441fn refuse_uncovered_geometry(file: &IgesFile) -> Result<(), String> {
444 let free_faces = [144, 143]
445 .iter()
446 .flat_map(|&kind| file.pointers_of_type(kind))
447 .filter(|(_, entity)| entity.de.subordinate == 0)
448 .count();
449 if free_faces > 0 {
450 return Err(format!(
451 "iges_import: the file holds {free_faces} trimmed surface(s) outside its solids, which this import cannot build yet; importing the solids alone would leave them out"
452 ));
453 }
454 Ok(())
455}
456
457fn free_shell_bodies(
462 file: &IgesFile,
463 scale: f64,
464 diagnostics: &mut crate::KernelDiagnostics,
465) -> Result<Vec<Body>, String> {
466 let mut bodies = Vec::new();
467 for pointer in free_shells(file)? {
468 let entity = file.entity(pointer)?;
469 let whole_text = free_shell_to_step(file, pointer, scale, false)?;
470 let report = match surface_model_report(&whole_text) {
471 Ok(report) => report,
472 Err(first_failure) => {
473 let split_text = free_shell_to_step(file, pointer, scale, true)?;
474 if split_text == whole_text {
475 return Err(format!("shell DE {pointer}: {first_failure}"));
476 }
477 surface_model_report(&split_text).map_err(|_| format!("shell DE {pointer}: {first_failure}"))?
478 }
479 };
480 if report.solids.is_empty() && report.sheets_skipped == 0 {
481 diagnostics.count_n(crate::step_import::SHEETS_SKIPPED, 1);
484 }
485 merge_diagnostics(diagnostics, report.diagnostics);
486 for solid in report.solids {
487 bodies.push(Body {
488 solid,
489 name: structure::entity_name(file, entity),
490 color: structure::entity_color(file, entity),
491 level: entity.de.level,
492 });
493 }
494 }
495 Ok(bodies)
496}
497
498fn surface_model_report(step: &str) -> Result<crate::StepImportReport, String> {
501 let report = crate::import_step_report(step).map_err(|error| format!("iges_import: {error}"))?;
502 if report.failed > 0 {
503 return Err(format!(
504 "iges_import: {}",
505 report.first_error.as_deref().unwrap_or("the shell did not import")
506 ));
507 }
508 for solid in &report.solids {
509 collapsed_face_refusal(solid)?;
510 }
511 Ok(report)
512}
513
514fn manifold_solid(file: &IgesFile, pointer: i64, scale: f64) -> Result<Built, String> {
519 let whole_text = manifold_solid_to_step(file, pointer, scale, false)?;
520 let first_failure = match solids_or_refusal(&whole_text) {
521 Ok(built) => return Ok(built),
522 Err(error) => error,
523 };
524 let split_text = manifold_solid_to_step(file, pointer, scale, true)?;
525 if split_text == whole_text {
526 return Err(format!("solid DE {pointer}: {first_failure}"));
527 }
528 solids_or_refusal(&split_text).map_err(|_| format!("solid DE {pointer}: {first_failure}"))
529}
530
531type Built = (Vec<BrepSolid>, crate::KernelDiagnostics);
536
537fn solids_or_refusal(step: &str) -> Result<Built, String> {
538 let report = crate::import_step_report(step).map_err(|error| format!("iges_import: {error}"))?;
539 if report.failed > 0 {
540 return Err(format!(
541 "iges_import: {}",
542 report.first_error.as_deref().unwrap_or("the body did not import")
543 ));
544 }
545 if report.solids.is_empty() {
546 return Err("iges_import: the solid produced no body".into());
547 }
548 for solid in &report.solids {
549 collapsed_face_refusal(solid)?;
550 }
551 Ok((report.solids, report.diagnostics))
552}
553
554const COLLAPSED_FACE_RATIO: f64 = 1e-5;
558
559fn collapsed_face_refusal(solid: &BrepSolid) -> Result<(), String> {
563 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
564 let (Ok(area), Ok(length)) = (crate::face_area(face), crate::face_boundary_length(solid, face)) else {
565 continue;
566 };
567 if length > 0.0 && area < COLLAPSED_FACE_RATIO * length * length {
568 return Err(format!(
569 "iges_import: a face collapsed on import, enclosing {area:.3e} mm2 inside a boundary {length:.3e} mm long"
570 ));
571 }
572 }
573 Ok(())
574}