use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) enum StepBody {
SolidBrep(usize),
BrepWithVoids(usize),
SurfaceModelShell { model_ref: usize, shell_ref: usize },
}
impl StepBody {
fn order_key(self) -> (usize, usize) {
match self {
Self::SolidBrep(entity_ref) | Self::BrepWithVoids(entity_ref) => (entity_ref, 0),
Self::SurfaceModelShell {
model_ref,
shell_ref,
} => (model_ref, shell_ref),
}
}
}
pub(super) fn step_body_for_entity(entity_ref: usize, entity: &Entity) -> Option<StepBody> {
if entity.has("BREP_WITH_VOIDS") {
Some(StepBody::BrepWithVoids(entity_ref))
} else if entity.has("MANIFOLD_SOLID_BREP") || entity.has("FACETED_BREP") {
Some(StepBody::SolidBrep(entity_ref))
} else {
None
}
}
fn surface_model_shell_refs(resolver: &Resolver, model_ref: usize) -> Option<Vec<usize>> {
let args = resolver
.get(model_ref)
.ok()?
.find("SHELL_BASED_SURFACE_MODEL")?;
let list = args.get(1)?.as_list().ok()?;
let mut refs = Vec::with_capacity(list.len());
for value in list {
refs.push(value.as_ref_id().ok()?);
}
Some(refs)
}
fn push_step_bodies(
resolver: &Resolver,
entity_ref: usize,
entity: &Entity,
out: &mut Vec<StepBody>,
) {
if let Some(body) = step_body_for_entity(entity_ref, entity) {
out.push(body);
return;
}
if entity.has("SHELL_BASED_SURFACE_MODEL") {
if let Some(shell_refs) = surface_model_shell_refs(resolver, entity_ref) {
for shell_ref in shell_refs {
out.push(StepBody::SurfaceModelShell {
model_ref: entity_ref,
shell_ref,
});
}
}
}
}
pub(super) fn build_step_body(resolver: &Resolver, body: StepBody) -> Result<BrepSolid, String> {
match body {
StepBody::SolidBrep(entity_ref) => build_solid(resolver, entity_ref),
StepBody::BrepWithVoids(entity_ref) => build_brep_with_voids(resolver, entity_ref),
StepBody::SurfaceModelShell { shell_ref, .. } => {
build_solid_from_shell(resolver, shell_ref)
}
}
}
pub(super) fn step_body_appearance(
resolver: &Resolver,
styles: &StyleTable,
body: StepBody,
solid: &BrepSolid,
) -> BodyAppearance {
let mut appearance = BodyAppearance::default();
if styles.is_empty() {
return appearance;
}
let shell_refs = step_body_claimed_shells(resolver, body);
let mut body_items: Vec<usize> = match body {
StepBody::SolidBrep(entity_ref) | StepBody::BrepWithVoids(entity_ref) => vec![entity_ref],
StepBody::SurfaceModelShell { model_ref, .. } => vec![model_ref],
};
body_items.extend(shell_refs.as_ref().and_then(|shells| shells.first()).copied());
appearance.body = body_items
.into_iter()
.find_map(|item_ref| styles.color_for(item_ref));
let Some(shell_refs) = shell_refs else {
return appearance;
};
if shell_refs.len() != solid.shells.len() {
return appearance;
}
let mut faces: Vec<Option<ImportedColor>> = Vec::new();
let mut any = false;
for (shell_ref, shell) in shell_refs.into_iter().zip(&solid.shells) {
let Some(face_refs) = shell_face_refs(resolver, shell_ref) else {
return appearance;
};
if face_refs.len() != shell.faces.len() {
return appearance;
}
for face_ref in face_refs {
let color = styles.color_for(face_ref);
any |= color.is_some();
faces.push(color);
}
}
if any {
appearance.faces = faces;
}
appearance
}
fn shell_face_refs(resolver: &Resolver, shell_ref: usize) -> Option<Vec<usize>> {
let entity = resolver.get(shell_ref).ok()?;
let list = entity
.find("CLOSED_SHELL")
.or_else(|| entity.find("OPEN_SHELL"))?
.get(1)?
.as_list()
.ok()?;
list.iter().map(|value| value.as_ref_id().ok()).collect()
}
pub(super) fn step_body_claimed_shells(resolver: &Resolver, body: StepBody) -> Option<Vec<usize>> {
match body {
StepBody::SolidBrep(entity_ref) => resolver
.get(entity_ref)
.ok()?
.find("MANIFOLD_SOLID_BREP")
.or_else(|| resolver.get(entity_ref).ok()?.find("FACETED_BREP"))
.and_then(|args| args.get(1))
.and_then(|value| value.as_ref_id().ok())
.map(|shell_ref| vec![shell_ref]),
StepBody::BrepWithVoids(entity_ref) => {
let args = resolver.get(entity_ref).ok()?.find("BREP_WITH_VOIDS")?;
let mut claims = vec![args.get(1)?.as_ref_id().ok()?];
for oriented_value in args.get(2)?.as_list().ok()? {
let oriented_ref = oriented_value.as_ref_id().ok()?;
let oriented = resolver
.get(oriented_ref)
.ok()?
.find("ORIENTED_CLOSED_SHELL")?;
claims.push(oriented.get(2)?.as_ref_id().ok()?);
}
Some(claims)
}
StepBody::SurfaceModelShell { shell_ref, .. } => Some(vec![shell_ref]),
}
}
fn dedupe_step_bodies(resolver: &Resolver, bodies: &mut Vec<StepBody>) {
bodies.sort_unstable_by_key(|body| body.order_key());
let ordinary_claims: HashSet<usize> = bodies
.iter()
.filter(|body| matches!(body, StepBody::SolidBrep(_)))
.filter_map(|body| step_body_claimed_shells(resolver, *body))
.flatten()
.collect();
let mut seen = HashSet::default();
bodies.retain(|body| {
let Some(claims) = step_body_claimed_shells(resolver, *body) else {
return true;
};
if matches!(body, StepBody::BrepWithVoids(_))
&& claims.iter().any(|shell| ordinary_claims.contains(shell))
{
return false;
}
if claims.iter().any(|shell| seen.contains(shell)) {
return false;
}
seen.extend(claims);
true
});
}
pub(super) struct ImportedBodies {
pub(super) solids: Vec<BrepSolid>,
pub(super) appearances: Vec<BodyAppearance>,
pub(super) face_refs: Vec<Vec<usize>>,
pub(super) failed: usize,
pub(super) first_error: Option<String>,
}
pub(super) fn shell_face_refs_pub(resolver: &Resolver, shell_ref: usize) -> Option<Vec<usize>> {
shell_face_refs(resolver, shell_ref)
}
pub(super) fn collect_step_solids(text: &str) -> Result<ImportedBodies, String> {
if !text.contains("ISO-10303-21") {
return Err("step_import: not an ISO-10303-21 Part 21 file".into());
}
let debug_timing = std::env::var("BREP_DEBUG_STEP_LOOP").is_ok();
let parse_started = debug_timing.then(web_time::Instant::now);
let entities = parse_data_section(text)?;
if let Some(started) = parse_started {
eprintln!("PHASE parse: {:?} ({} entities)", started.elapsed(), entities.len());
}
let resolver = Resolver {
entities: &entities,
length_scale: derive_length_scale_mm(&entities),
};
let styles = StyleTable::read(&entities);
let edges = assembly_edges(&entities);
if !edges.is_empty() {
let assembly_started = debug_timing.then(web_time::Instant::now);
let occurrences = resolve_assembly(&entities, &resolver, &styles, &edges);
if let Some(started) = assembly_started {
eprintln!("PHASE assembly: {:?}", started.elapsed());
}
if !occurrences.solids.is_empty() {
if std::env::var("BREP_DEBUG_STEP_ASM").is_ok() {
eprintln!(
"[step-asm] {} positioned occurrence(s) from {} NAUO edge(s):",
occurrences.solids.len(),
edges.len()
);
for line in &occurrences.debug {
eprintln!("[step-asm] {line}");
}
}
return Ok(ImportedBodies {
solids: occurrences.solids,
appearances: occurrences.appearances,
face_refs: occurrences.face_refs,
failed: occurrences.failed,
first_error: occurrences.first_error,
});
}
}
let mut bodies: Vec<StepBody> = Vec::new();
for (id, entity) in entities.iter() {
push_step_bodies(&resolver, *id, entity, &mut bodies);
}
dedupe_step_bodies(&resolver, &mut bodies);
let loop_started = debug_timing.then(web_time::Instant::now);
let build = |body: StepBody| {
(
matches!(body, StepBody::SurfaceModelShell { .. }),
build_step_body(&resolver, body).map(|solid| {
let appearance = step_body_appearance(&resolver, &styles, body, &solid);
let face_refs = super::pmi::body_face_refs(&resolver, body, &solid);
(solid, appearance, face_refs)
}),
)
};
type BuiltBody = (bool, Result<(BrepSolid, BodyAppearance, Vec<usize>), String>);
#[cfg(feature = "parallel")]
let results: Vec<BuiltBody> = {
use rayon::prelude::*;
bodies.into_par_iter().map(build).collect()
};
#[cfg(not(feature = "parallel"))]
let results: Vec<BuiltBody> = bodies.into_iter().map(build).collect();
if let Some(started) = loop_started {
eprintln!("PHASE reconstruct: {:?} ({} bodies)", started.elapsed(), results.len());
}
let mut solids = Vec::new();
let mut appearances = Vec::new();
let mut face_refs = Vec::new();
let mut failed = 0usize;
let mut first_error = None;
for (opportunistic, result) in results {
match result {
Ok((solid, _, _))
if opportunistic
&& solid.shells.iter().map(|s| s.faces.len()).sum::<usize>() <= 1 => {}
Ok((solid, appearance, refs)) => {
solids.push(solid);
appearances.push(appearance);
face_refs.push(refs);
}
Err(_) if opportunistic => {} Err(error) => {
failed += 1;
if first_error.is_none() {
first_error = Some(error);
}
}
}
}
Ok(ImportedBodies {
solids,
appearances,
face_refs,
failed,
first_error,
})
}
pub(super) use crate::step_matrix::{mat4_mul, Mat4};
pub(super) fn mat4_identity() -> Mat4 {
crate::step_matrix::MAT4_IDENTITY
}
fn frame_matrix(frame: &Frame) -> Mat4 {
[
frame.x.x,
frame.y.x,
frame.z.x,
frame.origin.x,
frame.x.y,
frame.y.y,
frame.z.y,
frame.origin.y,
frame.x.z,
frame.y.z,
frame.z.z,
frame.origin.z,
0.0,
0.0,
0.0,
1.0,
]
}
fn mat4_affine_inverse(m: &Mat4) -> Result<Mat4, String> {
let (a, b, c) = (m[0], m[1], m[2]);
let (d, e, f) = (m[4], m[5], m[6]);
let (g, h, i) = (m[8], m[9], m[10]);
let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
if det.abs() <= 1e-14 {
return Err("step_import: singular assembly transform".into());
}
let inv_det = 1.0 / det;
let r = [
(e * i - f * h) * inv_det,
(c * h - b * i) * inv_det,
(b * f - c * e) * inv_det,
(f * g - d * i) * inv_det,
(a * i - c * g) * inv_det,
(c * d - a * f) * inv_det,
(d * h - e * g) * inv_det,
(b * g - a * h) * inv_det,
(a * e - b * d) * inv_det,
];
let (tx, ty, tz) = (m[3], m[7], m[11]);
let it = [
-(r[0] * tx + r[1] * ty + r[2] * tz),
-(r[3] * tx + r[4] * ty + r[5] * tz),
-(r[6] * tx + r[7] * ty + r[8] * tz),
];
Ok([
r[0], r[1], r[2], it[0], r[3], r[4], r[5], it[1], r[6], r[7], r[8], it[2], 0.0, 0.0, 0.0,
1.0,
])
}
#[derive(Clone, Copy)]
pub(crate) struct AssemblyEdge {
pub(crate) nauo_id: usize,
pub(crate) parent_pd: usize,
pub(crate) child_pd: usize,
}
pub(crate) fn assembly_edges(entities: &HashMap<usize, Entity>) -> Vec<AssemblyEdge> {
let mut edges: Vec<AssemblyEdge> = entities
.iter()
.filter_map(|(id, entity)| {
let args = entity.find("NEXT_ASSEMBLY_USAGE_OCCURRENCE")?;
let parent_pd = args.get(3)?.as_ref_id().ok()?;
let child_pd = args.get(4)?.as_ref_id().ok()?;
Some(AssemblyEdge {
nauo_id: *id,
parent_pd,
child_pd,
})
})
.collect();
edges.sort_by_key(|edge| edge.nauo_id);
edges
}
fn representation_args(entity: &Entity) -> Option<&[Value]> {
entity
.find("ADVANCED_BREP_SHAPE_REPRESENTATION")
.or_else(|| entity.find("FACETED_BREP_SHAPE_REPRESENTATION"))
.or_else(|| entity.find("MANIFOLD_SURFACE_SHAPE_REPRESENTATION"))
.or_else(|| entity.find("GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION"))
.or_else(|| entity.find("SHAPE_REPRESENTATION"))
}
fn representation_items(entity: &Entity) -> Option<&[Value]> {
representation_args(entity)?.get(1)?.as_list().ok()
}
pub(super) fn representation_context(entity: &Entity) -> Option<usize> {
representation_args(entity)?.get(2)?.as_ref_id().ok()
}
pub(super) fn pd_root_srs(entities: &HashMap<usize, Entity>, pd: usize) -> Vec<usize> {
let pds_ids: Vec<usize> = entities
.iter()
.filter_map(|(id, entity)| {
let args = entity.find("PRODUCT_DEFINITION_SHAPE")?;
(args.get(2)?.as_ref_id().ok()? == pd).then_some(*id)
})
.collect();
let mut srs: Vec<usize> = entities
.values()
.filter_map(|entity| {
let args = entity.find("SHAPE_DEFINITION_REPRESENTATION")?;
let definition = args.first()?.as_ref_id().ok()?;
let used = args.get(1)?.as_ref_id().ok()?;
pds_ids.contains(&definition).then_some(used)
})
.collect();
srs.sort_unstable();
srs.dedup();
srs
}
fn identity_related_srs(entities: &HashMap<usize, Entity>, sr: usize) -> Vec<usize> {
let mut out = Vec::new();
for entity in entities.values() {
if entity.has("REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION") {
continue;
}
let Some(args) = entity.find("SHAPE_REPRESENTATION_RELATIONSHIP") else {
continue;
};
if args.len() < 4 {
continue;
}
let (Ok(rep1), Ok(rep2)) = (args[2].as_ref_id(), args[3].as_ref_id()) else {
continue;
};
if rep1 == sr {
out.push(rep2);
} else if rep2 == sr {
out.push(rep1);
}
}
out
}
pub(super) fn pd_step_bodies(
entities: &HashMap<usize, Entity>,
resolver: &Resolver,
pd: usize,
) -> Vec<StepBody> {
let mut visited: HashSet<usize> = HashSet::default();
let mut queue: VecDeque<usize> = pd_root_srs(entities, pd).into_iter().collect();
let mut bodies = Vec::new();
while let Some(sr) = queue.pop_front() {
if !visited.insert(sr) {
continue;
}
if let Ok(entity) = resolver.get(sr) {
if let Some(items) = representation_items(entity) {
for item in items {
if let Ok(item_id) = item.as_ref_id() {
if let Ok(item_entity) = resolver.get(item_id) {
push_step_bodies(resolver, item_id, item_entity, &mut bodies);
}
}
}
}
}
for other in identity_related_srs(entities, sr) {
if !visited.contains(&other) {
queue.push_back(other);
}
}
}
bodies.sort_unstable_by_key(|body| body.order_key());
bodies.dedup();
dedupe_step_bodies(resolver, &mut bodies);
bodies
}
pub(crate) fn edge_transform(
entities: &HashMap<usize, Entity>,
resolver: &Resolver,
edge: &AssemblyEdge,
) -> Mat4 {
let child_srs = pd_root_srs(entities, edge.child_pd);
for entity in entities.values() {
let Some(args) = entity.find("CONTEXT_DEPENDENT_SHAPE_REPRESENTATION") else {
continue;
};
let Some(pds_id) = args.get(1).and_then(|value| value.as_ref_id().ok()) else {
continue;
};
let Ok(pds) = resolver.get(pds_id) else {
continue;
};
let owns_nauo = pds
.find("PRODUCT_DEFINITION_SHAPE")
.and_then(|pds_args| pds_args.get(2))
.and_then(|value| value.as_ref_id().ok())
== Some(edge.nauo_id);
if !owns_nauo {
continue;
}
let Some(rep_rel_id) = args.first().and_then(|value| value.as_ref_id().ok()) else {
continue;
};
if let Some(transform) = resolve_rep_rel_transform(resolver, rep_rel_id, &child_srs) {
return transform;
}
}
mat4_identity()
}
fn resolve_rep_rel_transform(
resolver: &Resolver,
rep_rel_id: usize,
child_srs: &[usize],
) -> Option<Mat4> {
let entity = resolver.get(rep_rel_id).ok()?;
let idt_id = entity
.find("REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION")?
.first()?
.as_ref_id()
.ok()?;
let rel = entity.find("REPRESENTATION_RELATIONSHIP")?;
let rep1 = rel.get(2)?.as_ref_id().ok()?;
let rep2 = rel.get(3)?.as_ref_id().ok()?;
let idt = resolver.get(idt_id).ok()?;
let idt_args = idt.find("ITEM_DEFINED_TRANSFORMATION")?;
let item1 = idt_args.get(2)?.as_ref_id().ok()?;
let item2 = idt_args.get(3)?.as_ref_id().ok()?;
let m1 = frame_matrix(&resolver.placement(item1).ok()?);
let m2 = frame_matrix(&resolver.placement(item2).ok()?);
let forward = mat4_mul(&m2, &mat4_affine_inverse(&m1).ok()?);
if child_srs.contains(&rep2) && !child_srs.contains(&rep1) {
mat4_affine_inverse(&forward).ok()
} else {
Some(forward)
}
}
pub(super) struct AssemblyResult {
pub(super) solids: Vec<BrepSolid>,
pub(super) face_refs: Vec<Vec<usize>>,
pub(super) appearances: Vec<BodyAppearance>,
failed: usize,
first_error: Option<String>,
debug: Vec<String>,
}
pub(super) fn assembly_roots(edges: &[AssemblyEdge]) -> Vec<usize> {
let children: HashSet<usize> = edges.iter().map(|edge| edge.child_pd).collect();
let mut roots: Vec<usize> = edges
.iter()
.map(|edge| edge.parent_pd)
.filter(|pd| !children.contains(pd))
.collect();
roots.sort_unstable();
roots.dedup();
roots
}
pub(super) struct OccurrenceNode {
pub(super) pd: usize,
pub(super) world: Mat4,
pub(super) name: String,
ancestors: Vec<usize>,
}
pub(super) fn walk_occurrences(
resolver: &Resolver,
edges: &[AssemblyEdge],
transforms: &HashMap<usize, Mat4>,
mut visit: impl FnMut(&OccurrenceNode),
) {
let mut stack: Vec<OccurrenceNode> = assembly_roots(edges)
.iter()
.rev()
.map(|&pd| OccurrenceNode {
pd,
world: mat4_identity(),
name: product_name(resolver, pd),
ancestors: vec![pd],
})
.collect();
while let Some(node) = stack.pop() {
visit(&node);
let mut child_edges: Vec<&AssemblyEdge> = edges
.iter()
.filter(|edge| edge.parent_pd == node.pd)
.collect();
child_edges.sort_by_key(|edge| edge.nauo_id);
for edge in child_edges.into_iter().rev() {
if node.ancestors.contains(&edge.child_pd) {
continue; }
let transform = transforms
.get(&edge.nauo_id)
.copied()
.unwrap_or_else(mat4_identity);
let world = mat4_mul(&node.world, &transform);
let mut ancestors = node.ancestors.clone();
ancestors.push(edge.child_pd);
stack.push(OccurrenceNode {
pd: edge.child_pd,
world,
name: format!("{}/{}", node.name, product_name(resolver, edge.child_pd)),
ancestors,
});
}
}
}
pub(super) fn resolve_assembly(
entities: &HashMap<usize, Entity>,
resolver: &Resolver,
styles: &StyleTable,
edges: &[AssemblyEdge],
) -> AssemblyResult {
let mut transforms: HashMap<usize, Mat4> = HashMap::default();
for edge in edges {
transforms.insert(edge.nauo_id, edge_transform(entities, resolver, edge));
}
let mut result = AssemblyResult {
solids: Vec::new(),
face_refs: Vec::new(),
appearances: Vec::new(),
failed: 0,
first_error: None,
debug: Vec::new(),
};
let mut solid_cache: HashMap<StepBody, Result<(BrepSolid, BodyAppearance, Vec<usize>), String>> =
HashMap::default();
let debug_on = std::env::var("BREP_DEBUG_STEP_ASM").is_ok();
walk_occurrences(resolver, edges, &transforms, |node| {
for body in pd_step_bodies(entities, resolver, node.pd) {
let base = solid_cache.entry(body).or_insert_with(|| {
build_step_body(resolver, body)
.map(|solid| {
let appearance = step_body_appearance(resolver, styles, body, &solid);
let face_refs = super::pmi::body_face_refs(resolver, body, &solid);
(solid, appearance, face_refs)
})
.map_err(|error| {
format!("step_import: part body {body:?} failed to build: {error}")
})
});
let (base_solid, base_appearance, base_refs) = match base {
Ok((solid, appearance, refs)) => (solid.clone(), appearance.clone(), refs.clone()),
Err(_) if matches!(body, StepBody::SurfaceModelShell { .. }) => continue,
Err(error) => {
result.failed += 1;
if result.first_error.is_none() {
result.first_error = Some(error.clone());
}
continue;
}
};
match AffineTransform::new(node.world)
.and_then(|transform| transform_brep(&base_solid, transform, false))
{
Ok(positioned) => {
if debug_on {
result.debug.push(format!(
"{} body{body:?} @ ({:.1}, {:.1}, {:.1})",
node.name, node.world[3], node.world[7], node.world[11]
));
}
result.solids.push(positioned);
result.appearances.push(base_appearance);
result.face_refs.push(base_refs);
}
Err(error) => {
result.failed += 1;
if result.first_error.is_none() {
result.first_error = Some(error);
}
}
}
}
});
result
}
fn product_entity<'a>(resolver: &'a Resolver, pd: usize) -> Option<&'a Entity> {
let pd_entity = resolver.get(pd).ok()?;
let formation_id = pd_entity
.find("PRODUCT_DEFINITION")?
.get(2)?
.as_ref_id()
.ok()?;
let formation = resolver.get(formation_id).ok()?;
let product_ref = formation
.find("PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE")
.or_else(|| formation.find("PRODUCT_DEFINITION_FORMATION"))?
.get(2)?
.as_ref_id()
.ok()?;
resolver.get(product_ref).ok()
}
fn product_field(resolver: &Resolver, pd: usize, index: usize) -> Option<String> {
match product_entity(resolver, pd)?.find("PRODUCT")?.get(index)? {
Value::Str(text) => Some(text.clone()),
_ => None,
}
}
pub(super) fn product_name(resolver: &Resolver, pd: usize) -> String {
product_field(resolver, pd, 1).unwrap_or_else(|| format!("pd#{pd}"))
}
pub(super) fn product_id(resolver: &Resolver, pd: usize) -> String {
product_field(resolver, pd, 0).unwrap_or_default()
}