Clone Stamp / Healing Brush: the source crosshair shows only while painting (#668) (#969)

Setting the source with Alt-click left a + on the canvas until the next stroke.
As in Photoshop, the crosshair now shows where the stroke samples only while
the stroke is painted.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-08 04:01:38 -04:00 committed by GitHub
parent 3d003bd9d0
commit 4c4c58d6e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 29 additions and 11 deletions

View File

@ -1128,4 +1128,19 @@ mod tests {
app.session.presets.clone.active_mut().anchor = Some([20.0, 20.0]);
assert_eq!(clone_sample_point(&app, Some([30.0, 30.0])), Some([20.0, 20.0]));
}
#[test]
fn clone_marker_shows_only_while_painting() {
// #668: ⌥-click used to leave a "+" on the canvas until the next stroke.
let (mut app, _) = app();
app.ui.tool = Tool::CloneStamp;
crate::retouch_ui::set_source(&mut app, 10.0, 10.0);
app.hover_doc = Some([40.0, 40.0]);
assert_eq!(crate::retouch_ui::source_marker_point(&app), None, "hovering after ⌥-click shows nothing");
app.drag = Some(crate::canvas::Drag::new(Tool::CloneStamp, [40.0, 40.0], vec![[40.0, 40.0, 1.0]], egui::Modifiers::NONE, false));
assert!(crate::retouch_ui::source_marker_point(&app).is_some(), "painting shows where it samples");
app.drag = None;
app.ui.tool = Tool::Brush;
assert_eq!(crate::retouch_ui::source_marker_point(&app), None);
}
}

View File

@ -169,20 +169,23 @@ pub fn set_source(app: &mut PhotocraftApp, x: f64, y: f64) {
app.ui.status_error = false;
}
/// Crosshair where the clone source is sampled from for the current pointer position.
pub fn draw_source_marker(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform) {
/// Where the clone source is sampled from, shown only while a Clone Stamp or Healing Brush stroke
/// is painted, as in Photoshop: setting the source (⌥-click) leaves nothing on the canvas (#668).
pub fn source_marker_point(app: &PhotocraftApp) -> Option<[f64; 2]> {
if !matches!(app.ui.tool, Tool::CloneStamp | Tool::Healing) {
return;
return None;
}
let at =
app.drag.as_ref().filter(|d| matches!(d.tool, Tool::CloneStamp | Tool::Healing)).and_then(|d| d.points.last().map(|p| [p[0], p[1]])).or(app.hover_doc);
let src = match (crate::preset_panels::clone_sample_point(app, at), app.ui.clone_offset, at, app.ui.clone_source) {
let at = app.drag.as_ref().filter(|d| matches!(d.tool, Tool::CloneStamp | Tool::Healing)).and_then(|d| d.points.last().map(|p| [p[0], p[1]]))?;
match (crate::preset_panels::clone_sample_point(app, Some(at)), app.ui.clone_offset, app.ui.clone_source) {
(Some(p), ..) => Some(p),
(None, Some(off), Some(h), _) => Some([h[0] + off[0], h[1] + off[1]]),
(None, None, _, Some(s)) => Some(s),
_ => None,
};
let Some(s) = src else { return };
(None, Some(off), _) => Some([at[0] + off[0], at[1] + off[1]]),
(None, None, s) => s,
}
}
/// Crosshair at [`source_marker_point`].
pub fn draw_source_marker(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform) {
let Some(s) = source_marker_point(app) else { return };
let c = xf.to_screen(s[0] as f32, s[1] as f32);
for (w, col) in [(3.0, Color32::from_black_alpha(160)), (1.0, Color32::WHITE)] {
painter.line_segment([c - vec2(7.0, 0.0), c + vec2(7.0, 0.0)], Stroke::new(w, col));