From 47a4bf19b3fb7a4051682f93974b8c7d57d59b3e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 31 May 2026 19:47:07 +0300 Subject: [PATCH] fix(select): pick 3D solids by clicking their body, not just edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-click selection only tested wire-edge proximity, so clicking a solid's face missed — only its thin projected edges were selectable. Add mesh_click_hit: project each mesh triangle to screen and test the cursor against it (front-most wins), wired as a fallback after the wire and hatch hit-tests. Works for any meshed solid. Co-Authored-By: Claude Opus 4.8 --- src/app/update.rs | 5 +++++ src/scene/hit_test.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ src/scene/mod.rs | 15 +++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/src/app/update.rs b/src/app/update.rs index 2e192c8b3..02bdf2b93 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2850,6 +2850,11 @@ impl OpenCADStudio { vp_mat, bounds, ) + }) + .or_else(|| { + // 3D solids: click anywhere on the shaded + // body, not just the thin projected edges. + self.tabs[i].scene.mesh_click_hit(p, vp_mat, bounds) }); if let Some(handle) = hit { // Individual picks accumulate (issue #47): diff --git a/src/scene/hit_test.rs b/src/scene/hit_test.rs index 7ffe445e8..ae8ae0787 100644 --- a/src/scene/hit_test.rs +++ b/src/scene/hit_test.rs @@ -11,6 +11,7 @@ use glam::{Mat4, Vec3}; use iced::{Point, Rectangle}; use super::hatch_model::HatchModel; +use super::mesh_model::MeshModel; use super::wire_model::WireModel; /// Pixel radius used for single-click wire detection. @@ -54,6 +55,50 @@ pub fn click_hit<'a>( best } +/// Pick a 3D solid by clicking anywhere on its shaded body: project each +/// mesh triangle to screen space and test whether the cursor lands inside it. +/// Returns the front-most hit (smallest projected depth). Lets meshed solids +/// (whose only wire geometry is thin edges) be selected on their faces. +pub fn mesh_click_hit<'a>( + cursor: Point, + meshes: impl Iterator, + view_proj: Mat4, + bounds: Rectangle, +) -> Option { + let mut best: Option<(f32, Handle)> = None; + for (handle, mesh) in meshes { + let v = &mesh.verts; + let idx = &mesh.indices; + let mut t = 0; + while t + 2 < idx.len() { + let tri = [ + v[idx[t] as usize], + v[idx[t + 1] as usize], + v[idx[t + 2] as usize], + ]; + t += 3; + let mut sp = [Point::ORIGIN; 3]; + let mut depth = 0.0f32; + for (j, w) in tri.iter().enumerate() { + let ndc = view_proj.project_point3(Vec3::new(w[0], w[1], w[2])); + sp[j] = Point::new( + (ndc.x + 1.0) * 0.5 * bounds.width, + (1.0 - ndc.y) * 0.5 * bounds.height, + ); + depth += ndc.z; + } + if point_in_polygon(cursor, &sp) { + let d = depth / 3.0; + if best.map_or(true, |(bd, _)| d < bd) { + best = Some((d, handle)); + } + break; // one hit per mesh is enough + } + } + } + best.map(|(_, h)| h) +} + // ── Box / window selection ──────────────────────────────────────────────── /// Return the names of wires selected by a completed rectangular selection box. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index cff884639..540a9253d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1593,6 +1593,21 @@ impl Scene { } } + /// Pick a meshed 3D solid by clicking on its shaded body (face), not just + /// its thin projected edges. Returns the front-most mesh under `cursor`. + pub fn mesh_click_hit( + &self, + cursor: iced::Point, + view_proj: glam::Mat4, + bounds: iced::Rectangle, + ) -> Option { + let iter = self + .meshes + .iter() + .filter_map(|(h, set)| set.lods.first().map(|m| (*h, m))); + hit_test::mesh_click_hit(cursor, iter, view_proj, bounds) + } + /// Tessellate all non-invisible entities owned by `block_handle`. fn wires_for_block(&self, block_handle: Handle) -> Vec { // Default culling is driven by the live `Scene::camera`. Multi-tile