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

+68 -11
View File
@@ -17,11 +17,11 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("open")
.key("Ctrl+O")
.noundo()
.params("path: .solidcraft design, or a .step/.stp file (opens as a new design)"),
.params("path: .solidcraft design, or a .step/.stp file (opens as a new design); up?: z|y and names?: solid|product|instance for a STEP or IGES file (see file.insert_step)"),
CommandSpec::new("file.insert_step", "Insert STEP", insert_step)
.at("SOLID", "INSERT")
.icon("import")
.params("path: .step/.stp file (its bodies join the design as an Import base feature); name?"),
.params("path: .step/.stp file (its bodies join the design as an Import base feature); name?; up?: z|y (default z: the file's axes are kept; y: the file has Y up, so its bodies are turned a quarter turn about X, Y becoming Z, by a Move feature after the import); names?: solid|product|instance (what the bodies are named after: the solid's own name, else the product's (default); the name of the product (part) that holds the body; the name of the assembly occurrence (instance), else the product's)"),
CommandSpec::new("file.insert_mesh", "Insert Mesh", insert_mesh)
.at("SOLID", "INSERT")
.icon("import")
@@ -59,15 +59,60 @@ fn path_arg<'a>(p: &'a Value, cmd: &str) -> Result<&'a str> {
str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "`path` is required"))
}
/// How a STEP or IGES file comes in beyond its path: which way is up in the file, and what its
/// bodies are named after.
struct StepOptions {
/// `up: "y"`: the file has Y up, so the bodies it brings are turned a quarter turn about X
/// (Y becomes Z), as the app's "Stand it up" offer does. `z` (the default) keeps the file's axes.
y_up: bool,
names: solidcraft_io::StepNames,
}
fn step_options(p: &Value, cmd: &str) -> Result<StepOptions> {
let y_up = match p.get("up") {
None | Some(Value::Null) => false,
Some(v) => match v.as_str().map(|u| u.trim().to_ascii_lowercase()).as_deref() {
Some("z") => false,
Some("y") => true,
_ => return Err(bad(cmd, "`up` must be \"z\" (the file's axes are kept) or \"y\" (the file has Y up: its Y becomes Z)")),
},
};
let names = match p.get("names") {
None | Some(Value::Null) => solidcraft_io::StepNames::default(),
Some(v) => {
v.as_str().and_then(solidcraft_io::StepNames::parse).ok_or_else(|| bad(cmd, "`names` must be \"solid\", \"product\" or \"instance\""))?
}
};
Ok(StepOptions { y_up, names })
}
/// Read a STEP file as an Import feature.
fn read_step(path: &str) -> Result<solidcraft_io::StepFeature> {
fn read_step(path: &str, names: solidcraft_io::StepNames) -> Result<solidcraft_io::StepFeature> {
let bytes = solidcraft_io::vfs::read(path, solidcraft_io::MAX_STEP_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::step_import_feature(&bytes, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
solidcraft_io::step_import_feature_named(&bytes, path, names).map_err(|e| EngineError::Other(format!("{path}: {e}")))
}
/// Turn bodies a quarter turn about X (a file's Y up becomes Z up) with one Move feature, the
/// same one the app's "Stand it up" offer adds, and report its id.
fn stand_up(s: &mut Session, bodies: &[String]) -> Result<u64> {
let kind = solidcraft_doc::FeatureKind::Move {
bodies: bodies.to_vec(),
translate: ["0".into(), "0".into(), "0".into()],
rotate_axis: Some(solidcraft_geom::Vec3::X),
angle: Some("90 deg".into()),
copy: false,
};
let id = s.doc_mut().add_feature(kind, None)?;
s.refresh();
match s.model.result(id).and_then(|r| r.error.clone()) {
Some(e) => Err(EngineError::Other(e)),
None => Ok(id),
}
}
/// Add an Import feature for a STEP file to the session's design.
fn add_step(s: &mut Session, path: &str, name: Option<&str>) -> Result<Value> {
let sf = read_step(path)?;
fn add_step(s: &mut Session, path: &str, name: Option<&str>, options: &StepOptions) -> Result<Value> {
let sf = read_step(path, options.names)?;
let fname = name.filter(|n| !n.trim().is_empty()).unwrap_or(&sf.name).to_string();
let id = s.doc_mut().add_feature(sf.kind, Some(&fname))?;
if let Some(f) = s.doc_mut().feature_mut(id) {
@@ -78,10 +123,14 @@ fn add_step(s: &mut Session, path: &str, name: Option<&str>) -> Result<Value> {
if let Some(e) = s.model.result(id).and_then(|r| r.error.clone()) {
return Err(EngineError::Other(e));
}
let st = s.model.state();
let bodies: Vec<&str> = st.bodies.iter().filter(|b| b.feature == id).map(|b| b.name.as_str()).collect();
let created: Vec<String> = s.model.state().bodies.iter().filter(|b| b.feature == id).map(|b| b.name.clone()).collect();
let turned = if options.y_up && !created.is_empty() { Some(stand_up(s, &created)?) } else { None };
let fname = s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default();
Ok(json!({"path": path, "feature": id, "name": fname, "bodies": bodies, "created_bodies": bodies, "warnings": sf.warnings}))
let mut out = json!({"path": path, "feature": id, "name": fname, "bodies": created, "created_bodies": created, "warnings": sf.warnings});
if let Some(turned) = turned {
out["stand_up"] = json!(turned);
}
Ok(out)
}
/// Add a MeshImport feature for a 3MF or STL file.
@@ -119,16 +168,24 @@ fn insert_step(s: &mut Session, p: &Value) -> Result<Value> {
if !solidcraft_io::is_step_path(path) {
return Err(bad(cmd, "only STEP files (.step, .stp) can be inserted"));
}
add_step(s, path, str_(p, "name"))
let options = step_options(p, cmd)?;
add_step(s, path, str_(p, "name"), &options)
}
fn open(s: &mut Session, p: &Value) -> Result<Value> {
let path = path_arg(p, "doc.open")?;
if !solidcraft_io::is_step_path(path) && ["up", "names"].iter().any(|k| p.get(*k).is_some_and(|v| !v.is_null())) {
return Err(bad("doc.open", "`up` and `names` apply to STEP and IGES files only"));
}
if solidcraft_io::is_step_path(path) || solidcraft_io::is_mesh_path(path) {
// A new, unsaved design holding the file's bodies.
let stem = std::path::Path::new(path).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| "Untitled".into());
let mut fresh = Session::new(Document::new(&stem));
let r = if solidcraft_io::is_step_path(path) { add_step(&mut fresh, path, None)? } else { add_mesh(&mut fresh, path, None)? };
let r = if solidcraft_io::is_step_path(path) {
add_step(&mut fresh, path, None, &step_options(p, "doc.open")?)?
} else {
add_mesh(&mut fresh, path, None)?
};
fresh.undo.clear();
s.replace_design(fresh);
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();