Files
OneCAD/crates/kernel/src/step_in/mod.rs
T
bherbruckandClaude Opus 5.5 9330ee5698 STEP export: AP242 file of our own, names, colours, components as assemblies
Our writer assembles the file: AP242 header and contexts, a product per part
(or per component, with NEXT_ASSEMBLY_USAGE_OCCURRENCE and item-defined
placements per occurrence), named solids, surface colours as styled items,
ADVANCED_FACE for advanced breps. truck now writes only each body's B-rep,
which we copy in; identical geometric entities are written once and shared.
Designs with components export as assemblies (bodies in component frames).

step_validate also rejects unreferenced non-root entities. Tests: the sample
plate and a component assembly export, validate and read back with no
warnings and the same volume, area and face counts; the oracle replay checks
area and faces too (39 rebuildable cases pass). An independent Part 21
parser (step-p21, run outside the repo) accepts our exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-08 00:16:52 +00:00

739 lines
30 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());
let color = self.colors.get(&item).copied();
self.bodies.push(ImportedBody { name, body: body.with_color(color), color, 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
}
}
/// Check a STEP file's structure: it parses, no entity id is defined twice, every reference
/// names a defined entity, nothing but root entities (relationships, definitions,
/// presentation) is left unreferenced, and ids are dense (`#1`…`#n`). Returns the entity count.
pub fn step_validate(text: &str) -> std::result::Result<usize, String> {
let ex = p21::parse(text)?;
if let Some(d) = ex.duplicates.first() {
return Err(format!("{} duplicate entity ids (first #{d})", ex.duplicates.len()));
}
fn refs(p: &Param, out: &mut Vec<u64>) {
match p {
Param::Ref(r) => out.push(*r),
Param::List(v) | Param::Typed(_, v) => v.iter().for_each(|x| refs(x, out)),
_ => {}
}
}
let mut ids: Vec<&u64> = ex.entities.keys().collect();
ids.sort();
for id in &ids {
let mut out = Vec::new();
if let Some(e) = ex.get(**id) {
e.records.iter().flat_map(|r| &r.params).for_each(|p| refs(p, &mut out));
}
if let Some(r) = out.iter().find(|r| ex.get(**r).is_none()) {
return Err(format!("#{id} refers to undefined #{r}"));
}
}
// Unreferenced entities must be roots of the exchange (relationships, definitions,
// presentation); anything else is dead data.
let mut used = std::collections::HashSet::new();
for e in ex.entities.values() {
let mut out = Vec::new();
e.records.iter().flat_map(|r| &r.params).for_each(|p| refs(p, &mut out));
used.extend(out);
}
const ROOTS: &[&str] = &[
"SHAPE_DEFINITION_REPRESENTATION",
"APPLICATION_PROTOCOL_DEFINITION",
"PRODUCT_RELATED_PRODUCT_CATEGORY",
"PRODUCT_CATEGORY_RELATIONSHIP",
"SHAPE_REPRESENTATION_RELATIONSHIP",
"REPRESENTATION_RELATIONSHIP",
"CONTEXT_DEPENDENT_SHAPE_REPRESENTATION",
"MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION",
"DRAUGHTING_MODEL",
"PROPERTY_DEFINITION_REPRESENTATION",
"PRESENTATION_LAYER_ASSIGNMENT",
];
for id in &ids {
if used.contains(*id) {
continue;
}
let Some(e) = ex.get(**id) else { continue };
if !e.records.iter().any(|r| ROOTS.contains(&r.name.as_str()) || r.name.starts_with("APPLIED_")) {
return Err(format!("#{id} ({}) is not referenced by anything", e.name()));
}
}
let n = ex.entities.len();
if ids.last().is_some_and(|m| **m != n as u64) || ids.first().is_some_and(|m| **m != 1) {
return Err(format!("entity ids are not dense: {n} entities, #{}…#{}", ids.first().map_or(0, |x| **x), ids.last().map_or(0, |x| **x)));
}
Ok(n)
}
/// 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;