feat: STL export (STLOUT / EXPORTSTL command)

Exports all tessellated 3D mesh models in the current drawing to a
binary STL file via a save-file dialog. Normals are taken from the
MeshModel where available, otherwise computed from triangle vertices.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-08 00:46:05 +03:00
1 parent 9ede70ef1c
commit 25b91bc09e
6 files changed
+145 -1

No files matched your search

+5
View File
@@ -3185,6 +3185,11 @@ impl H7CAD {
}
}
// ── STL export ────────────────────────────────────────────────
"STLOUT"|"EXPORTSTL" => {
return Task::done(Message::StlExport);
}
// ── Plot / Page Setup ──────────────────────────────────────────
"PRINT"|"PLOT"|"EXPORT" => {
return Task::done(Message::PlotExport);
+5
View File
@@ -435,6 +435,11 @@ pub enum Message {
DataExtractionSave(String),
/// Path chosen (or None = cancelled).
DataExtractionSaveResult(String, Option<std::path::PathBuf>),
// ── STL export ────────────────────────────────────────────────────────
/// Trigger STL export: collect meshes and show save dialog.
StlExport,
/// Callback after the user picks (or cancels) the STL save path.
StlExportPath(Option<std::path::PathBuf>),
}
impl H7CAD {
+42
View File
@@ -246,6 +246,48 @@ impl H7CAD {
Message::DataExtractionSaveResult(_, None) => Task::none(),
Message::StlExport => {
let i = self.active_tab;
if self.tabs[i].scene.meshes.is_empty() {
self.command_line.push_error("STLOUT: no 3D mesh data in this drawing.");
return Task::none();
}
Task::perform(
async {
rfd::AsyncFileDialog::new()
.set_title("Export STL")
.set_file_name("export.stl")
.add_filter("STL Files", &["stl"])
.add_filter("All Files", &["*"])
.save_file()
.await
.map(|h| h.path().to_path_buf())
},
Message::StlExportPath,
)
}
Message::StlExportPath(Some(path)) => {
// Re-build STL bytes (we can't easily pass them through the message).
let i = self.active_tab;
let meshes: Vec<crate::scene::mesh_model::MeshModel> =
self.tabs[i].scene.meshes.values().cloned().collect();
let mesh_refs: Vec<&crate::scene::mesh_model::MeshModel> = meshes.iter().collect();
match crate::io::stl::build_stl(&mesh_refs) {
Some(bytes) => match std::fs::write(&path, bytes) {
Ok(()) => self.command_line.push_output(&format!(
"STLOUT: exported to \"{}\"",
path.display()
)),
Err(e) => self.command_line.push_error(&format!("STLOUT: write error: {e}")),
},
None => self.command_line.push_error("STLOUT: no mesh data to export."),
}
Task::none()
}
Message::StlExportPath(None) => Task::none(),
Message::SaveFile => {
let i = self.active_tab;
if let Some(path) = &self.tabs[i].current_path {