Mixer Brush: Clean Brush empties the brush, and the automatic Load and Clean can be switched off

The Mixer Brush could not be emptied. MixerState::clean dropped only
the pickup well, paint.mixerBrush refilled the reservoir from the
foreground before every stroke unless loadAfterStroke was false, and
the app always sent the defaults. A stroke on a new layer over an image
always carried the foreground color, and the options bar had no Load
Brush or Clean Brush control.

Cleaning now empties both wells, so a clean brush lays down only the
color it picks up from the canvas. The new commands brush.mixer.load
and brush.mixer.clean load and empty the brush. paint.mixerBrush reads
loadAfterStroke and cleanAfterStroke from the brush and applies them
after the stroke. Both are on by default, and a stroke with both on
paints as before. A stroke whose color differs from the one recorded
by the last stroke, load or clean loads that color first. A load or
clean records the foreground, so choosing a new foreground loads the
reservoir.

The options bar has the Current Brush Load swatch, whose menu holds
Load Brush and Clean Brush, and two toggles for the automatic Load and
Clean. Brush Settings shows the same two options. The new strings have
rows in every catalog.
This commit is contained in:
dotCooCoo
2026-10-16 22:49:47 -07:00
parent 5f65579ecb
commit 8f971cc79f
30 changed files with 443 additions and 16 deletions
+59 -1
View File
@@ -97,7 +97,15 @@ fn edit_section(i: usize, b: &mut BrushSettings) {
b.transfer.flow = dynamic(0.25, Control::Fade, 0.3);
b.transfer.wetness = dynamic(0.35, Control::PenPressure, 0.1);
b.transfer.mix = dynamic(0.45, Control::Fade, 0.2);
b.mixer = paint::MixerSettings { wet: 0.9, load: 0.15, mix: 0.7, flow: 0.8, sample_all_layers: true };
b.mixer = paint::MixerSettings {
wet: 0.9,
load: 0.15,
mix: 0.7,
flow: 0.8,
sample_all_layers: true,
load_after_stroke: false,
clean_after_stroke: false,
};
}
7 => {
let p = &mut b.pose;
@@ -488,6 +496,56 @@ fn mixer_brush_options_update_the_persisted_mixer_settings() {
assert_eq!(params["brush"]["mixer"]["sampleAllLayers"], true);
}
#[test]
fn mixer_brush_clean_brush_and_the_automatic_load_keep_the_foreground_out() {
use egui_kittest::kittest::Queryable;
let mut h = options_bar_harness(crate::state::Tool::MixerBrush);
// A red image, an empty layer above it, a blue foreground, and a brush that samples all layers.
for (id, p) in [
("file.new", json!({"width": 200, "height": 40, "background": "transparent"})),
("edit.fill", json!({"color": "#ff0000"})),
("layer.new.layer", json!({})),
("tools.setColors", json!({"foreground": "#0000ff"})),
("tools.setBrush", json!({"brush": {"size": 8, "pressureSize": false, "mixer": {"sampleAllLayers": true}}})),
] {
h.state_mut().run(id, p).unwrap();
}
h.run_steps(3);
for label in ["Current Brush Load", "Load the brush after each stroke", "Clean the brush after each stroke"] {
assert!(h.get_by_label(label).rect().is_positive(), "missing Mixer Brush option {label}");
}
// Turn the automatic Load off: one tools.setBrush call.
h.get_by_label("Load the brush after each stroke").click();
h.run_steps(3);
assert!(!h.state().session.tools.brush.mixer.load_after_stroke);
let (id, params) = last_journal(h.state()).unwrap();
assert_eq!((id.as_str(), &params["brush"]["mixer"]["loadAfterStroke"]), ("tools.setBrush", &json!(false)));
// Current Brush Load › Clean Brush empties the brush.
h.get_by_label("Current Brush Load").click();
h.run_steps(3);
h.get_by_label("Clean Brush").click();
h.run_steps(3);
assert_eq!(last_journal(h.state()).map(|(id, _)| id), Some("brush.mixer.clean".to_string()));
assert_eq!(h.state().session.tools.mixer.reservoir, None);
// The stroke the canvas commits on release lays down only the red it picks up.
crate::retouch_ui::finish_stroke(h.state_mut(), crate::state::Tool::MixerBrush, &[[5.0, 20.0, 1.0], [195.0, 20.0, 1.0]], egui::Modifiers::NONE);
let d = h.state().session.active().unwrap();
let c = d.doc.layer(d.active_layer.unwrap()).unwrap().surface().unwrap().rgba(100, 20);
assert!(c[3] > 0.1 && c[0] > 0.99 && c[2] < 0.01, "the stroke carries no foreground: {c:?}");
// With the automatic Load off, the stroke did not refill the brush either.
assert_eq!(h.state().session.tools.mixer.reservoir, None);
// Load Brush fills it with the foreground again.
h.get_by_label("Current Brush Load").click();
h.run_steps(3);
h.get_by_label("Load Brush").click();
h.run_steps(3);
assert_eq!(h.state().session.tools.mixer.reservoir, Some([0.0, 0.0, 1.0, 1.0]));
}
#[test]
fn drop_targets_and_actions_reorder_presets() {
use crate::brushes_tab::{Action, apply, drop_target, group_key};