forked from 1b-agent/opencadstudio-upstream
fix(stl): write each facet's own normal
build_stl took the facet normal from the first vertex's normal whenever the mesh had normals. On smoothed meshes (tessellated curved solids, OBJ imports with vn) that normal leans off the triangle, while STL defines the facet normal as the triangle's own unit normal. Use the triangle's own normal, and keep the vertex normal only for a triangle with no area. Meshes without normals export as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cba64631b8
commit
6219af60ed
1 file changed
+70
-8
+70
-8
@@ -39,16 +39,19 @@ pub fn build_stl(meshes: &[&MeshModel]) -> Option<Vec<u8>> {
|
||||
let b = verts[i1];
|
||||
let c = verts[i2];
|
||||
|
||||
// Compute face normal.
|
||||
let normal = if !mesh.normals.is_empty() && i0 < mesh.normals.len() {
|
||||
// The facet normal is the triangle's own. A smoothed vertex normal
|
||||
// leans off it, so only a triangle with no area falls back to the
|
||||
// normal the mesh gives it.
|
||||
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();
|
||||
let normal = if len <= f32::EPSILON && 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);
|
||||
let len = len.max(f32::EPSILON);
|
||||
[nx / len, ny / len, nz / len]
|
||||
};
|
||||
|
||||
@@ -91,3 +94,62 @@ pub fn build_stl(meshes: &[&MeshModel]) -> Option<Vec<u8>> {
|
||||
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
#[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 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]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user