diff --git a/crates/engine/src/tests/import_component.rs b/crates/engine/src/tests/import_component.rs index 061f40d9..6fb58874 100644 --- a/crates/engine/src/tests/import_component.rs +++ b/crates/engine/src/tests/import_component.rs @@ -322,3 +322,41 @@ fn a_design_with_a_loose_import_rebuilds_unchanged() { assert_eq!(shown_boxes(&s), file_boxes(&path)); let _ = std::fs::remove_dir_all(&dir); } + +/// A design whose `Outer` component holds a plate and an `Inner` component placed twice (the +/// first placement turned and moved), with `Outer` itself turned about X and lifted. +fn nested_file(dir: &std::path::Path) -> String { + let mut s = Session::default(); + let outer = run(&mut s, "component.create", json!({"name": "Outer"}))["occurrence"].clone(); + run(&mut s, "solid.box", json!({"length": 30, "width": 30, "height": 5, "body_name": "Plate"})); + let inner = run(&mut s, "component.create", json!({"name": "Inner"}))["occurrence"].clone(); + run(&mut s, "solid.box", json!({"length": 40, "width": 20, "height": 10, "body_name": "Block"})); + run(&mut s, "component.activate", json!({"component": "Outer"})); + run(&mut s, "occurrence.move", json!({"occurrence": inner, "axis": [0, 0, 1], "angle": "90 deg", "translate": [100, 0, 0]})); + run(&mut s, "occurrence.copy", json!({"component": "Inner", "translate": [0, 0, 30]})); + run(&mut s, "component.activate", json!({"component": "root"})); + run(&mut s, "occurrence.move", json!({"occurrence": outer, "space": "world", "axis": [1, 0, 0], "angle": "90 deg", "translate": [0, 0, 50]})); + let path = dir.join("nested.step").to_string_lossy().to_string(); + run(&mut s, "file.export", json!({"path": path})); + path +} + +#[test] +fn an_inserted_assembly_exports_its_nested_parts_where_they_are() { + let dir = temp_dir("reexport"); + let path = nested_file(&dir); + let placed = file_boxes(&path); + assert_eq!(placed.len(), 3, "a plate and two blocks: {placed:?}"); + let mut s = Session::default(); + run(&mut s, "file.insert_step", json!({"path": path})); + assert_eq!(shown_boxes(&s), placed); + // The whole design as an assembly: every part is placed once, by its occurrences. + let again = dir.join("again.step").to_string_lossy().to_string(); + run(&mut s, "file.export", json!({"path": again})); + assert_eq!(file_boxes(&again), placed); + // And inserted again, it shows as the first file did. + let mut back = Session::default(); + run(&mut back, "file.insert_step", json!({"path": again})); + assert_eq!(shown_boxes(&back), placed); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs index b80108a0..25ed46ac 100644 --- a/crates/io/src/lib.rs +++ b/crates/io/src/lib.rs @@ -488,7 +488,9 @@ fn mesh_object(b: &solidcraft_doc::ModelBody, tagged: bool) -> Result Result> { use solidcraft_kernel::{ExportBody, ExportProduct}; let mut index: std::collections::HashMap = std::collections::HashMap::new(); @@ -498,10 +500,18 @@ pub fn step_assembly(doc: &Document, local: &ModelState, name: &str) -> Result> = local + .bodies + .iter() + .map(|b| match doc.model_body_offset(b) { + Some(offset) if !solidcraft_doc::is_identity(offset) => solidcraft_kernel::transform_matrix(&b.body, *offset).map(Some), + _ => Ok(None), + }) + .collect::>()?; + for (b, framed) in local.bodies.iter().zip(&framed) { + let comp = doc.model_body_component(b); let Some(p) = index.get(&comp).and_then(|i| products.get_mut(*i)) else { continue }; - p.bodies.push(ExportBody { name: b.name.clone(), body: &b.body, color: b.body.color() }); + p.bodies.push(ExportBody { name: b.name.clone(), body: framed.as_ref().unwrap_or(&b.body), color: b.body.color() }); } for o in &doc.occurrences { let (Some(&parent), Some(&child)) = (index.get(&o.parent), index.get(&o.component)) else { continue };