diff --git a/docs/automation/README.md b/docs/automation/README.md index 5ec9b6247..86755d8ee 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -20,8 +20,18 @@ can act → observe → act. | `{"op":"run","cmd":"LAYER Walls"}` | `{"ok":true,"cmd":...,"entities":N,"added":D}` | | `{"op":"entities"}` | `{"ok":true,"total":N,"by_type":{"Line":42,...}}` | | `{"op":"query","type":"Line","layer":"Walls"}` | per-entity `{handle,type,layer,…geometry}` (filters + `limit` optional) | +| `{"op":"select","handles":["2B"]}` | set the selection (by `handles`, `type`, or `layer`; `clear` to deselect) → `{"ok":true,"selected":N}` | | `{"op":"save","path":"out.dwg"}` | `{"ok":true,"saved":"out.dwg"}` (path optional once opened/saved) | +Selection drives modify commands — `select` the targets (e.g. the handles a +`query` returned), then `run("ERASE")`: + +```python +ocs.select(type="Line"); ocs.run("ERASE") # erase every line +ids = [e["handle"] for e in ocs.query(layer="Walls")["entities"]] +ocs.select(handles=ids); ocs.run("ERASE") +``` + 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. diff --git a/docs/automation/ocs.py b/docs/automation/ocs.py index 11a380574..93e5f39c5 100644 --- a/docs/automation/ocs.py +++ b/docs/automation/ocs.py @@ -79,6 +79,19 @@ class Ocs: """List entities (handle, type, layer, geometry), optionally filtered.""" return self._send(op="query", type=type, layer=layer, limit=limit) + def select( + self, + handles: Optional[list[str]] = None, + type: Optional[str] = None, + layer: Optional[str] = None, + clear: bool = False, + ) -> dict[str, Any]: + """Set the selection by handle, type, or layer (a following selection + command like ``run("ERASE")`` then acts on it). `clear=True` deselects.""" + return self._send( + op="select", handles=handles, type=type, layer=layer, clear=clear + ) + def save(self, path: Optional[str] = None) -> dict[str, Any]: """Write the document (defaults to the opened/last-saved path).""" return self._send(op="save", path=path) diff --git a/src/app/automation.rs b/src/app/automation.rs index fcdffa926..157fc4b9b 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -151,6 +151,41 @@ impl OpenCADStudio { } "entities" => self.entity_summary(), "query" => self.entity_query(&req), + "select" => { + let i = self.active_tab; + self.tabs[i].scene.deselect_all(); + if req["clear"].as_bool() != Some(true) { + // By explicit handles (hex, as returned by `query`). + if let Some(arr) = req["handles"].as_array() { + for h in arr.iter().filter_map(|h| h.as_str()) { + if let Ok(v) = u64::from_str_radix(h.trim_start_matches("0x"), 16) { + self.tabs[i].scene.select_entity(acadrust::Handle::new(v), false); + } + } + } + // Or by type / layer. + let type_filter = req["type"].as_str(); + let layer_filter = req["layer"].as_str(); + if type_filter.is_some() || layer_filter.is_some() { + let handles: Vec = self.tabs[i] + .scene + .document + .entities() + .filter(|e| { + type_filter.is_none_or(|t| { + crate::entities::names::ui_name(e).eq_ignore_ascii_case(t) + }) + }) + .filter(|e| layer_filter.is_none_or(|l| e.common().layer == l)) + .map(|e| e.common().handle) + .collect(); + for h in handles { + self.tabs[i].scene.select_entity(h, false); + } + } + } + json!({ "ok": true, "selected": self.tabs[i].scene.selected_entities().len() }) + } "save" => { let i = self.active_tab; let path = req["path"] @@ -314,6 +349,13 @@ mod tests { assert_eq!(r["entities"][0]["type"], "Circle"); assert_eq!(r["entities"][0]["radius"], 3.0); + // select by type, then a selection command acts on it. + let r = app.automation_op(r#"{"op":"select","type":"Line"}"#); + assert_eq!(r["selected"], 2); + app.automation_op(r#"{"op":"run","cmd":"ERASE"}"#); + let r = app.automation_op(r#"{"op":"entities"}"#); + assert_eq!(r["total"], 1); // only the Circle remains + // Errors are reported, never panics. assert_eq!(app.automation_op(r#"{"op":"bogus"}"#)["ok"], false); assert_eq!(app.automation_op("not json")["ok"], false);