Add clone stamp tool
This commit is contained in:
parent
2c9d8c1589
commit
de24622f02
|
|
@ -299,9 +299,10 @@ impl BrushEngine {
|
|||
other => other,
|
||||
};
|
||||
let blend_mode_u = match base_blend {
|
||||
RasterBlendMode::Normal => 0u32,
|
||||
RasterBlendMode::Erase => 1u32,
|
||||
RasterBlendMode::Smudge => 2u32,
|
||||
RasterBlendMode::Normal => 0u32,
|
||||
RasterBlendMode::Erase => 1u32,
|
||||
RasterBlendMode::Smudge => 2u32,
|
||||
RasterBlendMode::CloneStamp => 3u32,
|
||||
};
|
||||
|
||||
let push_dab = |dabs: &mut Vec<GpuDab>,
|
||||
|
|
@ -322,7 +323,9 @@ impl BrushEngine {
|
|||
color_r: cr,
|
||||
color_g: cg,
|
||||
color_b: cb,
|
||||
color_a: stroke.color[3],
|
||||
// Clone stamp: color_r/color_g hold source canvas X/Y, so color_a = 1.0
|
||||
// (blend strength is opa_weight × opacity × 1.0 in the shader).
|
||||
color_a: if blend_mode_u == 3 { 1.0 } else { stroke.color[3] },
|
||||
ndx, ndy, smudge_dist,
|
||||
blend_mode: blend_mode_u,
|
||||
elliptical_dab_ratio: bs.elliptical_dab_ratio.max(1.0),
|
||||
|
|
@ -356,7 +359,14 @@ impl BrushEngine {
|
|||
state, bs, pt.x, pt.y, base_r, pt.pressure, stroke.color,
|
||||
);
|
||||
if !matches!(base_blend, RasterBlendMode::Smudge) {
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr, cg, cb,
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp) {
|
||||
// Store offset in color_r/color_g; shader adds it per-pixel.
|
||||
let (ox, oy) = stroke.clone_src_offset.unwrap_or((0.0, 0.0));
|
||||
(ox, oy, 0.0)
|
||||
} else {
|
||||
(cr, cg, cb)
|
||||
};
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,
|
||||
0.0, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
|
@ -468,6 +478,13 @@ impl BrushEngine {
|
|||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||
ndx, ndy, smudge_dist);
|
||||
} else if matches!(base_blend, RasterBlendMode::CloneStamp) {
|
||||
// Store the offset (not absolute position) in color_r/color_g.
|
||||
// The shader adds this to each pixel's own position for per-pixel sampling.
|
||||
let (ox, oy) = stroke.clone_src_offset.unwrap_or((0.0, 0.0));
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, ox, oy, 0.0,
|
||||
0.0, 0.0, 0.0);
|
||||
} else {
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||
|
|
@ -500,7 +517,14 @@ impl BrushEngine {
|
|||
last_smooth_x, last_smooth_y,
|
||||
base_r, last_pressure, stroke.color,
|
||||
);
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr, cg, cb,
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp) {
|
||||
// Store offset in color_r/color_g; shader adds it per-pixel.
|
||||
let (ox, oy) = stroke.clone_src_offset.unwrap_or((0.0, 0.0));
|
||||
(ox, oy, 0.0)
|
||||
} else {
|
||||
(cr, cg, cb)
|
||||
};
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,
|
||||
0.0, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ pub enum RasterBlendMode {
|
|||
Erase,
|
||||
/// Smudge / blend surrounding pixels
|
||||
Smudge,
|
||||
/// Clone stamp: copy pixels from a source region
|
||||
CloneStamp,
|
||||
}
|
||||
|
||||
impl Default for RasterBlendMode {
|
||||
|
|
@ -48,6 +50,11 @@ pub struct StrokeRecord {
|
|||
/// RGBA linear color [r, g, b, a]
|
||||
pub color: [f32; 4],
|
||||
pub blend_mode: RasterBlendMode,
|
||||
/// Clone stamp source offset: (source_x - drag_start_x, source_y - drag_start_y).
|
||||
/// For each dab at canvas position D, the source pixel is sampled from D + offset.
|
||||
/// None for all non-clone-stamp blend modes.
|
||||
#[serde(default)]
|
||||
pub clone_src_offset: Option<(f32, f32)>,
|
||||
pub points: Vec<StrokePoint>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,8 @@ impl InfopanelPane {
|
|||
|
||||
let is_raster_paint_tool = active_is_raster && matches!(
|
||||
tool,
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush | Tool::Erase | Tool::Smudge
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
|
||||
| Tool::Erase | Tool::Smudge | Tool::CloneStamp
|
||||
);
|
||||
|
||||
// Only show tool options for tools that have options
|
||||
|
|
@ -193,6 +194,7 @@ impl InfopanelPane {
|
|||
match tool {
|
||||
Tool::Erase => "Eraser",
|
||||
Tool::Smudge => "Smudge",
|
||||
Tool::CloneStamp => "Clone Stamp",
|
||||
_ => "Brush",
|
||||
}
|
||||
} else {
|
||||
|
|
@ -322,7 +324,8 @@ impl InfopanelPane {
|
|||
}
|
||||
|
||||
// Raster paint tools
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush | Tool::Erase if is_raster_paint_tool => {
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
|
||||
| Tool::Erase | Tool::CloneStamp if is_raster_paint_tool => {
|
||||
self.render_raster_tool_options(ui, shared, matches!(tool, Tool::Erase));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> {
|
|||
let new_a = current.a * (1.0 - dab_a);
|
||||
let scale = select(0.0, new_a / current.a, current.a > 1e-6);
|
||||
return vec4<f32>(current.r * scale, current.g * scale, current.b * scale, new_a);
|
||||
} else {
|
||||
} else if dab.blend_mode == 2u {
|
||||
// Smudge: directional warp — sample from position behind the stroke direction
|
||||
let alpha = opa_weight * dab.opacity;
|
||||
if alpha <= 0.0 { return current; }
|
||||
|
|
@ -140,6 +140,24 @@ fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> {
|
|||
alpha * src.b + da * current.b,
|
||||
alpha * src.a + da * current.a,
|
||||
);
|
||||
} else if dab.blend_mode == 3u {
|
||||
// Clone stamp: sample from (this_pixel + offset) in the source canvas.
|
||||
// color_r/color_g store the world-space offset (source_world - drag_start_world)
|
||||
// computed once when the stroke begins. Each pixel samples its own source texel.
|
||||
let alpha = opa_weight * dab.opacity;
|
||||
if alpha <= 0.0 { return current; }
|
||||
let src_x = f32(px) + 0.5 + dab.color_r;
|
||||
let src_y = f32(py) + 0.5 + dab.color_g;
|
||||
let src = bilinear_sample(src_x, src_y);
|
||||
let ba = 1.0 - alpha;
|
||||
return vec4<f32>(
|
||||
alpha * src.r + ba * current.r,
|
||||
alpha * src.g + ba * current.g,
|
||||
alpha * src.b + ba * current.b,
|
||||
alpha * src.a + ba * current.a,
|
||||
);
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -614,6 +614,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
brush_settings: scaled,
|
||||
color: [0.85f32, 0.88, 1.0, 1.0],
|
||||
blend_mode: RasterBlendMode::Normal,
|
||||
clone_src_offset: None,
|
||||
points: vec![
|
||||
StrokePoint { x: x0, y: y_lo, pressure: 1.0, tilt_x: 0.0, tilt_y: 0.0, timestamp: 0.0 },
|
||||
StrokePoint { x: mid_x, y: mid_y, pressure: 1.0, tilt_x: 0.0, tilt_y: 0.0, timestamp: 0.0 },
|
||||
|
|
@ -2482,6 +2483,11 @@ pub struct StagePane {
|
|||
/// Timestamp (ui time in seconds) of the last `compute_dabs` call for this stroke.
|
||||
/// Used to compute `dt` for the unified distance+time dab accumulator.
|
||||
raster_last_compute_time: f64,
|
||||
/// Clone stamp: world-space source point set by Alt+click.
|
||||
clone_source: Option<egui::Vec2>,
|
||||
/// Clone stamp: (source_world - drag_start_world) computed at stroke start.
|
||||
/// Constant for the entire stroke; cleared when the stroke ends.
|
||||
clone_stroke_offset: Option<(f32, f32)>,
|
||||
/// Synthetic drag/click override for test mode replay (debug builds only)
|
||||
#[cfg(debug_assertions)]
|
||||
replay_override: Option<ReplayDragState>,
|
||||
|
|
@ -2606,6 +2612,8 @@ impl StagePane {
|
|||
stroke_clip_selection: None,
|
||||
painting_float: false,
|
||||
raster_last_compute_time: 0.0,
|
||||
clone_source: None,
|
||||
clone_stroke_offset: None,
|
||||
#[cfg(debug_assertions)]
|
||||
replay_override: None,
|
||||
}
|
||||
|
|
@ -4926,6 +4934,16 @@ impl StagePane {
|
|||
&& self.raster_stroke_state.is_none())
|
||||
|| (self.rsp_clicked(response) && self.raster_stroke_state.is_none());
|
||||
if stroke_start {
|
||||
// Clone stamp: compute and store the source offset (source - drag_start).
|
||||
// This is constant for the entire stroke and used in every StrokeRecord below.
|
||||
if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::CloneStamp) {
|
||||
self.clone_stroke_offset = self.clone_source.map(|s| (
|
||||
s.x - world_pos.x, s.y - world_pos.y,
|
||||
));
|
||||
} else {
|
||||
self.clone_stroke_offset = None;
|
||||
}
|
||||
|
||||
// Determine if we are painting into the float (B) or the layer (A).
|
||||
let painting_float = shared.selection.raster_floating.is_some();
|
||||
self.painting_float = painting_float;
|
||||
|
|
@ -4954,6 +4972,7 @@ impl StagePane {
|
|||
brush_settings: brush.clone(),
|
||||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
points: vec![first_pt.clone()],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0);
|
||||
|
|
@ -5042,6 +5061,7 @@ impl StagePane {
|
|||
brush_settings: brush.clone(),
|
||||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
points: vec![first_pt.clone()],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0);
|
||||
|
|
@ -5121,10 +5141,12 @@ impl StagePane {
|
|||
};
|
||||
|
||||
if dx * dx + dy * dy >= MIN_DIST_SQ {
|
||||
let clone_src_offset = self.clone_stroke_offset;
|
||||
let seg = StrokeRecord {
|
||||
brush_settings: brush.clone(),
|
||||
color,
|
||||
blend_mode,
|
||||
clone_src_offset,
|
||||
points: vec![prev_pt, curr_local],
|
||||
};
|
||||
let current_time = ui.input(|i| i.time);
|
||||
|
|
@ -5186,6 +5208,7 @@ impl StagePane {
|
|||
brush_settings: brush.clone(),
|
||||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
points: vec![pt],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, stroke_state, dt);
|
||||
|
|
@ -7483,6 +7506,19 @@ impl StagePane {
|
|||
});
|
||||
}
|
||||
|
||||
// Clone stamp: Alt+click sets the source point regardless of the alt-pan guard below.
|
||||
{
|
||||
use lightningbeam_core::tool::Tool;
|
||||
if matches!(*shared.selected_tool, Tool::CloneStamp)
|
||||
&& alt_held
|
||||
&& self.rsp_primary_pressed(ui)
|
||||
&& response.hovered()
|
||||
{
|
||||
eprintln!("[clone stamp] set clone source to ({:.1}, {:.1})", world_pos.x, world_pos.y);
|
||||
self.clone_source = Some(world_pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tool input (only if not using Alt modifier for panning)
|
||||
if !alt_held {
|
||||
use lightningbeam_core::tool::Tool;
|
||||
|
|
@ -7527,6 +7563,11 @@ impl StagePane {
|
|||
Tool::Smudge => {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Smudge, shared);
|
||||
}
|
||||
Tool::CloneStamp => {
|
||||
// Alt+click (source-setting) is handled before this block.
|
||||
// Here alt_held is always false, so just paint.
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::CloneStamp, shared);
|
||||
}
|
||||
Tool::SelectLasso => {
|
||||
self.handle_raster_lasso_tool(ui, &response, world_pos, shared);
|
||||
}
|
||||
|
|
@ -8636,6 +8677,32 @@ impl PaneRenderer for StagePane {
|
|||
);
|
||||
}
|
||||
|
||||
// Draw clone source indicator when clone stamp tool is selected.
|
||||
if matches!(*shared.selected_tool, lightningbeam_core::tool::Tool::CloneStamp) {
|
||||
if let Some(src_world) = self.clone_source {
|
||||
let src_canvas = egui::vec2(
|
||||
src_world.x * self.zoom + self.pan_offset.x,
|
||||
src_world.y * self.zoom + self.pan_offset.y,
|
||||
);
|
||||
let src_screen = rect.min + src_canvas;
|
||||
let painter = ui.painter_at(rect);
|
||||
let r = 8.0_f32; // circle radius
|
||||
let arm = 14.0_f32; // arm half-length (extends past the circle)
|
||||
let gap = r + 2.0; // gap between circle edge and arm start
|
||||
for (width, color) in [
|
||||
(3.0_f32, egui::Color32::BLACK),
|
||||
(1.5_f32, egui::Color32::WHITE),
|
||||
] {
|
||||
let s = egui::Stroke::new(width, color);
|
||||
painter.circle_stroke(src_screen, r, s);
|
||||
painter.line_segment([src_screen - egui::vec2(arm, 0.0), src_screen - egui::vec2(gap, 0.0)], s);
|
||||
painter.line_segment([src_screen + egui::vec2(gap, 0.0), src_screen + egui::vec2(arm, 0.0)], s);
|
||||
painter.line_segment([src_screen - egui::vec2(0.0, arm), src_screen - egui::vec2(0.0, gap)], s);
|
||||
painter.line_segment([src_screen + egui::vec2(0.0, gap), src_screen + egui::vec2(0.0, arm)], s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set custom tool cursor when pointer is over the stage canvas.
|
||||
// Raster paint tools get a brush-size outline; everything else uses the SVG cursor.
|
||||
if let Some(pos) = ui.input(|i| i.pointer.hover_pos()) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue