From e97314cdef317e81c2eae047aaaf28fc01ad4136 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 28 Sep 2026 18:01:38 +0300 Subject: [PATCH] fix(pick): overlapping entities resolve to the front-most in draw order Fixes #1439 Co-Authored-By: Claude Opus 5.5 --- src/app/update/viewport.rs | 7 +++++++ src/scene/pick/hit_test.rs | 31 +++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 12463ebe0..1cf4958cb 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2591,6 +2591,7 @@ impl OpenCADStudio { bounds, self.tabs[i].scene.document.header.lineweight_display, crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), ) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { @@ -3581,6 +3582,7 @@ impl OpenCADStudio { bounds, self.tabs[i].scene.document.header.lineweight_display, crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), ) .and_then(Scene::handle_from_wire_name) .and_then(|handle| self.tabs[i].scene.document.get_entity(handle)) @@ -4110,6 +4112,7 @@ impl OpenCADStudio { bounds, self.tabs[i].scene.document.header.lineweight_display, crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), ) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { @@ -4858,6 +4861,7 @@ properties={:.1}ms picked={}", bounds, self.tabs[i].scene.document.header.lineweight_display, crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), ) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { @@ -5237,6 +5241,7 @@ properties={:.1}ms picked={}", bounds, self.tabs[i].scene.document.header.lineweight_display, crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), ) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { @@ -5382,6 +5387,7 @@ properties={:.1}ms picked={}", bounds, self.tabs[i].scene.document.header.lineweight_display, crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), ) .and_then(|s| Scene::handle_from_wire_name(s)); @@ -5915,6 +5921,7 @@ properties={:.1}ms picked={}", bounds, self.tabs[i].scene.document.header.lineweight_display, crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), ) .and_then(Scene::handle_from_wire_name); let wire_ms = wire_started.elapsed().as_secs_f64() * 1000.0; diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index dc4754370..043c25320 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -197,7 +197,24 @@ pub fn click_hit<'a, W: WireSource + ?Sized>( bounds: Rectangle, lw_display: bool, base_radius_px: f32, + draw_depth: &HashMap, ) -> Option<&'a str> { + // Coincident edges (a rectangle side on an xline) sit at the same pixel + // distance give or take float noise; within this band the front-most + // entity in draw order wins instead of whichever was measured first. (#1439) + const TIE_PX: f32 = 0.5; + let front = |name: &str| { + crate::scene::Scene::handle_from_wire_name(name) + .and_then(|handle| draw_depth.get(&handle.value())) + .map_or(f32::MIN, |depth| depth[0]) + }; + let beats = |d: f32, name: &str, best_dist: f32, best: Option<&str>| { + if (d - best_dist).abs() > TIE_PX { + return d < best_dist; + } + let (mine, theirs) = (front(name), best.map_or(f32::MIN, front)); + mine > theirs || (mine == theirs && d < best_dist) + }; // A click outside the pane rectangle (e.g. on the paper around a floating // viewport) must not reach geometry scissored out of the viewport. if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height { @@ -224,7 +241,7 @@ pub fn click_hit<'a, W: WireSource + ?Sized>( ); if let Some(start) = previous { let distance = dist_point_to_segment(cursor, start, screen); - if distance < tolerance && distance < best_dist { + if distance < tolerance && beats(distance, &wire.name, best_dist, best) { best_dist = distance; best = Some(wire.name.as_str()); } @@ -272,7 +289,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>( bounds, ); let d = dist_point_to_segment(cursor, p0, p1); - if d < pick_tolerance_px(wire, lw_display, base_radius_px) && d < best_dist { + if d < pick_tolerance_px(wire, lw_display, base_radius_px) + && beats(d, &wire.name, best_dist, best) + { best_dist = d; best = Some(&wire.name); } @@ -310,7 +329,7 @@ pub fn click_hit<'a, W: WireSource + ?Sized>( ); if let Some(p0) = prev { let d = dist_point_to_segment(cursor, p0, cur); - if d < tol && d < best_dist { + if d < tol && beats(d, &wire.name, best_dist, best) { best_dist = d; best = Some(&wire.name); } @@ -2681,16 +2700,16 @@ mod aabb_reject_tests { let eye = glam::DVec3::ZERO; assert_eq!( - click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds, true, 8.0), + click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds, true, 8.0, &HashMap::default()), Some("5") ); assert_eq!( - click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds, true, 8.0), + click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds, true, 8.0, &HashMap::default()), None ); // The far wire must be rejected without hiding the near one. assert_eq!( - click_hit(cursor, &[far, near], vp, eye, bounds, true, 8.0), + click_hit(cursor, &[far, near], vp, eye, bounds, true, 8.0, &HashMap::default()), Some("5") ); }