Files
solidcraft-upstream/crates/kernel/src/step_in/mod.rs
T
bherbruckandClaude Opus 5.5 b8d0fcc8e2 STEP: bound assembly traversal, name imports after a lone body
A visit budget stops shared sub-assemblies from growing exponentially (with
a test); the loose-solid pass gets its own budget. An Import feature is
named after the root product, else its only body, else the file. Restores
a test function closed off by the rebase.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-07 21:01:04 +00:00

682 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! STEP import (ISO 10303-21 files, AP203/AP214/AP242 geometry).
//!
//! Our own reader, written from the public standards: [`p21`] parses the exchange structure,
//! [`geom`] and [`brep`] turn geometry and topology into kernel solids, and this module walks
//! the product structure (products, shape representations, assemblies with their placements,
//! units, colours). STEP files are untrusted: every count and nesting depth is capped, every
//! truck call runs under [`guard`], and what cannot be read becomes a warning rather than a
//! failure wherever the rest of the file is still usable.
mod brep;
mod geom;
pub(crate) mod p21;
mod split;
use std::collections::{BTreeMap, HashMap, HashSet};
use serde::{Deserialize, Serialize};
use truck_modeling as mt;
use crate::body::Body;
use crate::{KernelError, Result, guard};
use geom::R;
use p21::{Entity, Exchange, Param};
/// Deepest assembly nesting followed.
const MAX_ASSEMBLY_DEPTH: usize = 32;
/// Most body instances in one import.
const MAX_BODIES: usize = 10_000;
/// Most product occurrences and mapped representations visited (a shared sub-assembly used
/// twice at every level would otherwise grow exponentially).
const MAX_VISITS: usize = 100_000;
/// A body read from a STEP file.
#[derive(Clone, Debug)]
pub struct ImportedBody {
/// Solid name (the brep's label), else the product name.
pub name: String,
pub body: Body,
/// Surface colour (linear-ish RGB 0..1) from the file's presentation styles.
pub color: Option<[f32; 3]>,
/// Product names from the root to the part that holds the body.
pub path: Vec<String>,
/// The body is a closed solid (false: faces were left out, or the file had a surface model).
pub closed: bool,
/// Faces read from the file. The body can have more: a face that wraps all the way around
/// a closed surface is split in two for the kernel.
pub file_faces: usize,
}
/// One product occurrence in the file's assembly tree.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StepNode {
pub name: String,
/// Placement in the parent (column-major 4×4, millimetres).
pub transform: [[f64; 4]; 4],
/// Indices into [`StepImport::bodies`] of this product's own bodies.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bodies: Vec<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<StepNode>,
}
/// Result of [`step_import`].
#[derive(Clone, Debug)]
pub struct StepImport {
pub bodies: Vec<ImportedBody>,
/// Root products (an assembly tree; a plain part is a single root).
pub tree: Vec<StepNode>,
/// What could not be read (unsupported entities, faces left out…).
pub warnings: Vec<String>,
/// FILE_SCHEMA (e.g. `AUTOMOTIVE_DESIGN`, `CONFIG_CONTROL_DESIGN`, `AP242_…`).
pub schema: String,
/// FILE_NAME originating system.
pub system: String,
}
/// Reading context: the file and the units of the representation being read.
pub(crate) struct Ctx<'a> {
pub ex: &'a Exchange,
/// Millimetres per file length unit.
pub len: f64,
/// Radians per file angle unit.
pub ang: f64,
/// Vertex-on-curve tolerance (mm).
pub tol: f64,
}
#[derive(Clone, Copy)]
struct Units {
len: f64,
ang: f64,
tol: f64,
}
const DEFAULT_UNITS: Units = Units { len: 1.0, ang: 1.0, tol: 0.01 };
fn si_prefix(p: Option<&Param>) -> f64 {
match p.and_then(Param::as_enum) {
Some("EXA") => 1e18,
Some("PETA") => 1e15,
Some("TERA") => 1e12,
Some("GIGA") => 1e9,
Some("MEGA") => 1e6,
Some("KILO") => 1e3,
Some("HECTO") => 1e2,
Some("DECA") => 1e1,
Some("DECI") => 1e-1,
Some("CENTI") => 1e-2,
Some("MILLI") => 1e-3,
Some("MICRO") => 1e-6,
Some("NANO") => 1e-9,
Some("PICO") => 1e-12,
Some("FEMTO") => 1e-15,
Some("ATTO") => 1e-18,
_ => 1.0,
}
}
/// A unit's size: millimetres for lengths, radians for plane angles.
fn unit_factor(ex: &Exchange, id: u64, depth: usize) -> Option<(&'static str, f64)> {
if depth > 4 {
return None;
}
let e = ex.get(id)?;
let kind = if e.has("LENGTH_UNIT") {
"length"
} else if e.has("PLANE_ANGLE_UNIT") {
"angle"
} else {
"other"
};
if let Some(si) = e.record("SI_UNIT") {
let base = match si.get(1).and_then(Param::as_enum) {
Some("METRE") => 1000.0,
Some("RADIAN") => 1.0,
_ => return Some((kind, 1.0)),
};
return Some((kind, si_prefix(si.first()) * base));
}
if let Some(cb) = e.record("CONVERSION_BASED_UNIT") {
let m = ex.get(cb.get(1)?.as_ref_id()?)?;
let mp = m.params();
let v = mp.first()?.as_f64()?;
let (_, f) = unit_factor(ex, mp.get(1)?.as_ref_id()?, depth + 1)?;
let x = v * f;
return (x.is_finite() && x > 0.0).then_some((kind, x));
}
Some((kind, 1.0))
}
/// Units of a representation context (GLOBAL_UNIT_ASSIGNED_CONTEXT, uncertainty).
fn context_units(ex: &Exchange, ctx: Option<u64>, cache: &mut HashMap<u64, Units>) -> Units {
let Some(id) = ctx else { return DEFAULT_UNITS };
if let Some(u) = cache.get(&id) {
return *u;
}
let mut u = DEFAULT_UNITS;
let mut tol_file = None;
if let Some(e) = ex.get(id) {
if let Some(units) = e.record("GLOBAL_UNIT_ASSIGNED_CONTEXT").and_then(|r| r.first()).and_then(Param::as_list) {
for uid in units.iter().filter_map(Param::as_ref_id) {
match unit_factor(ex, uid, 0) {
Some(("length", f)) if f.is_finite() && f > 0.0 => u.len = f,
Some(("angle", f)) if f.is_finite() && f > 0.0 => u.ang = f,
_ => {}
}
}
}
if let Some(unc) = e.record("GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT").and_then(|r| r.first()).and_then(Param::as_list) {
for uid in unc.iter().filter_map(Param::as_ref_id) {
if let Some(m) = ex.get(uid)
&& let Some(v) = m.params().first().and_then(Param::as_f64)
{
let f = m.params().get(1).and_then(Param::as_ref_id).and_then(|x| unit_factor(ex, x, 0)).map(|x| x.1);
tol_file = Some(v * f.unwrap_or(u.len));
}
}
}
}
u.tol = tol_file.filter(|t| t.is_finite() && *t > 0.0).map(|t| (t * 10.0).max(1e-3)).unwrap_or(u.len * 1e-2).clamp(1e-4, 1.0);
cache.insert(id, u);
u
}
/// Items and context of a representation entity.
fn representation(e: &Entity) -> Option<(Vec<u64>, Option<u64>)> {
let p = if e.is_complex() {
e.record("REPRESENTATION")
.or_else(|| e.records.iter().find(|r| r.name.ends_with("REPRESENTATION") && r.params.len() >= 3).map(|r| r.params.as_slice()))?
} else if e.name().ends_with("REPRESENTATION") {
e.params()
} else {
return None;
};
let items = p.get(1)?.as_list()?.iter().filter_map(Param::as_ref_id).collect();
Some((items, p.get(2).and_then(Param::as_ref_id)))
}
fn mat_mul(a: &mt::Matrix4, b: &mt::Matrix4) -> mt::Matrix4 {
a * b
}
fn to_array(m: &mt::Matrix4) -> [[f64; 4]; 4] {
[[m.x.x, m.x.y, m.x.z, m.x.w], [m.y.x, m.y.y, m.y.z, m.y.w], [m.z.x, m.z.y, m.z.z, m.z.w], [m.w.x, m.w.y, m.w.z, m.w.w]]
}
/// Inverse of a rigid placement matrix (orthonormal rotation + translation).
fn rigid_inverse(m: &mt::Matrix4) -> mt::Matrix4 {
use mt::SquareMatrix;
m.invert().unwrap_or_else(mt::Matrix4::identity)
}
struct Reader<'a> {
ex: &'a Exchange,
units: HashMap<u64, Units>,
warnings: BTreeMap<String, usize>,
/// Representation → its items and context.
reps: HashMap<u64, (Vec<u64>, Option<u64>)>,
/// Plain representation relationships (same product, no placement).
rep_links: HashMap<u64, Vec<u64>>,
/// Product definition → its shape representations.
pd_reps: HashMap<u64, Vec<u64>>,
/// Product definition → child occurrences (NAUO ids).
children: HashMap<u64, Vec<u64>>,
/// NAUO → placement relationship entity.
placements: HashMap<u64, u64>,
/// Styled item → colour.
colors: HashMap<u64, [f32; 3]>,
/// Converted items (brep id → solid, or the error).
solids: HashMap<u64, std::result::Result<(mt::Solid, bool, usize), String>>,
used_items: HashSet<u64>,
/// Items already instanced for the product being read.
node_items: HashSet<u64>,
/// Occurrences and mapped representations visited so far.
visits: usize,
bodies: Vec<ImportedBody>,
}
impl<'a> Reader<'a> {
fn warn(&mut self, msg: impl Into<String>) {
*self.warnings.entry(msg.into()).or_default() += 1;
}
fn index(&mut self) {
let ex = self.ex;
let mut pds: HashMap<u64, u64> = HashMap::new(); // PDS → definition
let mut sdrs: Vec<(u64, u64)> = Vec::new(); // (PDS, rep)
let mut cdsrs: Vec<(u64, u64)> = Vec::new(); // (relationship, PDS)
let mut nauos: Vec<(u64, u64, u64)> = Vec::new(); // (id, parent, child)
let mut ids: Vec<&u64> = ex.entities.keys().collect();
ids.sort();
for &id in ids {
let Some(e) = ex.get(id) else { continue };
if let Some(r) = representation(e) {
self.reps.insert(id, r);
}
let p = e.params();
let rf = |i: usize| p.get(i).and_then(Param::as_ref_id);
match e.name() {
"PRODUCT_DEFINITION_SHAPE" | "PROPERTY_DEFINITION" => {
if let Some(d) = rf(2) {
pds.insert(id, d);
}
}
"SHAPE_DEFINITION_REPRESENTATION" => {
if let (Some(d), Some(r)) = (rf(0), rf(1)) {
sdrs.push((d, r));
}
}
"CONTEXT_DEPENDENT_SHAPE_REPRESENTATION" => {
if let (Some(r), Some(d)) = (rf(0), rf(1)) {
cdsrs.push((r, d));
}
}
"NEXT_ASSEMBLY_USAGE_OCCURRENCE" | "ASSEMBLY_COMPONENT_USAGE" | "SPECIFIED_HIGHER_USAGE_OCCURRENCE" => {
if let (Some(a), Some(b)) = (rf(3), rf(4)) {
nauos.push((id, a, b));
}
}
"SHAPE_REPRESENTATION_RELATIONSHIP" | "REPRESENTATION_RELATIONSHIP" if !e.is_complex() => {
if let (Some(a), Some(b)) = (rf(2), rf(3)) {
self.rep_links.entry(a).or_default().push(b);
self.rep_links.entry(b).or_default().push(a);
}
}
"STYLED_ITEM" | "OVER_RIDING_STYLED_ITEM" => {
if let Some(item) = rf(2)
&& let Some(c) = self.find_colour(p.get(1))
{
self.colors.entry(item).or_insert(c);
}
}
_ => {}
}
}
for (d, r) in sdrs {
if let Some(&pd) = pds.get(&d) {
self.pd_reps.entry(pd).or_default().push(r);
}
}
for (r, d) in cdsrs {
if let Some(&nauo) = pds.get(&d) {
self.placements.insert(nauo, r);
}
}
for (id, parent, child) in nauos {
if parent != child {
self.children.entry(parent).or_default().push(id);
}
}
}
/// First colour reachable from a style list (bounded search).
fn find_colour(&self, styles: Option<&Param>) -> Option<[f32; 3]> {
let mut queue: Vec<u64> = styles?.as_list()?.iter().filter_map(Param::as_ref_id).collect();
let mut seen = HashSet::new();
while let Some(id) = queue.pop() {
if seen.len() > 64 || !seen.insert(id) {
continue;
}
let e = self.ex.get(id)?;
if e.name() == "COLOUR_RGB" {
let p = e.params();
let c = |i: usize| p.get(i).and_then(Param::as_f64).map(|x| x.clamp(0.0, 1.0) as f32);
return Some([c(1)?, c(2)?, c(3)?]);
}
if e.name() == "DRAUGHTING_PRE_DEFINED_COLOUR" {
return Some(match e.params().first().and_then(Param::as_str).unwrap_or("") {
"red" => [1.0, 0.0, 0.0],
"green" => [0.0, 1.0, 0.0],
"blue" => [0.0, 0.0, 1.0],
"yellow" => [1.0, 1.0, 0.0],
"magenta" => [1.0, 0.0, 1.0],
"cyan" => [0.0, 1.0, 1.0],
"black" => [0.0, 0.0, 0.0],
_ => [1.0, 1.0, 1.0],
});
}
fn walk(p: &Param, out: &mut Vec<u64>, depth: usize) {
match p {
Param::Ref(r) => out.push(*r),
Param::List(v) | Param::Typed(_, v) if depth < 4 => v.iter().for_each(|x| walk(x, out, depth + 1)),
_ => {}
}
}
for r in &e.records {
for p in &r.params {
walk(p, &mut queue, 0);
}
}
}
None
}
fn units_of(&mut self, rep: u64) -> Units {
let ctx = self.reps.get(&rep).and_then(|r| r.1);
context_units(self.ex, ctx, &mut self.units)
}
fn placement_matrix(&mut self, item: u64, u: Units) -> R<mt::Matrix4> {
let cx = Ctx { ex: self.ex, len: u.len, ang: u.ang, tol: u.tol };
let e = geom::entity(&cx, item)?;
if e.name() == "CARTESIAN_TRANSFORMATION_OPERATOR_3D" {
let p = e.params();
let o = geom::point(&cx, p.get(4).and_then(Param::as_ref_id).ok_or("transformation without origin")?)?;
return Ok(mt::Matrix4::from_translation(o.to_homogeneous().truncate()));
}
Ok(geom::frame(&cx, item)?.matrix())
}
/// Representations of a product (its own shape representations and those linked to them).
fn rep_closure(&self, roots: &[u64]) -> Vec<u64> {
let mut seen: Vec<u64> = Vec::new();
let mut stack: Vec<u64> = roots.to_vec();
while let Some(r) = stack.pop() {
if seen.contains(&r) || seen.len() > 1024 {
continue;
}
seen.push(r);
if let Some(l) = self.rep_links.get(&r) {
stack.extend(l.iter().copied());
}
}
seen
}
fn item_solid(&mut self, item: u64, u: Units) -> Option<(mt::Solid, bool, usize)> {
if let Some(r) = self.solids.get(&item) {
return r.as_ref().ok().cloned();
}
let cx = Ctx { ex: self.ex, len: u.len, ang: u.ang, tol: u.tol };
let r = match guard("step brep", || brep::read_item(&cx, item).map_err(KernelError::Failed)) {
Ok(out) => {
let label = self.label(item);
for f in &out.failed {
self.warn(format!("{label}: {f}"));
}
if !out.closed {
self.warn(format!("{label}: imported as an open body (the shell is not closed)"));
}
Ok((out.solid, out.closed, out.file_faces))
}
Err(e) => {
let label = self.label(item);
let msg = e.to_string().trim_start_matches("the operation failed: ").to_string();
self.warn(format!("{label}: not imported: {msg}"));
Err(msg)
}
};
self.solids.insert(item, r.clone());
r.ok()
}
fn label(&self, item: u64) -> String {
let name = self.ex.get(item).and_then(|e| e.params().first()).and_then(Param::as_str).unwrap_or("").trim().to_string();
if name.is_empty() { format!("solid #{item}") } else { format!("solid '{name}'") }
}
/// Bodies of a representation's items (following mapped items), placed by `m`.
fn rep_bodies(&mut self, rep: u64, m: &mt::Matrix4, path: &[String], product: &str, out: &mut Vec<usize>, depth: usize) {
if depth > MAX_ASSEMBLY_DEPTH {
self.warn("mapped items nested too deeply");
return;
}
self.visits += 1;
if self.visits > MAX_VISITS {
self.warn(format!("more than {MAX_VISITS} component instances: the rest are left out"));
return;
}
let u = self.units_of(rep);
let items = self.reps.get(&rep).map(|r| r.0.clone()).unwrap_or_default();
for item in items {
let Some(e) = self.ex.get(item) else { continue };
match e.name() {
"MANIFOLD_SOLID_BREP" | "BREP_WITH_VOIDS" | "FACETED_BREP" | "SHELL_BASED_SURFACE_MODEL" => {
self.used_items.insert(item);
if !self.node_items.insert(item) {
continue;
}
if self.bodies.len() >= MAX_BODIES {
self.warn(format!("more than {MAX_BODIES} bodies: the rest are left out"));
return;
}
let Some((solid, closed, file_faces)) = self.item_solid(item, u) else { continue };
let identity = *m == mt::Matrix4::from_scale(1.0);
let placed = if identity {
Ok(solid)
} else {
let mm = *m;
guard("step placement", || Ok(mt::builder::transformed(&solid, mm)))
};
let body = placed.and_then(Body::new);
match body {
Ok(body) => {
let own = e.params().first().and_then(Param::as_str).unwrap_or("").trim().to_string();
let name = if own.is_empty() { product.to_string() } else { own };
out.push(self.bodies.len());
self.bodies.push(ImportedBody {
name,
body,
color: self.colors.get(&item).copied(),
path: path.to_vec(),
closed,
file_faces,
});
}
Err(err) => {
let label = self.label(item);
self.warn(format!("{label}: not imported: {err}"));
}
}
}
"MAPPED_ITEM" => {
let p = e.params();
let (Some(map), Some(target)) = (p.get(1).and_then(Param::as_ref_id), p.get(2).and_then(Param::as_ref_id)) else { continue };
let Some(me) = self.ex.get(map) else { continue };
let (Some(origin), Some(mapped)) =
(me.params().first().and_then(Param::as_ref_id), me.params().get(1).and_then(Param::as_ref_id))
else {
continue;
};
let mu = self.units_of(mapped);
match (self.placement_matrix(target, u), self.placement_matrix(origin, mu)) {
(Ok(t), Ok(o)) => {
let mm = mat_mul(m, &mat_mul(&t, &rigid_inverse(&o)));
for r in self.rep_closure(&[mapped]) {
self.rep_bodies(r, &mm, path, product, out, depth + 1);
}
}
(Err(e), _) | (_, Err(e)) => self.warn(format!("mapped item #{item}: {e}")),
}
}
_ => {}
}
}
}
fn product_name(&self, pd: u64) -> String {
let name = (|| {
let pdf = self.ex.get(self.ex.get(pd)?.params().get(2)?.as_ref_id()?)?;
let prod = self.ex.get(pdf.params().get(2)?.as_ref_id()?)?;
let p = prod.params();
let n = p.get(1).and_then(Param::as_str).filter(|s| !s.trim().is_empty()).or_else(|| p.first().and_then(Param::as_str))?;
Some(n.trim().to_string())
})();
name.filter(|n| !n.is_empty()).unwrap_or_else(|| "Import".into())
}
/// Placement of a child occurrence in its parent.
fn occurrence_matrix(&mut self, nauo: u64, child_reps: &[u64]) -> mt::Matrix4 {
let Some(&rel) = self.placements.get(&nauo) else { return mt::Matrix4::from_scale(1.0) };
let r = (|| -> R<mt::Matrix4> {
let e = self.ex.get(rel).ok_or("missing placement")?;
let rr = e.record("REPRESENTATION_RELATIONSHIP").ok_or("placement without representations")?;
let (r1, r2) =
(rr.get(2).and_then(Param::as_ref_id).ok_or("bad placement")?, rr.get(3).and_then(Param::as_ref_id).ok_or("bad placement")?);
let op = e
.record("REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION")
.and_then(|x| x.first())
.and_then(Param::as_ref_id)
.ok_or("placement without transformation")?;
let ope = self.ex.get(op).ok_or("missing transformation")?;
let (u1, u2) = (self.units_of(r1), self.units_of(r2));
if ope.name() == "ITEM_DEFINED_TRANSFORMATION" {
let p = ope.params();
let (i1, i2) = (
p.get(2).and_then(Param::as_ref_id).ok_or("bad transformation")?,
p.get(3).and_then(Param::as_ref_id).ok_or("bad transformation")?,
);
let m1 = self.placement_matrix(i1, u1)?;
let m2 = self.placement_matrix(i2, u2)?;
// rep_1 is normally the component: map its item onto the assembly's item.
let child_is_1 = child_reps.contains(&r1) || !child_reps.contains(&r2);
Ok(if child_is_1 { mat_mul(&m2, &rigid_inverse(&m1)) } else { mat_mul(&m1, &rigid_inverse(&m2)) })
} else {
self.placement_matrix(op, u2)
}
})();
match r {
Ok(m) => m,
Err(e) => {
self.warn(format!("component placement #{rel}: {e} (placed at the origin)"));
mt::Matrix4::from_scale(1.0)
}
}
}
fn node(&mut self, pd: u64, local: mt::Matrix4, world: mt::Matrix4, path: &[String], depth: usize, stack: &mut Vec<u64>) -> StepNode {
let name = self.product_name(pd);
let mut node = StepNode { name: name.clone(), transform: to_array(&local), bodies: Vec::new(), children: Vec::new() };
if depth > MAX_ASSEMBLY_DEPTH || stack.contains(&pd) {
self.warn("assembly nested too deeply or recursive: some components were left out");
return node;
}
self.visits += 1;
if self.visits > MAX_VISITS {
self.warn(format!("more than {MAX_VISITS} component instances: the rest are left out"));
return node;
}
stack.push(pd);
let mut path = path.to_vec();
path.push(name.clone());
let own = self.pd_reps.get(&pd).cloned().unwrap_or_default();
let reps = self.rep_closure(&own);
let mut bodies = Vec::new();
self.node_items.clear();
for r in &reps {
self.rep_bodies(*r, &world, &path, &name, &mut bodies, 0);
}
node.bodies = bodies;
for nauo in self.children.get(&pd).cloned().unwrap_or_default() {
let Some(child) = self.ex.get(nauo).and_then(|e| e.params().get(4)).and_then(Param::as_ref_id) else { continue };
let child_own = self.pd_reps.get(&child).cloned().unwrap_or_default();
let child_reps = self.rep_closure(&child_own);
let m = self.occurrence_matrix(nauo, &child_reps);
let w = mat_mul(&world, &m);
let c = self.node(child, m, w, &path, depth + 1, stack);
node.children.push(c);
}
stack.pop();
node
}
}
/// Recent imports, so a file read for a command and then evaluated in the timeline is only
/// read once.
static RECENT: std::sync::Mutex<Vec<(u64, usize, std::sync::Arc<StepImport>)>> = std::sync::Mutex::new(Vec::new());
/// [`step_import`] through a small cache of recent results (keyed by the text).
pub fn step_import_shared(text: &str) -> Result<std::sync::Arc<StepImport>> {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut h);
let key = (h.finish(), text.len());
if let Ok(cache) = RECENT.lock()
&& let Some((_, _, imp)) = cache.iter().find(|(k, n, _)| (*k, *n) == key)
{
return Ok(imp.clone());
}
let imp = std::sync::Arc::new(step_import(text)?);
if let Ok(mut cache) = RECENT.lock() {
cache.retain(|(k, n, _)| (*k, *n) != key);
cache.push((key.0, key.1, imp.clone()));
if cache.len() > 4 {
cache.remove(0);
}
}
Ok(imp)
}
/// Read a STEP file: its solids (as bodies, placed by the assembly structure) and the tree.
pub fn step_import(text: &str) -> Result<StepImport> {
let ex = p21::parse(text).map_err(KernelError::Invalid)?;
let mut rd = Reader {
ex: &ex,
units: HashMap::new(),
warnings: BTreeMap::new(),
reps: HashMap::new(),
rep_links: HashMap::new(),
pd_reps: HashMap::new(),
children: HashMap::new(),
placements: HashMap::new(),
colors: HashMap::new(),
solids: HashMap::new(),
used_items: HashSet::new(),
node_items: HashSet::new(),
visits: 0,
bodies: Vec::new(),
};
rd.index();
// Roots: product definitions that are nobody's component.
let child_pds: HashSet<u64> =
rd.children.values().flatten().filter_map(|n| ex.get(*n).and_then(|e| e.params().get(4)).and_then(Param::as_ref_id)).collect();
let mut roots: Vec<u64> =
ex.entities.iter().filter(|(id, e)| e.name() == "PRODUCT_DEFINITION" && !child_pds.contains(id)).map(|(id, _)| *id).collect();
roots.sort();
let identity = mt::Matrix4::from_scale(1.0);
let mut tree = Vec::new();
for pd in roots {
let n = rd.node(pd, identity, identity, &[], 0, &mut Vec::new());
if !n.bodies.is_empty() || !n.children.is_empty() {
tree.push(n);
}
}
// Solids outside any product structure (bare geometry files).
let mut loose: Vec<u64> = rd
.reps
.iter()
.flat_map(|(r, (items, _))| items.iter().map(move |i| (*r, *i)))
.filter(|(_, i)| !rd.used_items.contains(i))
.filter(|(_, i)| {
ex.get(*i).is_some_and(|e| matches!(e.name(), "MANIFOLD_SOLID_BREP" | "BREP_WITH_VOIDS" | "FACETED_BREP" | "SHELL_BASED_SURFACE_MODEL"))
})
.map(|(r, _)| r)
.collect();
loose.sort();
loose.dedup();
if !loose.is_empty() {
let mut bodies = Vec::new();
rd.node_items = rd.used_items.clone();
rd.visits = 0;
for r in loose {
rd.rep_bodies(r, &identity, &[], "Import", &mut bodies, 0);
}
if !bodies.is_empty() {
tree.push(StepNode { name: "Import".into(), transform: to_array(&identity), bodies, children: Vec::new() });
}
}
if let Some(first) = ex.duplicates.first() {
rd.warn(format!("{} entity ids are defined twice (first #{first}); the first definitions were used", ex.duplicates.len()));
}
let warnings: Vec<String> = rd.warnings.iter().map(|(m, n)| if *n > 1 { format!("{m} (×{n})") } else { m.clone() }).collect();
if rd.bodies.is_empty() {
let why = warnings.first().cloned().unwrap_or_else(|| "the file has no solids".into());
return Err(KernelError::Failed(format!("STEP import: no bodies ({why})")));
}
Ok(StepImport { bodies: rd.bodies, tree, warnings, schema: ex.schemas.join(", "), system: ex.originating_system.clone() })
}
#[cfg(test)]
mod tests;