fix(select): box/lasso-select blocks whose body is a solid

Box/lasso only tested top-level solid meshes, so a block whose visible body
is a solid (its mesh lives in the per-INSERT instanced cache, and it has no
wires to catch) was never picked up by a window/crossing/lasso. Add
block_mesh_box_hit / block_mesh_poly_hit that test each INSERT's instanced
block solid meshes and resolve a hit to the parent INSERT, and wire them
into the selection paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-21 07:03:56 +03:00
commit ae9940929c
2 changed files with 100 additions and 0 deletions

View file

@ -3742,6 +3742,9 @@ impl OpenCADStudio {
handles.extend(
self.tabs[i].scene.mesh_box_hit(a, p, crossing, vp_mat, bounds),
);
handles.extend(self.tabs[i].scene.block_mesh_box_hit(
a, p, crossing, vp_mat, bounds,
));
// Box/lasso accumulates like individual picks
// (issue #83): a plain box adds to the current
// selection, Shift+box removes the boxed
@ -3796,6 +3799,9 @@ impl OpenCADStudio {
handles.extend(
self.tabs[i].scene.mesh_poly_hit(&poly_pts, crossing, vp_mat, bounds),
);
handles.extend(self.tabs[i].scene.block_mesh_poly_hit(
&poly_pts, crossing, vp_mat, bounds,
));
// Selection filter: keep only allowed types.
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
// Accumulate like the box path (issue #83): plain
@ -3935,6 +3941,9 @@ impl OpenCADStudio {
handles.extend(
self.tabs[i].scene.mesh_box_hit(a, p, crossing, vp_mat, bounds),
);
handles.extend(self.tabs[i].scene.block_mesh_box_hit(
a, p, crossing, vp_mat, bounds,
));
// Selection filter: keep only allowed types.
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
// Accumulate (issue #83): a plain box adds to the

View file

@ -2310,6 +2310,97 @@ impl Scene {
pick::hit_test::mesh_poly_hit(poly, crossing, iter, view_proj, bounds)
}
/// Parent INSERT handles whose block-internal solid meshes fall in a
/// rectangular selection box. A block whose visible body is a solid has
/// no wires to catch, so box/lasso selection must test its instanced
/// meshes too.
pub fn block_mesh_box_hit(
&self,
a: iced::Point,
b: iced::Point,
crossing: bool,
view_proj: glam::Mat4,
bounds: iced::Rectangle,
) -> Vec<Handle> {
if self.block_meshes.is_empty() {
return Vec::new();
}
let layout_block = self.current_layout_block_handle();
let woff = self.world_offset;
let mut out = Vec::new();
for e in self.document.entities() {
if e.common().owner_handle != layout_block {
continue;
}
let EntityType::Insert(ins) = e else { continue };
if !self.mesh_entity_visible(ins.common.handle) {
continue;
}
let mut sets = Vec::new();
self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, woff, &mut sets);
let hit = sets.iter().any(|set| {
set.lods.first().map_or(false, |m| {
!pick::hit_test::mesh_box_hit(
a,
b,
crossing,
std::iter::once((ins.common.handle, m)),
view_proj,
bounds,
)
.is_empty()
})
});
if hit {
out.push(ins.common.handle);
}
}
out
}
/// Parent INSERT handles whose block-internal solid meshes fall in a lasso.
pub fn block_mesh_poly_hit(
&self,
poly: &[iced::Point],
crossing: bool,
view_proj: glam::Mat4,
bounds: iced::Rectangle,
) -> Vec<Handle> {
if self.block_meshes.is_empty() {
return Vec::new();
}
let layout_block = self.current_layout_block_handle();
let woff = self.world_offset;
let mut out = Vec::new();
for e in self.document.entities() {
if e.common().owner_handle != layout_block {
continue;
}
let EntityType::Insert(ins) = e else { continue };
if !self.mesh_entity_visible(ins.common.handle) {
continue;
}
let mut sets = Vec::new();
self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, woff, &mut sets);
let hit = sets.iter().any(|set| {
set.lods.first().map_or(false, |m| {
!pick::hit_test::mesh_poly_hit(
poly,
crossing,
std::iter::once((ins.common.handle, m)),
view_proj,
bounds,
)
.is_empty()
})
});
if hit {
out.push(ins.common.handle);
}
}
out
}
/// 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