fix(ui): Reset Tool gives the Dodge tool a 65 px soft tip at 25 % spacing (#12684) (#12793)

fresh_brush gave the Dodge tool the plain 20 px hard default at 10 %
spacing. It now gets the reference's 65 px soft round at 25 % spacing
after Reset Tool and Reset All Tools, and on its first pick. The Burn
tool has the same measured tip, so both share TONING_TOOL_TIP.
This commit is contained in:
RickiNano authored and GitHub committed 2026-11-10 20:08:31 +01:00
1 parent 4af62e8abb
commit 65bd584259
5 files changed
+131 -22

No files matched your search

+13 -14
View File
@@ -88,19 +88,18 @@ fn reset_all_tools_gives_it_the_tip_active_or_picked_later() {
assert_eq!(tip(&mut app), BURN_TIP, "picked after the reset");
}
/// The Dodge tool shares the Burn tool's tip (#12684); the Sponge keeps the default.
#[test]
fn other_toning_tools_keep_their_tips() {
for tool in [Tool::Dodge, Tool::Sponge] {
let mut a = app();
pick(&mut a, tool);
scramble(&mut a);
invoke(&mut a, "tool.reset", json!({}));
assert_eq!(tip(&mut a), DEFAULT_TIP, "{tool:?}");
// A first use still takes the current tip, as before.
let mut a = app();
pick(&mut a, Tool::Pencil);
scramble(&mut a);
pick(&mut a, tool);
assert_eq!(tip(&mut a)[0], 57.0, "{tool:?}, first use");
}
fn the_sponge_tool_keeps_its_tip() {
let mut a = app();
pick(&mut a, Tool::Sponge);
scramble(&mut a);
invoke(&mut a, "tool.reset", json!({}));
assert_eq!(tip(&mut a), DEFAULT_TIP);
// A first use still takes the current tip, as before.
let mut a = app();
pick(&mut a, Tool::Pencil);
scramble(&mut a);
pick(&mut a, Tool::Sponge);
assert_eq!(tip(&mut a)[0], 57.0, "first use");
}
+9 -8
View File
@@ -41,10 +41,10 @@ pub fn sync_tool_brush(app: &mut PhotocraftApp) {
// in Normal mode: each tool's Mode starts at Normal, so a Multiply Brush doesn't make the
// first Pattern Stamp stroke multiply (or give Healing a mode its menu doesn't have).
// The Art History Brush also starts at full opacity (#10263): its Opacity is its own.
// The Background Eraser, the Healing Brush, the Burn tool and the Clone Stamp start with
// their own measured tips instead, as the whole brush Reset Tool gives them (#11911,
// #12099, #12671, #12601).
None if matches!(tool, Tool::BackgroundEraser | Tool::Healing | Tool::Burn | Tool::CloneStamp) => fresh_brush(tool, &current),
// The Background Eraser, the Healing Brush, the Burn and Dodge tools and the Clone Stamp
// start with their own measured tips instead, as the whole brush Reset Tool gives them
// (#11911, #12099, #12671, #12684, #12601).
None if matches!(tool, Tool::BackgroundEraser | Tool::Healing | Tool::Burn | Tool::Dodge | Tool::CloneStamp) => fresh_brush(tool, &current),
None => {
let opacity = if tool == Tool::ArtHistoryBrush { BrushSettings::default().opacity } else { current.opacity };
BrushSettings { smoothing: first_smoothing(), mode: photocraft_color::BlendMode::Normal, opacity, ..current }
@@ -78,15 +78,16 @@ const BRUSH_TOOL_TIP: (f32, f32, f32) = (13.0, 0.0, 0.25);
const BACKGROUND_ERASER_TIP: (f32, f32, f32) = (13.0, 1.0, 0.25);
/// The Healing Brush's tip after Reset Tool (#12099): a 19 px hard round at 25 % spacing.
const HEALING_BRUSH_TIP: (f32, f32, f32) = (19.0, 1.0, 0.25);
/// The Burn tool's tip after Reset Tool (#12671): a 65 px soft round at 25 % spacing.
const BURN_TOOL_TIP: (f32, f32, f32) = (65.0, 0.0, 0.25);
/// The Burn and Dodge tools' tip after Reset Tool (#12671, #12684): a 65 px soft round at 25 %
/// spacing.
const TONING_TOOL_TIP: (f32, f32, f32) = (65.0, 0.0, 0.25);
/// The Clone Stamp's tip after Reset Tool (#12601): a 21 px soft round at 25 % spacing.
const CLONE_STAMP_TIP: (f32, f32, f32) = (21.0, 0.0, 0.25);
/// `tool`'s brush as it starts out (Reset Tool, #2825): the default brush at Photoshop's
/// smoothing, with the session's own fields of `current`. The Brush tool's tip is
/// [`BRUSH_TOOL_TIP`], the Background Eraser's [`BACKGROUND_ERASER_TIP`], the Healing Brush's
/// [`HEALING_BRUSH_TIP`], the Burn tool's [`BURN_TOOL_TIP`], the Clone Stamp's
/// [`HEALING_BRUSH_TIP`], the Burn and Dodge tools' [`TONING_TOOL_TIP`], the Clone Stamp's
/// [`CLONE_STAMP_TIP`] with no smoothing; the other tools keep the default tip.
pub(crate) fn fresh_brush(tool: Tool, current: &BrushSettings) -> BrushSettings {
let mut brush = BrushSettings { smoothing: first_smoothing(), ..BrushSettings::default() };
@@ -94,7 +95,7 @@ pub(crate) fn fresh_brush(tool: Tool, current: &BrushSettings) -> BrushSettings
Tool::Brush => (brush.size, brush.hardness, brush.spacing) = BRUSH_TOOL_TIP,
Tool::BackgroundEraser => (brush.size, brush.hardness, brush.spacing) = BACKGROUND_ERASER_TIP,
Tool::Healing => (brush.size, brush.hardness, brush.spacing) = HEALING_BRUSH_TIP,
Tool::Burn => (brush.size, brush.hardness, brush.spacing) = BURN_TOOL_TIP,
Tool::Burn | Tool::Dodge => (brush.size, brush.hardness, brush.spacing) = TONING_TOOL_TIP,
Tool::CloneStamp => {
(brush.size, brush.hardness, brush.spacing) = CLONE_STAMP_TIP;
brush.smoothing.amount = 0.0;
@@ -0,0 +1,104 @@
//! #12684: the Dodge tool starts, and comes back after Reset Tool, with the reference application's
//! tip: a 65 px soft round (hardness 0 %, angle 0°, roundness 100 %) at 25 % spacing. Other tools'
//! starting tips are unchanged.
use serde_json::json;
use crate::PhotocraftApp;
use crate::control::{ControlRequest, handle};
use crate::state::Tool;
fn app() -> PhotocraftApp {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
app.sync_views();
app
}
/// Run a menu command as the control channel does, then the frame's sync.
fn invoke(app: &mut PhotocraftApp, id: &str, params: serde_json::Value) {
let ctx = egui::Context::default();
let (req, _rx) = ControlRequest::new("ui.menu.invoke", json!({"id": id, "params": params}));
handle(app, &ctx, &req);
app.sync_views();
}
/// Pick `tool`, and swap its brush in as a frame does.
fn pick(app: &mut PhotocraftApp, tool: Tool) {
app.ui.tool = tool;
crate::paint_mouse::sync_tool_brush(app);
app.sync_views();
}
/// Size, hardness, spacing, angle and roundness of the live brush, as `brush.get` reports them.
fn tip(app: &mut PhotocraftApp) -> [f64; 5] {
let b = app.run("brush.get", json!({})).unwrap();
["size", "hardness", "spacing", "angle", "roundness"].map(|k| (b[k].as_f64().unwrap() * 1e4).round() / 1e4)
}
/// A tip that is no tool's starting tip, so a reset has something to undo.
fn scramble(app: &mut PhotocraftApp) {
let b = &mut app.session.tools.brush;
(b.size, b.hardness, b.spacing, b.angle, b.roundness) = (57.0, 0.4, 0.6, 30.0, 0.5);
}
const DODGE_TIP: [f64; 5] = [65.0, 0.0, 0.25, 0.0, 1.0];
const DEFAULT_TIP: [f64; 5] = [20.0, 1.0, 0.1, 0.0, 1.0];
#[test]
fn reset_tool_gives_the_dodge_tool_a_65_px_soft_tip() {
let mut app = app();
pick(&mut app, Tool::Dodge);
scramble(&mut app);
invoke(&mut app, "tool.reset", json!({}));
assert_eq!(tip(&mut app), DODGE_TIP);
// The options-bar brush chip reads the live brush's rounded size.
assert_eq!(app.session.tools.brush.size.round(), 65.0);
}
#[test]
fn a_first_use_starts_with_the_tip() {
// No stored tool brushes, as on a fresh install: the Dodge tool's first pick, after another
// tool's tip.
let mut app = app();
pick(&mut app, Tool::Pencil);
scramble(&mut app);
pick(&mut app, Tool::Dodge);
assert_eq!(tip(&mut app), DODGE_TIP);
// Its own tip is then kept across tool switches.
app.session.tools.brush.size = 42.0;
pick(&mut app, Tool::Pencil);
pick(&mut app, Tool::Dodge);
assert_eq!(tip(&mut app)[0], 42.0);
}
#[test]
fn reset_all_tools_gives_it_the_tip_active_or_picked_later() {
let mut app = app();
pick(&mut app, Tool::Dodge);
scramble(&mut app);
invoke(&mut app, "tool.resetAll", json!({"confirmed": true}));
assert_eq!(tip(&mut app), DODGE_TIP, "active");
let mut app = self::app();
pick(&mut app, Tool::Dodge);
scramble(&mut app);
pick(&mut app, Tool::Pencil);
invoke(&mut app, "tool.resetAll", json!({"confirmed": true}));
pick(&mut app, Tool::Dodge);
assert_eq!(tip(&mut app), DODGE_TIP, "picked after the reset");
}
#[test]
fn the_sponge_tool_keeps_its_tip() {
let mut a = app();
pick(&mut a, Tool::Sponge);
scramble(&mut a);
invoke(&mut a, "tool.reset", json!({}));
assert_eq!(tip(&mut a), DEFAULT_TIP);
// A first use still takes the current tip, as before.
let mut a = app();
pick(&mut a, Tool::Pencil);
scramble(&mut a);
pick(&mut a, Tool::Sponge);
assert_eq!(tip(&mut a)[0], 57.0, "first use");
}
+2
View File
@@ -1061,6 +1061,8 @@ mod pencil_shift_digit_opacity_tests;
#[cfg(test)]
mod doc_tab_menu_tests;
#[cfg(test)]
mod dodge_default_tip_tests;
#[cfg(test)]
mod layers_empty_area_menu_tests;
#[cfg(test)]
mod pencil_auto_erase_tooltip_tests;
@@ -474,6 +474,9 @@ fn releasing_a_sequential_dab_stroke_commits_what_a_replay_paints() {
});
h.state_mut().ui.tool = tool;
h.run_steps(1);
// A tool with its own starting tip (Dodge, #12684) paints with that, not the setup's
// brush: give it the setup's tip, as the replay below has.
h.state_mut().run("tools.setBrush", json!({"brush": {"size": 24, "hardness": 0.2, "spacing": 0.1}})).unwrap();
move_to(&mut h, 40.0, 30.0);
button(&mut h, 40.0, 30.0, true);
for (x, y) in [(70.0, 40.0), (100.0, 55.0), (130.0, 50.0), (160.0, 70.0)] {