From f1a59b10946149c91616e861a5eff5cc346f842a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 17 Jun 2026 21:17:17 +0300 Subject: [PATCH] feat(automation): drive draw commands headless with coordinate args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/automation/README.md | 17 +++++++-- src/app/automation.rs | 76 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/docs/automation/README.md b/docs/automation/README.md index e41d91be4..bb76fdd90 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -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 coverage grows with the app. -> **Status (first increment):** `run` applies synchronous commands (system -> variables, layer ops, …). Pick-based interactive commands (drawing by clicking -> points) need coordinate feeding and are not wired headless yet. +Interactive draw commands take their points as coordinate tokens; the tool is +started, the points are fed, and it is terminated as if Enter were pressed: + +``` +{"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 diff --git a/src/app/automation.rs b/src/app/automation.rs index 360e0fd06..195ca1255 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -65,6 +65,9 @@ impl OpenCADStudio { let i = self.active_tab; self.tabs[i].scene.document = acadrust::CadDocument::new(); 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.entity_summary() } @@ -85,6 +88,7 @@ impl OpenCADStudio { let i = self.active_tab; self.tabs[i].scene.document = doc; self.tabs[i].current_path = Some(PathBuf::from(path)); + self.tabs[i].is_start = false; self.tabs[i].scene.bump_geometry(); self.entity_summary() } @@ -98,11 +102,7 @@ impl OpenCADStudio { } let i = self.active_tab; let before = self.tabs[i].scene.document.entities().count(); - // The returned Task drives GUI follow-up; synchronous commands - // (system variables, layer ops, …) have already applied. Pick- - // based interactive commands need coordinate feeding — not yet - // wired headless. - let _ = self.dispatch_command(&cmd); + self.run_headless(&cmd); let after = self.tabs[i].scene.document.entities().count(); json!({ "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. fn entity_summary(&self) -> Value { let i = self.active_tab; @@ -166,8 +222,18 @@ mod tests { assert_eq!(r["ok"], true); 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"}"#); 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. assert_eq!(app.automation_op(r#"{"op":"bogus"}"#)["ok"], false);