feat(commands): step the rest of the keyword/value commands

Convert the remaining bucket-B commands to prompt step by step instead of
requiring arguments or printing usage:

- KeywordCommand (buttons): VIEW, LINETYPE, SCALELISTEDIT, UCSICON, VSCURRENT.
- ValuePromptCommand (one value): LAYDEL (layer), ATTSYNC (block).
- TwoValuePromptCommand (new; two values): LAYMRG (source + target),
  SETVAR (variable + value, blank second reads).

Each bare form now starts the stepped front-end and delegates to the existing
inline handler; the argument form is kept and is the dispatch target. The
direct visual-style shortcuts (HIDDENLINE / XRAY / …) and UCSICON's argument
form are untouched.

COUNT is intentionally left alone: bare COUNT already reports the tally (a
useful inquiry action, not a usage line), so prompting would only add friction.
This commit is contained in:
Hakan Seven committed 2026-07-16 13:29:04 +03:00
1 parent 4fa6c49445
commit 4c3509fa51
7 files changed
+186 -16

No files matched your search

+7 -1
View File
@@ -324,7 +324,13 @@ impl OpenCADStudio {
// block's current attribute definitions: drop attributes whose tag
// no longer exists and add any newly-defined ones (keeping the values
// of attributes that remain).
cmd if cmd == "ATTSYNC" || cmd.starts_with("ATTSYNC ") => {
"ATTSYNC" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("ATTSYNC", "ATTSYNC block name to sync:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("ATTSYNC ") => {
let arg = cmd.trim_start_matches("ATTSYNC").trim();
let blocks = self.tabs[i].scene.custom_block_names();
if arg.is_empty() {
+30 -2
View File
@@ -72,7 +72,22 @@ impl OpenCADStudio {
// UCSICON OFF — hide UCS icon in all viewports
// UCSICON NOORIGIN — show icon but not at origin (show at corner)
// UCSICON ORIGIN — show icon at UCS origin
cmd if cmd == "UCSICON" || cmd.starts_with("UCSICON ") => {
"UCSICON" => {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"UCSICON",
"UCSICON [On / Off / NoOrigin / Origin]:",
vec![
("On", "ON", None),
("Off", "OFF", None),
("NoOrigin", "NOORIGIN", None),
("Origin", "ORIGIN", None),
],
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("UCSICON ") => {
let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
match sub.as_str() {
"ON" | "OFF" | "NOORIGIN" | "ORIGIN" => {
@@ -746,7 +761,20 @@ impl OpenCADStudio {
// SCALELISTEDIT list
// SCALELISTEDIT ADD 1:50 add (name is a paper:drawing ratio)
// SCALELISTEDIT DELETE 1:50 remove (not the current scale)
cmd if cmd == "SCALELISTEDIT" || cmd.starts_with("SCALELISTEDIT ") => {
"SCALELISTEDIT" => {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"SCALELISTEDIT",
"SCALELISTEDIT [Add / Delete]:",
vec![
("Add", "ADD", Some("SCALELISTEDIT ADD new scale ratio (e.g. 1:50):")),
("Delete", "DELETE", Some("SCALELISTEDIT DELETE scale ratio to remove:")),
],
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("SCALELISTEDIT ") => {
let rest = cmd.trim_start_matches("SCALELISTEDIT").trim();
let mut parts = rest.splitn(2, char::is_whitespace);
let sub = parts.next().unwrap_or("").to_uppercase();
+20 -4
View File
@@ -98,10 +98,26 @@ impl OpenCADStudio {
// chosen style is reported so the mapping is explicit. (`SOLID` is
// intentionally NOT a visual-style verb — it is the 2D filled-polygon
// draw command; the shaded ribbon button drives `SetWireframe`.)
cmd if cmd == "VS"
|| cmd == "VSCURRENT"
|| cmd == "SHADEMODE"
|| cmd == "HIDDENLINE"
"VS" | "VSCURRENT" | "SHADEMODE" => {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"VSCURRENT",
"VSCURRENT visual style [Shaded / Wireframe / Hidden / Realistic / Conceptual / X-ray]:",
vec![
("Shaded", "SHADED", None),
("Wireframe", "WIREFRAME", None),
("Hidden", "HIDDEN", None),
("Realistic", "REALISTIC", None),
("Conceptual", "CONCEPTUAL", None),
("X-Ray", "XRAY", None),
],
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
// The named visual-style shortcuts still switch directly, and the
// `<name> <style>` argument form (also what the picker dispatches).
cmd if cmd == "HIDDENLINE"
|| cmd == "XRAY"
|| cmd == "REALISTIC"
|| cmd == "CONCEPTUAL"
+16 -1
View File
@@ -400,7 +400,22 @@ impl OpenCADStudio {
)));
}
cmd if cmd == "VIEW" || cmd.starts_with("VIEW ") => {
"VIEW" => {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"VIEW",
"VIEW [List / Save / Restore / Delete]:",
vec![
("List", "LIST", None),
("Save", "SAVE", Some("VIEW SAVE new view name:")),
("Restore", "RESTORE", Some("VIEW RESTORE view name:")),
("Delete", "DELETE", Some("VIEW DELETE view name:")),
],
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("VIEW ") => {
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
match sub.as_str() {
+18 -2
View File
@@ -75,7 +75,13 @@ impl OpenCADStudio {
}
// LAYDEL <name> — delete a layer and erase the objects on it.
cmd if cmd == "LAYDEL" || cmd.starts_with("LAYDEL ") => {
"LAYDEL" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("LAYDEL", "LAYDEL layer to delete:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("LAYDEL ") => {
let name = cmd.trim_start_matches("LAYDEL").trim();
if name.is_empty() {
self.command_line.push_info("Usage: LAYDEL <layer name>");
@@ -127,7 +133,17 @@ impl OpenCADStudio {
// LAYMRG <source> <target> — move every object from <source> onto
// <target>, then delete the emptied <source> layer.
cmd if cmd == "LAYMRG" || cmd.starts_with("LAYMRG ") => {
"LAYMRG" => {
use crate::command::TwoValuePromptCommand;
let c = TwoValuePromptCommand::new(
"LAYMRG",
"LAYMRG source layer (merged away):",
"LAYMRG target layer (kept):",
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("LAYMRG ") => {
let rest = cmd.trim_start_matches("LAYMRG").trim();
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() != 2 {
+25 -6
View File
@@ -69,11 +69,20 @@ impl OpenCADStudio {
}
// ── LINETYPE management ───────────────────────────────────────
cmd if cmd == "LINETYPE"
|| cmd == "LT"
|| cmd.starts_with("LINETYPE ")
|| cmd.starts_with("LT ") =>
{
"LINETYPE" | "LT" => {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"LINETYPE",
"LINETYPE [List / Set]:",
vec![
("List", "LIST", None),
("Set", "SET", Some("LINETYPE SET linetype name (ByLayer / ByBlock / …):")),
],
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("LINETYPE ") || cmd.starts_with("LT ") => {
let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default();
@@ -587,7 +596,17 @@ impl OpenCADStudio {
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
}
cmd if cmd == "SETVAR" || cmd.starts_with("SETVAR ") => {
"SETVAR" => {
use crate::command::TwoValuePromptCommand;
let c = TwoValuePromptCommand::new(
"SETVAR",
"SETVAR variable name:",
"SETVAR new value (blank to read):",
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("SETVAR ") => {
let rest = cmd.strip_prefix("SETVAR").unwrap_or("").trim();
let mut it = rest.splitn(2, char::is_whitespace);
let name = it.next().unwrap_or("").to_uppercase();
+70
View File
@@ -409,6 +409,76 @@ impl CadCommand for KeywordCommand {
}
}
/// Generic interactive front-end for a two-argument command (LAYMRG source +
/// target, SETVAR variable + value…). Prompts for the two values in turn and
/// dispatches `<name> <first> <second>` to the existing inline handler. A bare
/// Enter on the second value dispatches `<name> <first>` (no second token) so a
/// getter-style command (SETVAR reading a variable) still works.
pub struct TwoValuePromptCommand {
name: &'static str,
prompt1: &'static str,
prompt2: &'static str,
first: Option<String>,
}
impl TwoValuePromptCommand {
pub fn new(name: &'static str, prompt1: &'static str, prompt2: &'static str) -> Self {
Self {
name,
prompt1,
prompt2,
first: None,
}
}
}
impl CadCommand for TwoValuePromptCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
match &self.first {
None => self.prompt1.to_string(),
Some(_) => self.prompt2.to_string(),
}
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
match &self.first {
None => {
if t.is_empty() {
return None;
}
self.first = Some(t.to_string());
None
}
Some(first) => Some(CmdResult::Dispatch(if t.is_empty() {
format!("{} {first}", self.name)
} else {
format!("{} {first} {t}", self.name)
})),
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
match &self.first {
// Enter on the second step with nothing typed = report / no-op form.
Some(first) => CmdResult::Dispatch(format!("{} {first}", self.name)),
None => CmdResult::Cancel,
}
}
}
// ── Result token ──────────────────────────────────────────────────────────
/// Returned by every `CadCommand` method to tell main.rs what to do.