feat(automation): undo/redo ops (move/copy already work via select+run)

Add undo and redo ops to the headless server (synchronous via update()).
Combined with select, the modify commands MOVE/COPY work through the
existing dispatcher — "select type=Line" then run("MOVE 0,0 10,0")
translates the selection. ocs.py gains undo()/redo().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-17 21:31:15 +03:00
1 parent c78bc62056
commit 8d3d0c30cc
3 files changed
+27

No files matched your search

+1
View File
@@ -21,6 +21,7 @@ can act → observe → act.
| `{"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":"undo"}` / `{"op":"redo"}` | step the document history → entity summary |
| `{"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
+8
View File
@@ -92,6 +92,14 @@ class Ocs:
op="select", handles=handles, type=type, layer=layer, clear=clear
)
def undo(self) -> dict[str, Any]:
"""Undo the last change."""
return self._send(op="undo")
def redo(self) -> dict[str, Any]:
"""Redo the last undone change."""
return self._send(op="redo")
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)
+18
View File
@@ -151,6 +151,14 @@ impl OpenCADStudio {
}
"entities" => self.entity_summary(),
"query" => self.entity_query(&req),
"undo" => {
let _ = self.update(super::Message::Undo);
self.entity_summary()
}
"redo" => {
let _ = self.update(super::Message::Redo);
self.entity_summary()
}
"select" => {
let i = self.active_tab;
self.tabs[i].scene.deselect_all();
@@ -356,6 +364,16 @@ mod tests {
let r = app.automation_op(r#"{"op":"entities"}"#);
assert_eq!(r["total"], 1); // only the Circle remains
// undo restores the erased lines.
let r = app.automation_op(r#"{"op":"undo"}"#);
assert_eq!(r["total"], 3);
// move a selected entity by a displacement.
app.automation_op(r#"{"op":"select","type":"Circle"}"#);
app.automation_op(r#"{"op":"run","cmd":"MOVE 0,0 100,0"}"#);
let r = app.automation_op(r#"{"op":"query","type":"Circle"}"#);
assert_eq!(r["entities"][0]["center"][0], 105.0); // 5 + 100
// Errors are reported, never panics.
assert_eq!(app.automation_op(r#"{"op":"bogus"}"#)["ok"], false);
assert_eq!(app.automation_op("not json")["ok"], false);