Background Eraser: Protect Foreground keeps the per-channel distance; tolerance doc

Only the sample match uses the measured increase-plus-decrease distance (#11901).
Protect Foreground Color keeps today's largest per-channel difference, as #11903
asks for its unmeasured cases. The tolerance doc describes the new distance and
the graded falloff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David MannandClaude Opus 5.5 committed 2026-11-08 15:12:49 +01:00
1 parent a65a23f829
commit cbdce347f0
2 files changed
+24 -2

No files matched your search

+12 -2
View File
@@ -17,7 +17,11 @@ use crate::retouch::{Footprint, Region, alpha_index, apply_dab_stroke};
pub struct BgEraseSettings {
pub sampling: Sampling,
pub limits: Limits,
/// 0..1: largest per-channel difference from the sampled colour that is erased.
/// 0..1: how far from the sampled colour a colour is still partly erased. The distance is the
/// largest increase plus the largest decrease over the colour channels (`dist`); an exact
/// match is erased fully, then less the further a colour is, and colours at or beyond the
/// Tolerance are kept (`graded_weight`). Protect Foreground Color still compares the
/// foreground by the largest per-channel difference (`max_channel_dist`).
pub tolerance: f32,
/// Keep pixels that match the foreground colour.
pub protect_foreground: bool,
@@ -52,6 +56,12 @@ fn dist(a: &[f32], b: &[f32]) -> f32 {
up + down
}
/// The largest per-channel difference of `a` from `b`: the distance Protect Foreground Color keeps
/// using (#11901 measured only the sample match; #11903 keeps today's measure for the foreground).
fn max_channel_dist(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).fold(0.0f32, |m, (x, y)| m.max((x - y).abs()))
}
/// The match weight of one dab for a colour `d` away from the sampled colour (#11901): an exact
/// match is erased fully, then the weight falls off linearly over the whole Tolerance `tol` after
/// a short plateau `d0 = min(tol / 20, 8 levels)`, and colours at or beyond the Tolerance are kept.
@@ -151,7 +161,7 @@ pub fn background_eraser_effect(pre: &Surface, bs: &BgEraseSettings) -> Option<i
continue;
}
let c = &p[..nc];
if protect_foreground && dist(c, &fg) <= tol {
if protect_foreground && max_channel_dist(c, &fg) <= tol {
continue;
}
weight[(y - r.y0) as usize * w + (x - r.x0) as usize] = match_w(dist(c, &sampled));
@@ -175,3 +175,15 @@ fn the_distance_is_measured_in_the_documents_own_channels() {
}
}
}
#[test]
fn protect_foreground_keeps_todays_distance_to_the_foreground() {
// #11903 measured the foreground only one channel away and keeps today's measure otherwise:
// the largest per-channel difference. This foreground is 100 levels off in red and in green
// (opposite signs): within Tolerance 50 by that measure, though the sample-match distance of
// #11901 adds the two to 200. The pixel is the sampled colour, so only the protection keeps it.
let bs = BgEraseSettings { protect_foreground: true, foreground: rgb([100, 140, 40]), ..swatch(50.0, rgb(SAMPLE)) };
assert_eq!(one_click(PixelFormat::RGBA8, rgb(SAMPLE), &bs), 255);
// Protection off: the same click erases the sample fully.
assert_eq!(one_click(PixelFormat::RGBA8, rgb(SAMPLE), &BgEraseSettings { protect_foreground: false, ..bs }), 0);
}