forked from 1b-agent/opencadstudio-upstream
Use feature edges for normal 3D selection (#971)
Use feature edges for normal 3D selection
This commit is contained in:
6 files changed
+225
-18
No files matched your search
@@ -4033,16 +4033,16 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
// 3D solids: click anywhere on the shaded
|
// Normal 3D selection follows the displayed B-rep
|
||||||
// body — top-level solids and block-internal
|
// edges. Face-interior picking remains reserved for
|
||||||
// ones together, front-most wins (a block in
|
// modelling commands that explicitly request it.
|
||||||
// front of a solid resolves to the block).
|
self.tabs[i].scene.solid_edge_click_hit(
|
||||||
self.tabs[i].scene.solid_click_hit(
|
|
||||||
p,
|
p,
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
candidate_handles.as_ref(),
|
candidate_handles.as_ref(),
|
||||||
|
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
// Selection filter: drop a pick whose type is excluded.
|
// Selection filter: drop a pick whose type is excluded.
|
||||||
@@ -4352,12 +4352,13 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
self.tabs[i].scene.solid_click_hit(
|
self.tabs[i].scene.solid_edge_click_hit(
|
||||||
p,
|
p,
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
candidate_handles.as_ref(),
|
candidate_handles.as_ref(),
|
||||||
|
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
if let Some(handle) = hit {
|
if let Some(handle) = hit {
|
||||||
@@ -4965,9 +4966,14 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
if hovered.is_none() {
|
if hovered.is_none() {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
hovered = self.tabs[i]
|
hovered = self.tabs[i].scene.solid_edge_hover_hit(
|
||||||
.scene
|
p,
|
||||||
.solid_hover_hit(p, view_rot, eye, bounds, candidate_handles.as_ref());
|
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;
|
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||||
|
|||||||
@@ -8215,6 +8215,60 @@ 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()?;
|
||||||
|
if candidate_handles.is_some_and(|handles| !handles.contains(&handle)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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(
|
pub fn solid_click_point_for(
|
||||||
&self,
|
&self,
|
||||||
cursor: iced::Point,
|
cursor: iced::Point,
|
||||||
|
|||||||
@@ -274,6 +274,16 @@ impl MeshLodSet {
|
|||||||
.map_or(self.lods.as_slice(), |source| source.lods.as_slice())
|
.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> {
|
pub fn entity_handle(&self) -> Option<acadrust::Handle> {
|
||||||
self.instance_handle.or_else(|| {
|
self.instance_handle.or_else(|| {
|
||||||
self.lods
|
self.lods
|
||||||
|
|||||||
@@ -731,6 +731,92 @@ type MeshPickItem<'a> = (
|
|||||||
[f64; 6],
|
[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>(
|
pub(crate) fn mesh_click_hit<'a>(
|
||||||
cursor: Point,
|
cursor: Point,
|
||||||
meshes: impl Iterator<Item = MeshPickItem<'a>>,
|
meshes: impl Iterator<Item = MeshPickItem<'a>>,
|
||||||
|
|||||||
@@ -3266,9 +3266,19 @@ impl Pipeline {
|
|||||||
&mut self,
|
&mut self,
|
||||||
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
|
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
|
||||||
hovered: &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();
|
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) {
|
if let Some(ranges) = self.mesh_ranges_by_handle.get(handle) {
|
||||||
out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw {
|
out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw {
|
||||||
range,
|
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) {
|
if let Some(ranges) = self.mesh_ranges_by_handle.get(&handle) {
|
||||||
out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw {
|
out.extend(ranges.iter().copied().map(|range| MeshHighlightDraw {
|
||||||
range,
|
range,
|
||||||
|
|||||||
@@ -1044,7 +1044,11 @@ impl shader::Primitive for Primitive {
|
|||||||
vp.selection_generation,
|
vp.selection_generation,
|
||||||
);
|
);
|
||||||
if hl_key != inner.cached_highlight_key {
|
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;
|
inner.cached_highlight_key = hl_key;
|
||||||
}
|
}
|
||||||
// Live overlay (command preview / interim / grip drag) — small and
|
// Live overlay (command preview / interim / grip drag) — small and
|
||||||
@@ -3324,6 +3328,7 @@ impl Scene {
|
|||||||
highlighted.sort_unstable_by_key(|(handle, _)| handle.value());
|
highlighted.sort_unstable_by_key(|(handle, _)| handle.value());
|
||||||
|
|
||||||
let empty_selection = rustc_hash::FxHashSet::default();
|
let empty_selection = rustc_hash::FxHashSet::default();
|
||||||
|
let interaction_meshes = self.interaction_meshes_arc();
|
||||||
let mut wires = Vec::new();
|
let mut wires = Vec::new();
|
||||||
for (handle, selected) in highlighted {
|
for (handle, selected) in highlighted {
|
||||||
let Some(entity) = self.document.get_entity(handle) else {
|
let Some(entity) = self.document.get_entity(handle) else {
|
||||||
@@ -3339,6 +3344,46 @@ impl Scene {
|
|||||||
continue;
|
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 matches!(entity, EntityType::Hatch(_)) {
|
||||||
if selected {
|
if selected {
|
||||||
if let Some(mut wire) = self.hatch_outline_wire(handle) {
|
if let Some(mut wire) = self.hatch_outline_wire(handle) {
|
||||||
@@ -3390,12 +3435,6 @@ impl Scene {
|
|||||||
.map(|context| context.scale)
|
.map(|context| context.scale)
|
||||||
})
|
})
|
||||||
.flatten();
|
.flatten();
|
||||||
let tint = if selected {
|
|
||||||
WireModel::SELECTED
|
|
||||||
} else {
|
|
||||||
WireModel::HOVER
|
|
||||||
};
|
|
||||||
|
|
||||||
for scale in scales {
|
for scale in scales {
|
||||||
if displayed_scale == Some(scale) {
|
if displayed_scale == Some(scale) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in new issue
Block a user