feat(statusbar): selection cycling for overlapping objects (SC pill)

Add a click_hits_all hit-test and an SC pill that, when on, makes
repeated clicks at the same spot step through the overlapping objects
there (nearest first) instead of accumulating the selection. The whole
cycling path is gated behind the toggle, so default picking is unchanged
when it is off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-04 23:44:39 +03:00
1 parent f7eac09b55
commit ac042c5dbf
6 files changed
+105

No files matched your search

+9
View File
@@ -183,6 +183,11 @@ pub(super) struct OpenCADStudio {
clean_screen: bool,
/// Quick Properties: show a compact floating property panel on selection.
quick_properties: bool,
/// Selection cycling: repeated clicks at one spot step through the
/// overlapping objects there instead of accumulating the selection.
selection_cycling: bool,
/// Cycling cursor: (screen point, ordered candidates, current index).
cycle_state: Option<(iced::Point, Vec<acadrust::Handle>, usize)>,
/// Which status-bar pills the user has chosen to show (persisted).
statusbar_config: crate::ui::statusbar_config::StatusBarConfig,
/// Whether Tangent snap was enabled before a tangent-pick command started.
@@ -758,6 +763,8 @@ pub enum Message {
ToggleTransparencyDisplay,
/// Toggle the Quick Properties floating panel.
ToggleQuickProperties,
/// Toggle selection cycling for overlapping objects.
ToggleSelectionCycling,
/// Toggle the selection-filter type picker open/closed.
ToggleSelectionFilterPopup,
/// Close the selection-filter type picker.
@@ -1237,6 +1244,8 @@ impl OpenCADStudio {
statusbar_config: crate::ui::statusbar_config::StatusBarConfig::load(),
clean_screen: false,
quick_properties: false,
selection_cycling: false,
cycle_state: None,
pre_cmd_tangent: None,
ortho_mode: false,
polar_mode: false,
+46
View File
@@ -2909,6 +2909,46 @@ impl OpenCADStudio {
if box_anchor.is_none() {
let all_wires = self.tabs[i].scene.hit_test_wires();
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
// Selection cycling: step through overlapping objects
// under the cursor on repeated clicks at the same spot.
// Gated behind the toggle, so default picking is unchanged.
let mut handled_by_cycling = false;
if self.selection_cycling {
let cands: Vec<Handle> = scene::hit_test::click_hits_all(
p,
&all_wires[..],
vp_mat,
bounds,
)
.into_iter()
.filter_map(|s| Scene::handle_from_wire_name(s))
.filter(|&h| self.tabs[i].scene.passes_selection_filter(h))
.collect();
if !cands.is_empty() {
let same =
self.cycle_state.as_ref().map_or(false, |(pt, list, _)| {
(pt.x - p.x).abs() < 3.0
&& (pt.y - p.y).abs() < 3.0
&& *list == cands
});
let idx = if same {
(self.cycle_state.as_ref().unwrap().2 + 1) % cands.len()
} else {
0
};
let handle = cands[idx];
self.cycle_state = Some((p, cands, idx));
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].scene.expand_selection_for_groups(&[handle]);
self.refresh_properties();
selection_just_completed = true;
handled_by_cycling = true;
}
}
if !handled_by_cycling {
let hit = scene::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -2961,6 +3001,7 @@ impl OpenCADStudio {
sel.box_current = Some(p_full);
sel.box_crossing = false;
}
}
} else {
let a = box_anchor.unwrap();
let crossing = box_crossing;
@@ -3601,6 +3642,11 @@ impl OpenCADStudio {
self.quick_properties ^= true;
Task::none()
}
Message::ToggleSelectionCycling => {
self.selection_cycling ^= true;
self.cycle_state = None;
Task::none()
}
Message::ToggleSelectionFilterPopup => {
self.selection_filter_popup_open ^= true;
Task::none()
+1
View File
@@ -1174,6 +1174,7 @@ impl OpenCADStudio {
tab.scene.transparency_display,
self.quick_properties,
tab.scene.selection_filter_active(),
self.selection_cycling,
&self.statusbar_config,
)
})