feat(automation): select op — target entities by handle/type/layer
Add a select op that sets the document selection (by hex handles from
query, or by type/layer, or clear). Selection drives modify commands, so
an agent can query → select → run("ERASE") to act on specific entities.
Completes the modify half of the act/observe loop. ocs.py gains select().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6d29b8458c
commit
c78bc62056
3 changed files with 65 additions and 0 deletions
|
|
@ -20,8 +20,18 @@ can act → observe → act.
|
||||||
| `{"op":"run","cmd":"LAYER Walls"}` | `{"ok":true,"cmd":...,"entities":N,"added":D}` |
|
| `{"op":"run","cmd":"LAYER Walls"}` | `{"ok":true,"cmd":...,"entities":N,"added":D}` |
|
||||||
| `{"op":"entities"}` | `{"ok":true,"total":N,"by_type":{"Line":42,...}}` |
|
| `{"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":"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) |
|
| `{"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
|
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.
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,19 @@ class Ocs:
|
||||||
"""List entities (handle, type, layer, geometry), optionally filtered."""
|
"""List entities (handle, type, layer, geometry), optionally filtered."""
|
||||||
return self._send(op="query", type=type, layer=layer, limit=limit)
|
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]:
|
def save(self, path: Optional[str] = None) -> dict[str, Any]:
|
||||||
"""Write the document (defaults to the opened/last-saved path)."""
|
"""Write the document (defaults to the opened/last-saved path)."""
|
||||||
return self._send(op="save", path=path)
|
return self._send(op="save", path=path)
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,41 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
"entities" => self.entity_summary(),
|
"entities" => self.entity_summary(),
|
||||||
"query" => self.entity_query(&req),
|
"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<acadrust::Handle> = 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" => {
|
"save" => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let path = req["path"]
|
let path = req["path"]
|
||||||
|
|
@ -314,6 +349,13 @@ mod tests {
|
||||||
assert_eq!(r["entities"][0]["type"], "Circle");
|
assert_eq!(r["entities"][0]["type"], "Circle");
|
||||||
assert_eq!(r["entities"][0]["radius"], 3.0);
|
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.
|
// 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);
|
||||||
assert_eq!(app.automation_op("not json")["ok"], false);
|
assert_eq!(app.automation_op("not json")["ok"], false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue