Eraser: Size follows the pen pressure too (#3961)

Eraser Tool Options' Size takes Fixed or Pressure and a Variation as the
Blob Brush's does: the tool records the pen, and path.eraseRegion outlines
the squashed drag at the pen's widths, stretched back to the tip's angle
and roundness. The pressure parsing and outline move to draw2's common
helpers, shared by both commands.
This commit is contained in:
bflatastic
2026-10-30 01:13:20 +07:00
parent 4a89ba84f8
commit 71fbdf5ff0
11 changed files with 154 additions and 89 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Eraser",
[],
None,
"{points: [[x,y]…], size?: pt (10), angle?: ° (0), roundness?: % (100, kept within 1–100)} erase the stroke of an elliptical tip from paths (selected, or all) → {ids}. The tip is `size` across its long axis, which runs at `angle` counter-clockwise from horizontal; its short axis is `roundness` percent of the long one",
"{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1), size?: pt (10), sizeMode?: \"fixed\" (default) | \"pressure\" (Eraser Tool Options' Size set to Pressure: the tip is size − sizeVariation across where the pen presses lightest and size + sizeVariation where it presses hardest, as path.blob), sizeVariation?: pt (0), angle?: ° (0), roundness?: % (100, kept within 1–100)} erase the stroke of an elliptical tip from paths (selected, or all) → {ids}. The tip is `size` across its long axis, which runs at `angle` counter-clockwise from horizontal; its short axis is `roundness` percent of the long one",
has_doc,
erase_region
),
+32 -1
View File
@@ -1,7 +1,8 @@
use super::{
Appearance, Arc, Beside, Cap, Color, Document, EngineError, FillRule, Join, Node, NodeId, NodeKind, Paint, PathData, Point, PressureProfile,
Rect, Result, Selection, Session, SubPath, Value, Vec2, bad, index, json, po,
Rect, Result, Selection, Session, SubPath, Value, Vec2, bad, index, json, number_or, po,
};
use vectorcraft_doc::{LineCap, LineJoin, StrokeLayer, WidthProfile};
// ---------- helpers ----------
@@ -64,6 +65,36 @@ pub(super) fn stroke_pressure(samples: &[(Point, Option<f64>)]) -> Option<Pressu
PressureProfile::from_samples(along.into_iter().map(|(s, p)| (if at > 1e-9 { s / at } else { 0.0 }, p)))
}
/// Blob Brush and Eraser Tool Options' Size set to Pressure (`sizeMode: "pressure"`, with
/// `sizeVariation` in points): the stroke's pen pressure along it and the variation, `None` for
/// Fixed (the default), no variation or a stroke without pressure (a mouse), which keeps the size.
pub(super) fn size_pressure(p: &Value, samples: &[(Point, Option<f64>)], cmd: &str) -> Result<Option<(PressureProfile, f64)>> {
match p.get("sizeMode").filter(|v| !v.is_null()).map(|v| v.as_str().ok_or(v)) {
None | Some(Ok("fixed")) => Ok(None),
Some(Ok("pressure")) => {
let variation = number_or(p, "sizeVariation", 0.0, cmd)?;
if !(variation.is_finite() && variation >= 0.0) {
return Err(bad(cmd, "sizeVariation must be 0 or more"));
}
Ok(stroke_pressure(samples).filter(|_| variation > 0.0).map(|prof| (prof, variation)))
}
Some(Ok(other)) => Err(bad(cmd, format!("sizeMode is \"fixed\" or \"pressure\", not `{other}`"))),
Some(Err(other)) => Err(bad(cmd, format!("sizeMode is \"fixed\" or \"pressure\", not {other}"))),
}
}
/// The area a stroke along `spine` covers with Size set to Pressure: as wide at each point as
/// [`vectorcraft_tools::draw2::pressure_size`] gives for the pressure `prof` records there, with
/// round ends and joins, its overlaps united into one outline.
pub(super) fn pressure_region(spine: &PathData, size: f64, variation: f64, prof: &PressureProfile) -> Result<PathData> {
let widest = size + variation;
let width = |t: f64| vectorcraft_tools::draw2::pressure_size(size, variation, prof.at(t)) / widest;
let profile = WidthProfile { points: prof.points.iter().map(|&(t, _)| (t, width(t), width(t))).collect() };
let round = StrokeLayer { cap: LineCap::Round, join: LineJoin::Round, ..StrokeLayer::new(Paint::None, widest) };
let outline = PathData::from_bezpath(&vectorcraft_render::width_outline(&spine.to_bezpath(), widest, &profile, &round, 0.05));
po::try_unite_all(&[(&outline, FillRule::NonZero)]).map_err(|e| EngineError::Other(e.to_string()))
}
/// The length of a subpath.
pub(super) fn subpath_len(sp: &SubPath) -> f64 {
use kurbo::ParamCurveArclen;
+26 -9
View File
@@ -1,6 +1,6 @@
use super::common::{
dist_to_polyline, fill_targets, ids_json, path_mut, path_of, points_param, poly_bounds, replace_with_pieces, sibling_with, target_paths,
usize_opt,
dist_to_polyline, fill_targets, ids_json, path_mut, path_of, points_param, poly_bounds, pressure_region, replace_with_pieces, samples_param,
sibling_with, size_pressure, target_paths, usize_opt,
};
use super::{
Anchor, AnchorKind, Beside, BezPath, BoolOp, Cap, EngineError, FillRule, Join, NodeId, ParamCurve, PathData, Point, Result, Session, SubPath,
@@ -264,30 +264,47 @@ pub(super) fn knife(s: &mut Session, p: &Value) -> Result<Value> {
pub(super) fn erase_region(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.eraseRegion";
let pts = points_param(p, "points", C)?;
let samples = samples_param(p, "points", C)?;
let size = number_or(p, "size", 10.0, C)?;
if !(size.is_finite() && size > 0.0) {
return Err(bad(C, "size must be positive"));
}
let angle = number_or(p, "angle", 0.0, C)?;
let roundness = number_or(p, "roundness", 100.0, C)?;
let pressure = size_pressure(p, &samples, C)?;
let pts: Vec<Point> = samples.into_iter().map(|(pt, _)| pt).collect();
let r = size / 2.0;
// The tip's short axis: an open path is sampled at least every half of it.
let short = size * roundness.clamp(1.0, 100.0) / 100.0;
// The tip's short axis (its narrowest with Pressure): an open path is sampled at least every half of it.
let narrowest = pressure.as_ref().map_or(size, |(_, v)| vectorcraft_tools::draw2::pressure_size(size, *v, 0.0));
let short = narrowest * roundness.clamp(1.0, 100.0) / 100.0;
let st = s.doc()?;
let region = eraser_area(&pts, size, angle, roundness);
// A point is under the tip when, with the tip squashed back into a circle, it is within the
// circle's radius of the squashed drag.
let squash = eraser_tip(angle, roundness).inverse();
let stretch = eraser_tip(angle, roundness);
let squash = stretch.inverse();
let round: Vec<Point> = pts.iter().map(|q| squash * *q).collect();
// With Size set to Pressure the tip is as wide as the pen pressed: the area is the squashed
// drag outlined at those widths, stretched back, and a point is under it when the area holds it.
let pressed = match (&pressure, round.len() > 1) {
(Some((prof, v)), true) => Some(pressure_region(&PathData::single(SubPath::polyline(&round, false)), size, *v, prof)?.transformed(stretch)),
(Some((prof, v)), false) => Some(eraser_area(&pts, vectorcraft_tools::draw2::pressure_size(size, *v, prof.at(0.0)), angle, roundness)),
(None, _) => None,
};
let reach = pressure.as_ref().map_or(r, |(_, v)| (size + v) / 2.0);
let region = pressed.clone().unwrap_or_else(|| eraser_area(&pts, size, angle, roundness));
let outline = pressed.map(|pd| pd.to_bezpath());
let under = |q: Point| match &outline {
Some(bp) => fill_contains(bp, FillRule::NonZero, q),
None => dist_to_polyline(&round, squash * q) <= r,
};
let mut plan: Vec<(NodeId, Vec<PathData>)> = vec![];
for (id, pd, rule) in fill_targets(&st.doc, &st.selection, poly_bounds(&pts, r))? {
for (id, pd, rule) in fill_targets(&st.doc, &st.selection, poly_bounds(&pts, reach))? {
if pd.is_closed() {
if po::area(&po::boolean(&pd, rule, &region, FillRule::NonZero, BoolOp::Intersect), FillRule::NonZero) <= 1e-6 {
continue;
}
plan.push((id, components(&po::boolean(&pd, rule, &region, FillRule::NonZero, BoolOp::Difference))));
} else if let Some(pieces) = erase_outline(&pd, short / 2.0, &|q| dist_to_polyline(&round, squash * q) <= r) {
} else if let Some(pieces) = erase_outline(&pd, short / 2.0, &under) {
plan.push((id, pieces));
}
}
+4 -30
View File
@@ -1,13 +1,12 @@
use super::common::{
brush_region, path_kind, path_mut, path_of, poly_bounds, samples_param, stroke_paint, stroke_pressure, subpath_len, target_paths,
within_input_bound,
brush_region, path_kind, path_mut, path_of, poly_bounds, pressure_region, samples_param, size_pressure, stroke_paint, stroke_pressure,
subpath_len, target_paths, within_input_bound,
};
use super::{
Anchor, Appearance, BTreeSet, BoolOp, Cap, Color, Document, EngineError, FillRule, Join, NewArt, Node, NodeId, NodeKind, Paint, PathData, Point,
PressureProfile, Result, Selection, Session, SimplifyOptions, SubPath, Value, bad, id_param, insert_look, json, number_or, po, remove_each,
str_param, switch_or,
};
use vectorcraft_doc::{LineCap, LineJoin, StrokeLayer, WidthProfile};
// ---------- Pencil / Paintbrush ----------
@@ -367,18 +366,6 @@ fn adjacent_to_topmost(doc: &Document, touched: &[NodeId], paint: &Paint) -> Vec
out
}
/// The area a Blob Brush stroke along `spine` covers with Size set to Pressure: as wide at each
/// point as [`vectorcraft_tools::draw2::blob_pressure_size`] gives for the pressure `prof` records
/// there, with round ends and joins, its overlaps united into one outline.
fn pressure_region(spine: &PathData, size: f64, variation: f64, prof: &PressureProfile) -> Result<PathData> {
let widest = size + variation;
let width = |t: f64| vectorcraft_tools::draw2::blob_pressure_size(size, variation, prof.at(t)) / widest;
let profile = WidthProfile { points: prof.points.iter().map(|&(t, _)| (t, width(t), width(t))).collect() };
let round = StrokeLayer { cap: LineCap::Round, join: LineJoin::Round, ..StrokeLayer::new(Paint::None, widest) };
let outline = PathData::from_bezpath(&vectorcraft_render::width_outline(&spine.to_bezpath(), widest, &profile, &round, 0.05));
po::try_unite_all(&[(&outline, FillRule::NonZero)]).map_err(|e| EngineError::Other(e.to_string()))
}
pub(super) fn blob(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.blob";
let samples = samples_param(p, "points", C)?;
@@ -386,20 +373,7 @@ pub(super) fn blob(s: &mut Session, p: &Value) -> Result<Value> {
if !(size.is_finite() && size > 0.0) {
return Err(bad(C, "size must be positive"));
}
// Size set to Pressure: the width follows the pen from size − sizeVariation to size +
// sizeVariation. A stroke without pressure (a mouse) keeps the size.
let pressure = match p.get("sizeMode").filter(|v| !v.is_null()).map(|v| v.as_str().ok_or(v)) {
None | Some(Ok("fixed")) => None,
Some(Ok("pressure")) => {
let variation = number_or(p, "sizeVariation", 0.0, C)?;
if !(variation.is_finite() && variation >= 0.0) {
return Err(bad(C, "sizeVariation must be 0 or more"));
}
stroke_pressure(&samples).filter(|_| variation > 0.0).map(|prof| (prof, variation))
}
Some(Ok(other)) => return Err(bad(C, format!("sizeMode is \"fixed\" or \"pressure\", not `{other}`"))),
Some(Err(other)) => return Err(bad(C, format!("sizeMode is \"fixed\" or \"pressure\", not {other}"))),
};
let pressure = size_pressure(p, &samples, C)?;
let pts: Vec<Point> = samples.into_iter().map(|(pt, _)| pt).collect();
// The widest the brush gets: the area merges look in.
let reach = pressure.as_ref().map_or(size, |(_, v)| size + v);
@@ -426,7 +400,7 @@ pub(super) fn blob(s: &mut Session, p: &Value) -> Result<Value> {
}
}
// A click: one dab as wide as the pen pressed.
(Some((prof, variation)), false) => brush_region(&pts, vectorcraft_tools::draw2::blob_pressure_size(size, *variation, prof.at(0.0))),
(Some((prof, variation)), false) => brush_region(&pts, vectorcraft_tools::draw2::pressure_size(size, *variation, prof.at(0.0))),
(None, _) => {
let fitted = fidelity
.filter(|_| pts.len() > 1)
+21
View File
@@ -503,6 +503,27 @@ fn blob_brush_size_follows_the_pen_pressure() {
assert!(inside(&s, drawn, 290.0, 707.0) && !inside(&s, drawn, 110.0, 703.0), "the tool's stroke follows the pen");
}
/// Eraser Tool Options' Size set to Pressure (#3961): the tip is Size − Variation across where the
/// pen pressed lightest and Size + Variation where it pressed hardest; Fixed erases at the Size.
#[test]
fn eraser_size_follows_the_pen_pressure() {
let pressed = |y: f64| (0..=20).map(|i| json!([100.0 + f64::from(i) * 10.0, y, f64::from(i) / 20.0])).collect::<Vec<_>>();
for (mode, thin_end_erased) in [("pressure", false), ("fixed", true)] {
let mut s = session();
rect(&mut s, 0.0, 0.0, 400.0, 200.0);
s.execute("path.eraseRegion", &json!({"points": pressed(100.0), "size": 10, "sizeMode": mode, "sizeVariation": 8})).unwrap();
let filled = |x: f64, y: f64| {
paths(&s).iter().any(|(_, pd)| vectorcraft_geom::hit::fill_contains(&pd.to_bezpath(), FillRule::NonZero, Point::new(x, y)))
};
assert!(!filled(290.0, 107.0), "{mode}: erased where the pen pressed hard");
assert_eq!(!filled(110.0, 103.0), thin_end_erased, "{mode}: the light end");
assert!(filled(110.0, 150.0), "{mode}: the rest stays");
}
let mut s = session();
let err = s.execute("path.eraseRegion", &json!({"points": [[0, 0], [10, 0]], "sizeMode": 5})).unwrap_err();
assert!(err.to_string().contains("sizeMode"), "{err}");
}
/// A Blob Brush stroke merges with the topmost matching path it touches and with the touched
/// matching paths next to it in stacking order in the same group or layer; matching paths in other
/// layers or groups, or below an object of another kind, are left alone.
+6 -6
View File
@@ -38,15 +38,15 @@ pub fn min_fidelity(id: &str) -> f64 {
if id == "smooth" { SMOOTH_MIN_FIDELITY } else { MIN_FIDELITY }
}
/// The Blob Brush's width at pen `pressure` (0..1) with Size set to Pressure: `size` −
/// The Blob Brush's or the Eraser's width at pen `pressure` (0..1) with Size set to Pressure: `size` −
/// `variation` at the lightest pressure to `size` + `variation` at the heaviest, at least
/// [`MIN_BLOB_SIZE`].
pub fn blob_pressure_size(size: f64, variation: f64, pressure: f64) -> f64 {
(size + variation * (2.0 * pressure.clamp(0.0, 1.0) - 1.0)).max(MIN_BLOB_SIZE)
/// [`MIN_BRUSH_SIZE`].
pub fn pressure_size(size: f64, variation: f64, pressure: f64) -> f64 {
(size + variation * (2.0 * pressure.clamp(0.0, 1.0) - 1.0)).max(MIN_BRUSH_SIZE)
}
/// The narrowest a Blob Brush stroke gets, in points (its Size field's minimum).
pub const MIN_BLOB_SIZE: f64 = 0.1;
/// The narrowest a Blob Brush or Eraser stroke gets, in points (their Size field's minimum).
pub const MIN_BRUSH_SIZE: f64 = 0.1;
/// Create a drawing tool by id (None = not one of ours).
pub fn create(id: &str) -> Option<Box<dyn Tool>> {
+37 -17
View File
@@ -10,8 +10,8 @@
//! Smart Guides ([`DrawSnap`]: an anchor, a path, in line with the art), hovering too; the points
//! 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, and
//! so does the Blob Brush while its Size varies with Pressure. The other tools collect the drag polyline (shown as an overlay) and run
//! one command on release. With Shift held, the Blob Brush's points stay on the horizontal or
//! so do the Blob Brush and the Eraser while their Size varies with Pressure. The other tools
//! collect the drag polyline (shown as an overlay) and run one command on release. With Shift held, the Blob Brush's points stay on the horizontal or
//! vertical line through the press point. Alt held at the press, or before the Blob Brush's drag
//! has moved, switches the Blob Brush to the Smooth tool: the drag smooths the selected paths.
@@ -38,8 +38,8 @@ pub struct GestureTool {
previewed: usize,
id: &'static str,
points: Vec<Point>,
/// The pen pressure of each of `points` (the Paintbrush, and the Blob Brush with
/// `size_pressure`).
/// The pen pressure of each of `points` (the Paintbrush, and the Blob Brush and the Eraser
/// with `size_pressure`).
pressures: Vec<f32>,
active: bool,
began: bool,
@@ -58,11 +58,11 @@ pub struct GestureTool {
pub fidelity: f64,
/// Brush / eraser diameter in points (the Eraser's across its tip's long axis).
pub size: f64,
/// Blob Brush Tool Options' Size set to Pressure (else Fixed): the brush is `size` −
/// Blob Brush and Eraser Tool Options' Size set to Pressure (else Fixed): the tip is `size` −
/// `size_variation` wide at the lightest pen pressure and `size` + `size_variation` at the
/// heaviest.
pub size_pressure: bool,
/// Blob Brush Tool Options' Size Variation, in points.
/// Blob Brush and Eraser Tool Options' Size Variation, in points.
pub size_variation: f64,
/// The angle of the Eraser tip's long axis, in degrees counter-clockwise from horizontal.
pub angle: f64,
@@ -184,7 +184,7 @@ impl GestureTool {
/// Add a stroke sample.
fn push(&mut self, p: Point, pressure: f32) {
self.points.push(p);
if self.id == "paintbrush" || (self.id == "blobBrush" && self.size_pressure) {
if self.id == "paintbrush" || (self.varies_size() && self.size_pressure) {
self.pressures.push(pressure);
}
}
@@ -245,11 +245,17 @@ impl GestureTool {
self.shift_line = None;
}
/// The pen pressure of each point of a Blob Brush stroke whose Size varies with Pressure, when
/// a pen pressed less than fully anywhere along it (a stroke without pressure keeps its Size).
fn blob_pressures(&self) -> Option<&[f32]> {
/// Does the tool's Size take Fixed or Pressure (the Blob Brush and the Eraser)?
fn varies_size(&self) -> bool {
matches!(self.id, "blobBrush" | "eraser")
}
/// The pen pressure of each point of a Blob Brush or Eraser stroke whose Size varies with
/// Pressure, when a pen pressed less than fully anywhere along it (a stroke without pressure
/// keeps its Size).
fn size_pressures(&self) -> Option<&[f32]> {
let pressed = self.pressures.len() == self.points.len() && self.pressures.iter().any(|p| *p < 1.0);
(self.id == "blobBrush" && self.size_pressure && !self.smoothing && pressed).then_some(self.pressures.as_slice())
(self.varies_size() && self.size_pressure && !self.smoothing && pressed).then_some(self.pressures.as_slice())
}
/// The stroke's points, each `[x, y, pressure]` when a pen pressed less than fully anywhere
@@ -401,7 +407,14 @@ impl GestureTool {
)],
"eraser" => vec![Action::Exec(
"path.eraseRegion".into(),
json!({"points": points_json(pts), "size": self.size, "angle": self.angle, "roundness": self.roundness}),
json!({
"points": self.stroke_json(),
"size": self.size,
"sizeMode": if self.size_pressure { "pressure" } else { "fixed" },
"sizeVariation": self.size_variation,
"angle": self.angle,
"roundness": self.roundness
}),
)],
"knife" if pts.len() >= 2 => vec![Action::Exec("path.knife".into(), json!({"points": points_json(pts)}))],
"smooth" if has_sel && pts.len() >= 2 => {
@@ -635,16 +648,17 @@ impl Tool for GestureTool {
};
let color = if self.id == "eraser" { [0x80, 0x80, 0x80] } else { FEEDBACK };
// A tip that is not round shows the outline of the area it covers.
// shortcut: at its Size even with Pressure; the release erases at the pen's widths.
if self.id == "eraser" && self.roundness < 100.0 {
let area = eraser_area(&self.points, self.size, self.angle, self.roundness);
o.push(Overlay::Path { path: area.to_bezpath(), color, width: 1.0, dashed: false });
return o;
}
// With Size set to Pressure, each step of the drag shows as wide as the pen pressed there.
if let Some(pressures) = self.blob_pressures() {
if let Some(pressures) = self.size_pressures() {
for (seg, p) in self.points.windows(2).zip(pressures.windows(2)) {
let pressure = f64::from(p[0] + p[1]) / 2.0;
let width = super::blob_pressure_size(self.size, self.size_variation, pressure) * cx.zoom;
let width = super::pressure_size(self.size, self.size_variation, pressure) * cx.zoom;
o.push(Overlay::Path { path: polyline(seg), color, width: width as f32, dashed: false });
}
} else {
@@ -727,7 +741,13 @@ impl Tool for GestureTool {
"mergeOnlySelection": self.merge_only_selection
})
}
"eraser" => json!({"size": self.size, "angle": self.angle, "roundness": self.roundness}),
"eraser" => json!({
"size": self.size,
"sizeMode": if self.size_pressure { "pressure" } else { "fixed" },
"sizeVariation": self.size_variation,
"angle": self.angle,
"roundness": self.roundness
}),
_ => Value::Null,
}
}
@@ -735,12 +755,12 @@ impl Tool for GestureTool {
match key {
"fidelity" => self.fidelity = v.as_f64().unwrap_or(self.fidelity).clamp(super::min_fidelity(self.id), 100.0),
"size" => self.size = v.as_f64().unwrap_or(self.size).clamp(0.1, 1000.0),
"sizeMode" if self.id == "blobBrush" => match v.as_str() {
"sizeMode" if self.varies_size() => match v.as_str() {
Some("pressure") => self.size_pressure = true,
Some("fixed") => self.size_pressure = false,
_ => {}
},
"sizeVariation" if self.id == "blobBrush" => self.size_variation = v.as_f64().unwrap_or(self.size_variation).clamp(0.0, 1000.0),
"sizeVariation" if self.varies_size() => self.size_variation = v.as_f64().unwrap_or(self.size_variation).clamp(0.0, 1000.0),
"angle" => self.angle = v.as_f64().unwrap_or(self.angle).clamp(-180.0, 180.0),
"roundness" => self.roundness = v.as_f64().unwrap_or(self.roundness).clamp(1.0, 100.0),
"fill" => self.fill = v.as_bool().unwrap_or(self.fill),
+2 -2
View File
@@ -39,8 +39,8 @@ const ROWS: &[Row] = &[
// Paintbrush has none of them).
(&["pencil"], None, &["altSmooth", "livePreview", "liveFit"]),
(&["smooth"], None, &["fidelity"]),
(&["blobBrush", "eraser"], None, &["size"]),
(&["blobBrush"], None, &["fidelity", "keepSelected", "mergeOnlySelection", "sizeMode", "sizeVariation"]),
(&["blobBrush", "eraser"], None, &["size", "sizeMode", "sizeVariation"]),
(&["blobBrush"], None, &["fidelity", "keepSelected", "mergeOnlySelection"]),
// Eraser Tool Options' tip shape.
(&["eraser"], None, &["angle", "roundness"]),
// Live Paint Selection Tool Options (`builder::LivePaintSelectionTool`).
+6 -6
View File
@@ -8,9 +8,9 @@
//! Brush), `closeWithin` and `editWithin` (screen pixels, 0 turns it off; Pencil and Paintbrush),
//! `angle` (degrees) and `roundness` (percent; Eraser), `livePreview` (Pencil: the stroke shows
//! with its fill and stroke while it is drawn, or as a thin line until the release), `size`
//! (points; Blob Brush and Eraser), and `sizeMode` (`fixed` or `pressure`) and `sizeVariation` (points;
//! Blob Brush: with Pressure the brush is Size − Variation wide at the lightest pen pressure and Size +
//! Variation at the heaviest).
//! (points; Blob Brush and Eraser), and `sizeMode` (`fixed` or `pressure`) and `sizeVariation`
//! (points; Blob Brush and Eraser: with Pressure the tip is Size − Variation wide at the lightest
//! pen pressure and Size + Variation at the heaviest).
use serde_json::{Map, Value, json};
@@ -138,11 +138,11 @@ fn number(ui: &mut egui::Ui, d: &mut Dialog, (key, label): (&str, &str), suffix:
});
}
/// Width of the Blob Brush's Size mode menu.
/// Width of the Blob Brush's and the Eraser's Size mode menu.
const MODE_W: f32 = 96.0;
/// The Blob Brush's Size rows: Size with its Fixed or Pressure menu before the field, then the
/// Variation that Pressure adds and takes (greyed while Size is Fixed).
/// The Blob Brush's and the Eraser's Size rows: Size with its Fixed or Pressure menu before the
/// field, then the Variation that Pressure adds and takes (greyed while Size is Fixed).
fn blob_size(ui: &mut egui::Ui, d: &mut Dialog) {
const MODES: [&str; 2] = ["fixed", "pressure"];
let pressure = d.str("sizeMode") == "pressure";
+18 -16
View File
@@ -313,24 +313,26 @@ fn the_blob_brush_options_set_what_a_stroke_merges_with_and_selects() {
/// Eraser Tool Options' Angle and Roundness shape the tip the next Eraser drag erases with,
/// and the tool keeps them while another tool is chosen.
/// The Blob Brush's Size is Fixed or Pressure (#4590), with the Variation Pressure adds and takes;
/// OK sets both, and the Eraser has neither.
/// The Blob Brush's and the Eraser's Size is Fixed or Pressure (#4590, #3961), with the Variation
/// Pressure adds and takes; OK sets both, each tool keeping its own, and the Pencil has neither.
#[test]
fn the_blob_brush_size_can_follow_the_pen_pressure() {
fn the_blob_brush_and_eraser_size_can_follow_the_pen_pressure() {
let mut app = app();
app.run("tool.options", json!({"tool": "blobBrush"})).unwrap();
let text = painted(&mut app);
assert!(text.contains("Fixed") && text.contains("Variation:"), "{text}");
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((d.str("sizeMode"), d.f64("sizeVariation", -1.0)), ("fixed".to_string(), 0.0), "Fixed with no variation by default");
d.fields.insert("sizeMode".into(), json!("pressure"));
d.fields.insert("sizeVariation".into(), json!(6));
super::super::confirm(&mut app).unwrap();
let o = app.session.tool_options_of("blobBrush");
assert_eq!((o["sizeMode"].clone(), o["sizeVariation"].as_f64()), (json!("pressure"), Some(6.0)));
app.run("tool.options", json!({"tool": "blobBrush"})).unwrap();
assert!(painted(&mut app).contains("Pressure"));
app.run("tool.options", json!({"tool": "eraser"})).unwrap();
for (tool, variation) in [("blobBrush", 6.0), ("eraser", 4.0)] {
app.run("tool.options", json!({"tool": tool})).unwrap();
let text = painted(&mut app);
assert!(text.contains("Fixed") && text.contains("Variation:"), "{tool}: {text}");
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((d.str("sizeMode"), d.f64("sizeVariation", -1.0)), ("fixed".to_string(), 0.0), "{tool}: Fixed with no variation by default");
d.fields.insert("sizeMode".into(), json!("pressure"));
d.fields.insert("sizeVariation".into(), json!(variation));
super::super::confirm(&mut app).unwrap();
let o = app.session.tool_options_of(tool);
assert_eq!((o["sizeMode"].clone(), o["sizeVariation"].as_f64()), (json!("pressure"), Some(variation)), "{tool}");
app.run("tool.options", json!({"tool": tool})).unwrap();
assert!(painted(&mut app).contains("Pressure"), "{tool}");
}
app.run("tool.options", json!({"tool": "pencil"})).unwrap();
let text = painted(&mut app);
assert!(!text.contains("Variation:") && !text.contains("Fixed"), "{text}");
}
+1 -1
View File
@@ -2606,7 +2606,7 @@ These commands refuse a string or another value that is not a number with `` `dy
naming the key; the document does not change and no undo step is recorded: `t`, `dx` and `dy` in
`path.reshapeSegment` and `object.blend.spine.reshapeSegment`; `x`, `y` and `t` in `path.curvatureEdit`; `x` and `y`
in `path.convertAnchor`; `t` in `path.insertAnchor`; `fidelity` and the `within` of `redraw` in `path.freehand` (and in
`brush.freehand`, which runs it); `size`, `angle` and `roundness` in `path.eraseRegion`;
`brush.freehand`, which runs it); `size`, `sizeVariation`, `angle` and `roundness` in `path.eraseRegion`;
`size`, `sizeVariation` and `fidelity` in `path.blob`; `width` in `path.eraseSegments`;
`radius` and `fidelity` in `path.smoothRegion`; `tolerance` in `path.joinScrub`; `dx` and `dy` in `path.moveAnchors`;
and `dx`, `dy`, `tol` and `radius` in `path.reshape`. A null is read as a key left out. `path.convertAnchor` refuses