* Imports become components: a STEP assembly's tree as nested components (#1386) Every imported file (STEP, IGES, 3MF, STL, OBJ: inserted, dropped, opened as a design, or file.insert_step / file.insert_mesh / doc.open from scripts, the CLI and MCP) now comes in as a new component named after the file, with its Import feature inside it. A STEP assembly's products become nested components; a part placed more than once is one component with an occurrence per placement, and the Import feature skips the file's later copies (`skip`, format 21 note). The contents are held as one rigid group (rigid joints, glyphs hidden); moving an inner part is refused until component.unlock ("Unlock Contents") releases it. Y up turns the import's component. Replies report component, component_name and occurrence. Undo is one step; designs with loose imports are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep an import's rigid group hidden when the window loads the file in the background Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Import instances' skip list is format 22: format 21 ships in 0.12.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Catalog entries for Unlock Contents Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d43c414b7b
commit
27529d9742
37 files changed
+968
-103
No files matched your search
@@ -0,0 +1,324 @@
|
||||
//! An imported file becomes a component named after it (#1386): a STEP part is one component
|
||||
//! with its body, a STEP assembly nested components that mirror the file with a repeated part's
|
||||
//! placements as occurrences of one component, held together as one rigid group until it is
|
||||
//! unlocked; a mesh file one component. Undo takes the whole import away, and designs whose
|
||||
//! imports sit loose in a component open and rebuild as they did.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_kernel::{ExportBody, ExportProduct, StepHeader, box_solid, step_export_products};
|
||||
|
||||
use super::run;
|
||||
use crate::*;
|
||||
|
||||
fn temp_dir(tag: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-import-component-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
fn at(x: f64, y: f64, z: f64) -> [[f64; 4]; 4] {
|
||||
[[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, y, z, 1.0]]
|
||||
}
|
||||
|
||||
/// A quarter turn about Z, then moved to (x, y, z).
|
||||
fn turned(x: f64, y: f64, z: f64) -> [[f64; 4]; 4] {
|
||||
[[0.0, 1.0, 0.0, 0.0], [-1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [x, y, z, 1.0]]
|
||||
}
|
||||
|
||||
/// `Rig`: a base plate, two `Arm` sub-assemblies (each a 20 × 4 × 4 beam and a `Pin`) and a third
|
||||
/// `Pin` on its own; one arm is turned a quarter turn. Pins: 2 × 2 × 10. Six bodies in the
|
||||
/// file: four products, one of them a sub-assembly placed twice.
|
||||
fn rig_file(dir: &std::path::Path) -> String {
|
||||
let plate = box_solid(Vec3::ZERO, Vec3::new(100.0, 60.0, 5.0)).unwrap();
|
||||
let beam = box_solid(Vec3::ZERO, Vec3::new(20.0, 4.0, 4.0)).unwrap();
|
||||
let pin = box_solid(Vec3::ZERO, Vec3::new(2.0, 2.0, 10.0)).unwrap();
|
||||
let part = |name: &str, body| ExportProduct {
|
||||
name: name.into(),
|
||||
bodies: vec![ExportBody { name: name.into(), body, color: None }],
|
||||
children: Vec::new(),
|
||||
};
|
||||
let products = vec![
|
||||
ExportProduct {
|
||||
name: "Rig".into(),
|
||||
bodies: Vec::new(),
|
||||
children: vec![
|
||||
(1, at(0.0, 0.0, 0.0), "Base-1".into()),
|
||||
(2, at(10.0, 10.0, 5.0), "Arm-1".into()),
|
||||
(2, turned(60.0, 10.0, 5.0), "Arm-2".into()),
|
||||
(4, at(90.0, 50.0, 5.0), "Pin-3".into()),
|
||||
],
|
||||
},
|
||||
part("Base", &plate),
|
||||
ExportProduct {
|
||||
name: "Arm".into(),
|
||||
bodies: Vec::new(),
|
||||
children: vec![(3, at(0.0, 0.0, 0.0), "Beam-1".into()), (4, at(18.0, 1.0, 4.0), "Pin-1".into())],
|
||||
},
|
||||
part("Beam", &beam),
|
||||
part("Pin", &pin),
|
||||
];
|
||||
let text = step_export_products(&products, 0, &StepHeader::default()).unwrap();
|
||||
let path = dir.join("rig.step");
|
||||
std::fs::write(&path, text).unwrap();
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Where the file itself puts its bodies: their bounding boxes, sorted.
|
||||
fn file_boxes(path: &str) -> Vec<[i64; 6]> {
|
||||
let imp = solidcraft_kernel::step_import(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||
let mut out: Vec<[i64; 6]> = imp.bodies.iter().map(|b| key(solidcraft_kernel::measure(&b.body).unwrap().bbox)).collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
fn key(b: solidcraft_geom::Aabb3) -> [i64; 6] {
|
||||
let r = |v: f64| (v * 1000.0).round() as i64;
|
||||
[r(b.min.x), r(b.min.y), r(b.min.z), r(b.max.x), r(b.max.y), r(b.max.z)]
|
||||
}
|
||||
|
||||
/// Where the design shows its bodies, sorted.
|
||||
fn shown_boxes(s: &Session) -> Vec<[i64; 6]> {
|
||||
let mut out: Vec<[i64; 6]> = s.world_state().bodies.iter().map(|b| key(solidcraft_kernel::measure(&b.body).unwrap().bbox)).collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
fn components(s: &mut Session) -> Vec<Value> {
|
||||
run(s, "component.list", json!({}))["components"].as_array().unwrap().clone()
|
||||
}
|
||||
|
||||
fn component<'a>(list: &'a [Value], name: &str) -> &'a Value {
|
||||
list.iter().find(|c| c["name"] == name).unwrap_or_else(|| panic!("no component {name}: {list:?}"))
|
||||
}
|
||||
|
||||
fn occurrence_named(s: &Session, name: &str) -> u64 {
|
||||
s.doc.occurrences.iter().find(|o| o.name == name).unwrap_or_else(|| panic!("no occurrence {name}")).id
|
||||
}
|
||||
|
||||
/// The world corner of the bodies of one occurrence (by name) nearest -∞.
|
||||
fn corner_of(s: &Session, occurrence: &str) -> Vec3 {
|
||||
let occ = occurrence_named(s, occurrence);
|
||||
let path = {
|
||||
let mut path = vec![occ];
|
||||
let mut parent = s.doc.occurrences.iter().find(|o| o.id == occ).unwrap().parent;
|
||||
while parent != 0 {
|
||||
let o = s.doc.occurrence_of(parent).unwrap();
|
||||
path.insert(0, o.id);
|
||||
parent = o.parent;
|
||||
}
|
||||
path
|
||||
};
|
||||
let names = s.occurrence_bodies(&path, true).unwrap();
|
||||
assert!(!names.is_empty());
|
||||
let world = s.world_state();
|
||||
let mut lo = Vec3::new(f64::MAX, f64::MAX, f64::MAX);
|
||||
for b in world.bodies.iter().filter(|b| names.contains(&b.name)) {
|
||||
let bb = solidcraft_kernel::measure(&b.body).unwrap().bbox;
|
||||
lo = Vec3::new(lo.x.min(bb.min.x), lo.y.min(bb.min.y), lo.z.min(bb.min.z));
|
||||
}
|
||||
lo
|
||||
}
|
||||
|
||||
fn close(a: Vec3, b: Vec3) -> bool {
|
||||
a.dist(b) < 1e-6
|
||||
}
|
||||
|
||||
/// A box exported as `name.step` in `dir`.
|
||||
fn part_file(dir: &std::path::Path, name: &str) -> String {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 30, "width": 20, "height": 10}));
|
||||
let path = dir.join(format!("{name}.step")).to_string_lossy().to_string();
|
||||
run(&mut s, "file.export", json!({"path": path}));
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_step_part_comes_in_as_one_component_named_after_the_file() {
|
||||
let dir = temp_dir("part");
|
||||
let path = part_file(&dir, "bracket");
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [100, 0, 0]}));
|
||||
let r = run(&mut s, "file.insert_step", json!({"path": path}));
|
||||
let list = components(&mut s);
|
||||
assert_eq!(list.len(), 2, "the design and the import: {list:?}");
|
||||
let bracket = component(&list, "bracket");
|
||||
assert_eq!((r["component"].clone(), r["component_name"].clone()), (bracket["id"].clone(), json!("bracket")), "{r}");
|
||||
assert_eq!(bracket["bodies"].as_array().unwrap().len(), 1, "{bracket}");
|
||||
assert_eq!(component(&list, &s.doc.name.clone())["bodies"].as_array().unwrap().len(), 1, "my own box stays where it was");
|
||||
let feature = r["feature"].as_u64().unwrap();
|
||||
assert_eq!(s.doc.feature(feature).unwrap().component, r["component"].as_u64().unwrap());
|
||||
// One part: nothing to hold together.
|
||||
assert!(s.doc.assembly.joints.is_empty() && r.get("subcomponents").is_none(), "{r}");
|
||||
// The active component is still the one I was working in.
|
||||
assert_eq!(s.active_component, 0);
|
||||
|
||||
// Opening the file as a design makes the component too, so the browser shows its name.
|
||||
let r = run(&mut s, "doc.open", json!({"path": path}));
|
||||
assert_eq!(r["component_name"], "bracket", "{r}");
|
||||
assert_eq!(s.doc.components.iter().map(|c| c.name.as_str()).collect::<Vec<_>>(), ["bracket"]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_step_assembly_comes_in_as_nested_components_with_its_instances() {
|
||||
let dir = temp_dir("assembly");
|
||||
let path = rig_file(&dir);
|
||||
let mut s = Session::default();
|
||||
let r = run(&mut s, "file.insert_step", json!({"path": path}));
|
||||
// rig ⊃ Base, Arm (twice; Beam, Pin inside), Pin (once more, directly).
|
||||
let list = components(&mut s);
|
||||
let names: Vec<&str> = list.iter().filter_map(|c| c["name"].as_str()).collect();
|
||||
assert_eq!(names.len(), 6, "{names:?}");
|
||||
for n in ["rig", "Base", "Arm", "Beam", "Pin"] {
|
||||
assert!(names.contains(&n), "{n} in {names:?}");
|
||||
}
|
||||
let id = |n: &str| component(&list, n)["id"].as_u64().unwrap();
|
||||
let parent = |n: &str| component(&list, n)["parent"].as_u64().unwrap();
|
||||
assert_eq!(parent("rig"), 0);
|
||||
assert_eq!((parent("Base"), parent("Arm")), (id("rig"), id("rig")));
|
||||
assert_eq!((parent("Beam"), parent("Pin")), (id("Arm"), id("Arm")));
|
||||
assert_eq!(r["component"], id("rig"), "{r}");
|
||||
assert_eq!(r["subcomponents"], 4, "{r}");
|
||||
// A repeated part is one component placed again: Arm twice, Pin in the arm and on its own.
|
||||
let placements = |c: u64| s.doc.occurrences.iter().filter(|o| o.component == c).count();
|
||||
assert_eq!((placements(id("Arm")), placements(id("Pin")), placements(id("Beam"))), (2, 2, 1));
|
||||
// Each component holds its own part's body; the file's later instances are not built again.
|
||||
let own = |n: &str| component(&list, n)["bodies"].as_array().unwrap().len();
|
||||
assert_eq!((own("rig"), own("Base"), own("Arm"), own("Beam"), own("Pin")), (0, 1, 0, 1, 1));
|
||||
assert_eq!(s.model.state().bodies.len(), 3, "base, beam and pin, once each");
|
||||
// And every body shows where the file puts it.
|
||||
assert_eq!(shown_boxes(&s), file_boxes(&path));
|
||||
|
||||
// Saved and opened again, it is the same.
|
||||
let design = dir.join("rig.solidcraft").to_string_lossy().to_string();
|
||||
run(&mut s, "file.save_as", json!({"path": design}));
|
||||
let mut again = Session::default();
|
||||
run(&mut again, "doc.open", json!({"path": design}));
|
||||
assert_eq!(shown_boxes(&again), file_boxes(&path));
|
||||
assert_eq!(again.doc.components.len(), 5);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Each occurrence's degrees of freedom, as `joint.list` gives them.
|
||||
fn dofs(s: &mut Session) -> (std::collections::BTreeMap<u64, u64>, Value) {
|
||||
let r = run(s, "joint.list", json!({}));
|
||||
let dof = r["dof"].as_array().unwrap().iter().map(|d| (d["occurrence"].as_u64().unwrap(), d["dof"].as_u64().unwrap())).collect();
|
||||
(dof, r)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_imported_assembly_moves_as_one_until_it_is_unlocked() {
|
||||
let dir = temp_dir("rigid");
|
||||
let path = rig_file(&dir);
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "file.insert_step", json!({"path": path}));
|
||||
let rig = occurrence_named(&s, "rig:1");
|
||||
let inside: Vec<u64> = s.doc.occurrences.iter().filter(|o| o.id != rig).map(|o| o.id).collect();
|
||||
assert_eq!(inside.len(), 6);
|
||||
let pin = corner_of(&s, "Pin:2");
|
||||
let arm = corner_of(&s, "Arm:1");
|
||||
|
||||
// The whole import is one rigid body: no part of it moves against the others.
|
||||
let (dof, r) = dofs(&mut s);
|
||||
assert!(r["conflicts"].as_array().unwrap().is_empty(), "{r}");
|
||||
for o in &inside {
|
||||
assert_eq!(dof.get(o), dof.get(&rig), "occurrence {o} moves only with the import: {r}");
|
||||
}
|
||||
|
||||
// Moving the import's component moves everything in it.
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "rig:1", "translate": [0, 0, 50]}));
|
||||
assert!(close(corner_of(&s, "Pin:2"), pin + Vec3::new(0.0, 0.0, 50.0)), "{:?}", corner_of(&s, "Pin:2"));
|
||||
assert!(close(corner_of(&s, "Arm:1"), arm + Vec3::new(0.0, 0.0, 50.0)));
|
||||
// A part inside doesn't move on its own; the refusal says what to move instead.
|
||||
for part in ["Pin:2", "Pin:1"] {
|
||||
let e = s.execute("occurrence.move", &json!({"occurrence": part, "translate": [30, 0, 0]})).unwrap_err().to_string();
|
||||
assert!(e.contains("move rig"), "{e}");
|
||||
}
|
||||
assert!(close(corner_of(&s, "Pin:2"), pin + Vec3::new(0.0, 0.0, 50.0)));
|
||||
|
||||
// Next to a grounded part of mine the import is still one rigid body.
|
||||
run(&mut s, "component.create", json!({"name": "Mine"}));
|
||||
run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [-50, 0, 0]}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": "Mine:1", "grounded": true}));
|
||||
let (dof, r) = dofs(&mut s);
|
||||
for o in &inside {
|
||||
assert_eq!(dof.get(o), dof.get(&rig), "{r}");
|
||||
}
|
||||
assert!(r["conflicts"].as_array().unwrap().is_empty(), "{r}");
|
||||
|
||||
// Unlocked (one undo step), the parts move one by one.
|
||||
let undo = s.undo.len();
|
||||
let r = run(&mut s, "component.unlock", json!({"component": "rig"}));
|
||||
assert_eq!(r["joints"].as_array().unwrap().len(), 6, "{r}");
|
||||
assert_eq!(s.undo.len(), undo + 1);
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "Pin:2", "translate": [30, 0, 0]}));
|
||||
assert!(close(corner_of(&s, "Pin:2"), pin + Vec3::new(30.0, 0.0, 50.0)), "{:?}", corner_of(&s, "Pin:2"));
|
||||
assert!(close(corner_of(&s, "Arm:1"), arm + Vec3::new(0.0, 0.0, 50.0)), "the others stay");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc.assembly.joints.len(), 6, "undo locks it again");
|
||||
assert!(s.execute("component.unlock", &json!({"component": "Mine"})).is_err(), "nothing holds my own component");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mesh_file_comes_in_as_one_component() {
|
||||
let dir = temp_dir("stl");
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
|
||||
let stl = dir.join("cube.stl").to_string_lossy().to_string();
|
||||
run(&mut s, "file.save_mesh", json!({"path": stl}));
|
||||
let mut s = Session::default();
|
||||
let r = run(&mut s, "file.insert_mesh", json!({"path": stl}));
|
||||
assert_eq!(r["component_name"], "cube", "{r}");
|
||||
let list = components(&mut s);
|
||||
assert_eq!(list.len(), 2);
|
||||
assert_eq!(component(&list, "cube")["bodies"].as_array().unwrap().len(), 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_takes_the_whole_import_away() {
|
||||
let dir = temp_dir("undo");
|
||||
let path = rig_file(&dir);
|
||||
let mut s = Session::default();
|
||||
let undo = s.undo.len();
|
||||
run(&mut s, "file.insert_step", json!({"path": path}));
|
||||
assert_eq!(s.undo.len(), undo + 1, "one step");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.doc.features.is_empty() && s.doc.components.is_empty() && s.doc.occurrences.is_empty(), "nothing left");
|
||||
assert!(s.doc.assembly.joints.is_empty() && s.doc.body_components.is_empty() && s.doc.body_offsets.is_empty());
|
||||
assert!(s.world_state().bodies.is_empty());
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert_eq!(shown_boxes(&s), file_boxes(&path));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A design saved before imports made components has its Import loose in the design: it opens
|
||||
/// as it is (no component is made for it) and builds every body of the file where it was.
|
||||
#[test]
|
||||
fn a_design_with_a_loose_import_rebuilds_unchanged() {
|
||||
let dir = temp_dir("loose");
|
||||
let path = rig_file(&dir);
|
||||
let sf = solidcraft_io::step_import_feature(&std::fs::read(&path).unwrap(), &path).unwrap();
|
||||
let mut doc = solidcraft_doc::Document::new("Old");
|
||||
let id = doc.add_feature(sf.kind, Some(&sf.name)).unwrap();
|
||||
doc.feature_mut(id).unwrap().body_names = sf.body_names;
|
||||
// As a format 21 file (0.12.0) has it.
|
||||
let mut json: Value = serde_json::from_str(&doc.to_json()).unwrap();
|
||||
json["format"] = json!("solidcraft/21");
|
||||
assert!(json["features"][0]["kind"].get("skip").is_none());
|
||||
let file = dir.join("old.solidcraft");
|
||||
std::fs::write(&file, json.to_string()).unwrap();
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "doc.open", json!({"path": file.to_string_lossy()}));
|
||||
assert!(s.doc.components.is_empty() && s.doc.occurrences.is_empty(), "not migrated");
|
||||
assert_eq!(s.doc.features[0].component, 0);
|
||||
let names: Vec<String> = s.model.state().bodies.iter().map(|b| b.name.clone()).collect();
|
||||
assert_eq!(names, ["Base", "Beam", "Pin", "Beam (2)", "Pin (2)", "Pin (3)"]);
|
||||
assert_eq!(shown_boxes(&s), file_boxes(&path));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -90,9 +90,10 @@ fn a_y_up_step_file_comes_in_standing_up() {
|
||||
let r = run(&mut d, "file.insert_step", json!({"path": path, "up": "y"}));
|
||||
let (lo, hi) = extent(&d);
|
||||
assert!(near(lo, [0.0, -20.0, 0.0]) && near(hi, [40.0, 0.0, 34.0]), "stood up: {lo:?} {hi:?}");
|
||||
assert_eq!(kinds(&d), ["BaseFeature", "MoveFeature"]);
|
||||
assert_eq!(r["stand_up"], d.doc.features[1].id, "{r}");
|
||||
assert_eq!(r["created_bodies"].as_array().unwrap().len(), 3, "{r}");
|
||||
// The import's component is turned (#1386), so an assembly's instances turn with it.
|
||||
assert_eq!(kinds(&d), ["BaseFeature"]);
|
||||
assert_eq!(r["stand_up"], r["occurrence"], "{r}");
|
||||
assert_eq!(r["created_bodies"].as_array().unwrap().len(), 2, "the plate, and the post placed twice: {r}");
|
||||
assert!((total_volume(&mut d) - (40.0 * 4.0 * 20.0 + 2.0 * 4.0 * 30.0 * 4.0)).abs() < 1e-6);
|
||||
// The Import and the turn are one undo step.
|
||||
assert_eq!(d.undo.len(), undo + 1);
|
||||
@@ -106,7 +107,7 @@ fn a_y_up_step_file_comes_in_standing_up() {
|
||||
let (lo, hi) = extent(&d);
|
||||
assert!(near(lo, [0.0, -20.0, 0.0]) && near(hi, [105.0, 106.0, 107.0]), "{lo:?} {hi:?}");
|
||||
|
||||
// The turn is an ordinary Move feature: the saved design has it and needs no new field.
|
||||
// The turn is the component's placement: the saved design has it.
|
||||
let design = std::path::Path::new(&path).with_file_name("frame.solidcraft");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "file.insert_step", json!({"path": path, "up": "y"}));
|
||||
@@ -122,7 +123,7 @@ fn opening_a_y_up_step_file_can_stand_it_up() {
|
||||
let path = frame_file("open");
|
||||
let mut s = Session::default();
|
||||
let r = run(&mut s, "doc.open", json!({"path": path, "up": "y"}));
|
||||
assert_eq!(r["features"], 2, "{r}");
|
||||
assert_eq!(r["features"], 1, "{r}");
|
||||
assert!(near(extent(&s).1, [40.0, 0.0, 34.0]));
|
||||
assert!(s.undo.is_empty(), "a freshly opened design has no history");
|
||||
run(&mut s, "doc.open", json!({"path": path}));
|
||||
@@ -181,22 +182,23 @@ fn the_bodies_are_named_after_the_solid_the_product_or_the_instance() {
|
||||
assert_eq!(r["name"], "Frame", "the timeline entry is the root product's");
|
||||
body_names(&s)
|
||||
};
|
||||
let solids = ["Cut-Extrude3", "Boss-Extrude1", "Boss-Extrude1 (2)"];
|
||||
// The post is placed twice: one component, two occurrences, its body built once (#1386).
|
||||
let solids = ["Cut-Extrude3", "Boss-Extrude1"];
|
||||
assert_eq!(named(json!({"path": path})), solids);
|
||||
assert_eq!(named(json!({"path": path, "names": "solid"})), solids);
|
||||
assert_eq!(named(json!({"path": path, "names": "product"})), ["Plate", "Post", "Post (2)"]);
|
||||
assert_eq!(named(json!({"path": path, "names": "Instance"})), ["Plate-1", "Post-1", "Post-2"]);
|
||||
assert_eq!(named(json!({"path": path, "names": "product"})), ["Plate", "Post"]);
|
||||
assert_eq!(named(json!({"path": path, "names": "Instance"})), ["Plate-1", "Post-1"]);
|
||||
|
||||
// The names are stored with the import: a saved design keeps them, and `up` does not change them.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "doc.open", json!({"path": path, "names": "instance", "up": "y"}));
|
||||
assert_eq!(body_names(&s), ["Plate-1", "Post-1", "Post-2"]);
|
||||
assert_eq!(body_names(&s), ["Plate-1", "Post-1"]);
|
||||
let design = std::path::Path::new(&path).with_file_name("frame-named.solidcraft");
|
||||
run(&mut s, "file.save_as", json!({"path": design.to_string_lossy()}));
|
||||
let mut again = Session::default();
|
||||
run(&mut again, "doc.open", json!({"path": design.to_string_lossy()}));
|
||||
assert_eq!(body_names(&again), ["Plate-1", "Post-1", "Post-2"]);
|
||||
assert_eq!(body_names(&again), ["Plate-1", "Post-1"]);
|
||||
// A feature finds them by those names.
|
||||
run(&mut again, "solid.move", json!({"bodies": ["Post-2"], "translate": [0, 0, 5]}));
|
||||
assert_eq!(body_names(&again), ["Plate-1", "Post-1", "Post-2"]);
|
||||
run(&mut again, "solid.move", json!({"bodies": ["Post-1"], "translate": [0, 0, 5]}));
|
||||
assert_eq!(body_names(&again), ["Plate-1", "Post-1"]);
|
||||
}
|
||||
Reference in new issue
Block a user