diff --git a/src/app/mod.rs b/src/app/mod.rs index 2e53527e0..12bb6eded 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -177,6 +177,8 @@ pub(super) struct OpenCADStudio { units_popup_open: bool, /// True while the Isolate pill's action menu is open. isolate_popup_open: bool, + /// True while the selection-filter type picker is open. + selection_filter_popup_open: bool, /// Clean-screen mode: hide ribbon and side panels for a full canvas. clean_screen: bool, /// Quick Properties: show a compact floating property panel on selection. @@ -756,6 +758,12 @@ pub enum Message { ToggleTransparencyDisplay, /// Toggle the Quick Properties floating panel. ToggleQuickProperties, + /// Toggle the selection-filter type picker open/closed. + ToggleSelectionFilterPopup, + /// Close the selection-filter type picker. + CloseSelectionFilterPopup, + /// Include/exclude an entity type from interactive selection. + ToggleSelectionFilterType(String), /// Toggle the drawing-units picker open/closed. ToggleUnitsPopup, /// Close the drawing-units picker. @@ -1225,6 +1233,7 @@ impl OpenCADStudio { statusbar_menu_open: false, units_popup_open: false, isolate_popup_open: false, + selection_filter_popup_open: false, statusbar_config: crate::ui::statusbar_config::StatusBarConfig::load(), clean_screen: false, quick_properties: false, diff --git a/src/app/update.rs b/src/app/update.rs index ec8ed98bb..2b8709e1e 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2889,6 +2889,8 @@ impl OpenCADStudio { vp_mat, bounds, )); + // Selection filter: keep only allowed types. + handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h)); self.tabs[i].scene.deselect_all(); for h in &handles { self.tabs[i].scene.select_entity(*h, false); @@ -2932,6 +2934,9 @@ impl OpenCADStudio { // body, not just the thin projected edges. self.tabs[i].scene.mesh_click_hit(p, vp_mat, bounds) }); + // Selection filter: drop a pick whose type is excluded. + let hit = + hit.filter(|&h| self.tabs[i].scene.passes_selection_filter(h)); if let Some(handle) = hit { // Individual picks accumulate (issue #47): // each plain click adds to the selection, @@ -2980,6 +2985,8 @@ impl OpenCADStudio { vp_mat, bounds, )); + // Selection filter: keep only allowed types. + handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h)); self.tabs[i].scene.deselect_all(); for h in &handles { self.tabs[i].scene.select_entity(*h, false); @@ -3594,6 +3601,21 @@ impl OpenCADStudio { self.quick_properties ^= true; Task::none() } + Message::ToggleSelectionFilterPopup => { + self.selection_filter_popup_open ^= true; + Task::none() + } + Message::CloseSelectionFilterPopup => { + self.selection_filter_popup_open = false; + Task::none() + } + Message::ToggleSelectionFilterType(name) => { + let f = &mut self.tabs[self.active_tab].scene.selection_filter; + if !f.remove(&name) { + f.insert(name); + } + Task::none() + } Message::ToggleUnitsPopup => { self.units_popup_open ^= true; Task::none() diff --git a/src/app/view.rs b/src/app/view.rs index e41b6ffe4..74b6df313 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -1173,6 +1173,7 @@ impl OpenCADStudio { tab.scene.is_isolation_active(), tab.scene.transparency_display, self.quick_properties, + tab.scene.selection_filter_active(), &self.statusbar_config, ) }) @@ -1232,6 +1233,21 @@ impl OpenCADStudio { iced::widget::Space::new().width(0).height(0).into() }; + let sel_filter_layer: Element<'_, Message> = if self.selection_filter_popup_open { + let types: Vec = tab + .scene + .entity_type_names_in_layout() + .into_iter() + .map(|s| s.to_string()) + .collect(); + crate::ui::selection_filter_popup::selection_filter_popup_overlay( + types, + &tab.scene.selection_filter, + ) + } else { + iced::widget::Space::new().width(0).height(0).into() + }; + let dropdown_layer: Element<'_, Message> = self .ribbon .dropdown_overlay( @@ -1270,6 +1286,7 @@ impl OpenCADStudio { statusbar_menu_layer, units_layer, isolate_layer, + sel_filter_layer, dropdown_layer, layout_ctx_layer, qselect_layer, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0060082bd..b057098f5 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -556,6 +556,9 @@ pub struct Scene { /// Whether entity transparency is honoured on screen. When false the /// wire shader forces every line opaque (a uniform toggle, no retessellate). pub transparency_display: bool, + /// Selection filter: entity-type names excluded from interactive picking. + /// Empty = every type is selectable. + pub selection_filter: HashSet, /// In-progress preview wires while a command is active (rubber-band + object ghosts). pub preview_wires: Vec, /// Committed-segment wire drawn during multi-point commands (normal colour). @@ -702,6 +705,7 @@ impl Scene { selected: HashSet::new(), hidden: HashSet::new(), transparency_display: true, + selection_filter: HashSet::new(), preview_wires: vec![], interim_wire: None, camera_generation: 0, @@ -4267,6 +4271,25 @@ impl Scene { names.into_iter().collect() } + /// True when `handle`'s entity type is allowed by the selection filter. + /// The filter stores excluded type names; empty = everything allowed. + pub fn passes_selection_filter(&self, handle: Handle) -> bool { + if self.selection_filter.is_empty() { + return true; + } + match self.document.get_entity(handle) { + Some(e) => !self + .selection_filter + .contains(crate::entities::traits::entity_type_name(e)), + None => true, + } + } + + /// True when the selection filter is excluding at least one type. + pub fn selection_filter_active(&self) -> bool { + !self.selection_filter.is_empty() + } + /// Returns the list of `(field, label)` pairs the Quick Select /// "Properties" dropdown should show given the current type filter: /// diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1d3aeadcc..2e6a27973 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -19,6 +19,7 @@ pub mod plotstyle; pub mod properties; pub mod ribbon; pub mod scale_popup; +pub mod selection_filter_popup; pub mod shortcuts; pub mod snap_popup; pub mod statusbar; diff --git a/src/ui/selection_filter_popup.rs b/src/ui/selection_filter_popup.rs new file mode 100644 index 000000000..91e449d09 --- /dev/null +++ b/src/ui/selection_filter_popup.rs @@ -0,0 +1,131 @@ +//! Selection-filter type picker — choose which entity types are selectable. +//! A checked row means that type can be picked; unchecking it excludes the +//! type from interactive selection. Opened from the FILTER status pill. + +use std::collections::HashSet; + +use iced::widget::{button, column, container, mouse_area, row, text}; +use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; + +use crate::app::Message; + +/// Full-screen overlay: transparent click-catcher + type list pinned +/// bottom-right, above the status bar. +/// +/// - `types`: entity-type names present in the current layout. +/// - `excluded`: types currently filtered out (unchecked). +pub fn selection_filter_popup_overlay( + types: Vec, + excluded: &HashSet, +) -> Element<'static, Message> { + let rows: Vec> = if types.is_empty() { + vec![empty_row()] + } else { + types + .into_iter() + .map(|name| { + let included = !excluded.contains(&name); + type_row(name, included) + }) + .collect() + }; + + let panel = container(column(rows)) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(PANEL_BG)), + border: Border { + color: PANEL_BORDER, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }) + .width(Length::Fixed(160.0)); + + let positioned = container(panel) + .align_right(Fill) + .align_bottom(Fill) + .padding(Padding { + bottom: 27.0, + right: 4.0, + top: 0.0, + left: 0.0, + }) + .width(Fill) + .height(Fill); + + mouse_area(positioned) + .on_press(Message::CloseSelectionFilterPopup) + .into() +} + +fn type_row(name: String, included: bool) -> Element<'static, Message> { + let check = text(if included { "✓" } else { " " }) + .size(11) + .color(if included { CHECK_COLOR } else { Color::TRANSPARENT }) + .width(Length::Fixed(14.0)); + + let lbl = text(name.clone()) + .size(11) + .color(if included { LABEL_ON } else { LABEL_OFF }); + + let content = row![check, lbl].spacing(6).align_y(iced::Center); + + button(content) + .on_press(Message::ToggleSelectionFilterType(name)) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered => ROW_HOVER, + _ => Color::TRANSPARENT, + })), + ..Default::default() + }) + .width(Fill) + .padding([4, 10]) + .into() +} + +fn empty_row() -> Element<'static, Message> { + container(text("No objects").size(11).color(LABEL_OFF)) + .padding([4, 10]) + .into() +} + +// ── Colours ─────────────────────────────────────────────────────────────── + +const PANEL_BG: Color = Color { + r: 0.15, + g: 0.15, + b: 0.15, + a: 1.0, +}; +const PANEL_BORDER: Color = Color { + r: 0.32, + g: 0.32, + b: 0.32, + a: 1.0, +}; +const ROW_HOVER: Color = Color { + r: 0.22, + g: 0.22, + b: 0.22, + a: 1.0, +}; +const CHECK_COLOR: Color = Color { + r: 0.35, + g: 0.75, + b: 1.00, + a: 1.0, +}; +const LABEL_ON: Color = Color { + r: 0.92, + g: 0.92, + b: 0.92, + a: 1.0, +}; +const LABEL_OFF: Color = Color { + r: 0.6, + g: 0.6, + b: 0.6, + a: 1.0, +}; diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index f4d45c47e..f5d27043a 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -65,6 +65,8 @@ impl StatusBar { transparency_display: bool, // Whether the Quick Properties floating panel is enabled. quick_properties: bool, + // True when the selection filter is excluding at least one type. + selection_filter_active: bool, // Which pills the user has chosen to show on the bar. config: &'a StatusBarConfig, ) -> Element<'a, Message> { @@ -201,6 +203,16 @@ impl StatusBar { "Quick Properties\nFloating panel on selection", )); } + if vis(StatusPill::SelFilter) { + right_status = right_status.push(tip( + toggle_pill( + "FILTER", + selection_filter_active, + Message::ToggleSelectionFilterPopup, + ), + "Selection Filtering\nLimit which object types can be picked", + )); + } if vis(StatusPill::Vp) && !vp_label.is_empty() { right_status = right_status.push(tip( status_pill(vp_label).into(), diff --git a/src/ui/statusbar_config.rs b/src/ui/statusbar_config.rs index e183147bc..ddd780ed2 100644 --- a/src/ui/statusbar_config.rs +++ b/src/ui/statusbar_config.rs @@ -26,6 +26,7 @@ pub enum StatusPill { Transparency, Isolate, QuickProps, + SelFilter, Vp, CleanScreen, } @@ -49,6 +50,7 @@ impl StatusPill { StatusPill::Transparency, StatusPill::Isolate, StatusPill::QuickProps, + StatusPill::SelFilter, StatusPill::Vp, StatusPill::CleanScreen, ]; @@ -71,6 +73,7 @@ impl StatusPill { StatusPill::Transparency => "transparency", StatusPill::Isolate => "isolate", StatusPill::QuickProps => "quickprops", + StatusPill::SelFilter => "selfilter", StatusPill::Vp => "vp", StatusPill::CleanScreen => "cleanscreen", } @@ -94,6 +97,7 @@ impl StatusPill { StatusPill::Transparency => "Show Transparency", StatusPill::Isolate => "Isolate Objects", StatusPill::QuickProps => "Quick Properties", + StatusPill::SelFilter => "Selection Filtering", StatusPill::Vp => "Viewport Count", StatusPill::CleanScreen => "Clean Screen", }