Skip to main content

brep_kernel/io/step_import/
mod.rs

1//! STEP Part 21 import for NURBS and analytic surfaces, assemblies, and PMI.
2//!
3//! The importer reconstructs and validates BREP topology. Analytic carriers are
4//! converted to NURBS sized to the face bounds. Seam edges occupy opposite
5//! parameter boundaries, and collapsed pole bounds become degenerate edges so
6//! faces close and integrate correctly.
7//!
8//! Pcurves are DERIVED, by projecting each edge onto its carrier; a
9//! `SURFACE_CURVE`/`SEAM_CURVE` bundle contributes only its 3D curve.
10//!
11//! [`supplied`] can read the bundle's `PCURVE` associations instead and seat
12//! the vendor's own stated trim, and does so correctly, but it is OFF unless
13//! `BREP_SUPPLIED_PCURVES=1`. Not caution — measurement. Preferring a supplied
14//! pcurve helps only where the edge's 3D curve has drifted off its own carrier,
15//! and nothing distinguishes that case from the ones where it hurts: our own
16//! exporter writes exact analytic 3D curves with fitted pcurves, so seating the
17//! fit cost a round trip through our writer a factor of 4800 in volume
18//! fidelity. See `supplied::supplied_pcurves_enabled` for the full derivation.
19
20use crate::topology::{
21    BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
22};
23use crate::{
24    build_pcurve_on_surface_range, make_arc, make_extrusion, make_hyperbola, make_line,
25    make_parabola, make_plane, make_revolution, offset_surface, transform_brep, AffineTransform,
26    NurbsCurve, NurbsSurface, Vec3, Vec4,
27};
28use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
29use std::collections::VecDeque;
30
31const TAU: f64 = std::f64::consts::TAU;
32
33pub(crate) mod parse;
34mod supplied;
35mod bodies;
36mod assembly;
37mod assembly_pmi;
38mod mapped;
39mod builder;
40mod geometry;
41mod styles;
42mod pmi;
43mod precision;
44mod trim_readings;
45
46use crate::appearance::{BodyAppearance, ImportedColor};
47use parse::*;
48use supplied::*;
49use bodies::*;
50use mapped::*;
51use builder::*;
52use geometry::*;
53use styles::*;
54pub use assembly::{read_step_assembly, StepAssembly, StepOccurrence, StepProduct};
55pub use assembly_pmi::{occurrence_ref_parts, rewrite_occurrence_refs, OCCURRENCE_REF_PREFIX};
56pub use mapped::StepMappedItemError;
57pub use pmi::read_step_pmi;
58pub use precision::STATED_PRECISION_INCONSISTENCY_RATIO;
59pub use trim_readings::{
60    import_step_trim_readings, StepBodyTrimReadings, StepEdgeReading, StepFaceReading,
61    StepSuppliedTrim, StepTrimReadings,
62};
63pub(crate) use pmi::decode_step_text as decode_step_text_for_tests;
64
65
66// ---------------------------------------------------------------------------
67// Topology assembly
68// ---------------------------------------------------------------------------
69
70/// Public entry: parse a STEP Part 21 document and return one `BrepSolid` per
71/// MANIFOLD_SOLID_BREP, FACETED_BREP, or certified BREP_WITH_VOIDS. Every
72/// returned solid passes `validate()`.
73pub fn import_step(text: &str) -> Result<Vec<BrepSolid>, String> {
74    Ok(import_step_with_appearance(text)?.0)
75}
76
77/// [`import_step`] plus the file's PRESENTATION colours: one
78/// [`BodyAppearance`] per returned solid, in the same order.
79///
80/// This is the lane the IMPORT3D feature uses, because colour is stamped as
81/// scene metadata on the names IT chooses — see
82/// `feature_pipeline::features::import3d` and `io/appearance.rs` for the record
83/// shape. A file with no presentation entities returns default (empty)
84/// appearances, never a short vector, so callers can `zip` unconditionally.
85pub fn import_step_with_appearance(
86    text: &str,
87) -> Result<(Vec<BrepSolid>, Vec<BodyAppearance>), String> {
88    let imported = collect_step_solids(text)?;
89    if imported.solids.is_empty() {
90        return Err(imported.first_error.unwrap_or_else(|| {
91            "step_import: no MANIFOLD_SOLID_BREP/FACETED_BREP/BREP_WITH_VOIDS body imported (unsupported or invalid representation)"
92                .into()
93        }));
94    }
95    Ok((imported.solids, imported.appearances))
96}
97
98/// What [`import_step_report`] returns: the bodies, how gracefully the file
99/// imported, and the import's diagnostics.
100#[derive(Clone, Debug)]
101pub struct StepImportReport {
102    /// One solid per body that reconstructed, in file order.
103    pub solids: Vec<BrepSolid>,
104    /// Things that did not come in (skipped, not fatal): a body that failed to
105    /// reconstruct, an occurrence whose transform failed, or a mapping that was
106    /// refused (`mapped::StepMappedItemError`).
107    pub failed: usize,
108    /// The first such failure's text, when `failed > 0`.
109    pub first_error: Option<String>,
110    /// Every precision the file states, in millimetres, ascending — see
111    /// `parse::stated_precisions_mm`. Empty when it states none.
112    pub stated_precisions_mm: Vec<f64>,
113    /// The import's diagnostics: measurements always, plus one
114    /// `import.stated_precision_inconsistent` warning when the file's stated
115    /// precision is far under the deviation its own geometry shows
116    /// ([`precision::stated_precision_diagnostics`]). Informational only —
117    /// acceptance never depends on it.
118    pub diagnostics: crate::KernelDiagnostics,
119}
120
121/// The event code for a body that carries a derived trim off the fit bar (the
122/// 1e-7 floor plus its stations' standoff) after the refit at the floor, which
123/// the importer accepts as it did every fit before the bar, and the counters
124/// and measure beside it. The residual
125/// is the band `EntityTolerances` measures for that edge; the event names how
126/// many are the file's residual and how many the fitter's miss, and the worst
127/// with its numbers, so what the importer accepted before the bar silently is
128/// visible (2026-09-26).
129pub const TRIM_BOUNDED: &str = "import.trim_bounded";
130pub const TRIMS_BOUNDED: &str = "import.trims_bounded";
131pub const TRIMS_FITTER_MISS: &str = "import.trims_fitter_miss";
132pub const TRIM_BOUND_MM: &str = "import.trim_bound_mm";
133
134fn bounded_trim_diagnostics(
135    bounded: &[Vec<builder::readings::BoundedTrim>],
136    diagnostics: &mut crate::KernelDiagnostics,
137) {
138    for (index, trims) in bounded.iter().enumerate() {
139        let Some(worst) = trims
140            .iter()
141            .max_by(|a, b| a.residual.partial_cmp(&b.residual).unwrap_or(std::cmp::Ordering::Equal))
142        else {
143            continue;
144        };
145        let fitters = trims.iter().filter(|trim| trim.fitter).count();
146        diagnostics.count_n(TRIMS_BOUNDED, trims.len() as u64);
147        diagnostics.count_n(TRIMS_FITTER_MISS, fitters as u64);
148        diagnostics.measure_max(TRIM_BOUND_MM, worst.residual);
149        diagnostics.event(
150            crate::DiagnosticSeverity::Degraded,
151            crate::KernelStage::Collect,
152            TRIM_BOUNDED,
153            format!(
154                "body {index}: {} trim(s) sit off the {:.0e} mm fit bar and carry their measured residual as their band ({} of them the file's residual, the image on the stations' feet; {} the fitter's miss); worst ADVANCED_FACE #{} (surface #{}) edge {}: {:.3e} mm, its curve {:.3e} mm off the carrier, the image {:.3e} mm off its feet ({}; {} at {} samples).",
155                trims.len(),
156                crate::PCURVE_REFINEMENT_TOLERANCE,
157                trims.len() - fitters,
158                fitters,
159                worst.face_ref,
160                worst.surface_ref,
161                worst.edge_id,
162                worst.residual,
163                worst.standoff,
164                worst.image_to_foot,
165                if worst.fitter { "the fitter's miss" } else { "the file's residual" },
166                worst.exit,
167                worst.samples
168            ),
169        );
170    }
171}
172
173/// Like [`import_step`] but reports how many bodies failed and the first error,
174/// so callers can distinguish a fully- from a partially-imported assembly —
175/// and runs the stated-precision consistency check over the imported bodies,
176/// whose result rides in [`StepImportReport::diagnostics`].
177///
178/// The check measures every edge and vertex of every body (the same
179/// `EntityTolerances` reading the per-entity-tolerance census took), which is
180/// why it lives on this lane and not on [`import_step_with_appearance`]: the
181/// feature pipeline has no diagnostics channel to carry the answer, and a
182/// measurement nobody reads is not worth its cost on every IMPORT3D.
183pub fn import_step_report(text: &str) -> Result<StepImportReport, String> {
184    let imported = collect_step_solids(text)?;
185    let mut diagnostics =
186        precision::stated_precision_diagnostics(&imported.stated_precisions_mm, &imported.solids);
187    bounded_trim_diagnostics(&imported.bounded_trims, &mut diagnostics);
188    Ok(StepImportReport {
189        solids: imported.solids,
190        failed: imported.failed,
191        first_error: imported.first_error,
192        stated_precisions_mm: imported.stated_precisions_mm,
193        diagnostics,
194    })
195}