diff --git a/ROADMAP.md b/ROADMAP.md index a8ea9ca4..0636274d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -133,6 +133,7 @@ app's JSON control channel (`deckcraft --control PORT`). |---|---| | 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort | | 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape | +| 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-16 | Export: PNG with a transparent background — the slide background is left out, only the slide's content is drawn (Export… dialog, File › Export, web download, `file.export`/`file.render` `transparentBackground`, `deckcraft-cli render --transparent`) (#784) | | 2026-10-15 | CLI: .txt and .md export includes table cell text, which it used to skip silently (#814) | 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 d9a34030..ff22a0cd 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3953,3 +3953,35 @@ fn shape_geometry_commands_limit_size_and_position() { let x = xfrm(&s); assert!(within(&s), "rotated resize stored {x:?}"); } + +/// 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).