diff --git a/crates/engine/src/cmd/draw2/freehand.rs b/crates/engine/src/cmd/draw2/freehand.rs index 977f3f921a..55f5fe267b 100644 --- a/crates/engine/src/cmd/draw2/freehand.rs +++ b/crates/engine/src/cmd/draw2/freehand.rs @@ -227,12 +227,18 @@ pub(crate) fn freehand_brushed(s: &mut Session, p: &Value, brush_name: Option<&s let Some(p0) = pts.first_mut() else { return Err(bad(C, "needs points")) }; *p0 = end; if pts.len() < 2 { - // No stroke to add: the brush alone changes, when it is not the path's already. + // No stroke to add: the brush alone changes, when it is not the path's already, and the + // path is selected or deselected as a longer continuation leaves it. if let Some(name) = brush_name && s.doc()?.doc.node(target).and_then(|n| n.appearance.stroke()).is_some_and(|st| st.brush.as_deref() != Some(name)) { - s.edit("Paintbrush", |d, _| { + s.edit("Paintbrush", |d, sel| { set_brush(d, target, name); + if keep_selected { + sel.set([id]); + } else { + sel.remove(id); + } Ok(()) })?; } diff --git a/crates/engine/src/tests/brushsym.rs b/crates/engine/src/tests/brushsym.rs index a2e9eb3012..706bf2a508 100644 --- a/crates/engine/src/tests/brushsym.rs +++ b/crates/engine/src/tests/brushsym.rs @@ -1037,6 +1037,40 @@ fn brush_freehand_direct_call_has_one_undo_step() { assert_eq!(s.doc().unwrap().history.undo.last().map(|e| e.label.as_str()), Some("Paintbrush"), "a stroke with a brush is a Paintbrush step"); } +/// A `brush.freehand` call that continues a path by one point changes only the path's brush, and +/// applies `keepSelected` as a longer continuation does: off, the path is deselected while the +/// rest of the selection stays; on, the path is selected. A call that leaves the brush as it is +/// changes neither the selection nor the undo history. +#[test] +fn brush_freehand_continued_by_one_point_follows_keep_selected() { + let mut s = session(); + let r = s.execute("brush.freehand", &json!({"points": [[10, 10], [60, 40], [120, 10]], "style": "brush", "brush": "Charcoal"})).unwrap(); + let p = NodeId(r["id"].as_u64().unwrap()); + let a = rect(&mut s, 200.0, 200.0, 10.0, 10.0); + let extend = json!({"id": p.0, "end": "end"}); + + select(&mut s, &[p, a]); + let off = json!({"points": [[120, 10]], "extend": extend, "brush": "Dots", "keepSelected": false}); + assert_eq!(s.execute("brush.freehand", &off).unwrap()["id"], json!(p.0)); + assert_eq!(brush_of(&s, p).as_deref(), Some("Dots")); + assert_eq!(selected(&s), [a], "Keep selected off: the path is deselected and the rest stays"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(brush_of(&s, p).as_deref(), Some("Charcoal"), "one Undo takes back the brush"); + assert_eq!(selected(&s), [p, a], "Undo gives back the earlier selection"); + + select(&mut s, &[a]); + let on = json!({"points": [[120, 10]], "extend": extend, "brush": "Dots"}); + s.execute("brush.freehand", &on).unwrap(); + assert_eq!(brush_of(&s, p).as_deref(), Some("Dots")); + assert_eq!(selected(&s), [p], "Keep selected on: the continued path is selected"); + + select(&mut s, &[a]); + let undo = s.doc().unwrap().history.undo.len(); + s.execute("brush.freehand", &on).unwrap(); + assert_eq!(selected(&s), [a], "the same brush again: the selection stays"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "the same brush again: no undo step"); +} + /// `brush.freehand` refuses a brush for a path with no stroke (a continued path, a redrawn one /// and a new filled pencil path while the stroke is None) instead of reporting it painted: the /// document, the selection and the undo history stay as they were. Without a brush the path is