// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged. use super::*; impl Scene { // ── Selection ───────────────────────────────────────────────────────── pub fn select_entity(&mut self, handle: Handle, exclusive: bool) { if exclusive { self.selected.clear(); } self.selected.insert(handle); self.bump_selection(); } pub fn deselect_all(&mut self) { self.selected.clear(); self.bump_selection(); } /// Remove a single entity from the selection (Shift+click subtractive pick). pub fn deselect_entity(&mut self, handle: Handle) { if self.selected.remove(&handle) { self.bump_selection(); } } pub fn selected_entities(&self) -> Vec<(Handle, &EntityType)> { self.selected .iter() .filter_map(|&h| self.document.get_entity(h).map(|e| (h, e))) .collect() } /// Iterates every entity owned by the current layout's block-record. /// Returns an empty vec when the block-record is missing or holds no /// entity handles (legacy DXF without group-code 330 — we err on the /// side of "no candidates" instead of scanning the whole document, so /// model-block entities don't leak into a paper-layout selection). fn current_layout_entity_handles(&self) -> Vec { let block = self.current_layout_block_handle(); self.document .block_records .iter() .find(|br| br.handle == block) .map(|br| br.entity_handles.clone()) .unwrap_or_default() } /// Extends the current selection with every entity in the active /// layout that matches one of the selected entities by `(variant, /// layer)`. The seed selection stays selected. No-op when nothing is /// selected. Returns the number of newly-added entities. pub fn select_similar(&mut self) -> usize { use crate::entities::traits::entity_type_name; if self.selected.is_empty() { return 0; } let pairs: rustc_hash::FxHashSet<(&'static str, String)> = self .selected .iter() .filter_map(|h| self.document.get_entity(*h)) .map(|e| (entity_type_name(e), e.as_entity().layer().to_string())) .collect(); let handles = self.current_layout_entity_handles(); let mut added = 0; for h in handles { if self.selected.contains(&h) { continue; } if let Some(e) = self.document.get_entity(h) { let key = (entity_type_name(e), e.as_entity().layer().to_string()); if pairs.contains(&key) { self.selected.insert(h); added += 1; } } } if added > 0 { self.bump_selection(); } added } /// Replace the selection with its complement: every selectable object /// in the active layout that isn't currently selected. The candidate /// set is the visible wire set, so objects on off/frozen layers (which /// can't be picked anyway) are excluded. Returns the new count. pub fn invert_selection(&mut self) -> usize { let prev: rustc_hash::FxHashSet = self.selected.iter().copied().collect(); let all: Vec = self .entity_wires() .iter() .filter_map(|w| Self::handle_from_wire_name(&w.name)) .collect(); self.selected.clear(); for h in all { if !prev.contains(&h) { self.selected.insert(h); } } self.bump_selection(); self.selected.len() } /// Replaces (or extends, when `append` is true) the current /// selection with every entity in the active layout that matches /// the filter. Returns the number of newly-matching entities. /// /// `type_name` of `None` means "any type". `property_field` of /// `None` skips the property test (only the type filter applies). /// The operator's `Any` variant also skips the property test. /// Numeric operators (`Gt` / `Lt`) parse both sides as `f64` and /// reject anything non-numeric. pub fn qselect( &mut self, type_name: Option<&str>, property_field: Option<&str>, op: crate::app::QSelectOp, value: &str, append: bool, ) -> usize { use crate::app::QSelectOp; use crate::entities::traits::entity_type_name; if !append { self.selected.clear(); } let handles = self.current_layout_entity_handles(); let mut matched = 0; for h in handles { let Some(e) = self.document.get_entity(h) else { continue; }; // Never quick-select objects on a locked layer. if self .document .layers .get(&e.common().layer) .map(|l| l.is_locked()) .unwrap_or(false) { continue; } if let Some(t) = type_name { if entity_type_name(e) != t { continue; } } let prop_ok = match (property_field, op) { (None, _) | (_, QSelectOp::Any) => true, (Some(field), op) => { let Some(actual) = self.entity_property_value(e, field) else { continue; }; match op { QSelectOp::Eq => actual.eq_ignore_ascii_case(value), QSelectOp::Neq => !actual.eq_ignore_ascii_case(value), QSelectOp::Gt | QSelectOp::Lt => { let (Ok(a), Ok(b)) = (actual.parse::(), value.parse::()) else { continue; }; if matches!(op, QSelectOp::Gt) { a > b } else { a < b } } QSelectOp::Any => true, } } }; if prop_ok { self.selected.insert(h); matched += 1; } } self.bump_selection(); matched } /// Returns the sorted set of entity-type names present in the active /// layout. Used to populate the Quick Select "Object type" dropdown /// with only the types that actually exist in the drawing. pub fn entity_type_names_in_layout(&self) -> Vec<&'static str> { use crate::entities::traits::entity_type_name; let mut names: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new(); for h in self.current_layout_entity_handles() { if let Some(e) = self.document.get_entity(h) { names.insert(entity_type_name(e)); } } 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: /// /// * Common properties (Layer, Color, Linetype, Lineweight) are /// always included. /// * When `type_name` names a specific entity type present in the /// active layout, the first entity of that type contributes its /// `geometry_properties()` rows (Start X, Length, Radius, …) so /// type-specific filtering works. pub fn qselect_properties( &self, type_name: Option<&str>, ) -> Vec<(String, String)> { use crate::entities::traits::{entity_type_name, EntityTypeOps}; let mut out: Vec<(String, String)> = vec![ ("layer".to_string(), "Layer".to_string()), ("color".to_string(), "Color".to_string()), ("linetype".to_string(), "Linetype".to_string()), ("lineweight".to_string(), "Lineweight".to_string()), ]; if let Some(t) = type_name { let text_style_names: Vec = self .document .text_styles .iter() .map(|s| s.name.clone()) .collect(); let sample = self .current_layout_entity_handles() .into_iter() .filter_map(|h| self.document.get_entity(h)) .find(|e| entity_type_name(e) == t); if let Some(sample) = sample { for section in sample.geometry_properties(&text_style_names) { for prop in section.props { // Skip rows that don't sensibly compare via // `entity_property_value` (read-only labels are // fine — users can still match against them). out.push((prop.field.to_string(), prop.label.clone())); } } } } out } /// Reads a property value from an entity for QSELECT comparison. /// Returns the canonical string used as the left-hand side of the /// operator test. Common properties have hand-rolled formatting so /// `"ByLayer"` / `"7"` / `"0.30mm"` are stable; everything else /// goes through `geometry_properties()` and pulls the matching /// row's value out. pub fn entity_property_value( &self, entity: &acadrust::EntityType, field: &str, ) -> Option { use crate::entities::traits::EntityTypeOps; use crate::scene::model::object::PropValue; match field { "layer" => Some(entity.common().layer.clone()), "color" => Some(Self::format_color(entity.common().color)), "linetype" => Some(entity.common().linetype.clone()), "lineweight" => Some(Self::format_lineweight(entity.common().line_weight)), _ => { let text_style_names: Vec = self .document .text_styles .iter() .map(|s| s.name.clone()) .collect(); let prop = entity .geometry_properties(&text_style_names) .into_iter() .flat_map(|s| s.props) .find(|p| p.field == field)?; Some(match prop.value { PropValue::ReadOnly(s) | PropValue::EditText(s) => s, PropValue::LayerChoice(s) => s, PropValue::Choice { selected, .. } => selected, PropValue::ColorChoice(c) => Self::format_color(c), PropValue::LwChoice(lw) => Self::format_lineweight(lw), PropValue::LinetypeChoice(s) => s, PropValue::HatchPatternChoice(s) => s, PropValue::BoolToggle { value, .. } => value.to_string(), PropValue::AttrText { value, .. } => value, PropValue::ColorVaries | PropValue::LwVaries => return None, }) } } } fn format_color(c: acadrust::types::Color) -> String { use acadrust::types::Color; match c { Color::ByLayer => "ByLayer".to_string(), Color::ByBlock => "ByBlock".to_string(), Color::Index(i) => i.to_string(), Color::Rgb { r, g, b } => format!("{},{},{}", r, g, b), } } fn format_lineweight(lw: acadrust::types::LineWeight) -> String { use acadrust::types::LineWeight; match lw { LineWeight::ByLayer => "ByLayer".to_string(), LineWeight::ByBlock => "ByBlock".to_string(), LineWeight::Default => "Default".to_string(), LineWeight::Value(v) => format!("{:.2}mm", v as f64 / 100.0), } } // ── Erase ───────────────────────────────────────────────────────────── pub fn erase_entities(&mut self, handles: &[Handle]) { for &h in handles { // Objects on a locked layer can't be erased. if self.is_layer_locked(h) { continue; } self.document.remove_entity(h); self.selected.remove(&h); self.hatches.remove(&h); self.meshes.remove(&h); self.solid_models.remove(&h); self.mark_entity_dirty(h); } // Remove erased handles from all groups; delete groups that become empty. let group_dict_handle = self.document.header.acad_group_dict_handle; let to_remove: Vec = self .document .objects .values_mut() .filter_map(|obj| match obj { ObjectType::Group(g) => { g.entities.retain(|h| !handles.contains(h)); if g.entities.is_empty() { Some(g.handle) } else { None } } _ => None, }) .collect(); for gh in &to_remove { if let Some(ObjectType::Dictionary(dict)) = self.document.objects.get_mut(&group_dict_handle) { dict.entries.retain(|(_, h)| h != gh); } self.document.objects.remove(gh); } // Deleting top-level entities/inserts leaves block definitions intact; // the erased handles were already dropped from the memo above. self.bump_geometry_no_blocks(); } }