From 7195e1e8fbc0bf0d53eecc2f183c6fa5c798a916 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 16 Jul 2026 13:55:49 +0300 Subject: [PATCH] feat(commands): step the selection-first keyword commands Add SelectThenKeywordCommand: if nothing is selected it first gathers a selection (Enter confirms, following the ALIGN contract), then shows the sub-verbs as buttons like KeywordCommand and dispatches ` [value]` to the inline handler, which reads the still-selected set. Convert CHPROP, ADJUST, XDATA, UNDERLAY and DRAWORDER. Verbs the single-value generic can't express keep working by typing the full line: XDATA SET and DRAWORDER Above/Under (a reference pick) are left off the buttons; CHPROP/ADJUST/UNDERLAY are fully covered. Verified end to end: SELECTALL then `CHPROP LAYER ` moves the selected entity to that layer through the stepped front-end. --- src/app/commands/display.rs | 53 ++++++++++++++- src/app/commands/styleprops.rs | 20 +++++- src/app/commands/view.rs | 14 +++- src/command.rs | 116 +++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 5 deletions(-) diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 2a52d0ad8..2ac2ba4a3 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -207,7 +207,19 @@ impl OpenCADStudio { // XDATA SET — append a string xdata value for // XDATA CLEAR — remove all xdata from selected entities // XDATA CLEAR — remove xdata for a specific application - cmd if cmd == "XDATA" || cmd.starts_with("XDATA ") => { + "XDATA" => { + use crate::command::SelectThenKeywordCommand; + let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); + let c = SelectThenKeywordCommand::new( + "XDATA", + "XDATA [List / Clear] (SET by typing):", + vec![("List", "LIST", None), ("Clear", "CLEAR", None)], + has_sel, + ); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("XDATA ") => { use acadrust::xdata::{ExtendedDataRecord, XDataValue}; let rest = cmd.trim_start_matches("XDATA").trim(); let parts: Vec<&str> = rest.splitn(3, char::is_whitespace).collect(); @@ -461,7 +473,26 @@ impl OpenCADStudio { // UNDERLAY ON | OFF // UNDERLAY CLIP ON | OFF // UNDERLAY MONO ON | OFF - cmd if cmd == "UNDERLAY" || cmd.starts_with("UNDERLAY ") => { + "UNDERLAY" => { + use crate::command::SelectThenKeywordCommand; + let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); + let c = SelectThenKeywordCommand::new( + "UNDERLAY", + "UNDERLAY [Fade / Contrast / On / Off / Mono / Clip]:", + vec![ + ("Fade", "FADE", Some("UNDERLAY fade 0-100:")), + ("Contrast", "CONTRAST", Some("UNDERLAY contrast 0-100:")), + ("On", "ON", None), + ("Off", "OFF", None), + ("Mono", "MONO", Some("UNDERLAY MONO [On / Off]:")), + ("Clip", "CLIP", Some("UNDERLAY CLIP [On / Off]:")), + ], + has_sel, + ); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("UNDERLAY ") => { let sub = cmd .split_once(' ') .map(|(_, r)| r.trim().to_uppercase()) @@ -641,7 +672,23 @@ impl OpenCADStudio { // ADJUST — set brightness / contrast / fade on selected raster images // ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> - cmd if cmd == "ADJUST" || cmd.starts_with("ADJUST ") => { + "ADJUST" => { + use crate::command::SelectThenKeywordCommand; + let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); + let c = SelectThenKeywordCommand::new( + "ADJUST", + "ADJUST [Brightness / Contrast / Fade]:", + vec![ + ("Brightness", "BRIGHTNESS", Some("ADJUST brightness 0-100:")), + ("Contrast", "CONTRAST", Some("ADJUST contrast 0-100:")), + ("Fade", "FADE", Some("ADJUST fade 0-100:")), + ], + has_sel, + ); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("ADJUST ") => { let rest = cmd.trim_start_matches("ADJUST").trim(); let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect(); let action = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index fc046b445..a10297556 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -334,7 +334,25 @@ impl OpenCADStudio { } // ── CHPROP — change entity properties from command line ─────── - cmd if cmd == "CHPROP" || cmd.starts_with("CHPROP ") => { + "CHPROP" => { + use crate::command::SelectThenKeywordCommand; + let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); + let c = SelectThenKeywordCommand::new( + "CHPROP", + "CHPROP property [Layer / Color / Linetype / LtScale / Transparency]:", + vec![ + ("Layer", "LAYER", Some("CHPROP new layer name:")), + ("Color", "COLOR", Some("CHPROP new colour (name / 1-255 / ByLayer):")), + ("Linetype", "LINETYPE", Some("CHPROP new linetype name:")), + ("LtScale", "LTSCALE", Some("CHPROP new linetype scale:")), + ("Transparency", "TRANSPARENCY", Some("CHPROP transparency 0-90:")), + ], + has_sel, + ); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("CHPROP ") => { // Usage: CHPROP // Applies to currently selected entities. // Properties: LAYER, COLOR, LINETYPE, LTSCALE diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index 40120c92f..da60aa083 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -750,7 +750,19 @@ impl OpenCADStudio { ); } - cmd if cmd.starts_with("DRAWORDER") => { + "DRAWORDER" => { + use crate::command::SelectThenKeywordCommand; + let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); + let c = SelectThenKeywordCommand::new( + "DRAWORDER", + "DRAWORDER [Front / Back] (Above/Under by typing):", + vec![("Front", "FRONT", None), ("Back", "BACK", None)], + has_sel, + ); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("DRAWORDER ") => { use acadrust::objects::{ObjectType, SortEntitiesTable}; let parts: Vec<&str> = cmd.split_whitespace().collect(); let option = parts.get(1).unwrap_or(&"").to_uppercase(); diff --git a/src/command.rs b/src/command.rs index 74047d8d7..f879d738e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -479,6 +479,122 @@ impl CadCommand for TwoValuePromptCommand { } } +/// Generic interactive front-end for a keyword command that operates on the +/// current selection (CHPROP, ADJUST, XDATA, UNDERLAY, DRAWORDER…). If nothing +/// is selected when it starts it first gathers a selection (Enter confirms), +/// then shows the sub-verbs as buttons exactly like [`KeywordCommand`] and +/// dispatches ` [value]` to the inline handler, which reads the +/// (still-selected) set. Verbs the generic form can't express — a second value +/// (XDATA SET) or a reference pick (DRAWORDER ABOVE) — stay available by typing +/// the full argument line. +pub struct SelectThenKeywordCommand { + name: &'static str, + prompt: &'static str, + options: Vec<(&'static str, &'static str, Option<&'static str>)>, + gathering: bool, + selected: Vec, + pending: Option<(&'static str, &'static str)>, +} + +impl SelectThenKeywordCommand { + pub fn new( + name: &'static str, + prompt: &'static str, + options: Vec<(&'static str, &'static str, Option<&'static str>)>, + has_selection: bool, + ) -> Self { + Self { + name, + prompt, + options, + gathering: !has_selection, + selected: Vec::new(), + pending: None, + } + } +} + +impl CadCommand for SelectThenKeywordCommand { + fn name(&self) -> &'static str { + self.name + } + + fn prompt(&self) -> String { + if self.gathering { + return format!("{} select objects, then press Enter:", self.name); + } + match self.pending { + Some((_, value_prompt)) => value_prompt.to_string(), + None => self.prompt.to_string(), + } + } + + fn options(&self) -> Vec { + if self.gathering || self.pending.is_some() { + return Vec::new(); + } + self.options + .iter() + .map(|(label, keyword, _)| CmdOption::new(label, keyword)) + .collect() + } + + fn wants_text_input(&self) -> bool { + !self.gathering + } + + fn is_selection_gathering(&self) -> bool { + self.gathering + } + + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + // The normal selection system has set the scene selection; remember the + // set so Enter knows whether anything was picked, and keep gathering. + self.selected = handles; + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + if self.gathering { + if self.selected.is_empty() { + return CmdResult::Cancel; + } + self.gathering = false; + return CmdResult::NeedPoint; + } + CmdResult::Cancel + } + + fn on_text_input(&mut self, text: &str) -> Option { + let t = text.trim(); + if t.is_empty() { + return None; + } + match self.pending { + Some((keyword, _)) => Some(CmdResult::Dispatch(format!("{} {keyword} {t}", self.name))), + None => { + let up = t.to_uppercase(); + let Some((_, keyword, value_prompt)) = + self.options.iter().find(|(_, k, _)| k.eq_ignore_ascii_case(&up)) + else { + return None; + }; + match value_prompt { + Some(vp) => { + self.pending = Some((keyword, vp)); + None + } + None => Some(CmdResult::Dispatch(format!("{} {keyword}", self.name))), + } + } + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } +} + // ── Result token ────────────────────────────────────────────────────────── /// Returned by every `CadCommand` method to tell main.rs what to do.