Add healing brush
This commit is contained in:
parent
de24622f02
commit
1d9d702a59
|
|
@ -303,6 +303,7 @@ impl BrushEngine {
|
|||
RasterBlendMode::Erase => 1u32,
|
||||
RasterBlendMode::Smudge => 2u32,
|
||||
RasterBlendMode::CloneStamp => 3u32,
|
||||
RasterBlendMode::Healing => 4u32,
|
||||
};
|
||||
|
||||
let push_dab = |dabs: &mut Vec<GpuDab>,
|
||||
|
|
@ -325,7 +326,7 @@ impl BrushEngine {
|
|||
color_b: cb,
|
||||
// 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] },
|
||||
color_a: if blend_mode_u == 3 || blend_mode_u == 4 { 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),
|
||||
|
|
@ -359,7 +360,7 @@ impl BrushEngine {
|
|||
state, bs, pt.x, pt.y, base_r, pt.pressure, stroke.color,
|
||||
);
|
||||
if !matches!(base_blend, RasterBlendMode::Smudge) {
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp) {
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp | RasterBlendMode::Healing) {
|
||||
// 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)
|
||||
|
|
@ -478,7 +479,7 @@ 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) {
|
||||
} else if matches!(base_blend, RasterBlendMode::CloneStamp | RasterBlendMode::Healing) {
|
||||
// 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));
|
||||
|
|
@ -517,7 +518,7 @@ impl BrushEngine {
|
|||
last_smooth_x, last_smooth_y,
|
||||
base_r, last_pressure, stroke.color,
|
||||
);
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp) {
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp | RasterBlendMode::Healing) {
|
||||
// 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)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ pub enum RasterBlendMode {
|
|||
Smudge,
|
||||
/// Clone stamp: copy pixels from a source region
|
||||
CloneStamp,
|
||||
/// Healing brush: color-corrected clone stamp (preserves source texture, shifts color to match destination)
|
||||
Healing,
|
||||
}
|
||||
|
||||
impl Default for RasterBlendMode {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ 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::CloneStamp
|
||||
| Tool::Erase | Tool::Smudge | Tool::CloneStamp | Tool::HealingBrush
|
||||
);
|
||||
|
||||
// Only show tool options for tools that have options
|
||||
|
|
@ -195,6 +195,7 @@ impl InfopanelPane {
|
|||
Tool::Erase => "Eraser",
|
||||
Tool::Smudge => "Smudge",
|
||||
Tool::CloneStamp => "Clone Stamp",
|
||||
Tool::HealingBrush => "Healing Brush",
|
||||
_ => "Brush",
|
||||
}
|
||||
} else {
|
||||
|
|
@ -325,7 +326,7 @@ impl InfopanelPane {
|
|||
|
||||
// Raster paint tools
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
|
||||
| Tool::Erase | Tool::CloneStamp if is_raster_paint_tool => {
|
||||
| Tool::Erase | Tool::CloneStamp | Tool::HealingBrush if is_raster_paint_tool => {
|
||||
self.render_raster_tool_options(ui, shared, matches!(tool, Tool::Erase));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,47 @@ fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> {
|
|||
alpha * src.b + ba * current.b,
|
||||
alpha * src.a + ba * current.a,
|
||||
);
|
||||
} else if dab.blend_mode == 4u {
|
||||
// Healing brush: per-pixel color-corrected clone stamp.
|
||||
// color_r/color_g = source offset (ox, oy), same as clone stamp.
|
||||
// For each pixel: result = src_pixel + (local_dest_mean - local_src_mean)
|
||||
// Means are computed from 4 cardinal neighbors at ±half-radius — per-pixel, no banding.
|
||||
let alpha = opa_weight * dab.opacity;
|
||||
if alpha <= 0.0 { return current; }
|
||||
|
||||
let cw = i32(params.canvas_w);
|
||||
let ch = i32(params.canvas_h);
|
||||
let ox = dab.color_r;
|
||||
let oy = dab.color_g;
|
||||
let hr = max(dab.radius * 0.5, 1.0);
|
||||
let ihr = i32(hr);
|
||||
|
||||
// Per-pixel DESTINATION mean: 4 cardinal neighbors from canvas_src (pre-batch state)
|
||||
let d_n = textureLoad(canvas_src, vec2<i32>(px, clamp(py - ihr, 0, ch - 1)), 0);
|
||||
let d_s = textureLoad(canvas_src, vec2<i32>(px, clamp(py + ihr, 0, ch - 1)), 0);
|
||||
let d_w = textureLoad(canvas_src, vec2<i32>(clamp(px - ihr, 0, cw - 1), py ), 0);
|
||||
let d_e = textureLoad(canvas_src, vec2<i32>(clamp(px + ihr, 0, cw - 1), py ), 0);
|
||||
let d_mean = (d_n + d_s + d_w + d_e) * 0.25;
|
||||
|
||||
// Per-pixel SOURCE mean: 4 cardinal neighbors at offset position (bilinear for sub-pixel offsets)
|
||||
let spx = f32(px) + 0.5 + ox;
|
||||
let spy = f32(py) + 0.5 + oy;
|
||||
let s_mean = (bilinear_sample(spx, spy - hr)
|
||||
+ bilinear_sample(spx, spy + hr)
|
||||
+ bilinear_sample(spx - hr, spy )
|
||||
+ bilinear_sample(spx + hr, spy )) * 0.25;
|
||||
|
||||
// Source pixel + color correction
|
||||
let s_pixel = bilinear_sample(spx, spy);
|
||||
let corrected = clamp(s_pixel + (d_mean - s_mean), vec4<f32>(0.0), vec4<f32>(1.0));
|
||||
|
||||
let ba = 1.0 - alpha;
|
||||
return vec4<f32>(
|
||||
alpha * corrected.r + ba * current.r,
|
||||
alpha * corrected.g + ba * current.g,
|
||||
alpha * corrected.b + ba * current.b,
|
||||
alpha * corrected.a + ba * current.a,
|
||||
);
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4934,9 +4934,10 @@ 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).
|
||||
// Clone stamp / healing brush: 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) {
|
||||
if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::CloneStamp
|
||||
| lightningbeam_core::raster_layer::RasterBlendMode::Healing) {
|
||||
self.clone_stroke_offset = self.clone_source.map(|s| (
|
||||
s.x - world_pos.x, s.y - world_pos.y,
|
||||
));
|
||||
|
|
@ -7506,15 +7507,15 @@ impl StagePane {
|
|||
});
|
||||
}
|
||||
|
||||
// Clone stamp: Alt+click sets the source point regardless of the alt-pan guard below.
|
||||
// Clone stamp / healing brush: 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)
|
||||
if matches!(*shared.selected_tool, Tool::CloneStamp | Tool::HealingBrush)
|
||||
&& alt_held
|
||||
&& self.rsp_primary_pressed(ui)
|
||||
&& response.hovered()
|
||||
{
|
||||
eprintln!("[clone stamp] set clone source to ({:.1}, {:.1})", world_pos.x, world_pos.y);
|
||||
eprintln!("[clone/healing] set clone source to ({:.1}, {:.1})", world_pos.x, world_pos.y);
|
||||
self.clone_source = Some(world_pos);
|
||||
}
|
||||
}
|
||||
|
|
@ -7568,6 +7569,10 @@ impl StagePane {
|
|||
// 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::HealingBrush => {
|
||||
// Alt+click (source-setting) is handled before this block.
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Healing, shared);
|
||||
}
|
||||
Tool::SelectLasso => {
|
||||
self.handle_raster_lasso_tool(ui, &response, world_pos, shared);
|
||||
}
|
||||
|
|
@ -8677,8 +8682,8 @@ impl PaneRenderer for StagePane {
|
|||
);
|
||||
}
|
||||
|
||||
// Draw clone source indicator when clone stamp tool is selected.
|
||||
if matches!(*shared.selected_tool, lightningbeam_core::tool::Tool::CloneStamp) {
|
||||
// Draw clone source indicator when clone stamp or healing brush tool is selected.
|
||||
if matches!(*shared.selected_tool, lightningbeam_core::tool::Tool::CloneStamp | lightningbeam_core::tool::Tool::HealingBrush) {
|
||||
if let Some(src_world) = self.clone_source {
|
||||
let src_canvas = egui::vec2(
|
||||
src_world.x * self.zoom + self.pan_offset.x,
|
||||
|
|
|
|||
Loading…
Reference in New Issue