Use solid edges for normal selection

This commit is contained in:
ramox81 committed 2026-09-02 14:38:22 +03:00
1 parent bde9cc2715
commit a706858ff1
6 files changed
+222 -18

No files matched your search

+15 -9
View File
@@ -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);
+51
View File
@@ -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<Handle>>,
tolerance_px: f32,
) -> Option<Handle> {
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<Handle>>,
tolerance_px: f32,
) -> Option<Handle> {
self.solid_edge_click_hit(
cursor,
view_rot,
eye,
bounds,
candidate_handles,
tolerance_px,
)
}
pub fn solid_click_point_for(
&self,
cursor: iced::Point,
+10
View File
@@ -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<acadrust::Handle> {
self.instance_handle.or_else(|| {
self.lods
+86
View File
@@ -731,6 +731,92 @@ type MeshPickItem<'a> = (
[f64; 6],
);
type MeshEdgePickItem<'a> = (
Handle,
&'a [[f32; 3]],
&'a [[f32; 3]],
Option<acadrust::types::Transform>,
);
/// 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<Item = MeshEdgePickItem<'a>>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
tolerance_px: f32,
) -> Option<Handle> {
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<Item = MeshPickItem<'a>>,
+14 -2
View File
@@ -3266,9 +3266,19 @@ impl Pipeline {
&mut self,
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
hovered: &rustc_hash::FxHashSet<acadrust::Handle>,
edge_wires: &[WireModel],
) {
let edge_handles: rustc_hash::FxHashSet<acadrust::Handle> = edge_wires
.iter()
.filter_map(|wire| wire.name.strip_prefix("mesh-edge:"))
.filter_map(|value| value.parse::<u64>().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,
+46 -7
View File
@@ -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;