use super::*;
#[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,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct StepOccurrence {
pub nauo_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>,
}
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 edges = assembly_edges(&entities);
if edges.is_empty() {
return Ok(None); }
let global_scale = derive_length_scale_mm(&entities);
let mut scales = ProductScales::new(&entities, global_scale);
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 mut transforms: HashMap<usize, Mat4> = HashMap::default();
for edge in &edges {
let parent_resolver = Resolver {
entities: &entities,
length_scale: scales.get(edge.parent_pd),
};
transforms.insert(
edge.nauo_id,
edge_transform(&entities, &parent_resolver, edge),
);
}
let styles = StyleTable::read(&entities);
let mut solid_cache: HashMap<(StepBody, u64), Result<(BrepSolid, BodyAppearance), String>> =
HashMap::default();
let mut first_error: Option<String> = None;
let mut products: Vec<StepProduct> = Vec::with_capacity(pd_refs.len());
for &pd in &pd_refs {
let scale = scales.get(pd);
let resolver = Resolver {
entities: &entities,
length_scale: scale,
};
let scale_key = scale.to_bits();
let mut bodies = Vec::new();
let mut appearances = 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| {
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)) => {
bodies.push(solid.clone());
appearances.push(appearance.clone());
}
Err(_) if matches!(body, StepBody::SurfaceModelShell { .. }) => {}
Err(error) => {
failed_bodies += 1;
if first_error.is_none() {
first_error = Some(error.clone());
}
}
}
}
products.push(StepProduct {
pd_ref: pd,
name: product_name(&resolver, pd),
id: product_id(&resolver, pd),
bodies,
appearances,
failed_bodies,
});
}
let mut reached: HashSet<usize> = HashSet::default();
{
let resolver = Resolver {
entities: &entities,
length_scale: 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 {
entities: &entities,
length_scale: global_scale,
};
let occurrences = edges
.iter()
.map(|edge| {
let placement = transforms
.get(&edge.nauo_id)
.copied()
.unwrap_or_else(mat4_identity);
StepOccurrence {
nauo_ref: edge.nauo_id,
parent: product_index[&edge.parent_pd],
child: product_index[&edge.child_pd],
designator: nauo_designator(&label_resolver, edge.nauo_id),
placement,
rigid: transform_is_rigid(&placement),
}
})
.collect();
let roots = assembly_roots(&edges)
.into_iter()
.map(|pd| product_index[&pd])
.collect();
Ok(Some(StepAssembly {
products,
occurrences,
roots,
first_error,
}))
}
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 {
const TOLERANCE: f64 = 1e-9;
let column = |index: usize| [matrix[index], matrix[4 + index], matrix[8 + index]];
let (x, y, z) = (column(0), column(1), column(2));
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
for (a, b, expected) in [
(x, x, 1.0),
(y, y, 1.0),
(z, z, 1.0),
(x, y, 0.0),
(x, z, 0.0),
(y, z, 0.0),
] {
if (dot(a, b) - expected).abs() > TOLERANCE {
return false;
}
}
let cross = [
y[1] * z[2] - y[2] * z[1],
y[2] * z[0] - y[0] * z[2],
y[0] * z[1] - y[1] * z[0],
];
(dot(x, cross) - 1.0).abs() <= TOLERANCE
}
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}"))
}