Files
OneCAD-Draft/src/io/stl.rs
T
Afonsop 6cb8c00fa0 web: enable STL/STEP export and OBJ import, drag-and-drop open
STLOUT/STEPOUT wrote via std::fs and IMPORTOBJ read via std::fs, all
of which fail on wasm32. The export pickers now mirror the PDF path
(pick_*_owned returns a name-only PathBuf; the browser download names
the file) and the write tail swaps fs::write for sys::download_bytes.
OBJ import reads the picked file's bytes directly, like the web open
picker.

Opening by drag-and-drop was dead on the web build: winit never emits
FileDropped there. A wasm-gated io::web_drop module installs DOM
dragover/drop listeners, filters .dwg/.dxf, reads the File bytes
async, and queues them; the existing 50 ms PollWebControl tick drains
the queue into the same open path the picker uses (progress overlay,
recovery, OPFS recent cache).

Also floors the model-space plot sheet: extents smaller than 10 mm
(or the 0x0 paper a fresh unsaved drawing carries) now fall back to
the standard sheet instead of producing a ~3 pt page.

(cherry picked from commit ad60be11ed)
2026-10-12 12:02:04 +03:00

191 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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::model::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];
// The facet normal is the triangle's own. A smoothed vertex normal
// leans off it, so only a triangle whose cross product is zero
// falls back to the normal the mesh gives it. Small facets have a
// tiny cross product but still a normal of their own.
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.hypot(ny).hypot(nz);
let normal = if len == 0.0 && i0 < mesh.normals.len() {
mesh.normals[i0]
} else {
let len = len.max(f32::MIN_POSITIVE);
[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"Open CAD Studio 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)
}
/// Web: no save dialog — the export continues with a virtual filename and
/// the generated bytes are delivered as a browser download.
#[cfg(target_arch = "wasm32")]
pub async fn pick_stl_path_owned(stem: String) -> Option<std::path::PathBuf> {
Some(std::path::PathBuf::from(format!("{stem}.stl")))
}
#[cfg(test)]
mod tests {
use super::build_stl;
use crate::scene::model::mesh_model::MeshModel;
fn triangle(verts: Vec<[f32; 3]>, normals: Vec<[f32; 3]>) -> MeshModel {
MeshModel {
name: String::new(),
verts,
verts_low: Vec::new(),
normals,
indices: vec![0, 1, 2],
triangle_material_handles: Vec::new(),
triangle_colors: Vec::new(),
color: [1.0; 4],
selected: false,
}
}
/// The first facet's normal: the 12 bytes after the 80-byte header and
/// the 4-byte triangle count.
fn first_facet_normal(stl: &[u8]) -> [f32; 3] {
[0, 1, 2].map(|k| {
let at = 84 + 4 * k;
f32::from_le_bytes(stl[at..at + 4].try_into().expect("4 bytes"))
})
}
/// A curved surface shares smoothed normals across its facets, so a
/// vertex normal leans away from the triangle. STL stores the facet's
/// own normal.
#[test]
fn the_facet_normal_is_the_triangles_own() {
let leaning = [
std::f32::consts::FRAC_1_SQRT_2,
0.0,
std::f32::consts::FRAC_1_SQRT_2,
];
let mesh = triangle(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
vec![leaning; 3],
);
let stl = build_stl(&[&mesh]).expect("stl");
assert_eq!(first_facet_normal(&stl), [0.0, 0.0, 1.0]);
}
/// A small facet still has a normal of its own. Its cross product is tiny
/// (1e-8 here), far below f32::EPSILON, but it is not zero.
#[test]
fn a_small_facet_keeps_its_own_normal() {
let leaning = [
std::f32::consts::FRAC_1_SQRT_2,
0.0,
std::f32::consts::FRAC_1_SQRT_2,
];
let mesh = triangle(
vec![[0.0, 0.0, 0.0], [1e-4, 0.0, 0.0], [0.0, 1e-4, 0.0]],
vec![leaning; 3],
);
let stl = build_stl(&[&mesh]).expect("stl");
assert_eq!(first_facet_normal(&stl), [0.0, 0.0, 1.0]);
}
#[test]
fn a_facet_whose_squared_cross_product_underflows_keeps_its_own_normal() {
let mesh = triangle(
vec![[0.0, 0.0, 0.0], [1.0e-12, 0.0, 0.0], [0.0, 1.0e-12, 0.0]],
vec![[0.0, 1.0, 0.0]; 3],
);
let stl = build_stl(&[&mesh]).expect("stl");
assert_eq!(first_facet_normal(&stl), [0.0, 0.0, 1.0]);
}
/// A facet with no area has no normal of its own, so it keeps the one
/// the mesh gives it rather than a zero vector.
#[test]
fn a_degenerate_facet_keeps_its_vertex_normal() {
let mesh = triangle(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]],
vec![[0.0, 1.0, 0.0]; 3],
);
let stl = build_stl(&[&mesh]).expect("stl");
assert_eq!(first_facet_normal(&stl), [0.0, 1.0, 0.0]);
}
}