From 4c1ddec646acf4ae70317815607366f9a3cbd0ab Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 28 Sep 2026 19:33:40 +0300 Subject: [PATCH] fix(obj): concave OBJ faces are triangulated, not fanned A fan only covers convex faces; architectural n-gons filled well outside their outline (the sample's faces covered 63% extra area). Fixes #680 Co-Authored-By: Claude Opus 5.5 --- src/entities/mesh.rs | 11 ++++++++++- src/io/obj.rs | 30 +++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index 7285e9e18..404d35f7b 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -16,6 +16,15 @@ use crate::t; /// Triangulate a planar polygon through the kernel. pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> { + triangulate_planar_indices(poly) + .into_iter() + .flat_map(|triangle| triangle.map(|index| poly[index])) + .collect() +} + +/// Triangles of a planar polygon as indices into `poly` — for callers that +/// carry per-vertex data (normals) alongside the positions. +pub(crate) fn triangulate_planar_indices(poly: &[[f64; 3]]) -> Vec<[usize; 3]> { if poly.len() < 3 { return Vec::new(); } @@ -51,7 +60,7 @@ pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> { } triangles .into_iter() - .flat_map(|triangle| triangle.map(|index| poly[source[index]])) + .map(|triangle| triangle.map(|index| source[index])) .collect() } diff --git a/src/io/obj.rs b/src/io/obj.rs index 06231c838..454aba6b2 100644 --- a/src/io/obj.rs +++ b/src/io/obj.rs @@ -51,11 +51,31 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option { Some((pos_i, norm_i)) }) .collect(); - // Fan-triangulate: (0,1,2), (0,2,3), … - for k in 1..(descs.len() as isize - 1) { - face_verts.push(descs[0]); - face_verts.push(descs[k as usize]); - face_verts.push(descs[k as usize + 1]); + // A fan is only right for convex faces; floor-plan n-gons + // are concave, so larger faces go through the kernel's + // triangulator and fall back to the fan if it declines. (#680) + let corners: Vec<[f64; 3]> = descs + .iter() + .map(|(pos_i, _)| { + let p = positions.get(*pos_i).copied().unwrap_or([0.0; 3]); + [p[0] as f64, p[1] as f64, p[2] as f64] + }) + .collect(); + let triangles = if descs.len() > 3 { + crate::entities::mesh::triangulate_planar_indices(&corners) + } else { + Vec::new() + }; + if triangles.is_empty() { + for k in 1..(descs.len() as isize - 1) { + face_verts.push(descs[0]); + face_verts.push(descs[k as usize]); + face_verts.push(descs[k as usize + 1]); + } + } else { + for triangle in triangles { + face_verts.extend(triangle.map(|index| descs[index])); + } } } _ => {}