STEP import takes up for Y-up files and names for the bodies' names (#1312)
file.insert_step and doc.open on a STEP file take `up` (turn a Y-up file so its up is +Z) and `names` (names for the imported bodies). The MCP tool and the app's control requests pass them through; an import without them works as before. Tests in the kernel's STEP reader, the engine, MCP and the file-load UI; docs/step-import.md and docs/mcp.md describe both.
This commit is contained in:
1 parent
81d9b7e04a
commit
0ee5e97db5
12 files changed
+572
-38
No files matched your search
@@ -48,8 +48,12 @@ pub struct ImportedBody {
|
||||
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.
|
||||
/// Product names from the root to the part that holds the body (the last is the part's own
|
||||
/// name; empty for a solid outside any product).
|
||||
pub path: Vec<String>,
|
||||
/// The name of the assembly occurrence (instance) that holds the body: the usage's name in
|
||||
/// the file, else its id. `None` for a body of a root product or outside any product.
|
||||
pub instance: Option<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
|
||||
@@ -599,7 +603,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
fn rep_bodies(&mut self, rep: u64, m: &mt::Matrix4, path: &[String], product: &str, instance: Option<&str>, out: &mut Vec<usize>, depth: usize) {
|
||||
if depth > MAX_ASSEMBLY_DEPTH {
|
||||
self.warn("mapped items nested too deeply");
|
||||
return;
|
||||
@@ -699,6 +703,7 @@ impl<'a> Reader<'a> {
|
||||
body,
|
||||
color,
|
||||
path: path.to_vec(),
|
||||
instance: instance.map(str::to_string),
|
||||
closed,
|
||||
file_faces,
|
||||
source_item: item,
|
||||
@@ -726,7 +731,7 @@ impl<'a> Reader<'a> {
|
||||
(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);
|
||||
self.rep_bodies(r, &mm, path, product, instance, out, depth + 1);
|
||||
}
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => self.warn(format!("mapped item #{item}: {e}")),
|
||||
@@ -737,6 +742,13 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of an assembly occurrence: the usage's name, else its id.
|
||||
fn occurrence_name(&self, nauo: u64) -> Option<String> {
|
||||
let p = self.ex.get(nauo)?.params();
|
||||
let name = p.get(1).and_then(Param::as_str).filter(|s| !s.trim().is_empty()).or_else(|| p.first().and_then(Param::as_str))?;
|
||||
Some(name.trim().to_string()).filter(|n| !n.is_empty())
|
||||
}
|
||||
|
||||
fn product_name(&self, pd: u64) -> String {
|
||||
let name = (|| {
|
||||
let pdf = self.ex.get(self.ex.get(pd)?.params().get(2)?.as_ref_id()?)?;
|
||||
@@ -787,7 +799,16 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn node(&mut self, pd: u64, local: mt::Matrix4, world: mt::Matrix4, path: &[String], depth: usize, stack: &mut Vec<u64>) -> StepNode {
|
||||
fn node(
|
||||
&mut self,
|
||||
pd: u64,
|
||||
local: mt::Matrix4,
|
||||
world: mt::Matrix4,
|
||||
path: &[String],
|
||||
instance: Option<&str>,
|
||||
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 self.stop() {
|
||||
@@ -810,7 +831,7 @@ impl<'a> Reader<'a> {
|
||||
let mut bodies = Vec::new();
|
||||
self.node_items.clear();
|
||||
for r in &reps {
|
||||
self.rep_bodies(*r, &world, &path, &name, &mut bodies, 0);
|
||||
self.rep_bodies(*r, &world, &path, &name, instance, &mut bodies, 0);
|
||||
}
|
||||
node.bodies = bodies;
|
||||
for nauo in self.children.get(&pd).cloned().unwrap_or_default() {
|
||||
@@ -819,7 +840,8 @@ impl<'a> Reader<'a> {
|
||||
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);
|
||||
let instance = self.occurrence_name(nauo);
|
||||
let c = self.node(child, m, w, &path, instance.as_deref(), depth + 1, stack);
|
||||
node.children.push(c);
|
||||
}
|
||||
stack.pop();
|
||||
@@ -1054,7 +1076,7 @@ fn step_import_with_workers(text: &str, workers: usize) -> Result<StepImport> {
|
||||
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());
|
||||
let n = rd.node(pd, identity, identity, &[], None, 0, &mut Vec::new());
|
||||
if !n.bodies.is_empty() || !n.children.is_empty() {
|
||||
tree.push(n);
|
||||
}
|
||||
@@ -1077,7 +1099,7 @@ fn step_import_with_workers(text: &str, workers: usize) -> Result<StepImport> {
|
||||
rd.node_items = rd.used_items.clone();
|
||||
rd.visits = 0;
|
||||
for r in loose {
|
||||
rd.rep_bodies(r, &identity, &[], "Import", &mut bodies, 0);
|
||||
rd.rep_bodies(r, &identity, &[], "Import", None, &mut bodies, 0);
|
||||
}
|
||||
if !bodies.is_empty() {
|
||||
tree.push(StepNode { name: "Import".into(), transform: to_array(&identity), bodies, children: Vec::new() });
|
||||
|
||||
@@ -184,7 +184,7 @@ fn parallel_import_does_not_prepare_past_body_budget_with_duplicates_or_mapped_i
|
||||
reader.bodies = vec![seed.clone(); MAX_BODIES - 2];
|
||||
reader.node_items.insert(2000);
|
||||
let mut out = Vec::new();
|
||||
reader.rep_bodies(60, &mt::Matrix4::from_scale(1.0), &[], "Budget", &mut out, 0);
|
||||
reader.rep_bodies(60, &mt::Matrix4::from_scale(1.0), &[], "Budget", None, &mut out, 0);
|
||||
assert_eq!(reader.bodies.len(), MAX_BODIES);
|
||||
assert_eq!(out, vec![MAX_BODIES - 2, MAX_BODIES - 1]);
|
||||
assert_eq!(reader.bodies.last().unwrap().name, "Second");
|
||||
@@ -453,6 +453,7 @@ fn hand_written_part_in_inches_with_colour() {
|
||||
let b = &imp.bodies[0];
|
||||
assert_eq!(b.name, "Cube");
|
||||
assert_eq!(b.path, vec!["Widget".to_string()]);
|
||||
assert_eq!(b.instance, None, "a root part is no occurrence");
|
||||
assert_eq!(b.color, Some([1.0, 0.5, 0.0]));
|
||||
let m = measure(&b.body).unwrap();
|
||||
assert!(rel(m.volume, 25.4f64.powi(3)) < 1e-9, "{}", m.volume);
|
||||
@@ -520,6 +521,7 @@ fn hand_written_assembly() {
|
||||
assert_eq!(root.children.len(), 2);
|
||||
assert!(root.children.iter().all(|c| c.name == "Block" && c.bodies.len() == 1));
|
||||
assert_eq!(imp.bodies[1].path, vec!["Assembly".to_string(), "Block".to_string()]);
|
||||
assert_eq!([imp.bodies[0].instance.as_deref(), imp.bodies[1].instance.as_deref()], [Some("Block:0"), Some("Block:1")]);
|
||||
assert!((root.children[1].transform[3][0] - 100.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
@@ -548,6 +550,45 @@ fn later_instances_show_the_first_instances_mesh_placed() {
|
||||
assert!(split.display_of.is_none());
|
||||
}
|
||||
|
||||
/// A body knows the product that holds it and the assembly occurrence it is, next to the name of
|
||||
/// its solid: a caller can name bodies after the part or the instance when the solids carry the
|
||||
/// name of the last feature that made them (`Cut-Extrude3`), which says nothing about the part.
|
||||
#[test]
|
||||
fn bodies_know_their_product_and_instance() {
|
||||
use crate::step_out::{ExportBody, ExportProduct, StepHeader, step_export_products};
|
||||
let plate = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 2.0)).unwrap();
|
||||
let post = box_solid(Vec3::ZERO, Vec3::new(2.0, 2.0, 8.0)).unwrap();
|
||||
let at = |x: f64| [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [x, 0.0, 0.0, 1.0]];
|
||||
let part = |name: &str, label: &str, body| ExportProduct {
|
||||
name: name.into(),
|
||||
bodies: vec![ExportBody { name: label.into(), body, color: None }],
|
||||
children: Vec::new(),
|
||||
};
|
||||
let products = vec![
|
||||
ExportProduct {
|
||||
name: "Frame".into(),
|
||||
bodies: Vec::new(),
|
||||
children: vec![(1, at(0.0), "Plate-1".into()), (2, at(20.0), "Post-1".into()), (2, at(30.0), "Post-2".into())],
|
||||
},
|
||||
part("Plate", "Cut-Extrude3", &plate),
|
||||
part("Post", "Boss-Extrude1", &post),
|
||||
];
|
||||
let text = step_export_products(&products, 0, &StepHeader::default()).unwrap();
|
||||
let imp = step_import(&text).unwrap();
|
||||
let seen: Vec<(&str, Vec<&str>, Option<&str>)> =
|
||||
imp.bodies.iter().map(|b| (b.name.as_str(), b.path.iter().map(String::as_str).collect(), b.instance.as_deref())).collect();
|
||||
assert_eq!(
|
||||
seen,
|
||||
[
|
||||
("Cut-Extrude3", vec!["Frame", "Plate"], Some("Plate-1")),
|
||||
("Boss-Extrude1", vec!["Frame", "Post"], Some("Post-1")),
|
||||
("Boss-Extrude1", vec!["Frame", "Post"], Some("Post-2")),
|
||||
]
|
||||
);
|
||||
// The same product's two placements are two occurrences, told apart by their names.
|
||||
assert_eq!(imp.tree[0].children.iter().map(|c| c.name.as_str()).collect::<Vec<_>>(), ["Plate", "Post", "Post"]);
|
||||
}
|
||||
|
||||
/// The import keeps a conversion only where the file can ask for the item again. A flat file of
|
||||
/// separate solids converts each item exactly once (no second copy of the whole import is held);
|
||||
/// an assembly that instances one item keeps it, and each instance still gets geometry of its
|
||||
|
||||
Reference in new issue
Block a user