Blob Brush: Size follows the pen pressure (#4590)

Blob Brush Tool Options' Size takes Fixed or Pressure and a Variation, as
the brush options' Size does: with Pressure the brush is Size − Variation
wide where the pen presses lightest and Size + Variation where it presses
hardest. The tool records the pen pressure of each point and shows each
step of the drag as wide as the pen pressed there; path.blob takes
[x, y, pressure] points with sizeMode and sizeVariation and outlines the
stroke with that width profile. A stroke without pressure keeps the Size,
and Fixed with no variation stays the default.

https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/blob-brush-options-and-best-practices.html
This commit is contained in:
bflatastic
2026-10-30 01:13:13 +07:00
parent 31124c8709
commit c65286666c
9 changed files with 210 additions and 22 deletions
+1 -1
View File
@@ -128,7 +128,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Blob Brush",
[],
None,
"{points: [[x,y]…], size?: pt (10), fidelity?: pt (0.01–100; the brush follows the stroke fitted at this tolerance as path.freehand fits a Pencil stroke, and a higher value gives a smoother shape; without it the brush follows every point), merge?: true, mergeOnlySelection?: bool (false: merge with any touching same-color blob; true: only with selected ones), keepSelected?: bool (false: the drawn or merged path is not selected, and the rest of the selection stays; true: it is selected)} filled brush shape, merged with the topmost touching same-color blob and the touching ones next to it in stacking order in its group or layer → {id}",
"{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1), size?: pt (10), sizeMode?: \"fixed\" (default) | \"pressure\" (Blob Brush Tool Options' Size set to Pressure: the brush is size − sizeVariation wide at the lightest pen pressure and size + sizeVariation at the heaviest, at least 0.1 pt; a stroke without pressure keeps the size), sizeVariation?: pt (0), fidelity?: pt (0.01–100; the brush follows the stroke fitted at this tolerance as path.freehand fits a Pencil stroke, and a higher value gives a smoother shape; without it the brush follows every point), merge?: true, mergeOnlySelection?: bool (false: merge with any touching same-color blob; true: only with selected ones), keepSelected?: bool (false: the drawn or merged path is not selected, and the rest of the selection stays; true: it is selected)} filled brush shape, merged with the topmost touching same-color blob and the touching ones next to it in stacking order in its group or layer → {id}",
has_doc,
blob
),
+58 -8
View File
@@ -1,5 +1,5 @@
use super::common::{
brush_region, path_kind, path_mut, path_of, points_param, poly_bounds, samples_param, stroke_paint, stroke_pressure, subpath_len, target_paths,
brush_region, path_kind, path_mut, path_of, poly_bounds, samples_param, stroke_paint, stroke_pressure, subpath_len, target_paths,
within_input_bound,
};
use super::{
@@ -7,6 +7,7 @@ use super::{
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 ----------
@@ -366,13 +367,42 @@ 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 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"));
}
// 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 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);
// With `fidelity` (pt), the brush follows the stroke fitted by [`fit_freehand`], as a Pencil
// stroke is fitted. Without it, or when the fitted stroke has no length (a short stroke back to
// its start), the brush follows every point.
@@ -380,11 +410,31 @@ pub(super) fn blob(s: &mut Session, p: &Value) -> Result<Value> {
None | Some(Value::Null) => None,
Some(_) => Some(number_or(p, "fidelity", 0.0, C)?.clamp(vectorcraft_tools::draw2::MIN_FIDELITY, 100.0)),
};
let fitted = fidelity
.filter(|_| pts.len() > 1)
.map(|tol| po::outline_stroke(&PathData::single(fit_freehand(&pts, tol, false)), size, Cap::Round, Join::Round, 4.0))
.filter(|r| !r.is_empty());
let region = fitted.unwrap_or_else(|| brush_region(&pts, size));
let spine = |tol: Option<f64>| match tol {
Some(tol) => PathData::single(fit_freehand(&pts, tol, false)),
None => PathData::single(SubPath::polyline(&pts, false)),
};
let region = match (&pressure, pts.len() > 1) {
(Some((prof, variation)), true) => {
let fitted = match fidelity {
Some(tol) => Some(pressure_region(&spine(Some(tol)), size, *variation, prof)?).filter(|r| !r.is_empty()),
None => None,
};
match fitted {
Some(r) => r,
None => pressure_region(&spine(None), size, *variation, prof)?,
}
}
// 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))),
(None, _) => {
let fitted = fidelity
.filter(|_| pts.len() > 1)
.map(|tol| po::outline_stroke(&spine(Some(tol)), size, Cap::Round, Join::Round, 4.0))
.filter(|r| !r.is_empty());
fitted.unwrap_or_else(|| brush_region(&pts, size))
}
};
if region.is_empty() {
return Err(EngineError::Other("empty brush stroke".into()));
}
@@ -404,7 +454,7 @@ pub(super) fn blob(s: &mut Session, p: &Value) -> Result<Value> {
let all = Selection::default();
let from = if only_selection { &st.selection } else { &all };
// A fitted stroke can curve outside the points' bounds, so the area also takes in the shape.
let area = region.control_bounds().map_or_else(|| poly_bounds(&pts, size), |b| b.union(poly_bounds(&pts, size)));
let area = region.control_bounds().map_or_else(|| poly_bounds(&pts, reach), |b| b.union(poly_bounds(&pts, reach)));
for id in target_paths(&st.doc, from, area) {
let Some(n) = st.doc.node(id) else { continue };
if !blob_match(n, &paint) {
+38
View File
@@ -465,6 +465,44 @@ fn blob_brush_merges_only_with_the_selection_and_keep_selected_off_deselects() {
assert!(s.doc().unwrap().doc.node(all).is_some() && s.doc().unwrap().doc.node(other).is_some());
}
/// Blob Brush Tool Options' Size set to Pressure (#4590): the brush is Size − Variation wide where
/// the pen pressed lightest and Size + Variation where it pressed hardest. A stroke without
/// pressure keeps the Size, and Fixed leaves the pressure out.
#[test]
fn blob_brush_size_follows_the_pen_pressure() {
let mut s = session();
let inside = |s: &Session, id: NodeId, x: f64, y: f64| {
vectorcraft_geom::hit::fill_contains(&path(s, id).to_bezpath(), FillRule::NonZero, Point::new(x, y))
};
let blob = |s: &mut Session, p: serde_json::Value| NodeId(s.execute("path.blob", &p).unwrap()["id"].as_u64().unwrap());
// Lightest at the left end, hardest at the right: about 2 pt wide to 18 pt wide.
let pressed = |y: f64| (0..=20).map(|i| json!([100.0 + f64::from(i) * 10.0, y, f64::from(i) / 20.0])).collect::<Vec<_>>();
let a = blob(&mut s, json!({"points": pressed(100.0), "size": 10, "sizeMode": "pressure", "sizeVariation": 8}));
assert!(inside(&s, a, 290.0, 107.0) && !inside(&s, a, 110.0, 103.0), "wide where pressed hard, thin where light");
// Fixed, and Pressure on a stroke without pressure: 10 pt wide all along.
let fixed = blob(&mut s, json!({"points": pressed(300.0), "size": 10, "sizeMode": "fixed", "sizeVariation": 8}));
let mouse = blob(&mut s, json!({"points": [[100, 500], [300, 500]], "size": 10, "sizeMode": "pressure", "sizeVariation": 8}));
for (id, y) in [(fixed, 300.0), (mouse, 500.0)] {
assert!(inside(&s, id, 110.0, y + 3.0) && !inside(&s, id, 290.0, y + 7.0), "10 pt at y {y}");
}
let err = s.execute("path.blob", &json!({"points": [[0, 0], [10, 0]], "sizeMode": "random"})).unwrap_err();
assert!(err.to_string().contains("sizeMode"), "{err}");
// The tool records the pen while its Size varies with Pressure.
let v = view();
s.select_tool("blobBrush", v).unwrap();
s.set_tool_option("sizeMode", &json!("pressure"));
s.set_tool_option("sizeVariation", &json!(8));
assert_eq!((s.tool_options()["sizeMode"].clone(), s.tool_options()["sizeVariation"].clone()), (json!("pressure"), json!(8.0)));
let ev = |kind, i: i32| PointerEvent { pressure: i as f32 / 20.0, ..PointerEvent::new(kind, 100.0 + f64::from(i) * 10.0, 700.0) };
s.pointer(&ev(PointerKind::Down, 0), v).unwrap();
for i in 1..20 {
s.pointer(&ev(PointerKind::Drag, i), v).unwrap();
}
s.pointer(&ev(PointerKind::Up, 20), v).unwrap();
let (drawn, _) = paths(&s).into_iter().find(|(_, p)| p.control_bounds().is_some_and(|b| b.y0 > 650.0)).unwrap();
assert!(inside(&s, drawn, 290.0, 707.0) && !inside(&s, drawn, 110.0, 703.0), "the tool's stroke follows the pen");
}
/// 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.
+10
View File
@@ -38,6 +38,16 @@ 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` −
/// `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)
}
/// The narrowest a Blob Brush stroke gets, in points (its Size field's minimum).
pub const MIN_BLOB_SIZE: f64 = 0.1;
/// Create a drawing tool by id (None = not one of ours).
pub fn create(id: &str) -> Option<Box<dyn Tool>> {
Some(match id {
+47 -7
View File
@@ -9,8 +9,8 @@
//! key toggles to Smooth Tool off) draws a straight line at any angle. 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 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
//! 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
//! 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,7 +38,8 @@ pub struct GestureTool {
previewed: usize,
id: &'static str,
points: Vec<Point>,
/// The pen pressure of each of `points` (the Paintbrush only).
/// The pen pressure of each of `points` (the Paintbrush, and the Blob Brush with
/// `size_pressure`).
pressures: Vec<f32>,
active: bool,
began: bool,
@@ -57,6 +58,12 @@ 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` −
/// `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.
pub size_variation: f64,
/// The angle of the Eraser tip's long axis, in degrees counter-clockwise from horizontal.
pub angle: f64,
/// The Eraser tip's short axis in percent of its long one (100: round).
@@ -137,6 +144,8 @@ impl GestureTool {
redraw: None,
fidelity,
size: 10.0,
size_pressure: false,
size_variation: 0.0,
angle: 0.0,
roundness: 100.0,
fill: false,
@@ -175,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" {
if self.id == "paintbrush" || (self.id == "blobBrush" && self.size_pressure) {
self.pressures.push(pressure);
}
}
@@ -236,6 +245,13 @@ 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]> {
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())
}
/// 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 {
@@ -374,8 +390,10 @@ impl GestureTool {
"blobBrush" => vec![Action::Exec(
"path.blob".into(),
json!({
"points": points_json(pts),
"points": self.stroke_json(),
"size": self.size,
"sizeMode": if self.size_pressure { "pressure" } else { "fixed" },
"sizeVariation": self.size_variation,
"fidelity": self.fidelity,
"keepSelected": self.keep_selected,
"mergeOnlySelection": self.merge_only_selection
@@ -622,7 +640,16 @@ impl Tool for GestureTool {
o.push(Overlay::Path { path: area.to_bezpath(), color, width: 1.0, dashed: false });
return o;
}
o.push(Overlay::Path { path: polyline(&self.points), color, width, dashed: self.id == "knife" });
// 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() {
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;
o.push(Overlay::Path { path: polyline(seg), color, width: width as f32, dashed: false });
}
} else {
o.push(Overlay::Path { path: polyline(&self.points), color, width, dashed: self.id == "knife" });
}
if cx.measurement_labels && self.id == "blobBrush" && !self.smoothing {
let last = self.points[self.points.len() - 1];
let length = self.points.windows(2).map(|w| w[0].distance(w[1])).sum();
@@ -691,7 +718,14 @@ impl Tool for GestureTool {
}
"smooth" => json!({"fidelity": self.fidelity}),
"blobBrush" => {
json!({"fidelity": self.fidelity, "size": self.size, "keepSelected": self.keep_selected, "mergeOnlySelection": self.merge_only_selection})
json!({
"fidelity": self.fidelity,
"size": self.size,
"sizeMode": if self.size_pressure { "pressure" } else { "fixed" },
"sizeVariation": self.size_variation,
"keepSelected": self.keep_selected,
"mergeOnlySelection": self.merge_only_selection
})
}
"eraser" => json!({"size": self.size, "angle": self.angle, "roundness": self.roundness}),
_ => Value::Null,
@@ -701,6 +735,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() {
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),
"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),
+1 -1
View File
@@ -40,7 +40,7 @@ const ROWS: &[Row] = &[
(&["pencil"], None, &["altSmooth", "livePreview", "liveFit"]),
(&["smooth"], None, &["fidelity"]),
(&["blobBrush", "eraser"], None, &["size"]),
(&["blobBrush"], None, &["fidelity", "keepSelected", "mergeOnlySelection"]),
(&["blobBrush"], None, &["fidelity", "keepSelected", "mergeOnlySelection", "sizeMode", "sizeVariation"]),
// Eraser Tool Options' tip shape.
(&["eraser"], None, &["angle", "roundness"]),
// Live Paint Selection Tool Options (`builder::LivePaintSelectionTool`).
+32 -4
View File
@@ -7,8 +7,10 @@
//! Paintbrush), `keepSelected` (Pencil, Paintbrush and Blob Brush), `mergeOnlySelection` (Blob
//! 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) and `size`
//! (points; Blob Brush and Eraser).
//! 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).
use serde_json::{Map, Value, json};
@@ -27,11 +29,13 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_wid
pub const TOOLS: [&str; 5] = ["pencil", "paintbrush", "smooth", "blobBrush", "eraser"];
/// The options the dialog edits, in the order it shows them (a tool has those it reports).
const KEYS: [&str; 12] = [
const KEYS: [&str; 14] = [
"fidelity",
"angle",
"roundness",
"size",
"sizeMode",
"sizeVariation",
"fill",
"altSmooth",
"keepSelected",
@@ -134,6 +138,28 @@ fn number(ui: &mut egui::Ui, d: &mut Dialog, (key, label): (&str, &str), suffix:
});
}
/// Width of the Blob Brush'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).
fn blob_size(ui: &mut egui::Ui, d: &mut Dialog) {
const MODES: [&str; 2] = ["fixed", "pressure"];
let pressure = d.str("sizeMode") == "pressure";
let labels = ["Fixed", "Pressure"];
let label_w = label_w(ui) - MODE_W - ui.spacing().item_spacing.x;
let size = d.f64("size", 10.0);
widgets::label_row(ui, tl!("Size:"), label_w, |ui| {
if let Some(m) = widgets::dropdown(ui, "freehand-size-mode", labels[usize::from(pressure)], &labels, MODE_W).and_then(|i| MODES.get(i)) {
d.fields.insert("sizeMode".into(), json!(m));
}
if let Some(x) = widgets::range_field(ui, ("freehand-field", "size"), size, 0.1..=1000.0, "pt", 1, FIELD_W) {
d.fields.insert("size".into(), json!(x));
}
});
ui.add_enabled_ui(pressure, |ui| number(ui, d, ("sizeVariation", tl!("Variation:")), "pt", 1, (0.0, 1000.0)));
}
/// The Fidelity rows: the label on a line of its own, then a logarithmic slider from the tool's
/// lowest Fidelity to [`FIDELITY_MAX`] across the row with the field at its end, both to two
/// decimals, and Accurate and Smooth under the slider's ends.
@@ -182,7 +208,9 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
if d.fields.contains_key("roundness") {
number(ui, d, ("roundness", tl!("Roundness:")), "%", 0, (1.0, 100.0));
}
if d.fields.contains_key("size") {
if d.fields.contains_key("sizeMode") {
blob_size(ui, d);
} else if d.fields.contains_key("size") {
number(ui, d, ("size", tl!("Size:")), "pt", 1, (0.1, 1000.0));
}
if d.fields.contains_key("fill") {
@@ -313,6 +313,28 @@ 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.
#[test]
fn the_blob_brush_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();
let text = painted(&mut app);
assert!(!text.contains("Variation:") && !text.contains("Fixed"), "{text}");
}
#[test]
fn the_eraser_options_set_the_tips_angle_and_roundness() {
let mut app = app();
+1 -1
View File
@@ -2607,7 +2607,7 @@ naming the key; the document does not change and no undo step is recorded: `t`,
`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`;
`size` and `fidelity` in `path.blob`; `width` in `path.eraseSegments`;
`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
an `x` without a `y`, or a `y` without an `x`, with "x and y must be given together". `path.freehand`,