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:
1 parent
81d9b7e04a
commit
0ee5e97db5
12 files changed
+572
-38
No files matched your search
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user