mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 12:07:02 -06:00
Edit Points: Undo and Redo bring back the point type remembered with each step
Undo restored the outline but kept the point type that a handle drag with Shift, Ctrl or Alt, or a Point Type choice, had set, so the next drag without a modifier applied the undone type again. Each undo step now keeps the point type remembered with the document it restores, and Undo and Redo put it back, including a Straight or Corner choice that the handles alone do not show. A command that fails leaves the remembered type as it was, and the type applies only to the shape it was chosen on.
This commit is contained in:
@@ -44,16 +44,19 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}
|
||||
|
||||
fn restore(s: &mut Session, redo: bool) -> Result<Value> {
|
||||
let vertex_kind = s.tool.vertex_kind.clone();
|
||||
let st = s.doc_mut()?;
|
||||
let (from, to) = if redo { (&mut st.history.redo, &mut st.history.undo) } else { (&mut st.history.undo, &mut st.history.redo) };
|
||||
let Some(e) = from.pop() else { return ok() };
|
||||
to.push(HistoryEntry { label: e.label.clone(), doc: st.doc.clone(), selection: st.selection.clone() });
|
||||
to.push(HistoryEntry { label: e.label.clone(), doc: st.doc.clone(), selection: st.selection.clone(), vertex_kind });
|
||||
st.doc = e.doc;
|
||||
st.selection = e.selection;
|
||||
st.interaction = None;
|
||||
// A later shape can get an undone copy's id: Duplicate starts over with the usual offset.
|
||||
st.last_duplicate = None;
|
||||
st.revision += 1;
|
||||
// The point type Edit Points remembered with the restored document: handle drags follow it.
|
||||
s.tool.vertex_kind = e.vertex_kind;
|
||||
Ok(json!({"label": e.label}))
|
||||
}
|
||||
|
||||
|
||||
@@ -837,7 +837,7 @@ fn point_type(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => return Err(bad("shape.pointType", "`type` is smooth, straight or corner")),
|
||||
};
|
||||
// A straight point's handles can be as long as each other; remember the choice for drags.
|
||||
s.tool.vertex_kind = Some((path, index, kind.clone()));
|
||||
s.tool.vertex_kind = Some((id, path, index, kind.clone()));
|
||||
edit_path(s, id, path, "shape.pointType", move |segs, vs, _| {
|
||||
let si = *vs.get(index).ok_or_else(|| bad("shape.pointType", "no such point"))?;
|
||||
set_type(segs, si, &kind);
|
||||
@@ -931,7 +931,7 @@ fn close_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// Opening or closing renumbers the points of `path`: a point type remembered for one of them
|
||||
/// would follow the wrong point.
|
||||
fn forget_kinds(s: &mut Session, path: usize) {
|
||||
if s.tool.vertex_kind.as_ref().is_some_and(|k| k.0 == path) {
|
||||
if s.tool.vertex_kind.as_ref().is_some_and(|k| k.1 == path) {
|
||||
s.tool.vertex_kind = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1646,7 +1646,7 @@ mod tests {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [10.0, 10.0, 30.0, 30.0]})).unwrap();
|
||||
let st = s.active_mut().unwrap();
|
||||
st.interaction = Some(crate::Interaction { label: "Move".into(), doc: st.doc.clone(), selection: st.selection.clone() });
|
||||
st.interaction = Some(crate::Interaction { label: "Move".into(), doc: st.doc.clone(), selection: st.selection.clone(), vertex_kind: None });
|
||||
s.execute("shape.rotate", &json!({"gesture": "end", "ids": []})).unwrap();
|
||||
assert_eq!(s.active().unwrap().interaction.as_ref().unwrap().label, "Move");
|
||||
}
|
||||
|
||||
@@ -163,6 +163,8 @@ pub struct HistoryEntry {
|
||||
pub label: String,
|
||||
pub doc: Arc<Presentation>,
|
||||
pub selection: Selection,
|
||||
/// The point type Edit Points remembered with `doc` (`ToolState::vertex_kind`).
|
||||
pub vertex_kind: Option<(ShapeId, usize, usize, String)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -184,6 +186,8 @@ pub struct Interaction {
|
||||
pub label: String,
|
||||
pub doc: Arc<Presentation>,
|
||||
pub selection: Selection,
|
||||
/// The point type Edit Points remembered when it began.
|
||||
pub vertex_kind: Option<(ShapeId, usize, usize, String)>,
|
||||
}
|
||||
|
||||
/// Text an input method is composing (#101): its preedit sits in the edited text body, at `start`
|
||||
@@ -599,6 +603,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
let before = self.active().map(|d| (d.uid, d.doc.clone(), d.selection.clone()));
|
||||
let vertex_kind = self.tool.vertex_kind.clone();
|
||||
let outer = self.depth == 0;
|
||||
self.depth += 1;
|
||||
let r = (spec.run)(self, params);
|
||||
@@ -617,6 +622,9 @@ impl Session {
|
||||
st.selection = sel;
|
||||
st.revision += 1;
|
||||
}
|
||||
if outer {
|
||||
self.tool.vertex_kind = vertex_kind;
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
@@ -630,7 +638,7 @@ impl Session {
|
||||
&& spec.undoable
|
||||
&& outer
|
||||
{
|
||||
push_undo(st, HistoryEntry { label: spec.label.to_string(), doc: old, selection: sel });
|
||||
push_undo(st, HistoryEntry { label: spec.label.to_string(), doc: old, selection: sel, vertex_kind });
|
||||
}
|
||||
if spec.journal && outer {
|
||||
self.journal.push((id.to_string(), params.clone()));
|
||||
@@ -661,10 +669,11 @@ impl Session {
|
||||
/// Keep an input method's composition as it stands (#101): its text stays and the whole
|
||||
/// composition becomes one undo step. Nothing happens without one.
|
||||
pub fn end_composition(&mut self) {
|
||||
let vertex_kind = self.tool.vertex_kind.clone();
|
||||
let Some(st) = self.active_mut() else { return };
|
||||
let Some(c) = st.composition.take() else { return };
|
||||
if !Arc::ptr_eq(&c.before, &st.doc) {
|
||||
push_undo(st, HistoryEntry { label: "Type Text".into(), doc: c.before, selection: c.before_selection });
|
||||
push_undo(st, HistoryEntry { label: "Type Text".into(), doc: c.before, selection: c.before_selection, vertex_kind });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2242,6 +2242,90 @@ fn edit_points_handle_drag_modifiers_set_the_point_type() {
|
||||
assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "straight");
|
||||
}
|
||||
|
||||
/// A handle drag without a modifier follows the point type remembered with the document that
|
||||
/// Undo or Redo restores, and only on the shape the type was chosen on.
|
||||
#[test]
|
||||
fn edit_points_remembered_point_type_follows_undo_redo_and_its_shape() {
|
||||
use crate::tools::{Mods, PointerEvent, PointerKind};
|
||||
use deckcraft_geom::Vec2;
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [100, 100, 200, 100]})).unwrap();
|
||||
s.execute("shape.editPoints", &json!({"on": true})).unwrap();
|
||||
let id = deckcraft_model::ShapeId(s.execute("shape.points", &json!({})).unwrap()["id"].as_u64().unwrap() as u32);
|
||||
let handles = |s: &Session| {
|
||||
let st = s.doc().unwrap();
|
||||
let (hi, ho, v) = crate::cmd::points::slide_handles(&st.doc, &st.selection, st.shape(id).unwrap(), 0, 0).unwrap();
|
||||
(hi.unwrap(), ho.unwrap(), v)
|
||||
};
|
||||
let ev = |kind, p: deckcraft_geom::Point, mods: Mods| PointerEvent { kind, x: p.x, y: p.y, mods, ..Default::default() };
|
||||
let drag = |s: &mut Session, from: deckcraft_geom::Point, to: deckcraft_geom::Point, mods: Mods| {
|
||||
s.pointer(ev(PointerKind::Down, from, Mods::default())).unwrap();
|
||||
s.pointer(ev(PointerKind::Drag, to, mods)).unwrap();
|
||||
s.pointer(ev(PointerKind::Up, to, mods)).unwrap();
|
||||
};
|
||||
let (_, ho, v) = handles(&s);
|
||||
drag(&mut s, v, v, Mods::default());
|
||||
assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "smooth");
|
||||
drag(&mut s, ho, ho + Vec2::new(10.0, -25.0), Mods { alt: true, ..Default::default() });
|
||||
assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "corner");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "smooth");
|
||||
// A plain drag moves the other handle with the dragged one, as for a smooth point.
|
||||
let (_, ho1, _) = handles(&s);
|
||||
let to = v + Vec2::new(0.0, 40.0);
|
||||
drag(&mut s, ho1, to, Mods::default());
|
||||
let (hi2, ho2, _) = handles(&s);
|
||||
assert!((ho2 - to).hypot() < 1e-3, "{ho2:?}");
|
||||
assert!((hi2 - (v - Vec2::new(0.0, 40.0))).hypot() < 1e-3, "the other handle mirrors the dragged one: {hi2:?}");
|
||||
assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "smooth");
|
||||
// Undo of a Corner Point choice that left the outline as it was: the point is smooth again.
|
||||
s.execute("shape.pointType", &json!({"index": 0, "type": "corner"})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let to = v + Vec2::new(0.0, 50.0);
|
||||
drag(&mut s, v + Vec2::new(0.0, 40.0), to, Mods::default());
|
||||
let (hi3, ho3, _) = handles(&s);
|
||||
assert!((ho3 - to).hypot() < 1e-3, "{ho3:?}");
|
||||
assert!((hi3 - (v - Vec2::new(0.0, 50.0))).hypot() < 1e-3, "the other handle mirrors the dragged one: {hi3:?}");
|
||||
// Undo of a step that leaves the outline alone keeps a Straight choice, which equal handles
|
||||
// cannot show: a plain drag keeps the other handle's length.
|
||||
s.execute("shape.pointType", &json!({"index": 0, "type": "straight"})).unwrap();
|
||||
s.execute("shape.fill", &json!({"color": "#ff0000"})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let to2 = v + Vec2::new(0.0, 70.0);
|
||||
drag(&mut s, to, to2, Mods::default());
|
||||
let (hi4, ho4, _) = handles(&s);
|
||||
assert!((ho4 - to2).hypot() < 1e-3, "{ho4:?}");
|
||||
assert!((hi4 - (v - Vec2::new(0.0, 50.0))).hypot() < 1e-3, "the other handle keeps its length: {hi4:?}");
|
||||
// Redo brings a Corner Point choice back with the step: a plain drag moves one handle.
|
||||
s.execute("shape.pointType", &json!({"index": 0, "type": "corner"})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
drag(&mut s, to2, to2 + Vec2::new(10.0, 0.0), Mods::default());
|
||||
let (hi5, ho5, _) = handles(&s);
|
||||
assert!((ho5 - (to2 + Vec2::new(10.0, 0.0))).hypot() < 1e-3, "{ho5:?}");
|
||||
assert!((hi5 - hi4).hypot() < 1e-3, "the other handle stays: {hi5:?}");
|
||||
// A Point Type choice that fails leaves the remembered type as it was.
|
||||
assert!(s.execute("shape.pointType", &json!({"index": 99, "type": "smooth"})).is_err());
|
||||
assert_eq!(s.tool.vertex_kind, Some((id, 0, 0, "corner".to_string())));
|
||||
// The remembered type belongs to its shape: the same point of another ellipse stays smooth.
|
||||
s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [100, 300, 200, 100]})).unwrap();
|
||||
s.execute("shape.editPoints", &json!({"on": true})).unwrap();
|
||||
let other = deckcraft_model::ShapeId(s.execute("shape.points", &json!({})).unwrap()["id"].as_u64().unwrap() as u32);
|
||||
assert_ne!(other, id);
|
||||
let handles_of = |s: &Session| {
|
||||
let st = s.doc().unwrap();
|
||||
let (hi, ho, v) = crate::cmd::points::slide_handles(&st.doc, &st.selection, st.shape(other).unwrap(), 0, 0).unwrap();
|
||||
(hi.unwrap(), ho.unwrap(), v)
|
||||
};
|
||||
let (_, ho6, v6) = handles_of(&s);
|
||||
drag(&mut s, v6, v6, Mods::default());
|
||||
let to6 = v6 + Vec2::new(0.0, 40.0);
|
||||
drag(&mut s, ho6, to6, Mods::default());
|
||||
let (hi6, _, _) = handles_of(&s);
|
||||
assert!((hi6 - (v6 - Vec2::new(0.0, 40.0))).hypot() < 1e-3, "the other handle mirrors the dragged one: {hi6:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_points_straight_and_curved_segments() {
|
||||
let mut s = Session::with_new();
|
||||
@@ -2300,8 +2384,7 @@ fn edit_points_open_and_close_path() {
|
||||
};
|
||||
assert!(s.execute("shape.closePath", &json!({"index": 0})).is_err(), "the outline is closed already");
|
||||
// A point type remembered for handle drags belongs to the old numbering: opening forgets it.
|
||||
s.execute("shape.pointType", &json!({"index": 2, "type": "straight"})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.tool.vertex_kind = s.tool.edit_points.map(|id| (id, 0, 2, "straight".to_string()));
|
||||
assert!(s.tool.vertex_kind.is_some());
|
||||
// Open Path at the bottom-right corner: the outline now starts and ends there, as two points.
|
||||
s.execute("shape.openPath", &json!({"index": 2})).unwrap();
|
||||
|
||||
@@ -269,8 +269,8 @@ pub struct ToolState {
|
||||
/// The segment (path, index of the vertex ending it) last right-clicked in Edit Points, for
|
||||
/// the segment menu.
|
||||
pub edit_segment: Option<(usize, usize)>,
|
||||
/// The point type last chosen for a vertex (path, index, type): handle drags follow it.
|
||||
pub vertex_kind: Option<(usize, usize, String)>,
|
||||
/// The point type last chosen for a vertex (shape, path, index, type): handle drags follow it.
|
||||
pub vertex_kind: Option<(ShapeId, usize, usize, String)>,
|
||||
/// Drawing guides are shown (View › Guides): they can be dragged and shapes snap to them.
|
||||
pub guides_visible: bool,
|
||||
/// The guide being dragged and where it is now (slide points); `None` once dropped.
|
||||
@@ -522,10 +522,11 @@ impl Session {
|
||||
}
|
||||
|
||||
pub(crate) fn begin(&mut self, label: &str) {
|
||||
let vertex_kind = self.tool.vertex_kind.clone();
|
||||
if let Some(st) = self.active_mut()
|
||||
&& st.interaction.is_none()
|
||||
{
|
||||
st.interaction = Some(crate::Interaction { label: label.into(), doc: st.doc.clone(), selection: st.selection.clone() });
|
||||
st.interaction = Some(crate::Interaction { label: label.into(), doc: st.doc.clone(), selection: st.selection.clone(), vertex_kind });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +535,7 @@ impl Session {
|
||||
&& let Some(i) = st.interaction.take()
|
||||
&& !Arc::ptr_eq(&i.doc, &st.doc)
|
||||
{
|
||||
crate::push_undo(st, HistoryEntry { label: i.label, doc: i.doc, selection: i.selection });
|
||||
crate::push_undo(st, HistoryEntry { label: i.label, doc: i.doc, selection: i.selection, vertex_kind: i.vertex_kind });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,12 +1029,12 @@ impl Session {
|
||||
fn execute_inner_move_handle(&mut self, id: ShapeId, path: usize, index: usize, side: &str, p: Point, held: Option<&str>) -> Result<Value> {
|
||||
let kind = match held {
|
||||
Some(k) => Some(k.to_string()),
|
||||
None => self.tool.vertex_kind.clone().filter(|(pp, i, _)| (*pp, *i) == (path, index)).map(|k| k.2),
|
||||
None => self.tool.vertex_kind.clone().filter(|(s, pp, i, _)| (*s, *pp, *i) == (id, path, index)).map(|k| k.3),
|
||||
};
|
||||
let r =
|
||||
cmd::points::move_handle_raw(self, &json!({"id": id.0, "path": path, "index": index, "side": side, "x": p.x, "y": p.y, "type": kind}))?;
|
||||
if let Some(k) = held {
|
||||
self.tool.vertex_kind = Some((path, index, k.to_string()));
|
||||
self.tool.vertex_kind = Some((id, path, index, k.to_string()));
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
@@ -1711,8 +1712,9 @@ impl Session {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let vertex_kind = self.tool.vertex_kind.clone();
|
||||
if let Some(st) = self.active_mut() {
|
||||
crate::push_undo(st, HistoryEntry { label: "Erase".into(), doc: before, selection: sel_before });
|
||||
crate::push_undo(st, HistoryEntry { label: "Erase".into(), doc: before, selection: sel_before, vertex_kind });
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user