From a706858ff1d366d3bb19af22d6f725f6f99710fc Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:38:22 +0300 Subject: [PATCH] Use solid edges for normal selection --- src/app/update/viewport.rs | 24 ++++++---- src/scene/mod.rs | 51 +++++++++++++++++++++ src/scene/model/mesh_model.rs | 10 ++++ src/scene/pick/hit_test.rs | 86 +++++++++++++++++++++++++++++++++++ src/scene/pipeline/mod.rs | 16 ++++++- src/scene/view/render.rs | 53 ++++++++++++++++++--- 6 files changed, 222 insertions(+), 18 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 55164a91b..f6bb56b18 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -4033,16 +4033,16 @@ impl OpenCADStudio { ) }) .or_else(|| { - // 3D solids: click anywhere on the shaded - // body — top-level solids and block-internal - // ones together, front-most wins (a block in - // front of a solid resolves to the block). - self.tabs[i].scene.solid_click_hit( + // Normal 3D selection follows the displayed B-rep + // edges. Face-interior picking remains reserved for + // modelling commands that explicitly request it. + self.tabs[i].scene.solid_edge_click_hit( p, view_rot, eye, bounds, candidate_handles.as_ref(), + crate::ui::overlay::pick_box_aperture_px(self.pick_box), ) }); // Selection filter: drop a pick whose type is excluded. @@ -4352,12 +4352,13 @@ impl OpenCADStudio { ) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { - self.tabs[i].scene.solid_click_hit( + self.tabs[i].scene.solid_edge_click_hit( p, view_rot, eye, bounds, candidate_handles.as_ref(), + crate::ui::overlay::pick_box_aperture_px(self.pick_box), ) }); if let Some(handle) = hit { @@ -5053,9 +5054,14 @@ impl OpenCADStudio { } if hovered.is_none() { let started = Instant::now(); - hovered = self.tabs[i] - .scene - .solid_hover_hit(p, view_rot, eye, bounds, candidate_handles.as_ref()); + hovered = self.tabs[i].scene.solid_edge_hover_hit( + p, + view_rot, + eye, + bounds, + candidate_handles.as_ref(), + crate::ui::overlay::pick_box_aperture_px(self.pick_box), + ); solid_ms = started.elapsed().as_secs_f64() * 1000.0; } self.tabs[i].scene.set_hover_highlight(hovered); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 13f5bf6c2..bf42ab915 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -8206,6 +8206,57 @@ impl Scene { ) } + /// Normal object selection for shaded 3D bodies is edge based. This keeps + /// the broad face interior available to modelling commands without making + /// it part of ordinary object picking. + pub fn solid_edge_click_hit( + &self, + cursor: iced::Point, + view_rot: glam::Mat4, + eye: glam::DVec3, + bounds: iced::Rectangle, + _candidate_handles: Option<&HashSet>, + tolerance_px: f32, + ) -> Option { + let meshes = self.interaction_meshes_arc(); + pick::hit_test::mesh_edge_click_hit( + cursor, + meshes.iter().filter_map(|set| { + let handle = set.entity_handle()?; + let (edges, edges_low) = set.geometry_edges(); + (!edges.is_empty()).then_some(( + handle, + edges, + edges_low, + set.instance_transform, + )) + }), + view_rot, + eye, + bounds, + tolerance_px, + ) + } + + pub fn solid_edge_hover_hit( + &self, + cursor: iced::Point, + view_rot: glam::Mat4, + eye: glam::DVec3, + bounds: iced::Rectangle, + candidate_handles: Option<&HashSet>, + tolerance_px: f32, + ) -> Option { + self.solid_edge_click_hit( + cursor, + view_rot, + eye, + bounds, + candidate_handles, + tolerance_px, + ) + } + pub fn solid_click_point_for( &self, cursor: iced::Point, diff --git a/src/scene/model/mesh_model.rs b/src/scene/model/mesh_model.rs index 97ee55441..628b7c408 100644 --- a/src/scene/model/mesh_model.rs +++ b/src/scene/model/mesh_model.rs @@ -274,6 +274,16 @@ impl MeshLodSet { .map_or(self.lods.as_slice(), |source| source.lods.as_slice()) } + pub fn geometry_edges(&self) -> (&[[f32; 3]], &[[f32; 3]]) { + self.instance_source.as_ref().map_or( + (self.edge_verts.as_slice(), self.edge_verts_low.as_slice()), + |source| ( + source.edge_verts.as_slice(), + source.edge_verts_low.as_slice(), + ), + ) + } + pub fn entity_handle(&self) -> Option { self.instance_handle.or_else(|| { self.lods diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index 857ff942f..209a88ba1 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -731,6 +731,92 @@ type MeshPickItem<'a> = ( [f64; 6], ); +type MeshEdgePickItem<'a> = ( + Handle, + &'a [[f32; 3]], + &'a [[f32; 3]], + Option, +); + +/// Return the closest solid whose actual B-rep feature edge passes through the +/// cursor aperture. Normal selection uses this path so a shaded body's broad +/// triangle faces do not turn its entire interior into a pick target. Commands +/// that deliberately acquire a face continue to use [`mesh_click_hit`]. +pub(crate) fn mesh_edge_click_hit<'a>( + cursor: Point, + meshes: impl Iterator>, + view_rot: Mat4, + eye: glam::DVec3, + bounds: Rectangle, + tolerance_px: f32, +) -> Option { + if cursor.x < 0.0 + || cursor.x > bounds.width + || cursor.y < 0.0 + || cursor.y > bounds.height + { + return None; + } + let tolerance = tolerance_px.max(1.0); + let mut best: Option<(f32, f32, Handle)> = None; + for (handle, edges, edges_low, transform) in meshes { + if edges.len() < 2 { + continue; + } + let model = transform.map(codec_transform_matrix); + if model.is_some_and(|matrix| { + !matrix.is_finite() || matrix.determinant().abs() <= 1e-18 + }) { + continue; + } + for pair_start in (0..edges.len() - 1).step_by(2) { + let world_point = |index: usize| { + let hi = edges[index]; + let low = edges_low.get(index).copied().unwrap_or([0.0; 3]); + let local = glam::DVec3::new( + hi[0] as f64 + low[0] as f64, + hi[1] as f64 + low[1] as f64, + hi[2] as f64 + low[2] as f64, + ); + model.map_or(local, |matrix| matrix.transform_point3(local)) + }; + let first = world_point(pair_start); + let second = world_point(pair_start + 1); + if !first.is_finite() || !second.is_finite() { + continue; + } + let first_ndc = view_rot.project_point3((first - eye).as_vec3()); + let second_ndc = view_rot.project_point3((second - eye).as_vec3()); + if !first_ndc.is_finite() || !second_ndc.is_finite() { + continue; + } + let to_screen = |point: glam::Vec3| { + Point::new( + (point.x + 1.0) * 0.5 * bounds.width, + (1.0 - point.y) * 0.5 * bounds.height, + ) + }; + let distance = dist_point_to_segment( + cursor, + to_screen(first_ndc), + to_screen(second_ndc), + ); + if distance > tolerance { + continue; + } + let depth = (first_ndc.z + second_ndc.z) * 0.5; + let replace = best.is_none_or(|(best_distance, best_depth, _)| { + distance + 1e-4 < best_distance + || ((distance - best_distance).abs() <= 1e-4 && depth < best_depth) + }); + if replace { + best = Some((distance, depth, handle)); + } + } + } + best.map(|(_, _, handle)| handle) +} + pub(crate) fn mesh_click_hit<'a>( cursor: Point, meshes: impl Iterator>, diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 969f9de35..218e7d5a3 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -3266,9 +3266,19 @@ impl Pipeline { &mut self, selected: &rustc_hash::FxHashSet, hovered: &rustc_hash::FxHashSet, + edge_wires: &[WireModel], ) { + let edge_handles: rustc_hash::FxHashSet = edge_wires + .iter() + .filter_map(|wire| wire.name.strip_prefix("mesh-edge:")) + .filter_map(|value| value.parse::().ok()) + .map(acadrust::Handle::new) + .collect(); let mut out = Vec::new(); - for handle in selected { + for handle in selected + .iter() + .filter(|handle| !edge_handles.contains(handle)) + { if let Some(ranges) = self.mesh_ranges_by_handle.get(handle) { out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw { range, @@ -3276,7 +3286,9 @@ impl Pipeline { })); } } - for handle in hovered.iter().filter(|handle| !selected.contains(handle)) { + for handle in hovered.iter().filter(|handle| { + !selected.contains(handle) && !edge_handles.contains(handle) + }) { if let Some(ranges) = self.mesh_ranges_by_handle.get(&handle) { out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw { range, diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index de64fc1df..2365ab5b8 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -1044,7 +1044,11 @@ impl shader::Primitive for Primitive { vp.selection_generation, ); if hl_key != inner.cached_highlight_key { - inner.update_mesh_highlight(&vp.selected_handles, &vp.hover_handles); + inner.update_mesh_highlight( + &vp.selected_handles, + &vp.hover_handles, + &vp.annotation_context_wires, + ); inner.cached_highlight_key = hl_key; } // Live overlay (command preview / interim / grip drag) — small and @@ -3324,6 +3328,7 @@ impl Scene { highlighted.sort_unstable_by_key(|(handle, _)| handle.value()); let empty_selection = rustc_hash::FxHashSet::default(); + let interaction_meshes = self.interaction_meshes_arc(); let mut wires = Vec::new(); for (handle, selected) in highlighted { let Some(entity) = self.document.get_entity(handle) else { @@ -3339,6 +3344,46 @@ impl Scene { continue; } + let tint = if selected { + WireModel::SELECTED + } else { + WireModel::HOVER + }; + for set in interaction_meshes + .iter() + .filter(|set| set.entity_handle() == Some(handle)) + { + let (edges, edges_low) = set.geometry_edges(); + if edges.len() < 2 { + continue; + } + let mut points = Vec::with_capacity(edges.len() / 2 * 3); + for pair_start in (0..edges.len() - 1).step_by(2) { + for index in [pair_start, pair_start + 1] { + let high = edges[index]; + let low = edges_low.get(index).copied().unwrap_or([0.0; 3]); + let local = acadrust::types::Vector3::new( + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + high[2] as f64 + low[2] as f64, + ); + let world = set + .instance_transform + .map_or(local, |transform| transform.apply(local)); + points.push([world.x, world.y, world.z]); + } + points.push([f64::NAN; 3]); + } + let mut edge_wire = WireModel::solid_f64( + format!("mesh-edge:{}", handle.value()), + points, + tint, + selected, + ); + edge_wire.line_weight_px = 2.0; + wires.push(edge_wire); + } + if matches!(entity, EntityType::Hatch(_)) { if selected { if let Some(mut wire) = self.hatch_outline_wire(handle) { @@ -3390,12 +3435,6 @@ impl Scene { .map(|context| context.scale) }) .flatten(); - let tint = if selected { - WireModel::SELECTED - } else { - WireModel::HOVER - }; - for scale in scales { if displayed_scale == Some(scale) { continue;