From d77a3bd774b180c1f9674b97309424f795a9eab0 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:37:49 +0900 Subject: [PATCH] fix(stl): keep the own normal of small facets The vertex-normal fallback compared the cross product's length with f32::EPSILON, an area-scale threshold, so a valid facet with 1e-4 edges (cross product 1e-8) took the leaning vertex normal again. Fall back only when the cross product is zero. Co-Authored-By: Claude Opus 5 --- src/io/stl.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/io/stl.rs b/src/io/stl.rs index 6bbc0eb4f..89aeac137 100644 --- a/src/io/stl.rs +++ b/src/io/stl.rs @@ -40,18 +40,19 @@ pub fn build_stl(meshes: &[&MeshModel]) -> Option> { let c = verts[i2]; // 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. + // 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 * nx + ny * ny + nz * nz).sqrt(); - let normal = if len <= f32::EPSILON && i0 < mesh.normals.len() { + let normal = if len == 0.0 && i0 < mesh.normals.len() { mesh.normals[i0] } else { - let len = len.max(f32::EPSILON); + let len = len.max(f32::MIN_POSITIVE); [nx / len, ny / len, nz / len] }; @@ -141,6 +142,23 @@ mod tests { 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]); + } + /// 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]