From 8d3d0c30cca9f31de40af506b7384fa48528b140 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 17 Jun 2026 21:31:15 +0300 Subject: [PATCH] feat(automation): undo/redo ops (move/copy already work via select+run) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/automation/README.md | 1 + docs/automation/ocs.py | 8 ++++++++ src/app/automation.rs | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/docs/automation/README.md b/docs/automation/README.md index 86755d8ee..1476e0465 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -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 diff --git a/docs/automation/ocs.py b/docs/automation/ocs.py index 93e5f39c5..a0a59eb76 100644 --- a/docs/automation/ocs.py +++ b/docs/automation/ocs.py @@ -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) diff --git a/src/app/automation.rs b/src/app/automation.rs index 157fc4b9b..dde192e07 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -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);