forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
cba64631b8
commit
bc3e63ff53
1 file changed
+68
-7
+68
-7
@@ -48,15 +48,19 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
||||
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<f64> = 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]]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user