IMPORT: read SAT, STEP, IGES and WMF files; ACISIN and WMFIN

ACIS text files come in one object per body (3DSOLID, REGION or SURFACE).
STEP B-rep models become a block per product with component inserts at
their placements, scaled to the drawing's units; IGES manifold solids come
in as 3DSOLIDs scaled to the drawing's units with their colours. Splines
that are exactly analytic shapes are kept as those shapes. Windows
metafiles become a WMF<n> block of polylines and text fitted to half the
view, placed at its top-left corner or by WMFIN's insertion prompts. The
Import File window lists the four formats as supported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-16 15:06:16 +03:00
1 parent 3c9404510f
commit 659bb06cd6
38 files changed
+2400 -113

No files matched your search

+829
View File
@@ -0,0 +1,829 @@
//! 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<P>),
Null,
/// A typed value, such as `LENGTH_MEASURE(1.)`.
Typed(Vec<P>),
}
impl P {
fn num(&self) -> Option<f64> {
match self {
P::Num(v) => Some(*v),
P::Typed(inner) => inner.first()?.num(),
_ => None,
}
}
fn reference(&self) -> Option<u64> {
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<P>)>;
struct Cursor<'a> {
s: &'a [u8],
i: 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[self.i..].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<u8> {
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<P> {
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()?)
}
})
}
fn params(&mut self) -> Option<Vec<P>> {
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<u64, Instance>,
/// Plane-angle unit in radians.
angle: f64,
}
impl Doc {
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<Plane> {
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.
fn knots(mults: &P, knots: &P) -> Option<Vec<f64>> {
let mut out = Vec::new();
for (m, k) in mults.list().iter().zip(knots.list()) {
for _ in 0..m.num()? as usize {
out.push(k.num()?);
}
}
Some(out)
}
fn curve(&self, p: &P) -> Option<Curve3> {
let id = p.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()?,
}),
"SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" | "TRIMMED_CURVE" => {
self.curve(params.get(1)?)?
}
"POLYLINE" => {
let points: Option<Vec<_>> =
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<NurbsCurve3> {
// 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[0], &base[1], &with[0], &with[1], weights)
};
let points: Option<Vec<_>> = points.list().iter().map(|q| self.point(q)).collect();
let weights: Option<Vec<f64>> = 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)?;
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<Surface> {
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<NurbsSurface3> {
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[0], &base[1], &base[2], &with[0], &with[1], &with[2], &with[3], weights)
};
let rows: Option<Vec<Vec<[f64; 3]>>> = points
.list()
.iter()
.map(|row| row.list().iter().map(|q| self.point(q)).collect())
.collect();
let weights: Option<Vec<Vec<f64>>> = weights.map(|w| {
w.list()
.iter()
.map(|row| row.list().iter().filter_map(P::num).collect())
.collect()
});
NurbsSurface3::new(
ud.num()? as usize,
vd.num()? as usize,
rows?,
Self::knots(um, uk)?,
Self::knots(vm, vk)?,
weights,
)
}
}
/// Topology of one body, keyed by the file's instance ids.
struct Assembly<'a> {
doc: &'a Doc,
build: Builder,
vertices: HashMap<u64, VertexKey>,
/// Each edge, and whether it runs from the file's edge end to its start.
edges: HashMap<u64, (EdgeKey, bool)>,
}
impl Assembly<'_> {
fn vertex(&mut self, p: &P) -> Option<VertexKey> {
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<Vec<(EdgeKey, bool)>> {
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<FaceSpec> {
let id = p.reference()?;
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<Vec<FaceSpec>> {
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]];
/// One product: its own bodies and where each of its components sits.
pub struct StepProduct {
pub name: String,
pub bodies: Vec<kernel::brep::Body>,
/// 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<StepProduct>,
/// The products no other one is a component of.
pub roots: Vec<usize>,
}
/// A named unit's size: millimetres for a length, radians for an angle.
fn unit_size(doc: &Doc, id: u64) -> Option<f64> {
if let Some(si) = doc.part(id, "SI_UNIT") {
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(match si.get(1) {
Some(P::Enum(name)) if name == "METRE" => prefix * 1000.0,
_ => prefix,
});
}
let conversion = doc.part(id, "CONVERSION_BASED_UNIT")?;
let (_, measure) = doc.simple(conversion.get(1)?.reference()?)?;
Some(measure.first()?.num()? * unit_size(doc, measure.get(1)?.reference()?)?)
}
/// 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<kernel::brep::Body> {
let Some((kind, params)) = doc.simple(id) else {
return Vec::new();
};
let lumps: Vec<Vec<&P>> = 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(),
};
let faces: Option<Vec<_>> = lump.iter().map(|shell| assembly.shell(shell)).collect();
if let Some(faces) = faces {
if assembly.build.lump(faces).is_some() {
bodies.push(assembly.build.body);
}
}
}
bodies
}
impl Doc {
/// An axis placement as a placement matrix.
fn frame(&self, id: u64) -> Option<Placement> {
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`.
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::<f64>() + 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::<f64>();
}
out
}
pub fn read(bytes: &[u8]) -> Option<StepModel> {
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,
};
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;
}
cursor.i += 1;
}
}
}
let mut doc = Doc {
instances,
angle: 1.0,
};
let mut unit_mm = 1.0;
let mut angle = 1.0;
let mut ids: Vec<u64> = 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;
let doc = doc;
// What each instance id is, by kind.
let of_kind = |kind: &str| -> Vec<u64> {
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<u64> = 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<u64, u64> = of_kind("PRODUCT_DEFINITION_SHAPE")
.into_iter()
.filter_map(|pds| Some((param(pds, 2)?, pds)))
.collect();
let mut reps_of_shape: HashMap<u64, Vec<u64>> = 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<u64, Vec<u64>> = 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<u64> {
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<u64, usize> = HashMap::new();
let mut products: Vec<StepProduct> = Vec::new();
let mut used_items: Vec<u64> = 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<u64> = 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,
})
}