use crate::topology::{
BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
};
use crate::{
build_pcurve_on_surface_range, make_arc, make_extrusion, make_hyperbola, make_line,
make_parabola, make_plane, make_revolution, offset_surface, transform_brep, AffineTransform,
KernelRefusal, NurbsCurve, NurbsSurface, Vec3, Vec4,
};
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::collections::VecDeque;
const TAU: f64 = std::f64::consts::TAU;
pub(crate) mod parse;
mod supplied;
mod bodies;
mod assembly;
mod assembly_pmi;
mod mapped;
mod builder;
mod geometry;
mod styles;
mod pmi;
mod precision;
mod trim_readings;
use crate::appearance::{BodyAppearance, ImportedColor};
use parse::*;
use supplied::*;
use bodies::*;
use mapped::*;
use builder::*;
use geometry::*;
use styles::*;
pub use assembly::{read_step_assembly, StepAssembly, StepOccurrence, StepProduct};
pub use assembly_pmi::{occurrence_ref_parts, rewrite_occurrence_refs, OCCURRENCE_REF_PREFIX};
pub use mapped::StepMappedItemError;
pub use pmi::read_step_pmi;
pub use precision::STATED_PRECISION_INCONSISTENCY_RATIO;
pub use trim_readings::{
import_step_trim_readings, StepBodyTrimReadings, StepEdgeReading, StepFaceReading,
StepSuppliedTrim, StepTrimReadings,
};
pub(crate) use pmi::decode_step_text as decode_step_text_for_tests;
#[derive(Clone, Debug, PartialEq)]
pub enum StepBodyError {
Text(String),
Refused(crate::KernelRefusal),
}
impl StepBodyError {
pub fn message(&self) -> &str {
match self {
Self::Text(text) => text,
Self::Refused(refusal) => &refusal.message,
}
}
pub fn closure_refusal(&self) -> bool {
matches!(
self,
Self::Refused(crate::KernelRefusal {
class: crate::RefusalClass::UnsoundResult {
defect: crate::SoundnessDefect::VectorArea,
..
},
..
})
)
}
pub fn with_context(self, f: impl FnOnce(&str) -> String) -> Self {
match self {
Self::Text(text) => Self::Text(f(&text)),
Self::Refused(refusal) => Self::Refused(refusal.with_message(f)),
}
}
}
impl From<String> for StepBodyError {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<&str> for StepBodyError {
fn from(text: &str) -> Self {
Self::Text(text.to_string())
}
}
impl From<crate::KernelRefusal> for StepBodyError {
fn from(refusal: crate::KernelRefusal) -> Self {
Self::Refused(refusal)
}
}
impl std::fmt::Display for StepBodyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message())
}
}
pub fn import_step(text: &str) -> Result<Vec<BrepSolid>, String> {
Ok(import_step_with_appearance(text)?.0)
}
pub fn import_step_bodies(text: &str) -> Result<ImportedStep, StepBodyError> {
let imported = collect_step_solids(text)?;
if let Some(first) = imported.refused_bodies.first() {
let others = imported.refused_bodies.len() - 1;
let refusal = if others == 0 {
first.clone()
} else {
let rest: Vec<String> = imported.refused_bodies[1..]
.iter()
.map(|refusal| refusal.message.clone())
.collect();
first.clone().with_message(|message| {
format!("{message} And {others} more: {}", rest.join(" "))
})
};
return Err(StepBodyError::Refused(refusal.with_message(|message| {
format!(
"{} of {} bodies refused as open; {}",
imported.refused_bodies.len(),
imported.solids.len() + imported.failed,
message
)
})));
}
if imported.solids.is_empty() {
return Err(StepBodyError::Text(imported.first_error.unwrap_or_else(|| {
"step_import: no MANIFOLD_SOLID_BREP/FACETED_BREP/BREP_WITH_VOIDS body imported (unsupported or invalid representation)"
.into()
})));
}
let approximations = imported
.closure_notes
.iter()
.enumerate()
.filter_map(|(index, note)| note.approximation().map(|a| (index, a.clone())))
.collect();
Ok(ImportedStep {
solids: imported.solids,
appearances: imported.appearances,
approximations,
})
}
#[derive(Clone, Debug)]
pub struct ImportedStep {
pub solids: Vec<BrepSolid>,
pub appearances: Vec<BodyAppearance>,
pub approximations: Vec<(usize, crate::Approximation)>,
}
pub fn import_step_with_appearance(
text: &str,
) -> Result<(Vec<BrepSolid>, Vec<BodyAppearance>), String> {
import_step_bodies(text)
.map(|imported| (imported.solids, imported.appearances))
.map_err(|error| error.to_string())
}
#[derive(Clone, Debug)]
pub struct StepImportReport {
pub solids: Vec<BrepSolid>,
pub failed: usize,
pub first_error: Option<String>,
pub refused: Vec<crate::KernelRefusal>,
pub failed_refusals: Vec<crate::KernelRefusal>,
pub approximations: Vec<(usize, crate::Approximation)>,
pub stated_precisions_mm: Vec<f64>,
pub diagnostics: crate::KernelDiagnostics,
}
pub const TRIM_BOUNDED: &str = "import.trim_bounded";
pub const TRIMS_BOUNDED: &str = "import.trims_bounded";
pub const TRIMS_FITTER_MISS: &str = "import.trims_fitter_miss";
pub const TRIM_BOUND_MM: &str = "import.trim_bound_mm";
pub const TRIMS_CLAMPED: &str = "import.trims_clamped";
pub const TRIM_CLAMP_MM: &str = "import.trim_clamp_mm";
pub const CLOSURE_RATIO_MAX: &str = "import.closure_ratio_max";
pub const CLOSURE_APPROXIMATE: &str = "import.closure_approximate";
pub const CLOSURE_UNJUDGED: &str = "import.closure_unjudged";
pub const CLOSURE_REFUSED: &str = "import.closure_refused";
fn closure_diagnostics(
notes: &[builder::ClosureNote],
refused: &[crate::KernelRefusal],
diagnostics: &mut crate::KernelDiagnostics,
) {
for (index, note) in notes.iter().enumerate() {
match note {
builder::ClosureNote::Closed { residual, bar } => {
diagnostics.measure_max(CLOSURE_RATIO_MAX, residual / bar);
}
builder::ClosureNote::Approximate(approximation) => {
diagnostics.measure_max(CLOSURE_RATIO_MAX, approximation.measured / approximation.bar);
diagnostics.count(CLOSURE_APPROXIMATE);
diagnostics.event(
crate::DiagnosticSeverity::Degraded,
crate::KernelStage::Validate,
CLOSURE_APPROXIMATE,
format!("body {index}: {}", approximation.message),
);
}
builder::ClosureNote::Unmeasured { reason, spans } => {
diagnostics.count(CLOSURE_UNJUDGED);
diagnostics.event(
crate::DiagnosticSeverity::Degraded,
crate::KernelStage::Validate,
CLOSURE_UNJUDGED,
format!("body {index}: shell closure not judged — {reason} ({spans} spans); the body is accepted unjudged, not certified closed."),
);
}
builder::ClosureNote::Unreadable { first, count } => {
diagnostics.count(CLOSURE_UNJUDGED);
diagnostics.event(
crate::DiagnosticSeverity::Degraded,
crate::KernelStage::Validate,
CLOSURE_UNJUDGED,
format!("body {index}: shell closure not judged — {count} face(s) unreadable by the scan, first: {first}; the body is accepted unjudged, not certified closed."),
);
}
builder::ClosureNote::Unjudged => {
diagnostics.count(CLOSURE_UNJUDGED);
}
builder::ClosureNote::Overridden(refusal) => {
diagnostics.count(CLOSURE_UNJUDGED);
diagnostics.event(
crate::DiagnosticSeverity::Degraded,
crate::KernelStage::Validate,
CLOSURE_UNJUDGED,
format!("body {index}: the closure guard is OFF (BREP_IMPORT_CLOSURE_GUARD=0); it would have refused: {}", refusal.message),
);
}
}
}
diagnostics.count_n(CLOSURE_REFUSED, refused.len() as u64);
}
fn bounded_trim_diagnostics(
bounded: &[Vec<builder::readings::BoundedTrim>],
diagnostics: &mut crate::KernelDiagnostics,
) {
for (index, trims) in bounded.iter().enumerate() {
let Some(worst) = trims
.iter()
.max_by(|a, b| a.residual.partial_cmp(&b.residual).unwrap_or(std::cmp::Ordering::Equal))
else {
continue;
};
let fitters = trims.iter().filter(|trim| trim.fitter).count();
let clamped = trims.iter().filter(|trim| trim.clamped_excursion > 0.0).count();
let worst_clamp = trims.iter().map(|trim| trim.clamped_distance).fold(0.0f64, f64::max);
diagnostics.count_n(TRIMS_BOUNDED, trims.len() as u64);
diagnostics.count_n(TRIMS_FITTER_MISS, fitters as u64);
diagnostics.measure_max(TRIM_BOUND_MM, worst.residual);
diagnostics.count_n(TRIMS_CLAMPED, clamped as u64);
diagnostics.measure_max(TRIM_CLAMP_MM, worst_clamp);
diagnostics.event(
crate::DiagnosticSeverity::Degraded,
crate::KernelStage::Collect,
TRIM_BOUNDED,
format!(
"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, within the floor of the curve's standoff read between the stations; {} 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).{}",
trims.len(),
crate::PCURVE_REFINEMENT_TOLERANCE,
trims.len() - fitters,
fitters,
worst.face_ref,
worst.surface_ref,
worst.edge_id,
worst.residual,
worst.standoff,
worst.image_to_foot,
if worst.fitter { "the fitter's miss" } else { "the file's residual" },
worst.exit,
worst.samples,
if clamped > 0 {
format!(" {clamped} of them were clamped into their chart, by up to {worst_clamp:.3e} mm.")
} else {
String::new()
}
),
);
}
}
pub fn import_step_report(text: &str) -> Result<StepImportReport, String> {
let imported = collect_step_solids(text)?;
let mut diagnostics =
precision::stated_precision_diagnostics(&imported.stated_precisions_mm, &imported.solids);
bounded_trim_diagnostics(&imported.bounded_trims, &mut diagnostics);
closure_diagnostics(&imported.closure_notes, &imported.refused_bodies, &mut diagnostics);
Ok(StepImportReport {
solids: imported.solids,
failed: imported.failed,
first_error: imported.first_error,
refused: imported.refused_bodies,
failed_refusals: imported.failed_refusals,
approximations: imported
.closure_notes
.iter()
.enumerate()
.filter_map(|(index, note)| note.approximation().map(|a| (index, a.clone())))
.collect(),
stated_precisions_mm: imported.stated_precisions_mm,
diagnostics,
})
}