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:
parent
9ede70ef1c
commit
25b91bc09e
6 changed files with 145 additions and 1 deletions
|
|
@ -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 | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
pub mod pdf_export;
|
||||
pub mod plot_style;
|
||||
pub mod stl;
|
||||
pub mod xref;
|
||||
|
||||
use acadrust::io::dwg::DwgReader;
|
||||
|
|
|
|||
90
src/io/stl.rs
Normal file
90
src/io/stl.rs
Normal file
|
|
@ -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<Vec<u8>> {
|
||||
// Collect all triangles.
|
||||
struct Tri {
|
||||
normal: [f32; 3],
|
||||
v: [[f32; 3]; 3],
|
||||
}
|
||||
|
||||
let mut tris: Vec<Tri> = 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<u8> = 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)
|
||||
}
|
||||
Loading…
Reference in a new issue