Imports become components; STEP assemblies as nested components (#1386) (#1426)

* 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:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-15 11:04:49 -04:00
1 parent d43c414b7b
commit 27529d9742
37 files changed
+968 -103

No files matched your search

+25 -7
View File
@@ -4681,15 +4681,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
Ok(())
}
FeatureKind::Import { step, .. } => {
FeatureKind::Import { step, skip, .. } => {
let imp = kernel::step_import_shared(step)?;
let digest = crate::config::payload_digest(step);
// Files from board and assembly tools give thousands of bodies the same name: name
// them from a set, not by searching the bodies for every candidate (cubic).
let mut names = BodyNames::new(st);
for (k, b) in imp.bodies.iter().enumerate() {
let base = f.body_names.get(k).filter(|n| !n.trim().is_empty()).unwrap_or(&b.name);
let name = names.unique(base);
let names = import_body_names(st, &f.body_names, &imp, skip);
for (b, name) in imp.bodies.iter().zip(names) {
let Some(name) = name else { continue };
let b = crate::lineage::numbered_tool(b.body.clone().with_import_certificate(&digest, f.id)?);
st.bodies.push(ModelBody::traced_new(name, b, f.id));
}
@@ -4722,6 +4719,27 @@ fn mesh_bodies(meshes: &[crate::MeshData]) -> Result<Vec<(String, Body)>> {
Ok(out)
}
/// The names an Import feature gives the file's bodies, added after the bodies of `st`, in the
/// file's order: `None` for a body it skips (a later instance of a repeated part). `body_names`
/// are the feature's stored names (by index), else the file's own.
pub fn import_body_names(st: &ModelState, body_names: &[String], imp: &kernel::StepImport, skip: &[u64]) -> Vec<Option<String>> {
let skip: std::collections::HashSet<u64> = skip.iter().copied().collect();
// Files from board and assembly tools give thousands of bodies the same name: name them
// from a set, not by searching the bodies for every candidate (cubic).
let mut names = BodyNames::new(st);
imp.bodies
.iter()
.enumerate()
.map(|(k, b)| {
if skip.contains(&(k as u64)) {
return None;
}
let base = body_names.get(k).filter(|n| !n.trim().is_empty()).unwrap_or(&b.name);
Some(names.unique(base))
})
.collect()
}
/// [`unique_body_name`] for many bodies in a row: the same names, in time linear in the bodies.
struct BodyNames {
taken: std::collections::HashSet<String>,