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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-05-31 19:47:07 +03:00
1 parent 7bf9ce31ae
commit 47a4bf19b3
3 files changed
+65

No files matched your search

+5
View File
@@ -2850,6 +2850,11 @@ impl OpenCADStudio {
vp_mat,
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 {
// Individual picks accumulate (issue #47):
+45
View File
@@ -11,6 +11,7 @@ use glam::{Mat4, Vec3};
use iced::{Point, Rectangle};
use super::hatch_model::HatchModel;
use super::mesh_model::MeshModel;
use super::wire_model::WireModel;
/// Pixel radius used for single-click wire detection.
@@ -54,6 +55,50 @@ pub fn click_hit<'a>(
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 ────────────────────────────────────────────────
/// Return the names of wires selected by a completed rectangular selection box.
+15
View File
@@ -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`.
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
// Default culling is driven by the live `Scene::camera`. Multi-tile