// SelectObjectsCommand — generic "select objects then run command" gather phase. // // Used when a modify command is invoked with nothing pre-selected. // The user may single-click, box-select, or polygon-select any number of objects. // Picks accumulate into the selection (Shift removes); the set is applied when // the user presses Enter or right-clicks (standard "Select objects:" behaviour). // // Single-object commands (e.g. LAYMCUR) use `instant()` instead: the first // completed selection action fires straight away, no Enter required. use acadrust::Handle; use glam::DVec3; use crate::t; use crate::command::{CadCommand, CmdOption, CmdResult}; use crate::modules::draw::fence::FencePick; use crate::scene::model::wire_model::WireModel; pub struct SelectObjectsCommand { prompt_cmd: String, pending_cmd: String, /// Selection accumulated so far (kept in sync with the scene selection by /// each `on_selection_complete` call). Applied on Enter when `commit_on_enter`. handles: Vec, /// When true, gathering continues until Enter / right-click commits the set. /// When false, the first completed selection action fires immediately. commit_on_enter: bool, /// Whether the generic selection methods are shown as command buttons. show_options: bool, /// A Fence / WPolygon / CPolygon path being picked point by point. While /// one is under way the host must send clicks here as points rather than /// treating them as picks, which is what `is_selection_gathering` reports. pick: Option, /// Whether the polygon being picked takes what it merely touches. Ignored /// by a fence, which has no inside to speak of. pick_crossing: bool, /// Add the command-specific Settings keyword used by Auto Constrain. auto_constrain_settings: bool, } impl SelectObjectsCommand { /// Standard selection set: accumulate picks, apply on Enter / right-click. pub fn new(pending_cmd: &str) -> Self { Self { prompt_cmd: pending_cmd.to_string(), pending_cmd: pending_cmd.to_string(), handles: Vec::new(), commit_on_enter: true, show_options: true, pick: None, pick_crossing: true, auto_constrain_settings: false, } } /// Plain gather prompt with no generic selection-method buttons. The /// visible command name may differ from the private apply command. pub fn plain(prompt_cmd: &str, pending_cmd: &str) -> Self { Self { prompt_cmd: prompt_cmd.to_string(), pending_cmd: pending_cmd.to_string(), handles: Vec::new(), commit_on_enter: true, show_options: false, pick: None, pick_crossing: true, auto_constrain_settings: false, } } /// Single-object variant: the first completed selection action applies /// immediately, with no Enter (used by commands that act on one object). pub fn instant(pending_cmd: &str) -> Self { Self { prompt_cmd: pending_cmd.to_string(), pending_cmd: pending_cmd.to_string(), handles: Vec::new(), commit_on_enter: false, show_options: false, pick: None, pick_crossing: true, auto_constrain_settings: false, } } pub fn auto_constrain(pending_cmd: &str) -> Self { let mut command = Self::new(pending_cmd); command.auto_constrain_settings = true; command } } impl CadCommand for SelectObjectsCommand { fn name(&self) -> &'static str { "SELECT" } fn prompt(&self) -> String { if let Some(pick) = &self.pick { let label = if !pick.closed { t!("Fence") } else if self.pick_crossing { t!("Crossing polygon") } else { t!("Window polygon") }; return t!( "%{mode}: pick points (%{count} placed, Enter to apply):", mode = label, count = pick.len() ) .into_owned(); } if self.commit_on_enter && !self.handles.is_empty() { if self.auto_constrain_settings { return t!( "%{cmd} Select objects or [Settings] (%{count} selected, Enter to apply):", cmd = self.prompt_cmd, count = self.handles.len() ) .into_owned(); } t!( "%{cmd} Select objects (%{count} selected, Enter to apply):", cmd = self.prompt_cmd, count = self.handles.len() ) .into_owned() } else { if self.auto_constrain_settings { return t!( "%{cmd} Select objects or [Settings]:", cmd = self.prompt_cmd ) .into_owned(); } t!("%{cmd} Select objects:", cmd = self.prompt_cmd).into_owned() } } // Opt into the selection-gathering path; host routes clicks through // the normal selection system and calls on_selection_complete after each action. fn is_selection_gathering(&self) -> bool { // A path being picked needs the clicks as points; handing them to the // selection system instead would pick objects under each vertex. self.pick.is_none() } // Clickable selection keywords (#426, #596). Window and Crossing fix the // sense of the next box, which dragging would otherwise take from the // direction the corner travels; All takes the whole space; Add and Remove // decide what a pick does for the rest of the selection; Previous // re-selects the set the last command worked on, Last the most recently // created object. All are consumed centrally in `try_selection_keyword`, so // they work typed as well — these buttons only surface them, which is what // the on-screen keyboard-less case needs. fn options(&self) -> Vec { if self.commit_on_enter && self.show_options { if self.auto_constrain_settings { return vec![CmdOption::new(t!("Settings").as_ref(), "S")]; } let options = vec![ CmdOption::new(t!("Window").as_ref(), "W"), CmdOption::new(t!("Crossing").as_ref(), "C"), CmdOption::new(t!("Fence").as_ref(), "F"), CmdOption::new(t!("WPolygon").as_ref(), "WP"), CmdOption::new(t!("CPolygon").as_ref(), "CP"), CmdOption::new(t!("All").as_ref(), "ALL"), CmdOption::new(t!("Add").as_ref(), "A"), CmdOption::new(t!("Remove").as_ref(), "R"), CmdOption::new(t!("Previous").as_ref(), "P"), CmdOption::new(t!("Last").as_ref(), "L"), ]; options } else { Vec::new() } } fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { if self.commit_on_enter { // Keep gathering: remember the running set and update the prompt. // The command is applied when the user presses Enter / right-clicks. self.handles = handles; return CmdResult::NeedPoint; } // Single-object variant: apply on the first non-empty selection. if handles.is_empty() { return CmdResult::NeedPoint; } CmdResult::Relaunch(std::mem::take(&mut self.pending_cmd), handles) } fn on_point(&mut self, pt: DVec3) -> CmdResult { if let Some(pick) = self.pick.as_mut() { pick.push([pt.x, pt.y]); } CmdResult::NeedPoint } fn wants_text_input(&self) -> bool { true } // Fence and the two polygons are picked here rather than centrally: they // need somewhere to accumulate points, and the command is what outlives the // individual clicks. The rest of the keywords act at once and are consumed // in `try_selection_keyword`, which sees the token first. (#596) fn on_text_input(&mut self, text: &str) -> Option { if !self.commit_on_enter { return None; } match text.trim().to_uppercase().as_str() { "S" | "SETTINGS" if self.auto_constrain_settings => { Some(CmdResult::OpenAutoConstrainSettings) } "F" | "FENCE" => { self.pick = Some(FencePick::fence()); Some(CmdResult::NeedPoint) } "CP" | "CPOLYGON" => { self.pick = Some(FencePick::polygon()); self.pick_crossing = true; Some(CmdResult::NeedPoint) } "WP" | "WPOLYGON" => { self.pick = Some(FencePick::polygon()); self.pick_crossing = false; Some(CmdResult::NeedPoint) } _ => None, } } fn on_preview_wires(&mut self, pt: DVec3) -> Vec { match &self.pick { Some(pick) if !pick.is_empty() => { vec![pick.preview([pt.x, pt.y], "select_fence")] } _ => vec![], } } // Enter / right-click ends the gather phase and fires the pending command // with the accumulated selection. Nothing selected → cancel. fn on_enter(&mut self) -> CmdResult { // Finishing a path applies it and returns to picking objects; the set // it produced joins whatever was already gathered. if let Some(pick) = self.pick.take() { if pick.is_usable() { return CmdResult::SelectByPath { path: pick.path(), closed: pick.closed, crossing: self.pick_crossing, }; } return CmdResult::NeedPoint; } if !self.commit_on_enter || self.handles.is_empty() { return CmdResult::Cancel; } CmdResult::Relaunch( std::mem::take(&mut self.pending_cmd), std::mem::take(&mut self.handles), ) } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } fn on_hover_entity(&mut self, _handle: Handle, _pt: DVec3) -> Vec { vec![] } } // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["ARRAY", "ARRAYPATH", "ARRAYPOLAR", "ARRAYRECT", "BLOCK", "COPY", "COPYCLIP", "CUTCLIP", "ERASE", "EXPLODE", "GROUP", "LAYFRZ", "LAYLCK", "LAYMCUR", "LAYOFF", "LAYULK", "MIRROR", "MOVE", "ROTATE", "SCALE", "STRETCH"] }); // SelectObjectsCommand