Blob Brush: Keep selected and Merge only with selection options

A Blob Brush stroke merged with every touching closed path of its
color that had no stroke, and the blob it drew or merged into was
always left selected. The Blob Brush Tool Options had Size only.

The Blob Brush Tool Options now have Keep selected and Merge only
with selection, both off by default. A blob that a stroke draws or
merges into is no longer left selected unless Keep selected is on,
and the rest of the selection stays as it was. While Merge only with
selection is on, a stroke merges only with selected blobs, and with
nothing selected it draws a new blob. The options persist like the
tool's Size, and path.blob takes keepSelected and mergeOnlySelection,
both false by default. Keep selected stays on by default for the
Pencil and Paintbrush.

Docs: docs/target-app-parity.md, docs/control-protocol.md.
This commit is contained in:
dotCooCoo
2026-10-16 15:27:53 +07:00
committed by bflatastic
parent f0d3f81f3b
commit ddfd08f321
27 changed files with 190 additions and 21 deletions
+41 -4
View File
@@ -33,8 +33,11 @@ pub struct GestureTool {
pub size: f64,
/// Fill new pencil/brush strokes.
pub fill: bool,
/// "Keep selected": select the path a Pencil or Paintbrush stroke draws or continues.
/// "Keep selected": select the path a Pencil, Paintbrush or Blob Brush stroke draws, continues
/// or merges into (on for the Pencil and Paintbrush, off for the Blob Brush).
pub keep_selected: bool,
/// "Merge only with selection": a Blob Brush stroke merges only with selected paths.
pub merge_only_selection: bool,
/// "Edit selected paths within N pixels".
pub edit_within: f64,
/// "Close paths when ends are within N pixels".
@@ -66,7 +69,8 @@ impl GestureTool {
fidelity,
size: 10.0,
fill: false,
keep_selected: true,
keep_selected: id != "blobBrush",
merge_only_selection: false,
edit_within: 12.0,
close_within: 15.0,
snap: DrawSnap::default(),
@@ -168,7 +172,15 @@ impl GestureTool {
let closed = m.alt || (self.extend.is_none() && near);
vec![Action::Preview("path.freehand".into(), self.freehand_params(closed)), Action::Commit]
}
"blobBrush" => vec![Action::Exec("path.blob".into(), json!({"points": points_json(pts), "size": self.size}))],
"blobBrush" => vec![Action::Exec(
"path.blob".into(),
json!({
"points": points_json(pts),
"size": self.size,
"keepSelected": self.keep_selected,
"mergeOnlySelection": self.merge_only_selection
}),
)],
"eraser" => vec![Action::Exec("path.eraseRegion".into(), json!({"points": points_json(pts), "size": self.size}))],
"knife" if pts.len() >= 2 => vec![Action::Exec("path.knife".into(), json!({"points": points_json(pts)}))],
"smooth" if has_sel && pts.len() >= 2 => {
@@ -314,7 +326,8 @@ impl Tool for GestureTool {
})
}
"smooth" => json!({"fidelity": self.fidelity}),
"blobBrush" | "eraser" => json!({"size": self.size}),
"blobBrush" => json!({"size": self.size, "keepSelected": self.keep_selected, "mergeOnlySelection": self.merge_only_selection}),
"eraser" => json!({"size": self.size}),
_ => Value::Null,
}
}
@@ -324,6 +337,7 @@ impl Tool for GestureTool {
"size" => self.size = v.as_f64().unwrap_or(self.size).clamp(0.1, 1000.0),
"fill" => self.fill = v.as_bool().unwrap_or(self.fill),
"keepSelected" => self.keep_selected = v.as_bool().unwrap_or(self.keep_selected),
"mergeOnlySelection" => self.merge_only_selection = v.as_bool().unwrap_or(self.merge_only_selection),
"editWithin" => self.edit_within = v.as_f64().unwrap_or(self.edit_within).clamp(0.0, 100.0),
"closeWithin" => self.close_within = v.as_f64().unwrap_or(self.close_within).clamp(0.0, 100.0),
_ => {}
@@ -426,6 +440,29 @@ mod tests {
}
}
/// The Blob Brush has Keep selected and Merge only with selection, both off by default, and its
/// stroke sends both.
#[test]
fn blob_brush_sends_keep_selected_and_merge_only_with_selection() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = GestureTool::new("blobBrush");
assert_eq!((t.options()["keepSelected"].clone(), t.options()["mergeOnlySelection"].clone()), (json!(false), json!(false)));
for (keep, only) in [(false, false), (true, false), (false, true)] {
t.set_option("keepSelected", &json!(keep));
t.set_option("mergeOnlySelection", &json!(only));
let acts = drag(&mut t, &cx, &[(50.0, 150.0), (250.0, 150.0)], Mods::default());
assert!(
matches!(&acts[2][0], Action::Exec(c, v) if c == "path.blob" && v["keepSelected"] == keep && v["mergeOnlySelection"] == only),
"{acts:?}"
);
}
// The Eraser keeps Size only.
assert_eq!(GestureTool::new("eraser").options(), json!({"size": 10.0}));
}
#[test]
fn pencil_alt_closes_and_continues_selected_path() {
let (d, id) = doc_with_rect();
+1
View File
@@ -33,6 +33,7 @@ const ROWS: &[Row] = &[
(&["pencil", "paintbrush"], None, &["fidelity", "fill", "keepSelected", "editWithin", "closeWithin"]),
(&["smooth"], None, &["fidelity"]),
(&["blobBrush", "eraser"], None, &["size"]),
(&["blobBrush"], None, &["keepSelected", "mergeOnlySelection"]),
(&["polygon"], None, &["sides"]),
(&["star"], None, &["points"]),
(&["freeTransform"], None, &["constrain"]),