//! STEP (ISO 10303-21) reading: the B-rep solids and shell models of an //! AP203/AP214/AP242 file, as kernel bodies, with the product's name and the //! file's length unit. use std::collections::HashMap; use kernel::brep::{ Circle3, Cone, Curve3, Cylinder, EdgeKey, Ellipse3, Line3, Sphere, Surface, Torus, VertexKey, }; use kernel::space::{NurbsCurve3, NurbsSurface3, Plane, Vec3}; use super::brep_build::{Builder, FaceSpec}; /// One parameter of an entity instance. #[derive(Debug, Clone)] enum P { Num(f64), Str(String), Enum(String), Ref(u64), List(Vec

), Null, /// A typed value, such as `LENGTH_MEASURE(1.)`. Typed(Vec

), } impl P { fn num(&self) -> Option { match self { P::Num(v) => Some(*v), P::Typed(inner) => inner.first()?.num(), _ => None, } } fn reference(&self) -> Option { match self { P::Ref(id) => Some(*id), _ => None, } } fn list(&self) -> &[P] { match self { P::List(items) => items, _ => &[], } } fn truth(&self) -> bool { matches!(self, P::Enum(e) if e == "T") } fn text(&self) -> Option<&str> { match self { P::Str(s) => Some(s), _ => None, } } } /// An instance: one `(type, parameters)` part, or several for a complex one. type Instance = Vec<(String, Vec

)>; struct Cursor<'a> { s: &'a [u8], i: usize, /// How many parameter lists deep the cursor is. depth: usize, } impl Cursor<'_> { fn skip(&mut self) { loop { while self.s.get(self.i).is_some_and(|c| c.is_ascii_whitespace()) { self.i += 1; } if self.s.get(self.i..).is_some_and(|rest| rest.starts_with(b"/*")) { match self.s[self.i + 2..].windows(2).position(|w| w == b"*/") { Some(end) => self.i += end + 4, None => self.i = self.s.len(), } } else { break; } } } fn peek(&mut self) -> Option { self.skip(); self.s.get(self.i).copied() } fn eat(&mut self, c: u8) -> Option<()> { (self.peek()? == c).then(|| self.i += 1) } fn word(&mut self) -> String { self.skip(); let start = self.i; while self .s .get(self.i) .is_some_and(|c| c.is_ascii_alphanumeric() || *c == b'_' || *c == b'-') { self.i += 1; } String::from_utf8_lossy(&self.s[start..self.i]).to_ascii_uppercase() } fn param(&mut self) -> Option

