From f4afe6d2df66e73078a6562952350f9a31d8ec98 Mon Sep 17 00:00:00 2001 From: Zetenypapp Date: Sat, 17 Oct 2026 22:16:42 +0200 Subject: [PATCH] Edit Points: turning a shape into a freeform is one undo step shape.editPoints was a noundo command, so the conversion from a preset to custom geometry was never recorded and Ctrl+Z took back the step before it. It is undoable now (only an actual change records a step), and Edit Points ends when Undo turns the shape back into a preset. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/engine/src/cmd/points.rs | 2 +- crates/engine/src/tests.rs | 32 ++++++++++++++++++++++++++++++++ crates/engine/src/tools.rs | 7 ++++++- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9e430a0c..598b76aa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) | | 2026-10-15 | PPTX trigger sequences keep separate On Click steps, including paragraph builds; synthetic import/edit/save/reopen and headless show regressions | | 2026-10-15 | CLI: .txt and .md export includes table cell text, which it used to skip silently (#814) | | 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) | diff --git a/crates/engine/src/cmd/points.rs b/crates/engine/src/cmd/points.rs index 38fd22b9..3af2c65f 100644 --- a/crates/engine/src/cmd/points.rs +++ b/crates/engine/src/cmd/points.rs @@ -16,7 +16,7 @@ use crate::{Result, Session}; pub fn specs() -> Vec { vec![ cmd!( - noundo "shape.editPoints", + "shape.editPoints", "Edit Points", ["Shape Format", "Insert Shapes", "Edit Shape"], None, diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index a464fd8b..3adb4687 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3623,3 +3623,35 @@ fn table_column_width_of_a_huge_column_is_ignored() { assert_eq!(table_of(&s, id).cols, vec![100.0, 100.0], "no column changes"); } } + +/// Edit Points turns a preset into a freeform: one undo step, so Undo brings the preset back +/// (and leaves Edit Points), Redo the freeform; leaving Edit Points adds no step. +#[test] +fn edit_points_conversion_is_one_undo_step() { + use deckcraft_model::Geom; + let mut s = Session::with_new(); + s.execute("slide.new", &json!({"layout": "blank"})).unwrap(); + s.execute("shape.insert", &json!({"preset": "star5", "rect": [100, 100, 200, 100]})).unwrap(); + let preset = |s: &Session| matches!(s.doc().unwrap().selected_shapes()[0].geom, Geom::Preset { .. }); + s.execute("shape.editPoints", &json!({"on": true})).unwrap(); + assert!(!preset(&s), "Edit Points made a freeform"); + assert!(s.tool.editing_points(s.doc().unwrap()).is_some()); + s.execute("shape.editPoints", &json!({"on": false})).unwrap(); + // Leaving changed nothing, so the next undo is the conversion itself. + s.execute("edit.undo", &json!({})).unwrap(); + assert!(preset(&s), "Undo brings the star back"); + s.execute("edit.redo", &json!({})).unwrap(); + assert!(!preset(&s), "Redo makes it a freeform again"); + // Undo while still editing points ends Edit Points: the star has no points to edit. + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("shape.editPoints", &json!({"on": true})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(preset(&s)); + assert!(s.tool.editing_points(s.doc().unwrap()).is_none()); + // A freeform needs no conversion: entering Edit Points again adds no undo step. + s.execute("edit.redo", &json!({})).unwrap(); + s.execute("shape.editPoints", &json!({"on": false})).unwrap(); + s.execute("shape.editPoints", &json!({"on": true})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(preset(&s), "the only step was the conversion"); +} diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index f1acd9e9..e5c4e9ee 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -280,7 +280,12 @@ impl ToolState { /// The shape in Edit Points, while it is still the one selected shape. pub fn editing_points(&self, st: &crate::DocState) -> Option { - self.edit_points.filter(|id| st.selection.shapes.as_slice() == [*id] && st.selection.text.is_none() && st.shape(*id).is_some()) + // Only a freeform has points: after Undo takes the conversion back, Edit Points is over. + self.edit_points.filter(|id| { + st.selection.shapes.as_slice() == [*id] + && st.selection.text.is_none() + && st.shape(*id).is_some_and(|sh| matches!(sh.geom, deckcraft_model::Geom::Custom { .. })) + }) } /// A freeform path is being drawn (Enter/Escape/double-click finish it).