forked from 1b-agent/opencadstudio-upstream
Use solid edges for normal selection
This commit is contained in:
1 parent
bde9cc2715
commit
a706858ff1
6 files changed
+222
-18
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user