forked from 1b-agent/opencadstudio-upstream
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:
1 parent
3c9404510f
commit
659bb06cd6
38 files changed
+2400
-113
No files matched your search
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user