From 2bbd3cfcd2b0bf36f0b88d1e9cc57b28c51e7751 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 4 Jul 2026 23:11:47 +0300 Subject: [PATCH] feat(pick): select mesh entities by clicking their shaded faces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mesh entities (PolyfaceMesh / PolygonMesh / SubD Mesh) could only be picked on their thin edges — click_hit tested wire segments, and the solid face pick only covers ACIS solids. When no edge is close, click_hit now also tests the mesh's fill_tris with the same projected-triangle containment mesh_click_hit uses for solids, so clicking a mesh's surface selects it, front-most first. Edge proximity still takes priority. Co-Authored-By: Claude Opus 4.8 --- src/scene/pick/hit_test.rs | 46 +++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index eb1753ced..345152564 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -90,7 +90,51 @@ pub fn click_hit<'a>( } } - best + if best.is_some() { + return best; + } + + // No edge close enough. Mesh entities (PolyfaceMesh / PolygonMesh / SubD + // Mesh) carry their shaded faces as `fill_tris`; test those so a mesh is + // selectable by clicking its surface — not only its thin edges — the way a + // 3D solid is. Same projected-triangle containment as `mesh_click_hit`, + // front-most wins. + let mut best_fill: Option<(f32, &str)> = None; + for wire in wires { + if wire.fill_tris.is_empty() { + continue; + } + let mut t = 0; + while t + 2 < wire.fill_tris.len() { + let mut sp = [Point::ORIGIN; 3]; + let mut depth = 0.0f32; + for j in 0..3 { + let k = t + j; + let hi = wire.fill_tris[k]; + let lo = wire.fill_tris_low.get(k).copied().unwrap_or([0.0; 3]); + let world = glam::DVec3::new( + hi[0] as f64 + lo[0] as f64, + hi[1] as f64 + lo[1] as f64, + hi[2] as f64 + lo[2] as f64, + ); + let ndc = view_rot.project_point3((world - eye).as_vec3()); + sp[j] = Point::new( + (ndc.x + 1.0) * 0.5 * bounds.width, + (1.0 - ndc.y) * 0.5 * bounds.height, + ); + depth += ndc.z; + } + t += 3; + if point_in_polygon(cursor, &sp) { + let d = depth / 3.0; + if best_fill.map_or(true, |(bd, _)| d < bd) { + best_fill = Some((d, wire.name.as_str())); + } + break; + } + } + } + best_fill.map(|(_, n)| n) } /// Like `click_hit` but returns every wire within the click threshold,