diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 4289adbda..a56bbc601 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -10,6 +10,14 @@ impl OpenCADStudio { /// through, so the routing from input → `on_*` method → `apply_cmd_result` /// lives in exactly one place. No-op when no command is active. pub(super) fn feed_command(&mut self, input: StepInput) -> Task { + // Selection keywords (P / PREVIOUS, L / LAST) consume the token + // before it reaches the command (#426). + if let StepInput::Text(s) = &input { + let kw_input = s.clone(); + if let Some(task) = self.try_selection_keyword(&kw_input) { + return task; + } + } let i = self.active_tab; let ctrl = self.ctrl_down; let result: Option = { @@ -91,6 +99,75 @@ impl OpenCADStudio { /// coordinate is parsed (and, like the GUI command line, interpreted in the /// active UCS); anything else is an option keyword / value. Used by both the /// GUI command line and headless automation. + /// Standard selection keywords at a "Select objects:" prompt (#426): + /// P / PREVIOUS re-selects the set the last command worked on, and + /// L / LAST selects the most recently created object in the current + /// space. Returns `Some` when the token was consumed as a keyword. + /// Handled centrally so every gathering command gets them for free. + pub(super) fn try_selection_keyword(&mut self, text: &str) -> Option> { + let i = self.active_tab; + let gathering = self.tabs[i] + .active_cmd + .as_ref() + .map_or(false, |c| c.is_selection_gathering()); + if !gathering { + return None; + } + let kw = text.trim().to_ascii_uppercase(); + let add: Vec = match kw.as_str() { + "P" | "PREVIOUS" => self.tabs[i] + .prev_selection + .iter() + .copied() + .filter(|&h| { + self.tabs[i].scene.document.get_entity(h).is_some() + && !self.tabs[i].scene.is_layer_locked(h) + }) + .collect(), + // Highest handle among the selectable wires of the current space — + // handles are handed out monotonically, so that is the most + // recently created object. + "L" | "LAST" => self.tabs[i] + .scene + .entity_wires() + .iter() + .filter_map(|w| crate::scene::Scene::handle_from_wire_name(&w.name)) + .filter(|&h| !self.tabs[i].scene.is_layer_locked(h)) + .max_by_key(|h| h.value()) + .into_iter() + .collect(), + _ => return None, + }; + if add.is_empty() { + self.command_line.push_info(match kw.as_str() { + "P" | "PREVIOUS" => "No previous selection set.", + _ => "No last object.", + }); + return Some(Task::none()); + } + let count = add.len(); + for h in add { + self.tabs[i].scene.select_entity(h, false); + } + self.command_line + .push_info(&format!("{count} object(s) added to selection.")); + self.refresh_properties(); + let handles: Vec = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(h, _)| h) + .collect(); + let result = self.tabs[i] + .active_cmd + .as_mut() + .map(|cmd| cmd.on_selection_complete(handles)); + Some(match result { + Some(r) => self.apply_cmd_result(r), + None => Task::none(), + }) + } + pub(super) fn feed_active_cmd(&mut self, token: &str) { let i = self.active_tab; // Object-pick step: the token is a handle (as returned by `query`). @@ -164,6 +241,10 @@ impl OpenCADStudio { && self.tabs[i].active_cmd.is_none() && !self.tabs[i].scene.selected.is_empty() { + // Remember the working set for the "Previous" selection keyword + // (#426) before dropping it. + self.tabs[i].prev_selection = + self.tabs[i].scene.selected.iter().copied().collect(); self.tabs[i].scene.deselect_all(); self.refresh_properties(); } diff --git a/src/app/document.rs b/src/app/document.rs index 1696a3694..7de1e5311 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -160,6 +160,10 @@ pub(super) struct DocumentTab { pub(super) properties: PropertiesPanel, pub(super) layers: LayerPanel, pub(super) active_cmd: Option>, + /// The selection set the most recent command worked on, captured when a + /// finishing command drops the live selection — re-selectable with the + /// "Previous" keyword at any Select objects prompt (#426). + pub(super) prev_selection: Vec, pub(super) last_cmd: Option, pub(super) snap_result: Option, pub(super) active_grip: Option, @@ -411,6 +415,7 @@ impl DocumentTab { scene, current_path: None, dirty: false, + prev_selection: Vec::new(), tab_title: format!("Drawing{}", n), properties: PropertiesPanel::empty(), layers: LayerPanel::default(), diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 2f4742271..97ba54799 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -254,6 +254,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // text falls through to the Enter / coordinate handling // below, so a bare Enter still terminates and typed points // still work when dynamic input is off. See #97. + // Selection keywords (P / L) at a Select objects prompt + // consume the token before any other interpretation (#426). + if let Some(task) = self.try_selection_keyword(&text) { + return task; + } if self.tabs[i] .active_cmd .as_ref() diff --git a/src/modules/draw/select.rs b/src/modules/draw/select.rs index 8bf36d465..2764e6d41 100644 --- a/src/modules/draw/select.rs +++ b/src/modules/draw/select.rs @@ -11,7 +11,7 @@ use acadrust::Handle; use glam::DVec3; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdOption, CmdResult}; use crate::scene::model::wire_model::WireModel; pub struct SelectObjectsCommand { @@ -68,6 +68,18 @@ impl CadCommand for SelectObjectsCommand { true } + // Clickable selection keywords (#426): Previous re-selects the set the + // last command worked on, Last the most recently created object. The + // keywords are consumed centrally in `try_selection_keyword`, so they + // also work typed — these buttons just surface them. + fn options(&self) -> Vec { + if self.commit_on_enter { + vec![CmdOption::new("Previous", "P"), CmdOption::new("Last", "L")] + } 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.