From bc3e63ff53227e8e9ddeb1373f034a7b43d4fbe4 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:04:00 +0900 Subject: [PATCH] fix(step): place each exported face on its triangle's own plane build_step took the PLANE 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, so the plane missed two of the face's corners and the first edge was no longer perpendicular to the placement axis. 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 --- src/io/step.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/src/io/step.rs b/src/io/step.rs index e5648c68c..fec965f01 100644 --- a/src/io/step.rs +++ b/src/io/step.rs @@ -48,15 +48,19 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option { let a = point(i0); let b = point(i1); let c = point(i2); - let n = if !normals.is_empty() && i0 < normals.len() { + // The face is placed on the triangle's own plane. 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 n = if len <= f64::EPSILON && i0 < normals.len() { 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(f64::EPSILON); + let len = len.max(f64::EPSILON); [(nx / len) as f32, (ny / len) as f32, (nz / len) as f32] }; tris.push(Tri { v: [a, b, c], n }); @@ -330,4 +334,61 @@ mod tests { let step = build_step(&[&mesh]).expect("step"); assert!(step.contains("CARTESIAN_POINT('',(1.500000,2.000000,0.000000));")); } + + /// The direction each face's PLANE is placed with, read back through its + /// AXIS2_PLACEMENT_3D. The export writes one face per triangle. + fn plane_normals(step: &str) -> Vec<[f64; 3]> { + let direction = |id: &str| -> [f64; 3] { + let prefix = format!("{id} = DIRECTION('',("); + let line = step + .lines() + .find(|line| line.starts_with(&prefix)) + .unwrap_or_else(|| panic!("no DIRECTION {id}")); + let values: Vec = line[prefix.len()..] + .trim_end_matches("));") + .split(',') + .map(|value| value.parse().expect("number")) + .collect(); + [values[0], values[1], values[2]] + }; + step.lines() + .filter_map(|line| line.split_once(" = AXIS2_PLACEMENT_3D('',")) + .map(|(_, refs)| { + let refs: Vec<&str> = refs.trim_end_matches(");").split(',').collect(); + direction(refs[1]) + }) + .collect() + } + + /// A curved surface shares smoothed normals across its facets, so a + /// vertex normal leans away from the triangle. The face's PLANE has to be + /// the triangle's own plane, or its corners do not lie on it. + #[test] + fn a_face_plane_follows_the_triangle_not_its_vertex_normal() { + let mut mesh = triangle( + vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + Vec::new(), + ); + let leaning = [ + std::f32::consts::FRAC_1_SQRT_2, + 0.0, + std::f32::consts::FRAC_1_SQRT_2, + ]; + mesh.normals = vec![leaning; 3]; + let step = build_step(&[&mesh]).expect("step"); + assert_eq!(plane_normals(&step), vec![[0.0, 0.0, 1.0]]); + } + + /// A triangle with no area has no plane of its own, so it keeps the + /// normal the mesh gives it rather than a zero direction. + #[test] + fn a_degenerate_triangle_keeps_its_vertex_normal() { + let mut mesh = triangle( + vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]], + Vec::new(), + ); + mesh.normals = vec![[0.0, 1.0, 0.0]; 3]; + let step = build_step(&[&mesh]).expect("step"); + assert_eq!(plane_normals(&step), vec![[0.0, 1.0, 0.0]]); + } }