use super::*;
use crate::feature_pipeline::pmi::PmiState;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct StepProduct {
pub pd_ref: usize,
pub name: String,
pub id: String,
pub bodies: Vec<BrepSolid>,
pub appearances: Vec<BodyAppearance>,
pub failed_bodies: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pmi: Option<PmiState>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct StepOccurrence {
pub edge_ref: usize,
pub parent: usize,
pub child: usize,
pub designator: String,
pub placement: [f64; 16],
pub rigid: bool,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct StepAssembly {
pub products: Vec<StepProduct>,
pub occurrences: Vec<StepOccurrence>,
pub roots: Vec<usize>,
pub first_error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pmi: Option<PmiState>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pmi_diagnostics: Vec<String>,
#[serde(default)]
pub sheets_skipped: usize,
}
pub fn read_step_assembly(text: &str) -> Result<Option<StepAssembly>, String> {
if !text.contains("ISO-10303-21") {
return Err("step_import: not an ISO-10303-21 Part 21 file".into());
}
let entities = parse_data_section(text)?;
let global_scale = derive_length_scale_mm(&entities);
let edges = structure_edges(&entities, &Resolver::new(&entities, global_scale));
if edges.is_empty() {
return Ok(None); }
let mut scales = ProductScales::new(&entities, global_scale);
let ResolvedEdges {
edges,
transforms,
refusals,
} = resolve_structure_edges(&entities, &edges, |node| scales.get(node));
let mut first_error: Option<String> = refusals.first().map(ToString::to_string);
let mut sheets_skipped = 0usize;
if edges.is_empty() {
return Ok(None); }
let mut pd_refs: Vec<usize> = edges
.iter()
.flat_map(|edge| [edge.parent_pd, edge.child_pd])
.collect();
pd_refs.sort_unstable();
pd_refs.dedup();
let product_index: HashMap<usize, usize> = pd_refs
.iter()
.enumerate()
.map(|(index, &pd)| (pd, index))
.collect();
let styles = StyleTable::read(&entities);
let mut solid_cache: HashMap<(StepBody, u64), Result<(BrepSolid, BodyAppearance), String>> =
HashMap::default();
let mut products: Vec<StepProduct> = Vec::with_capacity(pd_refs.len());
let wants_pmi = super::pmi::file_mentions_pmi(text);
let mut face_refs: super::assembly_pmi::ProductFaceRefs = Vec::with_capacity(pd_refs.len());
for &pd in &pd_refs {
let scale = scales.get(pd);
let resolver = Resolver::new(&entities, scale);
let scale_key = scale.to_bits();
let mut bodies = Vec::new();
let mut appearances = Vec::new();
let mut product_face_refs = Vec::new();
let mut failed_bodies = 0usize;
for body in pd_step_bodies(&entities, &resolver, pd) {
let built = solid_cache.entry((body, scale_key)).or_insert_with(|| {
build_step_body(&resolver, body)
.map(|(solid, _readings)| {
let appearance = step_body_appearance(&resolver, &styles, body, &solid);
(solid, appearance)
})
.map_err(|error| {
format!("step_import: part body {body:?} failed to build: {error}")
})
});
match built {
Ok((solid, appearance)) => {
if wants_pmi {
product_face_refs.push(super::pmi::body_face_refs(&resolver, body, solid));
}
bodies.push(solid.clone());
appearances.push(appearance.clone());
}
Err(_) if matches!(body, StepBody::SurfaceModelShell { .. }) => sheets_skipped += 1,
Err(error) => {
failed_bodies += 1;
if first_error.is_none() {
first_error = Some(error.clone());
}
}
}
}
face_refs.push(product_face_refs);
products.push(StepProduct {
pd_ref: pd,
name: product_name(&resolver, pd),
id: product_id(&resolver, pd),
bodies,
appearances,
failed_bodies,
pmi: None,
});
}
let mut reached: HashSet<usize> = HashSet::default();
{
let resolver = Resolver::new(&entities, global_scale);
walk_occurrences(&resolver, &edges, &transforms, |node| {
reached.insert(node.pd);
});
}
let reaches_geometry = products
.iter()
.any(|product| !product.bodies.is_empty() && reached.contains(&product.pd_ref));
if !reaches_geometry {
return Ok(None);
}
let label_resolver = Resolver::new(&entities, global_scale);
let occurrences: Vec<StepOccurrence> = edges
.iter()
.map(|edge| {
let placement = transforms
.get(&edge.edge_id)
.copied()
.unwrap_or_else(mat4_identity);
StepOccurrence {
edge_ref: edge.edge_id,
parent: product_index[&edge.parent_pd],
child: product_index[&edge.child_pd],
designator: edge_designator(&label_resolver, edge),
placement,
rigid: transform_is_rigid(&placement),
}
})
.collect();
let roots: Vec<usize> = assembly_roots(&edges)
.into_iter()
.map(|pd| product_index[&pd])
.collect();
let mut pmi = super::assembly_pmi::AssemblyPmi::default();
if wants_pmi {
pmi = super::assembly_pmi::lift(
text,
&entities,
&products,
&face_refs,
&occurrences,
&roots,
global_scale,
|pd| scales.get(pd),
);
for (product, state) in products.iter_mut().zip(pmi.per_product.drain(..)) {
product.pmi = state;
}
}
Ok(Some(StepAssembly {
products,
occurrences,
roots,
first_error,
pmi: pmi.assembly,
pmi_diagnostics: pmi.diagnostics,
sheets_skipped,
}))
}
pub(super) struct ProductScales<'a> {
entities: &'a HashMap<usize, Entity>,
global: f64,
cache: HashMap<usize, f64>,
}
impl<'a> ProductScales<'a> {
pub(super) fn new(entities: &'a HashMap<usize, Entity>, global: f64) -> Self {
Self {
entities,
global,
cache: HashMap::default(),
}
}
pub(super) fn get(&mut self, pd: usize) -> f64 {
if let Some(&scale) = self.cache.get(&pd) {
return scale;
}
let scale = pd_root_srs(self.entities, pd)
.into_iter()
.filter_map(|sr| self.entities.get(&sr).and_then(representation_context))
.find_map(|context| context_length_scale_mm(self.entities, context))
.unwrap_or(self.global);
self.cache.insert(pd, scale);
scale
}
}
pub(super) fn transform_is_rigid(matrix: &Mat4) -> bool {
crate::AffineTransform { elements: *matrix }.is_rigid(crate::RIGID_TOL)
}
fn edge_designator(resolver: &Resolver, edge: &AssemblyEdge) -> String {
match edge.kind {
StructureEdgeKind::Nauo => nauo_designator(resolver, edge.edge_id),
StructureEdgeKind::Mapped => mapped_item_designator(resolver, edge.edge_id),
}
}
fn nauo_designator(resolver: &Resolver, nauo: usize) -> String {
let field = |args: &[Value], index: usize| match args.get(index) {
Some(Value::Str(text)) if !text.trim().is_empty() => Some(text.trim().to_string()),
_ => None,
};
(|| {
let args = resolver
.get(nauo)
.ok()?
.find("NEXT_ASSEMBLY_USAGE_OCCURRENCE")?;
field(args, 5)
.or_else(|| field(args, 1))
.or_else(|| field(args, 2))
.or_else(|| field(args, 0))
})()
.unwrap_or_else(|| format!("nauo#{nauo}"))
}