Files
OneCAD/crates/kernel/src/step_in/brep.rs
T
SahenandClaude Opus 5.5 2bb24d1d2a perf: prism pairs before the mesh, bodies measured once, and a STEP import that copies less (#714)
A cut or an intersection between prisms (a plate and the tools through it) is now decided in 2D before either operand is meshed. A body's size and volume are measured once, a placement reads its orientation from its matrix, the evaluation cache is bounded to 64 MiB, and STEP import copies fewer shells. Hole plates evaluate 20-30% faster with less memory, and models are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:55:56 -04:00

424 lines
18 KiB
Rust

//! 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<String>,
/// 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<u64>,
}
#[derive(Default)]
struct Builder {
vertices: HashMap<u64, mt::Vertex>,
edges: HashMap<u64, Vec<mt::Edge>>,
poly_edges: HashMap<(u64, u64), mt::Edge>,
faces: usize,
}
fn refs(p: Option<&Param>) -> Vec<u64> {
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<u64> {
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<mt::Vertex> {
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<Vec<mt::Edge>> {
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<Vec<mt::Edge>> {
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<Vec<mt::Edge>> {
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<Option<mt::Wire>> {
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<mt::Face> {
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<mt::Point3> = 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::<Vec<_>>()
})
.collect();
// Seam edges: used twice by the face's loops.
let mut uses: HashMap<mt::EdgeID, usize> = 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<mt::Point3> = 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::<Vec<_>>()
})
.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<mt::Wire> = 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<mt::Edge> = 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::<Vec<_>>().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<mt::Edge> = 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::<Vec<_>>().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<String>) -> R<(mt::Shell, bool, usize, Vec<u64>)> {
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<BrepOut> {
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 })
}