Brackets: keep to the tools whose size the keys are documented for

The typed [ and ] now reach the Blob Brush, the Eraser and the Symbolism
tools, whose bracket keys are in the public shortcut lists. A size of the
Paintbrush's own (and path.freehand's scale) is left out until its
behaviour is decided: #1661 stays open for that.
This commit is contained in:
bflatastic committed 2026-10-14 03:55:41 +07:00
1 parent 3f1e72d983
commit 3152436020
4 files changed
+28 -94

No files matched your search

+2 -5
View File
@@ -24,7 +24,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Pencil",
[],
None,
"{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic brushes with Pressure variation; a point without one presses fully), fidelity?: pt (1.5), closed?, style?: \"pencil\"|\"brush\", fill?: bool, extend?: {id, end: \"start\"|\"end\"}, scale?: number (1; the Paintbrush's size, set with [ and ]: a new stroke drawn at this times the stroke weight, which stays as it is)} fit a freehand stroke → {id}",
"{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic brushes with Pressure variation; a point without one presses fully), fidelity?: pt (1.5), closed?, style?: \"pencil\"|\"brush\", fill?: bool, extend?: {id, end: \"start\"|\"end\"}} fit a freehand stroke → {id}",
has_doc,
freehand
),
@@ -618,10 +618,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
// A pencil path with a fill keeps a stroke of None (#1679); only one that would show nothing
// gets black. A brush is its stroke.
let stroke = if brush || fill.is_none() { stroke_paint(s) } else { s.paint.stroke.clone() };
// The Paintbrush's own size scales the stroke it draws, not the weight new art takes.
let scale = f64_or(p, "scale", 1.0);
let scale = if scale.is_finite() { scale.clamp(0.1, 100.0) } else { 1.0 };
let mut look = s.new_art_look(fill, stroke, s.paint.stroke_width.max(0.1) * scale);
let mut look = s.new_art_look(fill, stroke, s.paint.stroke_width.max(0.1));
if let Some(st) = look.appearance.stroke_mut() {
st.pressure = pressure;
}
+1 -29
View File
@@ -3,7 +3,7 @@
use serde_json::{Value, json};
use vectorcraft_doc::{Document, Node, NodeKind};
use vectorcraft_geom::Rect;
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
use super::*;
@@ -590,31 +590,3 @@ fn brushes_and_symbols_survive_vectorcraft_round_trip() {
let b = s2.doc().unwrap().doc.node(NodeId(r["id"].as_u64().unwrap())).unwrap().geometric_bounds().unwrap();
assert!(close(b, Rect::new(60.0, 80.0, 140.0, 120.0), 1e-9));
}
/// #1661: with the Paintbrush, `]` and `[` size the brush itself, a tenth a press: the stroke
/// weight new art takes stays, the selected path keeps its stroke, nothing goes on the undo list,
/// and the next stroke is drawn at the new size.
#[test]
fn paintbrush_brackets_size_the_next_stroke_and_leave_the_weight() {
let mut s = session();
let v = ViewInfo::default();
let selected = rect(&mut s, 0.0, 0.0, 50.0, 50.0);
s.execute("stroke.set", &json!({"weight": 3})).unwrap();
let before = node(&s, selected);
let undo = s.doc().unwrap().history.undo.len();
s.select_tool("paintbrush", v).unwrap();
for _ in 0..2 {
s.tool_key(ToolKey::BracketRight, Mods::default(), v).unwrap();
}
s.tool_key(ToolKey::BracketLeft, Mods::default(), v).unwrap();
assert_eq!(s.paint.stroke_width, 3.0, "the weight stays");
assert_eq!(node(&s, selected), before, "the selection keeps its stroke");
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "nothing to undo");
for (k, x, y) in [(PointerKind::Down, 100.0, 100.0), (PointerKind::Drag, 150.0, 130.0), (PointerKind::Up, 200.0, 110.0)] {
s.pointer(&PointerEvent::new(k, x, y), v).unwrap();
}
let drawn = s.doc().unwrap().selection.objects[0];
assert_ne!(drawn, selected);
assert!((node(&s, drawn).appearance.stroke().unwrap().width - 3.0 * 1.1).abs() < 1e-12);
assert_eq!(s.paint.stroke_width, 3.0);
}
+8 -42
View File
@@ -29,8 +29,7 @@ pub struct GestureTool {
extend: Option<(NodeId, bool)>,
/// Fit tolerance in points (Pencil/Paintbrush/Smooth "Fidelity").
pub fidelity: f64,
/// Brush / eraser diameter in points; the Paintbrush's size, its strokes drawn at
/// `size / PAINTBRUSH_SIZE` of the stroke weight.
/// Brush / eraser diameter in points.
pub size: f64,
/// Fill new pencil/brush strokes.
pub fill: bool,
@@ -42,11 +41,6 @@ pub struct GestureTool {
snap: DrawSnap,
}
/// The Paintbrush's starting size, as the Blob Brush's: strokes at the stroke weight. `[` and `]`
/// take a point off or add one, so each press draws the next stroke a tenth of the weight thinner
/// or thicker (our own step; the public references give none).
pub const PAINTBRUSH_SIZE: f64 = 10.0;
impl GestureTool {
pub fn new(id: &str) -> Self {
let id: &'static str = match id {
@@ -68,7 +62,7 @@ impl GestureTool {
began: false,
extend: None,
fidelity,
size: if id == "paintbrush" { PAINTBRUSH_SIZE } else { 10.0 },
size: 10.0,
fill: false,
edit_within: 12.0,
close_within: 15.0,
@@ -153,8 +147,6 @@ impl GestureTool {
});
if let Some((id, start)) = self.extend {
v["extend"] = json!({"id": id.0, "end": if start { "start" } else { "end" }});
} else if self.id == "paintbrush" && self.size != PAINTBRUSH_SIZE {
v["scale"] = json!(self.size / PAINTBRUSH_SIZE);
}
v
}
@@ -257,8 +249,9 @@ impl Tool for GestureTool {
_ => vec![],
}
}
/// `[` and `]` are the Blob Brush's and the Eraser's: their size (#1661).
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
matches!(key, ToolKey::BracketLeft | ToolKey::BracketRight) && matches!(self.id, "paintbrush" | "blobBrush" | "eraser")
matches!(key, ToolKey::BracketLeft | ToolKey::BracketRight) && matches!(self.id, "blobBrush" | "eraser")
}
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _m: Mods) -> Vec<Action> {
match key {
@@ -270,13 +263,11 @@ impl Tool for GestureTool {
self.clear();
if began { vec![Action::Cancel] } else { vec![] }
}
// The Paintbrush's size is its own: the stroke weight and the selection stay as they
// are, and the next stroke is drawn thinner or thicker.
ToolKey::BracketLeft if matches!(self.id, "blobBrush" | "eraser" | "paintbrush") => {
ToolKey::BracketLeft if matches!(self.id, "blobBrush" | "eraser") => {
self.size = (self.size - 1.0).max(1.0);
vec![]
}
ToolKey::BracketRight if matches!(self.id, "blobBrush" | "eraser" | "paintbrush") => {
ToolKey::BracketRight if matches!(self.id, "blobBrush" | "eraser") => {
self.size = (self.size + 1.0).min(1000.0);
vec![]
}
@@ -309,9 +300,8 @@ impl Tool for GestureTool {
}
fn options(&self) -> Value {
match self.id {
"pencil" => json!({"fidelity": self.fidelity, "fill": self.fill, "editWithin": self.edit_within, "closeWithin": self.close_within}),
"paintbrush" => {
json!({"fidelity": self.fidelity, "fill": self.fill, "editWithin": self.edit_within, "closeWithin": self.close_within, "size": self.size})
"pencil" | "paintbrush" => {
json!({"fidelity": self.fidelity, "fill": self.fill, "editWithin": self.edit_within, "closeWithin": self.close_within})
}
"smooth" => json!({"fidelity": self.fidelity}),
"blobBrush" | "eraser" => json!({"size": self.size}),
@@ -453,28 +443,4 @@ mod tests {
b.key(&cx, ToolKey::BracketRight, Mods::default());
assert_eq!(b.options()["size"], 11.0);
}
/// #1661: `[` and `]` size the Paintbrush itself, a tenth of its starting size a press:
/// the next stroke is drawn that much thinner or thicker; continuing a path keeps its weight.
#[test]
fn paintbrush_brackets_size_its_next_stroke() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = GestureTool::new("paintbrush");
assert!(t.claims_key(&cx, ToolKey::BracketRight) && t.freehand_params(false).get("scale").is_none());
for _ in 0..10 {
assert!(t.key(&cx, ToolKey::BracketRight, Mods::default()).is_empty(), "no command: the weight stays");
}
assert_eq!(t.options()["size"], 20.0);
assert_eq!(t.freehand_params(false)["scale"], json!(2.0));
for _ in 0..40 {
t.key(&cx, ToolKey::BracketLeft, Mods::default());
}
assert_eq!(t.options()["size"], 1.0);
t.extend = Some((NodeId(1), false));
assert!(t.freehand_params(false).get("scale").is_none());
assert!(!GestureTool::new("pencil").claims_key(&cx, ToolKey::BracketRight));
}
}
+17 -18
View File
@@ -391,8 +391,8 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
}
}
// `[` and `]` the active tool takes (the Paintbrush's, the Blob Brush's and the
// Eraser's size, the Symbolism tools' diameter, with Shift their intensity). Read as typed, so
// `[` and `]` the active tool takes (the Blob Brush's and the Eraser's size, the Symbolism
// tools' diameter, with Shift their intensity). Read as typed, so
// layouts where a bracket takes Option or AltGr count too; `{` and `}` are Shift's.
if !(m.command || m.ctrl) {
let typed: Vec<(String, ToolKey, bool)> = ctx.input(|i| {
@@ -731,28 +731,27 @@ mod tests {
assert_eq!(kind(&app), None, "another tool has its own Enter");
}
/// #1661: typed `[` and `]` reach the tool that takes them: the Paintbrush's own size, the Blob
/// Brush's (Option or AltGr brackets too), the stroke weight left as it is; other tools leave
/// them alone.
/// #1661: typed `[` and `]` reach the tool that takes them: the Blob Brush's and the Eraser's
/// size (Option or AltGr brackets too), the stroke weight left as it is; other tools leave them
/// alone.
#[test]
fn brackets_size_the_paintbrush_and_the_blob_brush() {
fn brackets_size_the_blob_brush_and_the_eraser() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
app.session.execute("stroke.set", &json!({"weight": 3})).unwrap();
let size = |app: &mut VectorcraftApp| app.session.tool_options()["size"].as_f64().unwrap();
let text = |t: &str| egui::Event::Text(t.into());
app.select_tool("paintbrush");
frame(&mut app, vec![text("]")]);
frame(&mut app, vec![text("]")]);
assert_eq!(size(&mut app), 12.0);
// Option-8 types `[` on a Turkish Mac keyboard.
let option_8 = egui::Event::Key { key: Key::Num8, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::ALT };
frame(&mut app, vec![option_8, text("[")]);
assert_eq!(size(&mut app), 11.0, "a bracket typed with Option");
app.select_tool("blobBrush");
let before = size(&mut app);
frame(&mut app, vec![text("]")]);
assert_eq!(size(&mut app), before + 1.0);
for tool in ["blobBrush", "eraser"] {
app.select_tool(tool);
let before = size(&mut app);
frame(&mut app, vec![text("]")]);
frame(&mut app, vec![text("]")]);
assert_eq!(size(&mut app), before + 2.0, "{tool}");
// Option-8 types `[` on a Turkish Mac keyboard.
let option_8 = egui::Event::Key { key: Key::Num8, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::ALT };
frame(&mut app, vec![option_8, text("[")]);
assert_eq!(size(&mut app), before + 1.0, "{tool}: a bracket typed with Option");
}
assert_eq!(app.session.paint.stroke_width, 3.0, "the stroke weight stays");
app.select_tool("rectangle");
frame(&mut app, vec![text("[")]);