feat(automation): query op — list entities with detail and filters

Add a query op to the headless server: list entities with handle, type,
layer and basic geometry (Line start/end, Circle/Arc centre+radius, Point
location), filtered by type and/or layer and capped by limit. Closes the
observe half of an agent's act/observe loop. ocs.py gains ocs.query().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-17 21:22:46 +03:00
1 parent f1a59b1094
commit 6d29b8458c
3 files changed
+89

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ can act → observe → act.
| `{"op":"open","path":"plan.dwg"}` | entity summary |
| `{"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":"save","path":"out.dwg"}` | `{"ok":true,"saved":"out.dwg"}` (path optional once opened/saved) |
Every response has `"ok"`; failures carry `"error"`. `run` drives Open CAD
+9
View File
@@ -70,6 +70,15 @@ class Ocs:
"""Total entity count and a breakdown by type."""
return self._send(op="entities")
def query(
self,
type: Optional[str] = None,
layer: Optional[str] = None,
limit: Optional[int] = None,
) -> dict[str, Any]:
"""List entities (handle, type, layer, geometry), optionally filtered."""
return self._send(op="query", type=type, layer=layer, limit=limit)
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)
+79
View File
@@ -53,6 +53,44 @@ fn err(msg: impl std::fmt::Display) -> Value {
json!({ "ok": false, "error": msg.to_string() })
}
fn v3(v: acadrust::types::Vector3) -> Value {
json!([v.x, v.y, v.z])
}
/// One entity as JSON: handle, type, layer, plus basic geometry for the common
/// types (others carry only the common fields).
fn entity_json(e: &acadrust::EntityType) -> Value {
use acadrust::EntityType as E;
let c = e.common();
let mut obj = json!({
"handle": format!("{:X}", c.handle.value()),
"type": crate::entities::names::ui_name(e),
"layer": c.layer,
});
let map = obj.as_object_mut().expect("json object");
match e {
E::Line(l) => {
map.insert("start".into(), v3(l.start));
map.insert("end".into(), v3(l.end));
}
E::Circle(cc) => {
map.insert("center".into(), v3(cc.center));
map.insert("radius".into(), json!(cc.radius));
}
E::Arc(a) => {
map.insert("center".into(), v3(a.center));
map.insert("radius".into(), json!(a.radius));
map.insert("start_angle".into(), json!(a.start_angle));
map.insert("end_angle".into(), json!(a.end_angle));
}
E::Point(p) => {
map.insert("location".into(), v3(p.location));
}
_ => {}
}
obj
}
impl OpenCADStudio {
/// Handle one JSON request line and return the JSON response.
pub(crate) fn automation_op(&mut self, line: &str) -> Value {
@@ -112,6 +150,7 @@ impl OpenCADStudio {
})
}
"entities" => self.entity_summary(),
"query" => self.entity_query(&req),
"save" => {
let i = self.active_tab;
let path = req["path"]
@@ -190,6 +229,40 @@ impl OpenCADStudio {
}
}
/// List entities (handle, type, layer, basic geometry), optionally filtered
/// by `type` and/or `layer`, capped by `limit` (default 1000).
fn entity_query(&self, req: &Value) -> Value {
let i = self.active_tab;
let type_filter = req["type"].as_str();
let layer_filter = req["layer"].as_str();
let limit = req["limit"].as_u64().unwrap_or(1000) as usize;
let mut entities = Vec::new();
let mut matched = 0u64;
for e in self.tabs[i].scene.document.entities() {
if let Some(tf) = type_filter {
if !crate::entities::names::ui_name(e).eq_ignore_ascii_case(tf) {
continue;
}
}
if let Some(lf) = layer_filter {
if e.common().layer != lf {
continue;
}
}
matched += 1;
if entities.len() < limit {
entities.push(entity_json(e));
}
}
json!({
"ok": true,
"count": matched,
"returned": entities.len(),
"entities": entities,
})
}
/// Count of entities in the active document, total and by type.
fn entity_summary(&self) -> Value {
let i = self.active_tab;
@@ -235,6 +308,12 @@ mod tests {
assert_eq!(r["by_type"]["Line"], 2);
assert_eq!(r["by_type"]["Circle"], 1);
// query returns per-entity detail and honours a type filter.
let r = app.automation_op(r#"{"op":"query","type":"Circle"}"#);
assert_eq!(r["count"], 1);
assert_eq!(r["entities"][0]["type"], "Circle");
assert_eq!(r["entities"][0]["radius"], 3.0);
// Errors are reported, never panics.
assert_eq!(app.automation_op(r#"{"op":"bogus"}"#)["ok"], false);
assert_eq!(app.automation_op("not json")["ok"], false);