Insert Design on the ASSEMBLE panel and in the File menu (#1240) (#1405)

component.insert_design (a saved part as a linked component) had no button, so an
assembly of separately saved parts looked impossible. It is now on the ASSEMBLE panel
of every tab and in the File menu; it asks for the file, and a STEP or mesh file picked
there is inserted as Insert STEP or Insert Mesh would.

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-15 10:28:02 -04:00
1 parent 4823c293dd
commit b156b5e09c
5 files changed
+64 -3

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ use crate::params::{bad, str_, vec3};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("component.insert_design", "Insert Design", insert_design).icon("component").params(
CommandSpec::new("component.insert_design", "Insert Design", insert_design).at("SOLID", "ASSEMBLE").icon("component").params(
"path: a .solidcraft design → a new component linked to it, holding its solid bodies as placed in that design; \
name? (default: the design's name); parent?: component id or name (default: the active one); translate?: [x,y,z]",
),
+22
View File
@@ -679,6 +679,21 @@ impl SolveApp {
self.pick_open_command(id, json!({}));
return;
}
if id == "component.insert_design" {
// Another design becomes a linked component; a STEP or mesh file picked here is
// inserted the way Insert STEP or Insert Mesh would (#1240).
self.pick_open(|app, path| {
let Some(path) = path else { return };
if is_design_path(&path) {
if app.run("component.insert_design", json!({ "path": path })).is_ok() {
app.fit_view();
}
} else {
app.insert_path_async(&path);
}
});
return;
}
if matches!(id, "file.insert_step" | "file.insert_mesh") {
self.pick_open(|app, path| {
if let Some(path) = path {
@@ -1419,3 +1434,10 @@ pub fn now_ms() -> f64 {
#[cfg(test)]
mod picker_test_support;
/// A SolidCraft design file (`.solidcraft`, or a design saved as JSON), as opposed to a STEP or
/// mesh file.
fn is_design_path(path: &str) -> bool {
let ext = std::path::Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase();
matches!(ext.as_str(), "solidcraft" | "solvecraft" | "json")
}
+4
View File
@@ -390,6 +390,10 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
app.start("file.insert_mesh");
close = true;
}
if item_if(ui, &format!("{}…", app.tr_label("Insert Design")), "", shown) {
app.start("component.insert_design");
close = true;
}
if item_if(ui, app.tr("ui.save"), "Ctrl+S", shown) {
save(app);
close = true;
+33
View File
@@ -11,6 +11,7 @@ const ASSEMBLE: &[&str] = &[
"component.create",
"joint.create",
"joint.as_built",
"component.insert_design",
"joint.origin",
"joint.rigid_group",
"joint.drive",
@@ -212,4 +213,36 @@ mod tests {
assert!(ids("PLASTIC", "MODIFY").contains(&"solid.shell"));
assert!(panels("PLASTIC").is_some_and(|p| p.first() == Some(&"SETUP")));
}
/// #1240: a saved part reaches an assembly from the ASSEMBLE panel (and the File menu) as a
/// linked component, and a STEP file picked there is inserted as Insert STEP would.
#[test]
fn insert_design_is_on_the_assemble_panel_and_inserts_a_saved_part() {
use serde_json::json;
for tab in ["SOLID", "SHEET METAL", "PLASTIC"] {
assert!(ids(tab, "ASSEMBLE").contains(&"component.insert_design"), "{tab}");
}
assert!(!ids("SOLID", "ASSEMBLE")[..3].contains(&"component.insert_design"), "Component, Joint and As-built Joint keep the big buttons");
let mut h = crate::scenario::Harness::new();
let part = h.dir.join("part1.solidcraft");
let mut s = solidcraft_engine::Session::default();
s.execute("solid.box", &json!({"length": 10, "width": 20, "height": 30})).unwrap();
s.execute("file.save", &json!({"path": part})).unwrap();
s.execute("file.export", &json!({"path": h.dir.join("part2.step")})).unwrap();
h.step(&json!({"choose_file": "part1.solidcraft"})).unwrap();
h.step(&json!({"start": "component.insert_design"})).unwrap();
h.frames(3);
let links = h.app.session.execute("component.links", &json!({})).unwrap();
assert_eq!(links["links"][0]["name"], "part1", "{links}");
assert_eq!(h.app.session.world_state().bodies.len(), 1);
h.step(&json!({"choose_file": "part2.step"})).unwrap();
h.step(&json!({"start": "component.insert_design"})).unwrap();
for _ in 0..600 {
if h.app.session.world_state().bodies.len() == 2 {
break;
}
h.frames(1);
}
assert_eq!(h.app.session.world_state().bodies.len(), 2, "the STEP file's body is inserted");
}
}
+4 -2
View File
@@ -64,8 +64,10 @@ instead, so two origins given at one point with the same z stay as they are. For
drive many steps, `quiet: true` leaves the per-part degrees of freedom out of joint answers, and
`motion.play` with `quiet: true` gives the poses of only the parts that moved.
**Insert Design** (`component.insert_design {path, translate?}`) adds a component linked to
another saved design, so each part can live in its own document. The part's solid bodies come
**Insert Design** (`component.insert_design {path, translate?}`, on the **ASSEMBLE** panel and
in the **File** menu) adds a component linked to another saved design, so each part can live in
its own document. A STEP or mesh file picked there is inserted as **Insert STEP** or **Insert
Mesh** would. The part's solid bodies come
in as they sit in that design, and they are kept in the assembly, so it opens even without the
part file. The link is stored relative to the assembly's folder once the assembly is saved.
After the part file changes, **Update Links** (`component.update_links`) reads it again.