feat(commands): step the selection-first text commands (bucket C)

Add SelectThenValueCommand (gather selection, then one value) and convert:
- SelectThenValueCommand: HYPERLINK (url), ARCTEXT (text), TEXTFIT (width),
  SCALETEXT (factor), TCOUNT (start), TEXTMASK (Enter to apply).
- SelectThenKeywordCommand: JUSTIFYTEXT (Left/Center/Right/Middle/Aligned/Fit),
  TCASE (Upper/Lower/Sentence/Title).

Each bare form gathers a selection when none is set, prompts, and delegates to
the inline handler. OBJECTSCALE already opens its dialog; TABLE CELL takes three
values (row/col/text) and stays typeable — TABLE bare already builds a table
interactively.
This commit is contained in:
Hakan Seven committed 2026-07-16 14:37:10 +03:00
1 parent cf5362aeab
commit 208e617868
4 files changed
+185 -8

No files matched your search

+83 -6
View File
@@ -56,7 +56,26 @@ impl OpenCADStudio {
// JUSTIFYTEXT <option> — change the justification of selected text and
// multiline-text objects.
cmd if cmd == "JUSTIFYTEXT" || cmd.starts_with("JUSTIFYTEXT ") => {
"JUSTIFYTEXT" => {
use crate::command::SelectThenKeywordCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenKeywordCommand::new(
"JUSTIFYTEXT",
"JUSTIFYTEXT [Left / Center / Right / Middle / Aligned / Fit]:",
vec![
("Left", "LEFT", None),
("Center", "CENTER", None),
("Right", "RIGHT", None),
("Middle", "MIDDLE", None),
("Aligned", "ALIGN", None),
("Fit", "FIT", None),
],
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("JUSTIFYTEXT ") => {
let opt = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
let handles: Vec<acadrust::Handle> = self.tabs[i]
.scene
@@ -157,7 +176,24 @@ impl OpenCADStudio {
// TCASE <Upper|Lower|Sentence|Title> — change the case of the text in
// selected text and multiline-text objects.
cmd if cmd == "TCASE" || cmd.starts_with("TCASE ") => {
"TCASE" => {
use crate::command::SelectThenKeywordCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenKeywordCommand::new(
"TCASE",
"TCASE [Upper / Lower / Sentence / Title]:",
vec![
("Upper", "UPPER", None),
("Lower", "LOWER", None),
("Sentence", "SENTENCE", None),
("Title", "TITLE", None),
],
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("TCASE ") => {
let opt = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
let handles: Vec<acadrust::Handle> = self.tabs[i]
.scene
@@ -226,7 +262,18 @@ impl OpenCADStudio {
// extent and bring the text in front, so the mask hides whatever is
// underneath while the text stays readable. (Same world-XY coordinate
// space the WIPEOUT command uses.)
cmd if cmd == "TEXTMASK" || cmd.starts_with("TEXTMASK ") => {
"TEXTMASK" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenValueCommand::new(
"TEXTMASK",
"TEXTMASK press Enter to mask the selected text:",
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("TEXTMASK ") => {
let handles: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
@@ -289,7 +336,15 @@ impl OpenCADStudio {
// (text width fit)
// TEXTFIT <width> — adjust the width factor of selected single-line
// text so its rendered width matches the target.
cmd if cmd == "TEXTFIT" || cmd.starts_with("TEXTFIT ") => {
"TEXTFIT" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c =
SelectThenValueCommand::new("TEXTFIT", "TEXTFIT target width:", has_sel);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("TEXTFIT ") => {
let target: f64 = match cmd.split_whitespace().nth(1).and_then(|s| s.parse().ok()) {
Some(v) if v > 0.0 => v,
_ => {
@@ -351,7 +406,18 @@ impl OpenCADStudio {
// (text sequential numbering)
// TCOUNT [start] — prefix selected single-line text with sequential
// numbers in reading order (top-to-bottom, then left-to-right).
cmd if cmd == "TCOUNT" || cmd.starts_with("TCOUNT ") => {
"TCOUNT" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenValueCommand::new(
"TCOUNT",
"TCOUNT starting number (Enter for 1):",
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("TCOUNT ") => {
let start: i64 = cmd
.split_whitespace()
.nth(1)
@@ -997,7 +1063,18 @@ impl OpenCADStudio {
// ARCTEXT <text> — lay the text out as one Text entity per character
// along the selected arc, each rotated to follow the tangent.
cmd if cmd == "ARCTEXT" || cmd.starts_with("ARCTEXT ") => {
"ARCTEXT" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenValueCommand::new(
"ARCTEXT",
"ARCTEXT text to place along the arc:",
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("ARCTEXT ") => {
use acadrust::entities::Text;
use acadrust::types::Vector3;
let text = cmd.strip_prefix("ARCTEXT").unwrap_or("").trim().to_string();
+12 -1
View File
@@ -640,7 +640,18 @@ impl OpenCADStudio {
// HYPERLINK <url> — attach a hyperlink to the selected objects, stored
// in the standard PE_URL XData record so it round-trips in the file.
cmd if cmd == "HYPERLINK" || cmd.starts_with("HYPERLINK ") => {
"HYPERLINK" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenValueCommand::new(
"HYPERLINK",
"HYPERLINK URL to attach:",
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("HYPERLINK ") => {
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
let url = cmd.strip_prefix("HYPERLINK").unwrap_or("").trim().to_string();
if url.is_empty() {
+12 -1
View File
@@ -1848,7 +1848,18 @@ impl OpenCADStudio {
// ── SCALETEXT — rescale selected Text/MText entities ─────────────────
// Usage: SCALETEXT <factor> e.g. SCALETEXT 2
// SCALETEXT H <height> set absolute height
cmd if cmd == "SCALETEXT" || cmd.starts_with("SCALETEXT ") => {
"SCALETEXT" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenValueCommand::new(
"SCALETEXT",
"SCALETEXT scale factor (or 'H <height>' for an absolute height):",
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("SCALETEXT ") => {
let rest = cmd.trim_start_matches("SCALETEXT").trim();
let parts: Vec<&str> = rest.split_whitespace().collect();
let selected_handles: Vec<acadrust::Handle> = self.tabs[i]
+78
View File
@@ -595,6 +595,84 @@ impl CadCommand for SelectThenKeywordCommand {
}
}
/// Generic interactive front-end for a single-value command that operates on
/// the current selection (HYPERLINK url, ARCTEXT text, TEXTFIT width, TCASE
/// aside…). Gathers a selection first when none is set (Enter confirms), then
/// prompts for one value and dispatches `<name> <value>` to the inline handler,
/// which reads the still-selected set. A bare Enter on the value step dispatches
/// `<name>` alone — for commands whose value is optional (TCOUNT, TEXTMASK).
pub struct SelectThenValueCommand {
name: &'static str,
value_prompt: &'static str,
gathering: bool,
selected: Vec<Handle>,
}
impl SelectThenValueCommand {
pub fn new(name: &'static str, value_prompt: &'static str, has_selection: bool) -> Self {
Self {
name,
value_prompt,
gathering: !has_selection,
selected: Vec::new(),
}
}
}
impl CadCommand for SelectThenValueCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
if self.gathering {
format!("{} select objects, then press Enter:", self.name)
} else {
self.value_prompt.to_string()
}
}
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 {
self.selected = handles;
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.gathering {
return None;
}
let t = text.trim();
if t.is_empty() {
return None;
}
Some(CmdResult::Dispatch(format!("{} {t}", self.name)))
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
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;
}
// Value step, nothing typed: the no-argument form (optional value).
CmdResult::Dispatch(format!("{} ", self.name))
}
}
// ── Result token ──────────────────────────────────────────────────────────
/// Returned by every `CadCommand` method to tell main.rs what to do.