Make undo while drawing with the pen remove only the last anchor

Fixes #1745

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilverandClaude Opus 5.5 committed 2026-10-31 12:03:45 +01:00
1 parent 0abbe44cd4
commit 0e6fcf9bf7
5 files changed
+119

No files matched your search

+2
View File
@@ -544,6 +544,8 @@ pub struct PenPath {
pub closed: bool,
/// The path ends connected to this anchor of its own (a vector network).
pub join: Option<usize>,
/// Anchors taken back by undo, the last one first to come back on redo.
pub undone: Vec<(Point, Vec2, Vec2)>,
}
#[derive(Clone, Debug, Default)]
+37
View File
@@ -118,10 +118,47 @@ impl Session {
p = at_45_degrees(*last, p);
}
pen.points.push((p, Vec2::ZERO, Vec2::ZERO));
pen.undone.clear();
self.tool.gesture = Gesture::PenDrag { anchor: p };
self.pen_update()
}
/// Undo while the pen draws (#1745): its last anchor goes (before two anchors, with the layer
/// they made; with the only one, the whole path). Returns whether there was a path.
pub(crate) fn pen_undo(&mut self) -> Result<bool> {
let Some(pen) = self.tool.pen.as_mut() else { return Ok(false) };
let Some(last) = pen.points.pop() else {
self.tool.pen = None;
return Ok(false);
};
pen.undone.push(last);
let drop_layer = pen.points.len() < 2 && pen.id.take().is_some();
let empty = pen.points.is_empty();
self.tool.gesture = Gesture::None;
self.tool.overlay.rubber = None;
self.tool.overlay.rubber_handle = None;
if drop_layer {
self.cancel_interaction()?;
}
if empty {
self.pen_finish()?;
return Ok(true);
}
self.pen_update()?;
Ok(true)
}
/// Redo while the pen draws: the last anchor undo took back returns. Returns whether there was
/// one.
pub(crate) fn pen_redo(&mut self) -> Result<bool> {
let Some(pen) = self.tool.pen.as_mut() else { return Ok(false) };
let Some(point) = pen.undone.pop() else { return Ok(false) };
pen.points.push(point);
self.tool.gesture = Gesture::None;
self.pen_update()?;
Ok(true)
}
/// Rebuild the pen's vector node from its anchors.
pub(super) fn pen_update(&mut self) -> Result<()> {
let Some(pen) = self.tool.pen.clone() else { return Ok(()) };
+2
View File
@@ -457,6 +457,8 @@ mod pen_end_tests;
#[cfg(test)]
mod pen_overlay_tests;
#[cfg(test)]
mod pen_undo_tests;
#[cfg(test)]
mod pen_vector_edit_tests;
#[cfg(test)]
mod penpot_export_tests;
+70
View File
@@ -0,0 +1,70 @@
//! Undo while the pen draws (#1745): each undo takes back the path's last anchor, not the whole
//! path (nor the step before it); redo puts the anchors back.
use crate::Session;
use crate::tools::{PointerEvent, PointerKind, ToolId};
use figcraft_doc::NodeKind;
use serde_json::json;
fn click(s: &mut Session, x: f64, y: f64) {
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::at(kind, x, y, 1.0)).unwrap();
}
}
fn layers(s: &Session) -> usize {
s.doc().unwrap().node(s.page().unwrap()).unwrap().children.len()
}
fn pen_points(s: &Session) -> Option<usize> {
s.tool.pen.as_ref().map(|p| p.points.len())
}
/// (vertices, segments) of the pen's vector, if it has one.
fn vector(s: &Session) -> Option<(usize, usize)> {
let id = s.tool.pen.as_ref().and_then(|p| p.id)?;
match &s.doc().unwrap().node(id)?.kind {
NodeKind::Vector { network } => Some((network.vertices.len(), network.segments.len())),
_ => None,
}
}
fn undo(s: &mut Session) -> bool {
s.execute("edit.undo", &json!({})).unwrap()["undone"] == true
}
fn redo(s: &mut Session) {
s.execute("edit.redo", &json!({})).unwrap();
}
#[test]
fn undo_while_drawing_takes_back_one_anchor() {
let mut s = Session::new();
s.execute("shape.rectangle", &json!({"x": 400, "y": 400, "width": 50, "height": 50})).unwrap();
assert_eq!(layers(&s), 1);
s.set_tool(ToolId::Pen);
click(&mut s, 100.0, 100.0);
click(&mut s, 200.0, 100.0);
click(&mut s, 200.0, 200.0);
assert_eq!((pen_points(&s), vector(&s), layers(&s)), (Some(3), Some((3, 2)), 2));
assert!(undo(&mut s));
assert_eq!((pen_points(&s), vector(&s), layers(&s)), (Some(2), Some((2, 1)), 2), "only the last anchor goes");
assert!(undo(&mut s));
assert_eq!((pen_points(&s), vector(&s), layers(&s)), (Some(1), None, 1), "one anchor, no layer yet");
// Redo puts the anchors back in order.
redo(&mut s);
assert_eq!((pen_points(&s), vector(&s), layers(&s)), (Some(2), Some((2, 1)), 2));
redo(&mut s);
assert_eq!((pen_points(&s), vector(&s), layers(&s)), (Some(3), Some((3, 2)), 2));
let pts: Vec<_> = s.tool.pen.as_ref().unwrap().points.iter().map(|(p, _, _)| (p.x, p.y)).collect();
assert_eq!(pts, vec![(100.0, 100.0), (200.0, 100.0), (200.0, 200.0)]);
for _ in 0..3 {
assert!(undo(&mut s));
}
assert_eq!((pen_points(&s), layers(&s)), (None, 1), "the path is gone, the rectangle stays");
assert!(undo(&mut s));
assert_eq!(layers(&s), 0, "the next undo takes the rectangle");
}
+8
View File
@@ -114,6 +114,10 @@ impl Session {
pub fn undo(&mut self) -> Result<bool> {
textpreview::end(self);
// While the pen draws, undo works on the anchors of its path (#1745).
if self.ds().is_ok_and(|d| d.preview.is_none()) && self.pen_undo()? {
return Ok(true);
}
let live = self.ds().is_ok_and(|d| self.live_doc(d.uid));
let ds = self.ds_mut()?;
if ds.preview.is_some() {
@@ -152,6 +156,10 @@ impl Session {
pub fn redo(&mut self) -> Result<bool> {
textpreview::end(self);
// While the pen draws, redo works on the anchors of its path (#1745).
if self.ds().is_ok_and(|d| d.preview.is_none()) && self.pen_redo()? {
return Ok(true);
}
let live = self.ds().is_ok_and(|d| self.live_doc(d.uid));
let ds = self.ds_mut()?;
if ds.preview.is_some() {