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:
NOWN-DEV authored and GitHub committed 2026-10-15 02:18:55 -04:00
1 parent 81d9b7e04a
commit 0ee5e97db5
12 files changed
+572 -38

No files matched your search

+18 -6
View File
@@ -39,20 +39,29 @@ pub struct Request {
pub reply: Option<Sender<ControlResponse>>,
/// The folders that caller's agent may use: the worker reads the file inside them.
pub scope: Option<solidcraft_engine::io::vfs::PathScope>,
/// What that caller asked of a STEP import besides its path (`up`, `names`): the command
/// gets them as given.
pub options: serde_json::Map<String, Value>,
}
impl Request {
pub fn new(path: &str, kind: Kind) -> Self {
Request { path: path.into(), kind, reply: None, scope: None }
Request { path: path.into(), kind, reply: None, scope: None, options: Default::default() }
}
fn insert(&self) -> bool {
self.kind == Kind::Insert
}
/// The command's parameters: the path and the caller's import options.
fn params(&self) -> Value {
let mut params = self.options.clone();
params.insert("path".into(), json!(self.path));
Value::Object(params)
}
fn command(&self) -> (&'static str, Value) {
match &self.kind {
Kind::Open => ("doc.open", json!({"path": self.path})),
Kind::Open => ("doc.open", self.params()),
Kind::Insert if solidcraft_engine::io::is_mesh_path(&self.path) => ("file.insert_mesh", json!({"path": self.path})),
Kind::Insert => ("file.insert_step", json!({"path": self.path})),
Kind::Insert => ("file.insert_step", self.params()),
Kind::Recover { id, dir } => ("doc.recover", json!({"id": id, "dir": dir})),
}
}
@@ -185,7 +194,8 @@ pub fn control(app: &mut SolveApp, req: crate::control::ControlRequest) -> Optio
Some("file.insert_step" | "file.insert_mesh") => Kind::Insert,
_ => return Some(req),
};
let Some(path) = req.params.get("params").and_then(|p| p.get("path")).and_then(Value::as_str) else { return Some(req) };
let params = req.params.get("params");
let Some(path) = params.and_then(|p| p.get("path")).and_then(Value::as_str) else { return Some(req) };
let scope = match crate::control::request_scope(app, &req.params) {
Ok(scope) => scope,
Err(e) => {
@@ -193,7 +203,8 @@ pub fn control(app: &mut SolveApp, req: crate::control::ControlRequest) -> Optio
return None;
}
};
let _ = begin_request(app, Request { path: path.into(), kind, reply: Some(req.reply.clone()), scope });
let options = ["up", "names"].iter().filter_map(|k| Some((k.to_string(), params?.get(*k).filter(|v| !v.is_null())?.clone()))).collect();
let _ = begin_request(app, Request { path: path.into(), kind, reply: Some(req.reply.clone()), scope, options });
None
}
@@ -550,7 +561,8 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
}
app.home.open = false;
app.fit_view();
if request.kind == Kind::Open || request.insert() {
// A caller that said which way is up has been answered: no offer to stand the part up.
if (request.kind == Kind::Open || request.insert()) && !request.options.contains_key("up") {
crate::stand_up::offer(app, &request.path, &running.before);
}
let warnings = made.result.get("warnings").and_then(Value::as_array);
+46
View File
@@ -225,3 +225,49 @@ fn a_control_open_answers_after_loading_without_freezing_the_window() {
assert_eq!(w.app.session.doc.features.len(), sync.doc.features.len() + 1, "the edit went into the opened design");
let _ = std::fs::remove_dir_all(&dir);
}
/// A control-channel (MCP) import hands its `up` and `names` to the command, as the engine
/// session takes them, and a caller that said which way is up is not offered to stand the part up.
#[test]
fn a_control_import_passes_up_and_names_on() {
let dir = scratch("import-options");
let path = dir.join("tall_in_y.step").to_string_lossy().to_string();
let mut src = Session::default();
src.execute("solid.box", &json!({"length": 10, "width": 40, "height": 5})).unwrap();
src.execute("file.export", &json!({"path": path})).unwrap();
// The reply, the part's height and whether the offer is up, for an import by this window.
let import = |params: serde_json::Value| {
let mut w = Win::new();
let (tx, rx) = std::sync::mpsc::channel();
w.app.control_rx = Some(rx);
let (req, reply_rx) = crate::control::ControlRequest::new("engine.execute", json!({"command": "file.insert_step", "params": params.clone()}));
tx.send(req).unwrap();
let mut reply = None;
for _ in 0..100_000 {
w.frame(vec![]);
reply = reply_rx.try_recv().ok();
if reply.is_some() {
break;
}
}
let reply = reply.expect("the import was answered");
assert_eq!(reply["ok"], true, "{params}: {reply}");
let mut sync = Session::default();
assert_eq!(reply["result"], sync.execute("file.insert_step", &params).unwrap(), "the window's reply is the session's");
let b = solidcraft_engine::view::bounds(&w.app.session);
(reply, [b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z], w.app.stand_up.is_some())
};
// As before: the file's axes, and the offer to stand it up.
let (_, size, offered) = import(json!({"path": path}));
assert!(size[1] > 39.9 && offered, "{size:?} {offered}");
// Y up said by the caller: stood up by the import, nothing left to offer.
let (reply, size, offered) = import(json!({"path": path, "up": "y", "names": "product"}));
assert!(size[2] > 39.9 && !offered, "{size:?} {offered}: {reply}");
assert!(reply["result"]["stand_up"].is_u64(), "{reply}");
// Z up said by the caller: kept as it is, and not asked about either.
let (_, size, offered) = import(json!({"path": path, "up": "z"}));
assert!(size[1] > 39.9 && !offered, "{size:?} {offered}");
let _ = std::fs::remove_dir_all(&dir);
}