Skip to main content

brep_kernel/io/iges/
import.rs

1//! IGES to kernel `BrepSolid`, by way of the STEP importer.
2//!
3//! Solids (186) and trimmed-surface sets (144, 143) are translated to Part 21
4//! ([`super::to_step`]) so they get the STEP builder, closure gate and refusals.
5//! Anything that cannot be read correctly is refused by name; a file never
6//! imports short.
7
8use 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
16/// Parse an IGES document and reconstruct one or more `BrepSolid`s.
17pub fn import_iges(text: &str) -> Result<Vec<BrepSolid>, String> {
18    Ok(import_iges_report(text)?.solids)
19}
20
21/// What [`import_iges_report`] returns: the solids, and the diagnostics the
22/// STEP lane measured while building them, merged over every body.
23#[derive(Clone, Debug)]
24pub struct IgesImportReport {
25    /// The solids, in file order: solids standing on their own, then each
26    /// subfigure instance's (408), placed.
27    pub solids: Vec<BrepSolid>,
28    /// Each solid's name (its 406 name property, else its directory label, else
29    /// the name of the instance that placed it), parallel to `solids`; empty
30    /// when the file gives none.
31    pub names: Vec<String>,
32    /// Each solid's colour (314, or the directory colour number), parallel to
33    /// `solids`, in the shape the STEP lane returns colours in.
34    pub appearances: Vec<BodyAppearance>,
35    /// Each solid's level (layer) number from its directory entry, parallel to
36    /// `solids`; 0 when unset, negative for a 406 list of levels.
37    pub levels: Vec<i64>,
38    /// The file's stated minimum resolution in mm (Global field 19), when it
39    /// states one.
40    pub stated_resolution_mm: Option<f64>,
41    /// Measurements and warnings, as `import_step_report` gives for STEP:
42    /// stated-precision consistency, bounded trims, closure readings.
43    pub diagnostics: crate::KernelDiagnostics,
44}
45
46/// [`import_iges`] with the import's diagnostics, the IGES counterpart of
47/// [`crate::import_step_report`].
48pub 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
71/// One imported solid and what the file said about it.
72struct 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
85/// Each 186 built once, through the STEP lane, in its definition frame.
86type 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
103/// A built solid moved by a placement; a mirroring placement turns it inside
104/// out unless the orientation is reversed with it.
105fn 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    // A file with explicit solids or shells goes down the STEP lane whole: same
120    // builder, same closure gate, same refusals. A solid a subfigure defines
121    // comes in once per instance, where the instance puts it; a shell no solid
122    // uses comes in as the STEP lane reads a surface model.
123    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
164/// The file's subfigure structure, the IGES counterpart of
165/// [`crate::read_step_assembly`]: one product for the file itself holding the
166/// solids that stand on their own, one per instanced subfigure (308) holding
167/// its own solids in its own frame, and one occurrence per instance (408) with
168/// its placement in millimetres. `Ok(None)` when the file places no
169/// subfigures, so the caller takes the flat import as for STEP.
170///
171/// Every solid is built once, as in the flat import, and a file the flat
172/// import refuses is refused here with the same words: an assembly never
173/// comes in short.
174pub 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    // The flat import's checks and refusals, once, before any structure.
187    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/// One 408 as an occurrence under `parent`, its subfigure as a product (added
242/// once, however many instances place it), and the subfigure's own instances
243/// beneath it.
244#[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
307/// Fold one body's diagnostics into the file's: events and counts add up, and
308/// each measurement keeps its worst reading, as `measure_max` does.
309fn 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
322/// Build a file of trimmed surfaces: one solid per connected, closed set of
323/// faces, and a set lying inside another becomes its cavity.
324fn 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    // Largest first, so a cavity is always tested against what can hold it.
339    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
381/// One closed face set through the STEP lane, in whichever sense gives it a
382/// positive volume; a twice-walked edge is read as written first.
383fn 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
414/// Whether `inner` lies inside `outer`, judged at one vertex.
415fn 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
423/// The 514 shells no 186 uses, in file order. An OCC-written file carries a
424/// STEP SHELL_BASED_SURFACE_MODEL this way.
425fn 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
441/// Refuse a file holding trimmed surfaces beside its solids. Importing only
442/// the solids would drop them and look like a complete import.
443fn 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
457/// Each free shell through the STEP lane: a closed shell of several faces as
458/// the solid a 186 would make of it, refused by name if it fails as one; a
459/// sheet (open, or a single untrimmed face) is not a solid and is counted in
460/// `SHEETS_SKIPPED`, never dropped silently.
461fn 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            // The STEP lane skips a single-face surface model as reference
482            // geometry without counting it; here it is counted with the sheets.
483            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
498/// A surface model through the STEP lane: what it built, refusing a shell that
499/// reached the solid builder and failed there.
500fn 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
514/// One 186 through the STEP lane. A twice-walked edge on a quadric is told apart
515/// by its position; elsewhere it is read as written first, because it is usually
516/// a seam the builder handles, and as a bridge (`split_at_repeated_edges`) only
517/// when that reading is refused. The first refusal is the one reported.
518fn 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
531/// Import translated STEP, refusing when the body did not build. The STEP lane
532/// skips a body that fails with text and returns the rest, but a file that
533/// comes in short looks like a complete import.
534/// Solids that built, with the diagnostics the STEP lane measured on them.
535type 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
554/// Area over boundary length squared below which a face has collapsed. Measured
555/// on foreign files: collapsed faces read a few times 1e-6. A 1 mm by 100 mm
556/// sliver reads 2.4e-3 and a 0.01 by 100 one 2.5e-5, so real faces clear it.
557const COLLAPSED_FACE_RATIO: f64 = 1e-5;
558
559/// A face with a real boundary that encloses almost nothing is a loop the
560/// builder read the wrong way round the carrier. It validates, and the solid
561/// then quietly lacks that face, so it is refused here.
562fn 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}