{ Some(match self.peek()? { b'#' => { self.i += 1; P::Ref(self.word().parse().ok()?) } b'\'' => { self.i += 1; let mut text = Vec::new(); loop { let c = *self.s.get(self.i)?; self.i += 1; if c == b'\'' { if self.s.get(self.i) == Some(&b'\'') { self.i += 1; } else { break; } } text.push(c); } P::Str(String::from_utf8_lossy(&text).into_owned()) } b'.' => { self.i += 1; let value = self.word(); self.eat(b'.')?; P::Enum(value) } b'$' | b'*' => { self.i += 1; P::Null } b'(' => P::List(self.params()?), c if c.is_ascii_digit() || c == b'-' || c == b'+' || c == b'.' => { let start = self.i; while self .s .get(self.i) .is_some_and(|c| c.is_ascii_digit() || b"+-.eE".contains(c)) { self.i += 1; } let raw = String::from_utf8_lossy(&self.s[start..self.i]).replace(".E", ".0E"); P::Num(raw.replace(".e", ".0e").parse().ok()?) } _ => { if self.word().is_empty() { return None; } P::Typed(self.params()?) } }) } /// A parameter list; lists nested past any real file's depth are /// refused rather than read on until the stack runs out. fn params(&mut self) -> Option> { if self.depth >= 64 { return None; } self.depth += 1; let items = self.list(); self.depth -= 1; items } fn list(&mut self) -> Option> { self.eat(b'(')?; let mut items = Vec::new(); if self.eat(b')').is_some() { return Some(items); } loop { items.push(self.param()?); match self.peek()? { b',' => self.i += 1, b')' => { self.i += 1; return Some(items); } _ => return None, } } } /// `#id = TYPE(...)` or `#id = (A(...) B(...))`, up to its `;`. fn instance(&mut self) -> Option<(u64, Instance)> { self.eat(b'#')?; let id = self.word().parse().ok()?; self.eat(b'=')?; let mut parts = Vec::new(); if self.eat(b'(').is_some() { while self.eat(b')').is_none() { let name = self.word(); if name.is_empty() { return None; } parts.push((name, self.params()?)); } } else { let name = self.word(); parts.push((name, self.params()?)); } self.eat(b';')?; Some((id, parts)) } } struct Doc { instances: HashMap, /// Plane-angle unit in radians. angle: f64, /// The colour each styled item (a solid, a shell or a face) is shown /// in, as 0–1 RGB. styles: HashMap, } /// An RGB colour as 0–1 values. pub type Rgb = [f64; 3]; impl Doc { /// A presentation colour: an RGB one or a predefined one. fn colour(&self, p: &P) -> Option { let (kind, params) = self.simple(p.reference()?)?; match kind { "COLOUR_RGB" => Some([params.get(1)?.num()?, params.get(2)?.num()?, params.get(3)?.num()?]), "DRAUGHTING_PRE_DEFINED_COLOUR" => Some(match params.first()?.text()? { "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], "white" => [1.0, 1.0, 1.0], _ => return None, }), _ => None, } } /// The colour a styled item's presentation styles give it: its surface /// fill (or rendering) colour, or failing that its curve colour. fn style_colour(&self, styles: &P) -> Option { let mut stack: Vec<&P> = vec![styles]; let mut seen = Vec::new(); let mut curve = None; while let Some(p) = stack.pop() { match p { P::List(items) | P::Typed(items) => stack.extend(items.iter().rev()), P::Ref(id) if !seen.contains(id) => { seen.push(*id); let Some((kind, params)) = self.simple(*id) else { continue }; match kind { "FILL_AREA_STYLE_COLOUR" => { if let Some(c) = params.get(1).and_then(|c| self.colour(c)) { return Some(c); } } "SURFACE_STYLE_RENDERING" | "SURFACE_STYLE_RENDERING_WITH_PROPERTIES" => { if let Some(c) = params.get(1).and_then(|c| self.colour(c)) { return Some(c); } } "CURVE_STYLE" => curve = curve.or_else(|| params.get(3).and_then(|c| self.colour(c))), _ => stack.extend(params.iter().rev()), } } _ => {} } } curve } fn parts(&self, id: u64) -> Option<&Instance> { self.instances.get(&id) } fn kind(&self, id: u64) -> &str { self.parts(id) .and_then(|parts| parts.first()) .map_or("", |(name, _)| name.as_str()) } /// The parameters of the part named `name`. fn part(&self, id: u64, name: &str) -> Option<&[P]> { self.parts(id)? .iter() .find(|(n, _)| n == name) .map(|(_, p)| p.as_slice()) } /// A simple instance's type and parameters. fn simple(&self, id: u64) -> Option<(&str, &[P])> { let parts = self.parts(id)?; let (name, params) = parts.first()?; Some((name.as_str(), params.as_slice())) } fn point(&self, p: &P) -> Option<[f64; 3]> { let (_, params) = self.simple(p.reference()?)?; let c = params.get(1)?.list(); Some([c.first()?.num()?, c.get(1)?.num()?, c.get(2).and_then(P::num).unwrap_or(0.0)]) } fn direction(&self, p: &P) -> Option<[f64; 3]> { self.point(p) } fn placement(&self, p: &P) -> Option { let (_, params) = self.simple(p.reference()?)?; let origin = self.point(params.get(1)?)?; let z = params.get(2).and_then(|d| self.direction(d)).unwrap_or([0.0, 0.0, 1.0]); let x = params .get(3) .and_then(|d| self.direction(d)) .filter(|x| Vec3::from(*x).cross(Vec3::from(z)).length() > 1e-9) // Any axis off the normal; the plane squares it up. .unwrap_or(if z[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] }); Plane::orthonormal(origin, x, z) } /// Knots expanded by their multiplicities. No knot repeats more than /// `degree + 1` times, so a bogus multiplicity cannot blow up the list. fn knots(mults: &P, knots: &P, degree: usize) -> Option> { let mut out = Vec::new(); for (m, k) in mults.list().iter().zip(knots.list()) { for _ in 0..(m.num()? as usize).min(degree.saturating_add(1)) { out.push(k.num()?); } } Some(out) } fn curve(&self, p: &P) -> Option { let mut id = p.reference()?; // Curves that wrap a basis curve are followed to it; the hop limit // stops curves that wrap each other in a loop. let mut hops = 0; while matches!( self.kind(id), "SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" | "TRIMMED_CURVE" ) { hops += 1; if hops > 32 { return None; } id = self.simple(id)?.1.get(1)?.reference()?; } let kind = self.kind(id); if kind == "B_SPLINE_CURVE_WITH_KNOTS" || self.part(id, "B_SPLINE_CURVE").is_some() { return self.spline_curve(id).map(Curve3::Nurbs); } let (_, params) = self.simple(id)?; Some(match kind { "LINE" => { let origin = self.point(params.get(1)?)?; let (_, vector) = self.simple(params.get(2)?.reference()?)?; let direction = Vec3::from(self.direction(vector.get(1)?)?).normalize()? * vector.get(2)?.num()?; Curve3::Line(Line3 { origin, direction: direction.to_array(), }) } "CIRCLE" => Curve3::Circle(Circle3 { plane: self.placement(params.get(1)?)?, radius: params.get(2)?.num()?, }), "ELLIPSE" => Curve3::Ellipse(Ellipse3 { plane: self.placement(params.get(1)?)?, major_radius: params.get(2)?.num()?, minor_radius: params.get(3)?.num()?, }), "POLYLINE" => { let points: Option> = params.get(1)?.list().iter().map(|q| self.point(q)).collect(); Curve3::Nurbs(NurbsCurve3::new(1, points?, Vec::new(), None)?) } _ => return None, }) } fn spline_curve(&self, id: u64) -> Option { // The simple form carries everything after its name; the complex // form splits it over parts that have no name. let (degree, points, mults, knots, weights) = if let Some(p) = self.part(id, "B_SPLINE_CURVE_WITH_KNOTS").filter(|p| p.len() >= 9) { (&p[1], &p[2], &p[6], &p[7], None) } else { let base = self.part(id, "B_SPLINE_CURVE")?; let with = self.part(id, "B_SPLINE_CURVE_WITH_KNOTS")?; let weights = self.part(id, "RATIONAL_B_SPLINE_CURVE").and_then(|w| w.first()); (base.first()?, base.get(1)?, with.first()?, with.get(1)?, weights) }; let points: Option> = points.list().iter().map(|q| self.point(q)).collect(); let weights: Option> = weights.map(|w| w.list().iter().filter_map(P::num).collect()); let degree = degree.num()? as usize; let points = points?; let knots = Self::knots(mults, knots, degree)?; match &weights { Some(w) => NurbsCurve3::new_strict(degree, points.clone(), knots.clone(), w.clone()) .or_else(|| NurbsCurve3::new(degree, points, knots, weights.clone())), None => NurbsCurve3::new(degree, points, knots, None), } } fn surface(&self, p: &P) -> Option { let id = p.reference()?; let kind = self.kind(id); if kind == "B_SPLINE_SURFACE_WITH_KNOTS" || self.part(id, "B_SPLINE_SURFACE").is_some() { return self.spline_surface(id).map(Surface::Nurbs); } let (_, params) = self.simple(id)?; let frame = self.placement(params.get(1)?)?; let value = |k: usize| params.get(k).and_then(P::num); Some(match kind { "PLANE" => Surface::Plane(frame), "CYLINDRICAL_SURFACE" => Surface::Cylinder(Cylinder { base: frame, radius: value(2)?, }), // A STEP cone widens along its axis by its semi-angle. "CONICAL_SURFACE" => Surface::Cone(Cone { base: frame, radius: value(2)?, half_angle: -value(3)? * self.angle, }), "SPHERICAL_SURFACE" => Surface::Sphere(Sphere { frame, radius: value(2)?, }), "TOROIDAL_SURFACE" => Surface::Torus(Torus { frame, major_radius: value(2)?, minor_radius: value(3)?, }), _ => return None, }) } fn spline_surface(&self, id: u64) -> Option { let (ud, vd, points, um, vm, uk, vk, weights) = if let Some(p) = self .part(id, "B_SPLINE_SURFACE_WITH_KNOTS") .filter(|p| p.len() >= 13) { (&p[1], &p[2], &p[3], &p[8], &p[9], &p[10], &p[11], None) } else { let base = self.part(id, "B_SPLINE_SURFACE")?; let with = self.part(id, "B_SPLINE_SURFACE_WITH_KNOTS")?; let weights = self.part(id, "RATIONAL_B_SPLINE_SURFACE").and_then(|w| w.first()); ( base.first()?, base.get(1)?, base.get(2)?, with.first()?, with.get(1)?, with.get(2)?, with.get(3)?, weights, ) }; let rows: Option>> = points .list() .iter() .map(|row| row.list().iter().map(|q| self.point(q)).collect()) .collect(); let weights: Option>> = weights.map(|w| { w.list() .iter() .map(|row| row.list().iter().filter_map(P::num).collect()) .collect() }); let (ud, vd) = (ud.num()? as usize, vd.num()? as usize); NurbsSurface3::new(ud, vd, rows?, Self::knots(um, uk, ud)?, Self::knots(vm, vk, vd)?, weights) } } /// Topology of one body, keyed by the file's instance ids. struct Assembly<'a> { doc: &'a Doc, build: Builder, vertices: HashMap, /// Each edge, and whether it runs from the file's edge end to its start. edges: HashMap, /// The file's face of each face built, in build order. faces: Vec, } impl Assembly<'_> { fn vertex(&mut self, p: &P) -> Option { let id = p.reference()?; if let Some(&v) = self.vertices.get(&id) { return Some(v); } let (_, params) = self.doc.simple(id)?; let v = self.build.vertex(self.doc.point(params.get(1)?)?); self.vertices.insert(id, v); Some(v) } fn edge(&mut self, p: &P) -> Option<(EdgeKey, bool)> { let id = p.reference()?; if let Some(&e) = self.edges.get(&id) { return Some(e); } let (_, params) = self.doc.simple(id)?; let (a, b) = (self.vertex(params.get(1)?)?, self.vertex(params.get(2)?)?); let curve = self.doc.curve(params.get(3)?)?; let same_sense = params.get(4).is_none_or(P::truth); let key = if same_sense { self.build.edge(curve, a, b)? } else { self.build.edge(curve, b, a)? }; self.edges.insert(id, (key, !same_sense)); Some((key, !same_sense)) } /// A bound's coedges, in the order the face runs them. fn bound(&mut self, p: &P) -> Option> { let (_, params) = self.doc.simple(p.reference()?)?; let ring_id = params.get(1)?.reference()?; let agrees = params.get(2).is_none_or(P::truth); let mut ring = Vec::new(); if self.doc.kind(ring_id) == "EDGE_LOOP" { let (_, loop_params) = self.doc.simple(ring_id)?; for oriented in loop_params.get(1)?.list() { let (_, o) = self.doc.simple(oriented.reference()?)?; let (edge, flipped) = self.edge(o.get(3)?)?; let along = o.get(4).is_none_or(P::truth); ring.push((edge, along != flipped)); } } if !agrees { ring.reverse(); for (_, forward) in &mut ring { *forward = !*forward; } } Some(ring) } fn face(&mut self, p: &P, flip: bool) -> Option { let id = p.reference()?; self.faces.push(id); let (_, params) = self.doc.simple(id)?; let surface = self.doc.surface(params.get(2)?)?; let mut outer = Vec::new(); let mut inner = Vec::new(); for bound in params.get(1)?.list() { let ring = self.bound(bound)?; if self.doc.kind(bound.reference()?) == "FACE_OUTER_BOUND" { outer.push(ring); } else { inner.push(ring); } } outer.extend(inner); let mut loops = outer; let mut forward = params.get(3).is_none_or(P::truth); if flip { forward = !forward; for ring in &mut loops { ring.reverse(); for (_, f) in ring.iter_mut() { *f = !*f; } } } Some(FaceSpec { surface, forward, loops, }) } /// A shell's faces; an oriented shell set against its faces flips them. fn shell(&mut self, p: &P) -> Option> { let mut id = p.reference()?; let mut flip = false; if self.doc.kind(id) == "ORIENTED_CLOSED_SHELL" || self.doc.kind(id) == "ORIENTED_OPEN_SHELL" { let (_, params) = self.doc.simple(id)?; flip = !params.get(3).is_none_or(P::truth); id = params.get(2)?.reference()?; } let (_, params) = self.doc.simple(id)?; params .get(1)? .list() .iter() .map(|face| self.face(face, flip)) .collect() } } /// A 3×4 placement: the columns are the x, y and z axes and the origin. pub type Placement = [[f64; 4]; 3]; const IDENTITY: Placement = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]]; /// A body with the colours the file shows it in. pub struct StepBody { pub body: kernel::brep::Body, /// The colour of the solid (or shell model) as a whole. pub colour: Option, /// Each face's own colour, in the body's face order. pub faces: Vec>, } /// One product: its own bodies and where each of its components sits. pub struct StepProduct { pub name: String, pub bodies: Vec, /// Components: the product (an index into [`StepModel::products`]) and /// its placement in this one. pub children: Vec<(usize, Placement)>, } /// What a STEP file holds. pub struct StepModel { /// Millimetres per file length unit. pub unit_mm: f64, pub products: Vec, /// The products no other one is a component of. pub roots: Vec, } /// A named unit's size: millimetres for a length, radians for an angle. /// A conversion-based unit is a multiple of another unit; the hop limit /// stops units defined in terms of each other. fn unit_size(doc: &Doc, mut id: u64) -> Option { let mut factor = 1.0; for _ in 0..16 { let Some(si) = doc.part(id, "SI_UNIT") else { let conversion = doc.part(id, "CONVERSION_BASED_UNIT")?; let (_, measure) = doc.simple(conversion.get(1)?.reference()?)?; factor *= measure.first()?.num()?; id = measure.get(1)?.reference()?; continue; }; let prefix = match si.first() { Some(P::Enum(prefix)) => match prefix.as_str() { "MILLI" => 1e-3, "CENTI" => 1e-2, "DECI" => 1e-1, "KILO" => 1e3, "MICRO" => 1e-6, _ => 1.0, }, _ => 1.0, }; return Some( factor * match si.get(1) { Some(P::Enum(name)) if name == "METRE" => prefix * 1000.0, _ => prefix, }, ); } None } /// The bodies of one B-rep item: a solid, a solid with voids, or each shell /// of a shell model. fn item_bodies(doc: &Doc, id: u64) -> Vec { let Some((kind, params)) = doc.simple(id) else { return Vec::new(); }; let lumps: Vec> = match kind { "MANIFOLD_SOLID_BREP" => vec![params.get(1).into_iter().collect()], "BREP_WITH_VOIDS" => vec![params .get(1) .into_iter() .chain(params.get(2).map(P::list).unwrap_or_default()) .collect()], "SHELL_BASED_SURFACE_MODEL" => params .get(1) .map(P::list) .unwrap_or_default() .iter() .map(|shell| vec![shell]) .collect(), _ => return Vec::new(), }; let mut bodies = Vec::new(); for lump in lumps { let mut assembly = Assembly { doc, build: Builder::new(), vertices: HashMap::new(), edges: HashMap::new(), faces: Vec::new(), }; let faces: Option> = lump.iter().map(|shell| assembly.shell(shell)).collect(); if let Some(faces) = faces { if assembly.build.lump(faces).is_some() { // The item's own style, or its (first) shell's. let colour = doc.styles.get(&id).copied().or_else(|| { lump.iter() .filter_map(|shell| shell.reference()) .find_map(|shell| doc.styles.get(&shell).copied()) }); bodies.push(StepBody { body: assembly.build.body, colour, faces: assembly.faces.iter().map(|face| doc.styles.get(face).copied()).collect(), }); } } } bodies } impl Doc { /// An axis placement as a placement matrix. fn frame(&self, id: u64) -> Option { let plane = self.placement(&P::Ref(id))?; let z = plane.normal()?; let [x, y, o] = [plane.x_axis, plane.y_axis, plane.origin]; Some([0, 1, 2].map(|r| [x[r], y[r], z[r], o[r]])) } } /// `a·b`. pub(super) fn compose(a: &Placement, b: &Placement) -> Placement { let mut out = [[0.0; 4]; 3]; for r in 0..3 { for c in 0..4 { out[r][c] = (0..3).map(|k| a[r][k] * b[k][c]).sum::() + if c == 3 { a[r][3] } else { 0.0 }; } } out } /// The inverse of a rigid placement. fn invert(a: &Placement) -> Placement { let mut out = [[0.0; 4]; 3]; for r in 0..3 { for c in 0..3 { out[r][c] = a[c][r]; } out[r][3] = -(0..3).map(|k| a[k][r] * a[k][3]).sum::(); } out } pub fn read(bytes: &[u8]) -> Option { let text = String::from_utf8_lossy(bytes); if !text.trim_start().starts_with("ISO-10303-21") { return None; } let data = text.find("DATA;")? + 5; let mut cursor = Cursor { s: &text.as_bytes()[data..], i: 0, depth: 0, }; let mut instances = HashMap::new(); while cursor.peek() == Some(b'#') { match cursor.instance() { Some((id, parts)) => { instances.insert(id, parts); } None => { // Skip a malformed instance up to the next one. while cursor.s.get(cursor.i).is_some_and(|c| *c != b';') { cursor.i += 1; } // Past the `;`, or at the end of a truncated file. cursor.i = (cursor.i + 1).min(cursor.s.len()); } } } let mut doc = Doc { instances, angle: 1.0, styles: HashMap::new(), }; let mut unit_mm = 1.0; let mut angle = 1.0; let mut ids: Vec = doc.instances.keys().copied().collect(); ids.sort_unstable(); for &id in &ids { let Some(units) = doc.part(id, "GLOBAL_UNIT_ASSIGNED_CONTEXT") else { continue; }; for unit in units.first().map(P::list).unwrap_or_default() { let Some(unit) = unit.reference() else { continue }; if doc.part(unit, "LENGTH_UNIT").is_some() { unit_mm = unit_size(&doc, unit).unwrap_or(1.0); } else if doc.part(unit, "PLANE_ANGLE_UNIT").is_some() { angle = unit_size(&doc, unit).unwrap_or(1.0); } } break; } doc.angle = angle; // Styled items: plain ones first, so an overriding style wins. let mut styled = Vec::new(); for kind in ["STYLED_ITEM", "OVER_RIDING_STYLED_ITEM"] { for &id in &ids { if doc.kind(id) != kind { continue; } let Some((_, params)) = doc.simple(id) else { continue }; let (Some(styles), Some(item)) = (params.get(1), params.get(2).and_then(P::reference)) else { continue; }; if let Some(colour) = doc.style_colour(styles) { styled.push((item, colour)); } } } doc.styles.extend(styled); let doc = doc; // What each instance id is, by kind. let of_kind = |kind: &str| -> Vec { ids.iter() .copied() .filter(|&id| doc.kind(id) == kind) .collect() }; let param = |id: u64, k: usize| doc.simple(id).and_then(|(_, p)| p.get(k)).and_then(P::reference); // Product definitions and the name of the product each defines. let definitions: Vec = ids .iter() .copied() .filter(|&id| { matches!(doc.kind(id), "PRODUCT_DEFINITION" | "PRODUCT_DEFINITION_WITH_ASSOCIATED_DOCUMENTS") }) .collect(); let product_name = |definition: u64| -> String { let product = param(definition, 2).and_then(|formation| param(formation, 2)); product .and_then(|p| doc.simple(p)) .and_then(|(_, params)| { params .get(1) .and_then(P::text) .filter(|s| !s.trim().is_empty()) .or_else(|| params.first().and_then(P::text)) }) .unwrap_or_default() .to_string() }; // The shape of a definition (or an occurrence), and the representations // attached to a shape. let shape_of: HashMap = of_kind("PRODUCT_DEFINITION_SHAPE") .into_iter() .filter_map(|pds| Some((param(pds, 2)?, pds))) .collect(); let mut reps_of_shape: HashMap> = HashMap::new(); for sdr in of_kind("SHAPE_DEFINITION_REPRESENTATION") { if let (Some(pds), Some(rep)) = (param(sdr, 0), param(sdr, 1)) { reps_of_shape.entry(pds).or_default().push(rep); } } // Representations tied without a transformation share their items. let mut linked: HashMap> = HashMap::new(); for id in &ids { let Some(("SHAPE_REPRESENTATION_RELATIONSHIP", params)) = doc.simple(*id) else { continue; }; if let (Some(a), Some(b)) = (params.get(2).and_then(P::reference), params.get(3).and_then(P::reference)) { linked.entry(a).or_default().push(b); linked.entry(b).or_default().push(a); } } let items_of = |rep: u64| -> Vec { let mut seen = vec![rep]; let mut at = 0; while at < seen.len() { for next in linked.get(&seen[at]).into_iter().flatten() { if !seen.contains(next) { seen.push(*next); } } at += 1; } seen.iter() .filter_map(|r| doc.simple(*r)) .flat_map(|(_, params)| params.get(1).map(P::list).unwrap_or_default()) .filter_map(P::reference) .collect() }; let mut index: HashMap = HashMap::new(); let mut products: Vec = Vec::new(); let mut used_items: Vec = Vec::new(); for &definition in &definitions { let mut bodies = Vec::new(); for rep in shape_of.get(&definition).and_then(|pds| reps_of_shape.get(pds)).into_iter().flatten() { for item in items_of(*rep) { if !used_items.contains(&item) { let found = item_bodies(&doc, item); if !found.is_empty() { used_items.push(item); } bodies.extend(found); } } } index.insert(definition, products.len()); products.push(StepProduct { name: product_name(definition), bodies, children: Vec::new(), }); } // Components, placed by the transformation of their occurrence. let mut component = vec![false; products.len()]; for occurrence in of_kind("NEXT_ASSEMBLY_USAGE_OCCURRENCE") { let (Some(&parent), Some(&child)) = ( param(occurrence, 3).and_then(|d| index.get(&d)), param(occurrence, 4).and_then(|d| index.get(&d)), ) else { continue; }; let placement = shape_of .get(&occurrence) .and_then(|pds| { ids.iter().copied().find(|&id| { doc.kind(id) == "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION" && param(id, 1) == Some(*pds) }) }) .and_then(|cdsr| param(cdsr, 0)) .and_then(|relation| doc.part(relation, "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION")) .and_then(|p| p.first()?.reference()) .and_then(|transformation| { let (_, t) = doc.simple(transformation)?; let from = doc.frame(t.get(2)?.reference()?)?; let to = doc.frame(t.get(3)?.reference()?)?; Some(compose(&to, &invert(&from))) }) .unwrap_or(IDENTITY); products[parent].children.push((child, placement)); component[child] = true; } // A file whose solids no product claims: one product holding them all, // named after the file's first product. let loose: Vec = ids .iter() .copied() .filter(|id| !used_items.contains(id)) .collect(); let mut loose_bodies = Vec::new(); for id in loose { loose_bodies.extend(item_bodies(&doc, id)); } if !loose_bodies.is_empty() { let name = definitions.first().map(|d| product_name(*d)).unwrap_or_default(); component.push(false); products.push(StepProduct { name, bodies: loose_bodies, children: Vec::new(), }); } let roots = (0..products.len()) .filter(|&k| !component[k] && (!products[k].bodies.is_empty() || !products[k].children.is_empty())) .collect(); Some(StepModel { unit_mm, products, roots, }) }