From 84620852fe9908e33c58a922793720ff602a0a14 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 11:01:51 +0300 Subject: [PATCH] feat(select): Previous / Last keywords at Select objects prompts Standard selection keywords, available typed or as prompt buttons in every gathering command (#426): P / PREVIOUS re-selects the set the last finishing command worked on (now captured before the working set is dropped), and L / LAST selects the most recently created object of the current space (highest handle among its selectable wires). Handled centrally in the step router, so the command line, the option buttons and the automation feeder share one implementation; deleted objects and locked layers are filtered out. Co-Authored-By: Claude Fable 5 --- src/app/command_driver.rs | 81 ++++++++++++++++++++++++++++++++++++++ src/app/document.rs | 5 +++ src/app/update/command.rs | 5 +++ src/modules/draw/select.rs | 14 ++++++- 4 files changed, 104 insertions(+), 1 deletion(-) 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.