Merge pull request #906 from storytold/feat/852-pressure-brushes

Part of #852: Brush Options for Calligraphic and Bristle brushes, pen pressure
This commit is contained in:
BFlat
2026-10-10 12:38:10 +07:00
committed by GitHub
29 changed files with 1333 additions and 82 deletions
+61 -8
View File
@@ -5,7 +5,9 @@
//! and commit on release; Alt on release (or ending near the start) closes the path, and starting
//! near an end of a selected open path continues it. The Pencil's new path starts and ends on
//! Smart Guides ([`DrawSnap`]: an anchor, a path, in line with the art), hovering too; the points
//! between follow the hand. The other tools collect the drag polyline (shown as an overlay) and run
//! between follow the hand. The Paintbrush records the pen pressure of each point (a stroke a pen
//! pressed less than fully on sends `[x, y, pressure]` points) for pressure-sensitive brushes. The
//! other tools collect the drag polyline (shown as an overlay) and run
//! one command on release.
use serde_json::{Value, json};
@@ -19,6 +21,8 @@ use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, Tool
pub struct GestureTool {
id: &'static str,
points: Vec<Point>,
/// The pen pressure of each of `points` (the Paintbrush only).
pressures: Vec<f32>,
active: bool,
began: bool,
/// Pencil/Paintbrush continuing a selected open path: (path, continue from its start).
@@ -53,6 +57,7 @@ impl GestureTool {
Self {
id,
points: vec![],
pressures: vec![],
active: false,
began: false,
extend: None,
@@ -74,6 +79,30 @@ impl GestureTool {
matches!(self.id, "pencil" | "paintbrush")
}
/// Add a stroke sample.
fn push(&mut self, p: Point, pressure: f32) {
self.points.push(p);
if self.id == "paintbrush" {
self.pressures.push(pressure);
}
}
/// Forget the stroke.
fn clear(&mut self) {
self.points.clear();
self.pressures.clear();
}
/// The stroke's points, each `[x, y, pressure]` when a pen pressed less than fully anywhere
/// along it (a mouse always presses fully).
fn stroke_json(&self) -> Value {
if self.pressures.len() == self.points.len() && self.pressures.iter().any(|p| *p < 1.0) {
Value::Array(self.points.iter().zip(&self.pressures).map(|(p, f)| json!([p.x, p.y, (f64::from(*f) * 1000.0).round() / 1000.0])).collect())
} else {
points_json(&self.points)
}
}
fn label(&self) -> &'static str {
match self.id {
"paintbrush" => "Paintbrush",
@@ -110,7 +139,7 @@ impl GestureTool {
/// Parameters of `path.freehand` for the current stroke.
pub fn freehand_params(&self, closed: bool) -> Value {
let mut v = json!({
"points": points_json(&self.points),
"points": self.stroke_json(),
"fidelity": self.fidelity,
"closed": closed,
"style": if self.id == "paintbrush" { "brush" } else { "pencil" },
@@ -168,7 +197,8 @@ impl Tool for GestureTool {
self.extend = if self.draws_path() { self.find_extend(cx, p) } else { None };
// A path continued starts at its end; a new one where it snaps.
let start = if self.snaps() && self.extend.is_none() { self.snap.press(cx, p, &[], None) } else { p };
self.points = vec![start];
self.clear();
self.push(start, ev.pressure);
vec![]
}
PointerKind::Drag => {
@@ -182,7 +212,7 @@ impl Tool for GestureTool {
if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) {
return vec![];
}
self.points.push(p);
self.push(p, ev.pressure);
if !self.draws_path() {
return vec![];
}
@@ -201,7 +231,7 @@ impl Tool for GestureTool {
let end = if self.snaps() { self.snap.drag(cx, p, None) } else { p };
self.snap.clear();
if self.points.last().is_some_and(|l| l.distance(p) >= cx.tol(1.0)) {
self.points.push(end);
self.push(end, ev.pressure);
} else if self.snaps()
&& self.points.len() > 1
&& let Some(last) = self.points.last_mut()
@@ -212,7 +242,7 @@ impl Tool for GestureTool {
let out = self.finish(cx, ev.mods);
self.active = false;
self.began = false;
self.points.clear();
self.clear();
self.extend = None;
out
}
@@ -226,7 +256,7 @@ impl Tool for GestureTool {
self.snap.clear();
self.active = false;
self.began = false;
self.points.clear();
self.clear();
if began { vec![Action::Cancel] } else { vec![] }
}
ToolKey::BracketLeft if matches!(self.id, "blobBrush" | "eraser") => {
@@ -245,7 +275,7 @@ impl Tool for GestureTool {
self.snap.clear();
self.active = false;
self.began = false;
self.points.clear();
self.clear();
if began { vec![Action::Commit] } else { vec![] }
}
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
@@ -321,6 +351,29 @@ mod tests {
assert_eq!(v["points"][0], json!([102.0, 98.0]));
}
/// #852: the Paintbrush sends each point's pen pressure; a mouse (always fully pressed) and
/// the Pencil send plain points.
#[test]
fn paintbrush_sends_pen_pressure() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let stroke = |id: &str, pressures: [f32; 3]| {
let mut t = GestureTool::new(id);
let kinds = [PointerKind::Down, PointerKind::Drag, PointerKind::Up];
let mut last = vec![];
for ((k, x), f) in kinds.into_iter().zip([0.0, 40.0, 80.0]).zip(pressures) {
last = t.pointer(&cx, &PointerEvent { pressure: f, ..PointerEvent::new(k, x, 0.0) });
}
let Some(Action::Preview(_, v)) = last.first() else { panic!("{last:?}") };
v["points"].clone()
};
assert_eq!(stroke("paintbrush", [0.25, 0.5, 1.0]), json!([[0.0, 0.0, 0.25], [40.0, 0.0, 0.5], [80.0, 0.0, 1.0]]));
assert_eq!(stroke("paintbrush", [1.0; 3]), json!([[0.0, 0.0], [40.0, 0.0], [80.0, 0.0]]));
assert_eq!(stroke("pencil", [0.25, 0.5, 1.0]), json!([[0.0, 0.0], [40.0, 0.0], [80.0, 0.0]]));
}
#[test]
fn pencil_previews_and_commits() {
let (d, _) = doc_with_rect();