feat(automation): drive draw commands headless with coordinate args
run now feeds coordinate tokens to interactive tools: "LINE 0,0 10,10" starts the tool, feeds the points, and terminates as if Enter were pressed — creating real geometry through the existing command system. The headless session also leaves the welcome (Start) tab so drawing commands aren'\''t blocked. Verified end-to-end: LINE/CIRCLE create entities that save and reopen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1e89226c89
commit
f1a59b1094
2 changed files with 85 additions and 8 deletions
|
|
@ -25,9 +25,20 @@ Every response has `"ok"`; failures carry `"error"`. `run` drives Open CAD
|
||||||
Studio's **own** command system — no separate bindings to maintain — so its
|
Studio's **own** command system — no separate bindings to maintain — so its
|
||||||
coverage grows with the app.
|
coverage grows with the app.
|
||||||
|
|
||||||
> **Status (first increment):** `run` applies synchronous commands (system
|
Interactive draw commands take their points as coordinate tokens; the tool is
|
||||||
> variables, layer ops, …). Pick-based interactive commands (drawing by clicking
|
started, the points are fed, and it is terminated as if Enter were pressed:
|
||||||
> points) need coordinate feeding and are not wired headless yet.
|
|
||||||
|
```
|
||||||
|
{"op":"run","cmd":"LINE 0,0 10,10 10,20"} → two Line segments
|
||||||
|
{"op":"run","cmd":"CIRCLE 5,5 3"} → centre 5,5 radius 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Coordinates are `x,y` or `x,y,z`; `@dx,dy` is relative to the previous point.
|
||||||
|
Inline-argument commands (`PDMODE 3`, `LAYER Walls`) are passed through as-is.
|
||||||
|
|
||||||
|
> Coverage is growing: commands whose options are typed keywords or coordinates
|
||||||
|
> work; ones that still rely on on-screen picking (object selection by clicking)
|
||||||
|
> are being wired next.
|
||||||
|
|
||||||
## Python client
|
## Python client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].scene.document = acadrust::CadDocument::new();
|
self.tabs[i].scene.document = acadrust::CadDocument::new();
|
||||||
self.tabs[i].current_path = None;
|
self.tabs[i].current_path = None;
|
||||||
|
// The headless session starts on the welcome (Start) tab, which
|
||||||
|
// blocks drawing commands; turn it into a real drawing.
|
||||||
|
self.tabs[i].is_start = false;
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry();
|
||||||
self.entity_summary()
|
self.entity_summary()
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +88,7 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].scene.document = doc;
|
self.tabs[i].scene.document = doc;
|
||||||
self.tabs[i].current_path = Some(PathBuf::from(path));
|
self.tabs[i].current_path = Some(PathBuf::from(path));
|
||||||
|
self.tabs[i].is_start = false;
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry();
|
||||||
self.entity_summary()
|
self.entity_summary()
|
||||||
}
|
}
|
||||||
|
|
@ -98,11 +102,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let before = self.tabs[i].scene.document.entities().count();
|
let before = self.tabs[i].scene.document.entities().count();
|
||||||
// The returned Task drives GUI follow-up; synchronous commands
|
self.run_headless(&cmd);
|
||||||
// (system variables, layer ops, …) have already applied. Pick-
|
|
||||||
// based interactive commands need coordinate feeding — not yet
|
|
||||||
// wired headless.
|
|
||||||
let _ = self.dispatch_command(&cmd);
|
|
||||||
let after = self.tabs[i].scene.document.entities().count();
|
let after = self.tabs[i].scene.document.entities().count();
|
||||||
json!({
|
json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
|
|
@ -134,6 +134,62 @@ 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.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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.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).
|
||||||
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
|
if let Some(r) = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()) {
|
||||||
|
let _ = self.apply_cmd_result(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed one token to the active command: a coordinate becomes a point, any
|
||||||
|
/// other token an option keyword.
|
||||||
|
fn feed_active_cmd(&mut self, token: &str) {
|
||||||
|
let i = self.active_tab;
|
||||||
|
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);
|
||||||
|
if let Some(r) = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)) {
|
||||||
|
let _ = self.apply_cmd_result(r);
|
||||||
|
}
|
||||||
|
} else if let Some(r) = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|c| c.on_text_input(token))
|
||||||
|
{
|
||||||
|
let _ = self.apply_cmd_result(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Count of entities in the active document, total and by type.
|
/// Count of entities in the active document, total and by type.
|
||||||
fn entity_summary(&self) -> Value {
|
fn entity_summary(&self) -> Value {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|
@ -166,8 +222,18 @@ mod tests {
|
||||||
assert_eq!(r["ok"], true);
|
assert_eq!(r["ok"], true);
|
||||||
assert_eq!(r["cmd"], "PDMODE 3");
|
assert_eq!(r["cmd"], "PDMODE 3");
|
||||||
|
|
||||||
|
// A draw command with coordinates creates real geometry.
|
||||||
|
let r = app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10 10,20"}"#);
|
||||||
|
assert_eq!(r["ok"], true);
|
||||||
|
assert_eq!(r["added"], 2); // two segments → two Line entities
|
||||||
|
let r = app.automation_op(r#"{"op":"run","cmd":"CIRCLE 5,5 3"}"#);
|
||||||
|
assert_eq!(r["added"], 1);
|
||||||
|
|
||||||
let r = app.automation_op(r#"{"op":"entities"}"#);
|
let r = app.automation_op(r#"{"op":"entities"}"#);
|
||||||
assert_eq!(r["ok"], true);
|
assert_eq!(r["ok"], true);
|
||||||
|
assert_eq!(r["total"], 3);
|
||||||
|
assert_eq!(r["by_type"]["Line"], 2);
|
||||||
|
assert_eq!(r["by_type"]["Circle"], 1);
|
||||||
|
|
||||||
// Errors are reported, never panics.
|
// Errors are reported, never panics.
|
||||||
assert_eq!(app.automation_op(r#"{"op":"bogus"}"#)["ok"], false);
|
assert_eq!(app.automation_op(r#"{"op":"bogus"}"#)["ok"], false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue