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 `<name> <verb>
[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
<app> <value> and DRAWORDER Above/Under <handle> (a reference pick) are left
off the buttons; CHPROP/ADJUST/UNDERLAY are fully covered.

Verified end to end: SELECTALL then `CHPROP LAYER <name>` moves the selected
entity to that layer through the stepped front-end.
This commit is contained in:
Hakan Seven committed 2026-07-16 13:55:49 +03:00
1 parent 4c3509fa51
commit 7195e1e8fb
4 files changed
+198 -5

No files matched your search

+50 -3
View File
@@ -207,7 +207,19 @@ impl OpenCADStudio {
// XDATA SET <app> <str> — append a string xdata value for <app>
// XDATA CLEAR — remove all xdata from selected entities
// XDATA CLEAR <app> — 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 <app> <value> 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();
+19 -1
View File
@@ -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 <property> <value>
// Applies to currently selected entities.
// Properties: LAYER, COLOR, LINETYPE, LTSCALE
+13 -1
View File
@@ -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 <handle> 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();
+116
View File
@@ -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 `<name> <verb> [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<Handle>,
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<CmdOption> {
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<Handle>) -> 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<CmdResult> {
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.