forked from 1b-agent/opencadstudio-upstream
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:
1 parent
36d2f33301
commit
52f6aed2f2
3 files changed
+149
-110
No files matched your search
+5
-94
@@ -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
|
||||
|
||||
Reference in new issue
Block a user