feat(cmdline): type whole command lines with spaces; unify with headless (F1, #169)

Space is now a literal character in the command line, so a whole command
line — `UCS Z 90`, `LINE 0,0 10,10`, `PDMODE 3` — is typed before Enter
instead of the first Space submitting the buffer. Enter tokenises the line
and runs it.

The command line and the headless automation feeder now share one runner,
`run_command_line` (moved to the non-wasm-gated cmd_result.rs): a single or
inline-argument command dispatches as-is; a multi-token line whose first
word starts an interactive tool feeds the rest as steps; plugin commands
get the whole line first (#162). The shared feeder is now UCS-aware —
typed coordinates are interpreted in the active UCS in both the GUI and
headless. `run_headless` is a thin wrapper over it.

A free-form text command (TEXT / MTEXT / a name with spaces) still receives
the whole line as one input, via the existing `wants_text_with_spaces`
guard. Unfocused Space still repeats the last command.

Closes the command-line half of #169 generically for every command, not
just UCS. Third phase of the unified step driver: every input source funnels
through feed_command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-25 13:16:28 +03:00
1 parent 36d2f33301
commit 52f6aed2f2
3 files changed
+149 -110

No files matched your search

+5 -94
View File
@@ -360,101 +360,12 @@ impl OpenCADStudio {
}
}
/// Run a command headlessly. Single-word and inline-argument commands
/// (`PDMODE 3`, `LAYER Walls`) dispatch as-is. For an interactive tool with
/// coordinate arguments (`LINE 0,0 10,10`) the first word starts the tool
/// and the remaining tokens are fed as points / option keywords, then the
/// command is terminated as if Enter were pressed.
/// Run a command line headlessly. Thin wrapper over the shared
/// [`OpenCADStudio::run_command_line`] (see `cmd_result.rs`), which the GUI
/// command line uses too so both process `UCS Z 90` / `LINE 0,0 10,10` /
/// `PDMODE 3` identically.
fn run_headless(&mut self, cmd: &str) {
let i = self.active_tab;
let tokens: Vec<&str> = cmd.split_whitespace().collect();
if tokens.len() <= 1 {
let _ = self.dispatch_command(cmd);
return;
}
// Plugin commands parse their own inline arguments from the whole line
// (e.g. `HC_PIPE 2B 2C 1.25 0.013`), so offer the full command to plugin
// dispatch first. A built-in interactive tool matches only its bare name
// (`LINE`), so the full line is not a plugin command and falls through to
// the first-word + fed-tokens path below. (#162)
if crate::plugin::try_dispatch(self, i, cmd) {
// The plugin either committed immediately (inline args consumed) or
// installed an interactive command — feed any remaining tokens and
// finish it on Enter, as for a built-in tool.
self.finish_headless_interactive(&tokens);
return;
}
let _ = self.dispatch_command(tokens[0]);
if self.tabs[i].active_cmd.is_none() {
// Not an interactive tool — an inline-argument command.
let _ = self.dispatch_command(cmd);
return;
}
self.finish_headless_interactive(&tokens);
}
/// Feed `tokens[1..]` to the active interactive command as points / option
/// keywords, then terminate it as if Enter were pressed (LINE / PLINE finish
/// on Enter). No-op when no command is active.
fn finish_headless_interactive(&mut self, tokens: &[&str]) {
let i = self.active_tab;
if self.tabs[i].active_cmd.is_none() {
return;
}
self.last_point = None;
for tok in &tokens[1..] {
if self.tabs[i].active_cmd.is_none() {
break;
}
self.feed_active_cmd(tok);
}
// Terminate a still-open command (LINE / PLINE finish on Enter).
let _ = self.feed_command(crate::command::StepInput::Enter);
}
/// Classify one headless token into a [`StepInput`] and route it through the
/// shared [`OpenCADStudio::feed_command`]. When the command is picking an
/// existing entity the token is a hex handle; otherwise it is a coordinate
/// point or an option keyword / value.
fn feed_active_cmd(&mut self, token: &str) {
use crate::command::StepInput;
let i = self.active_tab;
// Object-pick step: the token is a handle (as returned by `query`).
if self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|c| c.needs_entity_pick())
{
if let Ok(v) = u64::from_str_radix(token.trim_start_matches("0x"), 16) {
let handle = acadrust::Handle::new(v);
let pt = self.tabs[i]
.scene
.document
.get_entity(handle)
.map(|e| {
let bb = e.as_entity().bounding_box();
glam::Vec3::new(
((bb.min.x + bb.max.x) * 0.5) as f32,
((bb.min.y + bb.max.y) * 0.5) as f32,
0.0,
)
})
.unwrap_or(glam::Vec3::ZERO);
let _ = self.feed_command(StepInput::EntityPick(handle, pt.as_dvec3()));
}
return;
}
if let Some((mut pt, kind)) = super::helpers::parse_coord(token) {
if matches!(kind, super::helpers::CoordKind::Relative) {
if let Some(base) = self.last_point {
pt += base;
}
}
self.last_point = Some(pt);
let _ = self.feed_command(StepInput::Point(pt.as_dvec3()));
} else {
let _ = self.feed_command(StepInput::Text(token.to_string()));
}
let _ = self.run_command_line(cmd);
}
/// List entities (handle, type, layer, basic geometry), optionally filtered