fix(select): pick 3D solids by clicking their body, not just edges
Single-click selection only tested wire-edge proximity, so clicking a solid's face missed — only its thin projected edges were selectable. Add mesh_click_hit: project each mesh triangle to screen and test the cursor against it (front-most wins), wired as a fallback after the wire and hatch hit-tests. Works for any meshed solid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7bf9ce31ae
commit
47a4bf19b3
3 changed files with 65 additions and 0 deletions
|
|
@ -2850,6 +2850,11 @@ impl OpenCADStudio {
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
// 3D solids: click anywhere on the shaded
|
||||||
|
// body, not just the thin projected edges.
|
||||||
|
self.tabs[i].scene.mesh_click_hit(p, vp_mat, bounds)
|
||||||
});
|
});
|
||||||
if let Some(handle) = hit {
|
if let Some(handle) = hit {
|
||||||
// Individual picks accumulate (issue #47):
|
// Individual picks accumulate (issue #47):
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use glam::{Mat4, Vec3};
|
||||||
use iced::{Point, Rectangle};
|
use iced::{Point, Rectangle};
|
||||||
|
|
||||||
use super::hatch_model::HatchModel;
|
use super::hatch_model::HatchModel;
|
||||||
|
use super::mesh_model::MeshModel;
|
||||||
use super::wire_model::WireModel;
|
use super::wire_model::WireModel;
|
||||||
|
|
||||||
/// Pixel radius used for single-click wire detection.
|
/// Pixel radius used for single-click wire detection.
|
||||||
|
|
@ -54,6 +55,50 @@ pub fn click_hit<'a>(
|
||||||
best
|
best
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pick a 3D solid by clicking anywhere on its shaded body: project each
|
||||||
|
/// mesh triangle to screen space and test whether the cursor lands inside it.
|
||||||
|
/// Returns the front-most hit (smallest projected depth). Lets meshed solids
|
||||||
|
/// (whose only wire geometry is thin edges) be selected on their faces.
|
||||||
|
pub fn mesh_click_hit<'a>(
|
||||||
|
cursor: Point,
|
||||||
|
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
|
||||||
|
view_proj: Mat4,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> Option<Handle> {
|
||||||
|
let mut best: Option<(f32, Handle)> = None;
|
||||||
|
for (handle, mesh) in meshes {
|
||||||
|
let v = &mesh.verts;
|
||||||
|
let idx = &mesh.indices;
|
||||||
|
let mut t = 0;
|
||||||
|
while t + 2 < idx.len() {
|
||||||
|
let tri = [
|
||||||
|
v[idx[t] as usize],
|
||||||
|
v[idx[t + 1] as usize],
|
||||||
|
v[idx[t + 2] as usize],
|
||||||
|
];
|
||||||
|
t += 3;
|
||||||
|
let mut sp = [Point::ORIGIN; 3];
|
||||||
|
let mut depth = 0.0f32;
|
||||||
|
for (j, w) in tri.iter().enumerate() {
|
||||||
|
let ndc = view_proj.project_point3(Vec3::new(w[0], w[1], w[2]));
|
||||||
|
sp[j] = Point::new(
|
||||||
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||||
|
);
|
||||||
|
depth += ndc.z;
|
||||||
|
}
|
||||||
|
if point_in_polygon(cursor, &sp) {
|
||||||
|
let d = depth / 3.0;
|
||||||
|
if best.map_or(true, |(bd, _)| d < bd) {
|
||||||
|
best = Some((d, handle));
|
||||||
|
}
|
||||||
|
break; // one hit per mesh is enough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.map(|(_, h)| h)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Box / window selection ────────────────────────────────────────────────
|
// ── Box / window selection ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Return the names of wires selected by a completed rectangular selection box.
|
/// Return the names of wires selected by a completed rectangular selection box.
|
||||||
|
|
|
||||||
|
|
@ -1593,6 +1593,21 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pick a meshed 3D solid by clicking on its shaded body (face), not just
|
||||||
|
/// its thin projected edges. Returns the front-most mesh under `cursor`.
|
||||||
|
pub fn mesh_click_hit(
|
||||||
|
&self,
|
||||||
|
cursor: iced::Point,
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
) -> Option<Handle> {
|
||||||
|
let iter = self
|
||||||
|
.meshes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(h, set)| set.lods.first().map(|m| (*h, m)));
|
||||||
|
hit_test::mesh_click_hit(cursor, iter, view_proj, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
/// Tessellate all non-invisible entities owned by `block_handle`.
|
/// Tessellate all non-invisible entities owned by `block_handle`.
|
||||||
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
|
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
|
||||||
// Default culling is driven by the live `Scene::camera`. Multi-tile
|
// Default culling is driven by the live `Scene::camera`. Multi-tile
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue