mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
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:
1 parent
0abbe44cd4
commit
0e6fcf9bf7
5 files changed
+119
No files matched your search
@@ -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)]
|
||||
|
||||
@@ -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(()) };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user