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 <[email protected]>
This commit is contained in:
kevin9327andClaude Opus 5 committed 2026-09-14 20:37:49 +09:00
1 parent 6219af60ed
commit d77a3bd774
1 file changed
+22 -4
+22 -4
View File
@@ -40,18 +40,19 @@ pub fn build_stl(meshes: &[&MeshModel]) -> Option<Vec<u8>> {
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]