forked from 1b-agent/opencadstudio-upstream
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:
1 parent
f1a59b1094
commit
6d29b8458c
3 files changed
+89
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user