Add healing brush
This commit is contained in:
parent
de24622f02
commit
1d9d702a59
|
|
@ -303,6 +303,7 @@ impl BrushEngine {
|
||||||
RasterBlendMode::Erase => 1u32,
|
RasterBlendMode::Erase => 1u32,
|
||||||
RasterBlendMode::Smudge => 2u32,
|
RasterBlendMode::Smudge => 2u32,
|
||||||
RasterBlendMode::CloneStamp => 3u32,
|
RasterBlendMode::CloneStamp => 3u32,
|
||||||
|
RasterBlendMode::Healing => 4u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
let push_dab = |dabs: &mut Vec<GpuDab>,
|
let push_dab = |dabs: &mut Vec<GpuDab>,
|
||||||
|
|
@ -325,7 +326,7 @@ impl BrushEngine {
|
||||||
color_b: cb,
|
color_b: cb,
|
||||||
// Clone stamp: color_r/color_g hold source canvas X/Y, so color_a = 1.0
|
// 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).
|
// (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,
|
ndx, ndy, smudge_dist,
|
||||||
blend_mode: blend_mode_u,
|
blend_mode: blend_mode_u,
|
||||||
elliptical_dab_ratio: bs.elliptical_dab_ratio.max(1.0),
|
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,
|
state, bs, pt.x, pt.y, base_r, pt.pressure, stroke.color,
|
||||||
);
|
);
|
||||||
if !matches!(base_blend, RasterBlendMode::Smudge) {
|
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.
|
// 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));
|
let (ox, oy) = stroke.clone_src_offset.unwrap_or((0.0, 0.0));
|
||||||
(ox, oy, 0.0)
|
(ox, oy, 0.0)
|
||||||
|
|
@ -478,7 +479,7 @@ impl BrushEngine {
|
||||||
push_dab(&mut dabs, &mut bbox,
|
push_dab(&mut dabs, &mut bbox,
|
||||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||||
ndx, ndy, smudge_dist);
|
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.
|
// 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.
|
// 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));
|
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,
|
last_smooth_x, last_smooth_y,
|
||||||
base_r, last_pressure, stroke.color,
|
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.
|
// 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));
|
let (ox, oy) = stroke.clone_src_offset.unwrap_or((0.0, 0.0));
|
||||||
(ox, oy, 0.0)
|
(ox, oy, 0.0)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ pub enum RasterBlendMode {
|
||||||
Smudge,
|
Smudge,
|
||||||
/// Clone stamp: copy pixels from a source region
|
/// Clone stamp: copy pixels from a source region
|
||||||
CloneStamp,
|
CloneStamp,
|
||||||
|
/// Healing brush: color-corrected clone stamp (preserves source texture, shifts color to match destination)
|
||||||
|
Healing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RasterBlendMode {
|
impl Default for RasterBlendMode {
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ impl InfopanelPane {
|
||||||
let is_raster_paint_tool = active_is_raster && matches!(
|
let is_raster_paint_tool = active_is_raster && matches!(
|
||||||
tool,
|
tool,
|
||||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
|
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
|
// Only show tool options for tools that have options
|
||||||
|
|
@ -195,6 +195,7 @@ impl InfopanelPane {
|
||||||
Tool::Erase => "Eraser",
|
Tool::Erase => "Eraser",
|
||||||
Tool::Smudge => "Smudge",
|
Tool::Smudge => "Smudge",
|
||||||
Tool::CloneStamp => "Clone Stamp",
|
Tool::CloneStamp => "Clone Stamp",
|
||||||
|
Tool::HealingBrush => "Healing Brush",
|
||||||
_ => "Brush",
|
_ => "Brush",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -325,7 +326,7 @@ impl InfopanelPane {
|
||||||
|
|
||||||
// Raster paint tools
|
// Raster paint tools
|
||||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
|
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));
|
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.b + ba * current.b,
|
||||||
alpha * src.a + ba * current.a,
|
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 {
|
} else {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4934,9 +4934,10 @@ impl StagePane {
|
||||||
&& self.raster_stroke_state.is_none())
|
&& self.raster_stroke_state.is_none())
|
||||||
|| (self.rsp_clicked(response) && self.raster_stroke_state.is_none());
|
|| (self.rsp_clicked(response) && self.raster_stroke_state.is_none());
|
||||||
if stroke_start {
|
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.
|
// 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| (
|
self.clone_stroke_offset = self.clone_source.map(|s| (
|
||||||
s.x - world_pos.x, s.y - world_pos.y,
|
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;
|
use lightningbeam_core::tool::Tool;
|
||||||
if matches!(*shared.selected_tool, Tool::CloneStamp)
|
if matches!(*shared.selected_tool, Tool::CloneStamp | Tool::HealingBrush)
|
||||||
&& alt_held
|
&& alt_held
|
||||||
&& self.rsp_primary_pressed(ui)
|
&& self.rsp_primary_pressed(ui)
|
||||||
&& response.hovered()
|
&& 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);
|
self.clone_source = Some(world_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7568,6 +7569,10 @@ impl StagePane {
|
||||||
// Here alt_held is always false, so just paint.
|
// 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);
|
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 => {
|
Tool::SelectLasso => {
|
||||||
self.handle_raster_lasso_tool(ui, &response, world_pos, shared);
|
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.
|
// Draw clone source indicator when clone stamp or healing brush tool is selected.
|
||||||
if matches!(*shared.selected_tool, lightningbeam_core::tool::Tool::CloneStamp) {
|
if matches!(*shared.selected_tool, lightningbeam_core::tool::Tool::CloneStamp | lightningbeam_core::tool::Tool::HealingBrush) {
|
||||||
if let Some(src_world) = self.clone_source {
|
if let Some(src_world) = self.clone_source {
|
||||||
let src_canvas = egui::vec2(
|
let src_canvas = egui::vec2(
|
||||||
src_world.x * self.zoom + self.pan_offset.x,
|
src_world.x * self.zoom + self.pan_offset.x,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue