//! STEP topology (ISO 10303-42 topology entities) to kernel solids: breps, shells, faces, loops, //! edges and vertices. A face that cannot be built is left out and reported; the rest of the //! body is still imported (as an open body when a face is missing). use std::collections::HashMap; use mt::{BoundedCurve, InnerSpace, MetricSpace, ParametricCurve}; use truck_modeling as mt; use super::Ctx; use super::geom::{self, R, entity}; use super::p21::Param; use crate::guard; /// Most faces in one body. const MAX_FACES: usize = 500_000; /// Most edges in one loop. const MAX_LOOP: usize = 100_000; /// Most boundary loops on one face. Each bound clones its edge handles into a new wire and the /// face extent then samples nine points per edge, so one face listing the same 100,000-edge /// loop a million times would hold 1e11 handles. const MAX_BOUNDS: usize = 10_000; /// A body read from a brep entity. pub(crate) struct BrepOut { pub solid: mt::Solid, /// Faces left out (with the reason). pub failed: Vec, /// The shells are closed (a solid) rather than open (missing faces, or a surface model). pub closed: bool, /// Faces read from the file (before band faces are split for the kernel). pub file_faces: usize, /// The file face of each kernel face, in face order. pub face_origin: Vec, } #[derive(Default)] struct Builder { vertices: HashMap, edges: HashMap>, poly_edges: HashMap<(u64, u64), mt::Edge>, faces: usize, } fn refs(p: Option<&Param>) -> Vec { p.and_then(Param::as_list).map(|v| v.iter().filter_map(Param::as_ref_id).collect()).unwrap_or_default() } fn rid(p: Option<&Param>, what: &str) -> R { p.and_then(Param::as_ref_id).ok_or_else(|| format!("{what}: expected a reference")) } impl Builder { fn vertex(&mut self, cx: &Ctx, id: u64) -> R { if let Some(v) = self.vertices.get(&id) { return Ok(v.clone()); } let e = entity(cx, id)?; if e.name() != "VERTEX_POINT" { return Err(format!("#{id}: expected VERTEX_POINT, found {}", e.name())); } let p = geom::point(cx, rid(e.params().get(1), "VERTEX_POINT")?)?; let v = mt::Vertex::new(p); self.vertices.insert(id, v.clone()); Ok(v) } /// Kernel edges (start to end) of an EDGE_CURVE. fn edge_curve(&mut self, cx: &Ctx, id: u64) -> R> { if let Some(e) = self.edges.get(&id) { return Ok(e.clone()); } let e = entity(cx, id)?; if e.name() != "EDGE_CURVE" { return Err(format!("#{id}: unsupported edge {}", e.name())); } let p = e.params(); let (vs_id, ve_id) = (rid(p.get(1), "edge start")?, rid(p.get(2), "edge end")?); let vs = self.vertex(cx, vs_id)?; let ve = self.vertex(cx, ve_id)?; let sense = p.get(4).and_then(Param::as_bool).unwrap_or(true); let g = geom::curve(cx, rid(p.get(3), "edge geometry")?, 0)?; let closed = vs_id == ve_id; let pieces = guard("step edge", || geom::edge_pieces(&g, vs.point(), ve.point(), sense, closed, cx.tol).map_err(crate::KernelError::Failed)) .map_err(|e| e.to_string())?; let n = pieces.len(); let mut out = Vec::with_capacity(n); let mut cur = vs.clone(); for (i, c) in pieces.into_iter().enumerate() { let next = if i + 1 == n { ve.clone() } else { mt::Vertex::new(c.back()) }; out.push(mt::Edge::new_unchecked(&cur, &next, c)); cur = next; } self.edges.insert(id, out.clone()); Ok(out) } /// Edges of an ORIENTED_EDGE (or a bare EDGE_CURVE) in loop order. fn oriented_edge(&mut self, cx: &Ctx, id: u64, depth: usize) -> R> { if depth > 4 { return Err("oriented edges nested too deeply".into()); } let e = entity(cx, id)?; match e.name() { "ORIENTED_EDGE" => { let p = e.params(); let inner = self.oriented_edge(cx, rid(p.get(3), "ORIENTED_EDGE")?, depth + 1)?; if p.get(4).and_then(Param::as_bool).unwrap_or(true) { Ok(inner) } else { Ok(inner.iter().rev().map(mt::Edge::inverse).collect()) } } _ => self.edge_curve(cx, id), } } fn poly_loop(&mut self, cx: &Ctx, pts: &[u64]) -> R> { let n = pts.len(); if !(3..=MAX_LOOP).contains(&n) { return Err(format!("polygon loop with {n} points")); } let mut verts = Vec::with_capacity(n); for &id in pts { if let Some(v) = self.vertices.get(&id) { verts.push(v.clone()); } else { let v = mt::Vertex::new(geom::point(cx, id)?); self.vertices.insert(id, v.clone()); verts.push(v); } } let mut out = Vec::with_capacity(n); for i in 0..n { let (Some(&a), Some(&b)) = (pts.get(i), pts.get((i + 1) % n)) else { continue }; if let Some(e) = self.poly_edges.get(&(b, a)) { out.push(e.inverse()); continue; } let (Some(va), Some(vb)) = (verts.get(i), verts.get((i + 1) % n)) else { continue }; if va.point().distance(vb.point()) < 1e-12 { return Err("polygon loop with repeated points".into()); } let e = mt::Edge::try_new(va, vb, mt::Curve::Line(mt::Line(va.point(), vb.point()))).map_err(|e| format!("polygon edge: {e}"))?; self.poly_edges.insert((a, b), e.clone()); out.push(e); } Ok(out) } /// A face bound as a wire (oriented by the bound's orientation), or `None` for a vertex loop. fn bound(&mut self, cx: &Ctx, id: u64) -> R> { let b = entity(cx, id)?; if !matches!(b.name(), "FACE_BOUND" | "FACE_OUTER_BOUND") { return Err(format!("#{id}: unsupported face bound {}", b.name())); } let lp_id = rid(b.params().get(1), "face bound loop")?; let orient = b.params().get(2).and_then(Param::as_bool).unwrap_or(true); let lp = entity(cx, lp_id)?; let edges = match lp.name() { "EDGE_LOOP" => { let ids = refs(lp.params().get(1)); if ids.is_empty() || ids.len() > MAX_LOOP { return Err(format!("edge loop with {} edges", ids.len())); } let mut v = Vec::new(); for oe in ids { v.extend(self.oriented_edge(cx, oe, 0)?); } v } "POLY_LOOP" => self.poly_loop(cx, &refs(lp.params().get(1)))?, "VERTEX_LOOP" => return Ok(None), n => return Err(format!("unsupported loop {n}")), }; let mut w: mt::Wire = edges.into(); if !w.is_closed() { return Err("face boundary is not a closed loop".into()); } if !orient { w.invert(); } Ok(Some(w)) } /// A face (ADVANCED_FACE, FACE_SURFACE, ORIENTED_FACE or a polygonal FACE). fn face(&mut self, cx: &Ctx, id: u64, depth: usize) -> R { if depth > 4 { return Err("oriented faces nested too deeply".into()); } self.faces += 1; if self.faces > MAX_FACES { return Err(format!("more than {MAX_FACES} faces")); } let e = entity(cx, id)?; let p = e.params(); if e.name() == "ORIENTED_FACE" { let mut f = self.face(cx, rid(p.get(2), "ORIENTED_FACE")?, depth + 1)?; if !p.get(3).and_then(Param::as_bool).unwrap_or(true) { f.invert(); } return Ok(f); } if !matches!(e.name(), "ADVANCED_FACE" | "FACE_SURFACE" | "FACE") { return Err(format!("unsupported face {}", e.name())); } let bounds = refs(p.get(1)); if bounds.len() > MAX_BOUNDS { return Err(format!("a face with more than {MAX_BOUNDS} boundary loops")); } let mut wires = Vec::new(); let mut edges = 0usize; for b in bounds { if let Some(w) = self.bound(cx, b)? { edges = edges.saturating_add(w.len()); if edges > MAX_LOOP { return Err(format!("a face with more than {MAX_LOOP} boundary edges")); } wires.push(w); } } if wires.is_empty() { return Err("face without boundary edges".into()); } let extent: Vec = wires .iter() .flat_map(|w| w.edge_iter()) .flat_map(|e| { let c = e.curve(); let (t0, t1) = c.range_tuple(); (0..=8).map(move |i| c.subs(t0 + (t1 - t0) * i as f64 / 8.0)).collect::>() }) .collect(); // Seam edges: used twice by the face's loops. let mut uses: HashMap = HashMap::new(); for e in wires.iter().flat_map(|w| w.edge_iter()) { *uses.entry(e.id()).or_default() += 1; } let mut seen = std::collections::HashSet::new(); let seam: Vec = wires .iter() .flat_map(|w| w.edge_iter()) .filter(|e| uses.get(&e.id()).copied().unwrap_or(0) > 1 && seen.insert(e.id())) .flat_map(|e| { let c = e.curve(); let (t0, t1) = c.range_tuple(); (0..=8).map(move |i| c.subs(t0 + (t1 - t0) * i as f64 / 8.0)).collect::>() }) .collect(); let surface = if e.name() == "FACE" { crate::plane_fit::plane_through(&extent).map(mt::Surface::Plane).ok_or("polygonal face without a plane")? } else { let sid = rid(p.get(2), "face geometry")?; let sense = p.get(3).and_then(Param::as_bool).unwrap_or(true); guard("step surface", || geom::surface(cx, sid, &extent, &seam, sense, 0).map_err(crate::KernelError::Failed)) .map_err(|e| e.to_string())? }; // Loops on a surface of revolution start away from the axis: a pole (cone apex, sphere // pole) has no definite rotation parameter, and the mesher takes the first point's // parameter as the anchor for the rest of the loop. let wires: Vec = match &surface { mt::Surface::RevolutedCurve(rc) => { let (o, a) = (rc.entity().origin(), rc.entity().axis()); let dist = |e: &mt::Edge| { let v = e.front().point() - o; (v - a * v.dot(a)).magnitude() }; wires .into_iter() .map(|w| { let edges: Vec = w.edge_iter().cloned().collect(); let start = edges.iter().enumerate().max_by(|x, y| dist(x.1).total_cmp(&dist(y.1))).map(|(i, _)| i).unwrap_or(0); edges.iter().skip(start).chain(edges.iter().take(start)).cloned().collect::>().into() }) .collect() } // A loop passing a vertex twice (a figure eight through a pole, as written for a // torus whose tube touches its axis) starts at a vertex it passes once. _ => wires .into_iter() .map(|w| { let edges: Vec = w.edge_iter().cloned().collect(); // Counted once: a loop may hold 100,000 edges, and asking per edge how // often its front vertex appears would scan them all again each time. let mut fronts: HashMap<_, usize> = HashMap::new(); for e in &edges { *fronts.entry(e.front().id()).or_default() += 1; } let passes = |e: &mt::Edge| fronts.get(&e.front().id()).copied().unwrap_or(0); match edges.first().map(passes) { Some(n) if n > 1 => { let start = edges.iter().position(|e| passes(e) == 1).unwrap_or(0); edges.iter().skip(start).chain(edges.iter().take(start)).cloned().collect::>().into() } _ => w, } }) .collect(), }; match mt::Face::try_new(wires.clone(), surface.clone()) { Ok(f) => Ok(f), // Faces on periodic surfaces run along a seam edge twice: valid in STEP. Err(truck_topology::errors::Error::NotSimpleWire) => Ok(mt::Face::new_unchecked(wires, surface)), Err(e) => Err(format!("face: {e}")), } } fn shell(&mut self, cx: &Ctx, id: u64, failed: &mut Vec) -> R<(mt::Shell, bool, usize, Vec)> { let e = entity(cx, id)?; let p = e.params(); let (faces, flip, closed) = match e.name() { "CLOSED_SHELL" => (refs(p.get(1)), false, true), "OPEN_SHELL" => (refs(p.get(1)), false, false), "ORIENTED_CLOSED_SHELL" | "ORIENTED_OPEN_SHELL" => { let inner = entity(cx, rid(p.get(2), "oriented shell")?)?; (refs(inner.params().get(1)), !p.get(3).and_then(Param::as_bool).unwrap_or(true), e.name() == "ORIENTED_CLOSED_SHELL") } n => return Err(format!("unsupported shell {n}")), }; if faces.len() > MAX_FACES { return Err(format!("shell with {} faces", faces.len())); } let mut out = Vec::with_capacity(faces.len()); let mut origin = Vec::with_capacity(faces.len()); for f in faces { match self.face(cx, f, 0) { Ok(mut face) => { if flip { face.invert(); } out.push(face); origin.push(f); } Err(err) => { if self.faces > MAX_FACES { return Err(err); } failed.push(format!("face #{f}: {err}")); } } } if out.is_empty() { return Err("no face could be imported".into()); } let read = out.len(); let out = super::split::split_bands(out, &mut origin, failed); Ok((out.into(), closed, read, origin)) } } /// Read a solid-like representation item: MANIFOLD_SOLID_BREP, BREP_WITH_VOIDS, FACETED_BREP, /// or a SHELL_BASED_SURFACE_MODEL (closed shells give solids, open ones open bodies). pub(crate) fn read_item(cx: &Ctx, id: u64) -> R { let e = entity(cx, id)?; let p = e.params(); let mut b = Builder::default(); let mut failed = Vec::new(); let mut shells = Vec::new(); let mut closed = true; let mut file_faces = 0; // The file face each kernel face comes from (in face order). let mut face_origin = Vec::new(); match e.name() { "MANIFOLD_SOLID_BREP" | "FACETED_BREP" | "BREP_WITH_VOIDS" => { let (s, c, n, o) = b.shell(cx, rid(p.get(1), "brep outer shell")?, &mut failed)?; closed &= c; file_faces += n; shells.push(s); face_origin.extend(o); if e.name() == "BREP_WITH_VOIDS" { for v in refs(p.get(2)) { match b.shell(cx, v, &mut failed) { Ok((s, c, n, o)) => { closed &= c; file_faces += n; shells.push(s); face_origin.extend(o); } Err(err) => failed.push(format!("void shell #{v}: {err}")), } } } } "SHELL_BASED_SURFACE_MODEL" => { for s in refs(p.get(1)) { match b.shell(cx, s, &mut failed) { Ok((s, c, n, o)) => { closed &= c; file_faces += n; shells.push(s); face_origin.extend(o); } Err(err) => failed.push(format!("shell #{s}: {err}")), } } } n => return Err(format!("unsupported item {n}")), } if shells.is_empty() { return Err(failed.first().cloned().unwrap_or_else(|| "no shells".into())); } // `mt::Solid::try_new` only checks each shell (non-empty, connected, closed, no singular // vertex) and hands the shells back unchanged (truck-topology 0.6.0), so the same checks run // here on the borrowed shells. The successful path used to clone every face, edge, vertex, // curve and surface to keep a copy that was then dropped. let checked = guard("step solid", || { for shell in &shells { if shell.is_empty() { return Err(crate::KernelError::Failed("empty shell".into())); } if !shell.is_connected() { return Err(crate::KernelError::Failed("shell is not connected".into())); } if shell.shell_condition() != mt::ShellCondition::Closed { return Err(crate::KernelError::Failed("shell is not closed".into())); } if !shell.singular_vertices().is_empty() { return Err(crate::KernelError::Failed("shell is not manifold".into())); } } Ok(()) }); if checked.is_err() { closed = false; } Ok(BrepOut { solid: mt::Solid::new_unchecked(shells), failed, closed, file_faces, face_origin }) }