From ae9940929c74c96d0fd127202da4aa7b0ae658f3 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 21 Jun 2026 07:03:56 +0300 Subject: [PATCH] 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 --- src/app/update.rs | 9 +++++ src/scene/mod.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/app/update.rs b/src/app/update.rs index cb3ebde16..7d58a122a 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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 diff --git a/src/scene/mod.rs b/src/scene/mod.rs index ebbc240ba..5a82349c6 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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 { + 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 { + 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 { // Default culling is driven by the live `Scene::camera`. Multi-tile