From bb6a8076f3b20e8869264c3a19e16a6ba0c80a41 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 15 Nov 2026 06:55:16 -0800 Subject: [PATCH] Brushes: a one-point brush.freehand continuation follows keepSelected A brush.freehand call that continued a path by one point changed the path's brush and left the selection as it was. With keepSelected off the path stayed selected, and with it on the path was not selected, unlike a longer continuation and unlike what path.freehand's params text says. The brush edit now selects the path, or deselects it while the rest of the selection stays, as a longer continuation does. A call that leaves the brush as it is still changes nothing. --- crates/engine/src/cmd/draw2/freehand.rs | 10 ++++++-- crates/engine/src/tests/brushsym.rs | 34 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) 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