diff --git a/ROADMAP.md b/ROADMAP.md index bdc54041..a83254e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -295,7 +295,8 @@ Underlay (PDF/DWF/DGN) | Boolean operasyonlar (UNION/SUBTRACT/INTERSECT) | ⬜ | | EXTRUDE / REVOLVE / SWEEP / LOFT | ⬜ | | 3D ARRAY | ✅ | -| STL / STEP dışa aktarma | ⬜ | +| STL dışa aktarma (STLOUT) | ✅ | +| STEP dışa aktarma | ⬜ | --- diff --git a/src/app/commands.rs b/src/app/commands.rs index 170e710a..d1745c40 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -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); diff --git a/src/app/mod.rs b/src/app/mod.rs index 4ef97d38..2523d7cf 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -435,6 +435,11 @@ pub enum Message { DataExtractionSave(String), /// Path chosen (or None = cancelled). DataExtractionSaveResult(String, Option), + // ── 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), } impl H7CAD { diff --git a/src/app/update.rs b/src/app/update.rs index d5b85280..176ebf80 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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 = + 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 { diff --git a/src/io/mod.rs b/src/io/mod.rs index a51ecc31..77335b0c 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -5,6 +5,7 @@ pub mod pdf_export; pub mod plot_style; +pub mod stl; pub mod xref; use acadrust::io::dwg::DwgReader; diff --git a/src/io/stl.rs b/src/io/stl.rs new file mode 100644 index 00000000..46bfbf77 --- /dev/null +++ b/src/io/stl.rs @@ -0,0 +1,90 @@ +// STL binary export — converts all tessellated MeshModels in the scene to a +// single binary STL file. +// +// Binary STL format: +// 80-byte header +// 4-byte triangle count (u32 LE) +// Per triangle (50 bytes): +// 3 × f32 normal +// 3 × 3 × f32 vertices +// 2-byte attribute (0) + +use std::io::Write; + +use crate::scene::mesh_model::MeshModel; + +/// Build a binary STL byte buffer from a slice of mesh models. +/// Returns `None` if there are no triangles to export. +pub fn build_stl(meshes: &[&MeshModel]) -> Option> { + // Collect all triangles. + struct Tri { + normal: [f32; 3], + v: [[f32; 3]; 3], + } + + let mut tris: Vec = Vec::new(); + + for mesh in meshes { + let verts = &mesh.verts; + let idx = &mesh.indices; + let n_tri = idx.len() / 3; + for t in 0..n_tri { + let i0 = idx[t * 3] as usize; + let i1 = idx[t * 3 + 1] as usize; + let i2 = idx[t * 3 + 2] as usize; + if i0 >= verts.len() || i1 >= verts.len() || i2 >= verts.len() { + continue; + } + let a = verts[i0]; + let b = verts[i1]; + let c = verts[i2]; + + // Compute face normal. + let normal = if !mesh.normals.is_empty() && i0 < mesh.normals.len() { + mesh.normals[i0] + } else { + let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]]; + let nx = ab[1] * ac[2] - ab[2] * ac[1]; + let ny = ab[2] * ac[0] - ab[0] * ac[2]; + let nz = ab[0] * ac[1] - ab[1] * ac[0]; + let len = (nx * nx + ny * ny + nz * nz).sqrt().max(f32::EPSILON); + [nx / len, ny / len, nz / len] + }; + + tris.push(Tri { normal, v: [a, b, c] }); + } + } + + if tris.is_empty() { + return None; + } + + let mut buf: Vec = Vec::with_capacity(84 + tris.len() * 50); + + // 80-byte header. + let mut header = [0u8; 80]; + let title = b"H7CAD STL export"; + header[..title.len()].copy_from_slice(title); + buf.extend_from_slice(&header); + + // Triangle count. + buf.extend_from_slice(&(tris.len() as u32).to_le_bytes()); + + for tri in &tris { + // Normal. + for &f in &tri.normal { + buf.write_all(&f.to_le_bytes()).ok()?; + } + // Vertices. + for v in &tri.v { + for &f in v { + buf.write_all(&f.to_le_bytes()).ok()?; + } + } + // Attribute byte count = 0. + buf.extend_from_slice(&0u16.to_le_bytes()); + } + + Some(buf) +}