3MF: export, and 3MF/STL import as mesh bodies

Export: ExportCommand/CLI --out/MCP export with .3mf write a 3MF Core package
([Content_Types].xml, _rels/.rels, 3D/3dmodel.model; millimetres), one
mesh object and build item per body, named after it, vertices shared and
welded watertight (T-junctions between faces split), base-material colours
when bodies carry one (STEP colours now ride on bodies). Tessellation as
for STL. File > Export 3MF.

Import: doc.open/drag-drop/CLI/MCP open accept .3mf and .stl; Insert Mesh
(ParaMeshInsertAlignCommand) adds them to a design as a MeshImport feature.
Kernel mesh bodies render, measure exactly, move and export; solid
features refuse them with a clear error. 3MF reading flattens components
and build transforms, converts units, reads colours; sizes, entries,
nesting and element counts are capped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 21:30:38 +00:00
1 parent 043a542e29
commit ea3dd0ed6e
30 files changed
+1361 -47

No files matched your search

+9 -5
View File
@@ -170,7 +170,7 @@ impl SolveApp {
/// that have them, otherwise run it with defaults.
pub fn start(&mut self, id: &str) {
self.tool = None;
if id == "FusionImportCommandFromToolbar" {
if matches!(id, "FusionImportCommandFromToolbar" | "ParaMeshInsertAlignCommand") {
if let Some(p) = self.services.pick_open.as_ref().and_then(|f| f()) {
self.insert_path(&p);
}
@@ -250,9 +250,11 @@ impl SolveApp {
}
}
/// Insert a STEP file's bodies into the current design (an Import base feature).
/// Insert a STEP file's bodies (an Import base feature) or a 3MF/STL file's meshes into the
/// current design.
pub fn insert_path(&mut self, path: &str) {
if let Ok(r) = self.run("FusionImportCommandFromToolbar", json!({ "path": path })) {
let cmd = if solvecraft_engine::io::is_mesh_path(path) { "ParaMeshInsertAlignCommand" } else { "FusionImportCommandFromToolbar" };
if let Ok(r) = self.run(cmd, json!({ "path": path })) {
self.fit_view();
if let Some(w) = r["warnings"].as_array().filter(|w| !w.is_empty()) {
self.set_status(format!("imported with {} warning(s): {}", w.len(), w.first().and_then(Value::as_str).unwrap_or("")), false);
@@ -260,9 +262,11 @@ impl SolveApp {
}
}
/// A dropped file: a STEP file joins a design that has features, anything else opens.
/// A dropped file: a STEP, 3MF or STL file joins a design that has features, anything else
/// opens.
pub fn drop_path(&mut self, path: &str) {
if solvecraft_engine::io::is_step_path(path) && !self.session.doc.features.is_empty() {
let import = solvecraft_engine::io::is_step_path(path) || solvecraft_engine::io::is_mesh_path(path);
if import && !self.session.doc.features.is_empty() {
self.insert_path(path);
} else {
self.open_path(path);
+1 -1
View File
@@ -30,7 +30,7 @@ fn icon_of(k: &FeatureKind) -> &'static str {
FeatureKind::Draft { .. } => "draft",
FeatureKind::Split { .. } => "split",
FeatureKind::Move { .. } => "move",
FeatureKind::Import { .. } => "import",
FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } => "import",
}
}
+8
View File
@@ -131,6 +131,10 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
app.start("FusionImportCommandFromToolbar");
close = true;
}
if item(ui, "Insert Mesh…", "") {
app.start("ParaMeshInsertAlignCommand");
close = true;
}
if item(ui, "Save", "Ctrl+S") {
save(app);
close = true;
@@ -148,6 +152,10 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
export(app, "step");
close = true;
}
if item(ui, "Export 3MF…", "") {
export(app, "3mf");
close = true;
}
if item(ui, "Export OBJ…", "") {
export(app, "obj");
close = true;