Refactor tools
This commit is contained in:
parent
901aa04246
commit
922e8f78b6
|
|
@ -329,7 +329,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 || blend_mode_u == 4 || blend_mode_u == 6 || blend_mode_u == 7 { 1.0 } else { stroke.color[3] },
|
||||
color_a: if base_blend.uses_brush_color() { stroke.color[3] } else { 1.0 },
|
||||
ndx, ndy, smudge_dist,
|
||||
blend_mode: blend_mode_u,
|
||||
elliptical_dab_ratio: bs.elliptical_dab_ratio.max(1.0),
|
||||
|
|
@ -363,21 +363,15 @@ impl BrushEngine {
|
|||
state, bs, pt.x, pt.y, base_r, pt.pressure, stroke.color,
|
||||
);
|
||||
if !matches!(base_blend, RasterBlendMode::Smudge) {
|
||||
let (cr2, cg2, cb2, ndx2, ndy2) = 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, 0.0, 0.0)
|
||||
} else if matches!(base_blend, RasterBlendMode::PatternStamp) {
|
||||
// ndx = pattern_type, ndy = pattern_scale
|
||||
(cr, cg, cb, stroke.pattern_type as f32, stroke.pattern_scale)
|
||||
} else if matches!(base_blend, RasterBlendMode::DodgeBurn) {
|
||||
// color_r = mode (0=dodge, 1=burn); strength comes from opacity
|
||||
(stroke.dodge_burn_mode as f32, 0.0, 0.0, 0.0, 0.0)
|
||||
} else if matches!(base_blend, RasterBlendMode::Sponge) {
|
||||
// color_r = mode (0=saturate, 1=desaturate); strength comes from opacity
|
||||
(stroke.sponge_mode as f32, 0.0, 0.0, 0.0, 0.0)
|
||||
} else {
|
||||
(cr, cg, cb, 0.0, 0.0)
|
||||
let tp = &stroke.tool_params;
|
||||
let (cr2, cg2, cb2, ndx2, ndy2) = match base_blend {
|
||||
RasterBlendMode::CloneStamp | RasterBlendMode::Healing =>
|
||||
(tp[0], tp[1], 0.0, 0.0, 0.0),
|
||||
RasterBlendMode::PatternStamp =>
|
||||
(cr, cg, cb, tp[0], tp[1]),
|
||||
RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge =>
|
||||
(tp[0], 0.0, 0.0, 0.0, 0.0),
|
||||
_ => (cr, cg, cb, 0.0, 0.0),
|
||||
};
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,
|
||||
ndx2, ndy2, 0.0);
|
||||
|
|
@ -491,34 +485,20 @@ 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 | 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));
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, ox, oy, 0.0,
|
||||
0.0, 0.0, 0.0);
|
||||
} else if matches!(base_blend, RasterBlendMode::PatternStamp) {
|
||||
// ndx = pattern_type, ndy = pattern_scale
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||
stroke.pattern_type as f32, stroke.pattern_scale, 0.0);
|
||||
} else if matches!(base_blend, RasterBlendMode::DodgeBurn) {
|
||||
// color_r = mode (0=dodge, 1=burn)
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2,
|
||||
stroke.dodge_burn_mode as f32, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0);
|
||||
} else if matches!(base_blend, RasterBlendMode::Sponge) {
|
||||
// color_r = mode (0=saturate, 1=desaturate)
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2,
|
||||
stroke.sponge_mode as f32, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0);
|
||||
} else {
|
||||
let tp = &stroke.tool_params;
|
||||
let (cr2, cg2, cb2, ndx2, ndy2) = match base_blend {
|
||||
RasterBlendMode::CloneStamp | RasterBlendMode::Healing =>
|
||||
(tp[0], tp[1], 0.0, 0.0, 0.0),
|
||||
RasterBlendMode::PatternStamp =>
|
||||
(cr, cg, cb, tp[0], tp[1]),
|
||||
RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge =>
|
||||
(tp[0], 0.0, 0.0, 0.0, 0.0),
|
||||
_ => (cr, cg, cb, 0.0, 0.0),
|
||||
};
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||
0.0, 0.0, 0.0);
|
||||
ex, ey, radius2, opacity2, cr2, cg2, cb2,
|
||||
ndx2, ndy2, 0.0);
|
||||
}
|
||||
|
||||
state.partial_dabs = 0.0;
|
||||
|
|
@ -547,21 +527,15 @@ impl BrushEngine {
|
|||
last_smooth_x, last_smooth_y,
|
||||
base_r, last_pressure, stroke.color,
|
||||
);
|
||||
let (cr2, cg2, cb2, ndx2, ndy2) = 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, 0.0, 0.0)
|
||||
} else if matches!(base_blend, RasterBlendMode::PatternStamp) {
|
||||
// ndx = pattern_type, ndy = pattern_scale
|
||||
(cr, cg, cb, stroke.pattern_type as f32, stroke.pattern_scale)
|
||||
} else if matches!(base_blend, RasterBlendMode::DodgeBurn) {
|
||||
// color_r = mode (0=dodge, 1=burn)
|
||||
(stroke.dodge_burn_mode as f32, 0.0, 0.0, 0.0, 0.0)
|
||||
} else if matches!(base_blend, RasterBlendMode::Sponge) {
|
||||
// color_r = mode (0=saturate, 1=desaturate)
|
||||
(stroke.sponge_mode as f32, 0.0, 0.0, 0.0, 0.0)
|
||||
} else {
|
||||
(cr, cg, cb, 0.0, 0.0)
|
||||
let tp = &stroke.tool_params;
|
||||
let (cr2, cg2, cb2, ndx2, ndy2) = match base_blend {
|
||||
RasterBlendMode::CloneStamp | RasterBlendMode::Healing =>
|
||||
(tp[0], tp[1], 0.0, 0.0, 0.0),
|
||||
RasterBlendMode::PatternStamp =>
|
||||
(cr, cg, cb, tp[0], tp[1]),
|
||||
RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge =>
|
||||
(tp[0], 0.0, 0.0, 0.0, 0.0),
|
||||
_ => (cr, cg, cb, 0.0, 0.0),
|
||||
};
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,
|
||||
ndx2, ndy2, 0.0);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,15 @@ impl Default for RasterBlendMode {
|
|||
}
|
||||
}
|
||||
|
||||
impl RasterBlendMode {
|
||||
/// Returns false for blend modes that operate on existing pixels and don't
|
||||
/// use the brush color at all (clone, heal, dodge/burn, sponge).
|
||||
/// Used by brush_engine.rs to decide whether color_a should be 1.0 or stroke.color[3].
|
||||
pub fn uses_brush_color(self) -> bool {
|
||||
!matches!(self, Self::CloneStamp | Self::Healing | Self::DodgeBurn | Self::Sponge)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single point along a stroke
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct StrokePoint {
|
||||
|
|
@ -58,28 +67,16 @@ 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.
|
||||
/// Generic tool parameters — encoding depends on blend_mode:
|
||||
/// - CloneStamp / Healing: [offset_x, offset_y, 0, 0]
|
||||
/// - PatternStamp: [pattern_type, pattern_scale, 0, 0]
|
||||
/// - DodgeBurn / Sponge: [mode, 0, 0, 0]
|
||||
/// - all others: [0, 0, 0, 0]
|
||||
#[serde(default)]
|
||||
pub clone_src_offset: Option<(f32, f32)>,
|
||||
/// Pattern stamp: procedural pattern type (0=Checkerboard, 1=Dots, 2=H-Lines, 3=V-Lines, 4=Diagonal, 5=Crosshatch)
|
||||
#[serde(default)]
|
||||
pub pattern_type: u32,
|
||||
/// Pattern stamp: tile size in pixels
|
||||
#[serde(default = "default_pattern_scale")]
|
||||
pub pattern_scale: f32,
|
||||
/// Dodge/Burn mode: 0 = dodge (lighten), 1 = burn (darken)
|
||||
#[serde(default)]
|
||||
pub dodge_burn_mode: u32,
|
||||
/// Sponge mode: 0 = saturate, 1 = desaturate
|
||||
#[serde(default)]
|
||||
pub sponge_mode: u32,
|
||||
pub tool_params: [f32; 4],
|
||||
pub points: Vec<StrokePoint>,
|
||||
}
|
||||
|
||||
fn default_pattern_scale() -> f32 { 32.0 }
|
||||
|
||||
/// Specifies how the raster content transitions to the next keyframe
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TweenType {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ use uuid::Uuid;
|
|||
mod panes;
|
||||
use panes::{PaneInstance, PaneRenderer};
|
||||
|
||||
mod tools;
|
||||
|
||||
mod widgets;
|
||||
|
||||
mod menu;
|
||||
|
|
@ -784,40 +786,8 @@ struct EditorApp {
|
|||
draw_simplify_mode: lightningbeam_core::tool::SimplifyMode, // Current simplification mode for draw tool
|
||||
rdp_tolerance: f64, // RDP simplification tolerance (default: 10.0)
|
||||
schneider_max_error: f64, // Schneider curve fitting max error (default: 30.0)
|
||||
// Raster brush settings
|
||||
brush_radius: f32, // brush radius in pixels
|
||||
brush_opacity: f32, // brush opacity 0.0–1.0
|
||||
brush_hardness: f32, // brush hardness 0.0–1.0
|
||||
brush_spacing: f32, // dabs_per_radius (fraction of radius per dab)
|
||||
brush_use_fg: bool, // true = paint with FG (stroke) color, false = BG (fill) color
|
||||
/// Full brush settings for the currently active paint preset (carries elliptical, jitter, etc.)
|
||||
active_brush_settings: lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Eraser tool brush settings (separate from paint brush, defaults to "Brush" preset)
|
||||
eraser_radius: f32,
|
||||
eraser_opacity: f32,
|
||||
eraser_hardness: f32,
|
||||
eraser_spacing: f32,
|
||||
active_eraser_settings: lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Smudge tool settings (no preset picker)
|
||||
smudge_radius: f32,
|
||||
smudge_hardness: f32,
|
||||
smudge_spacing: f32,
|
||||
smudge_strength: f32,
|
||||
/// Pattern stamp settings
|
||||
pattern_type: u32,
|
||||
pattern_scale: f32,
|
||||
/// Dodge/Burn tool settings
|
||||
dodge_burn_radius: f32,
|
||||
dodge_burn_hardness: f32,
|
||||
dodge_burn_spacing: f32,
|
||||
dodge_burn_exposure: f32,
|
||||
dodge_burn_mode: u32,
|
||||
/// Sponge tool settings
|
||||
sponge_radius: f32,
|
||||
sponge_hardness: f32,
|
||||
sponge_spacing: f32,
|
||||
sponge_flow: f32,
|
||||
sponge_mode: u32,
|
||||
/// All per-tool raster paint settings (brush, eraser, smudge, clone, pattern, dodge/burn, sponge).
|
||||
raster_settings: tools::RasterToolSettings,
|
||||
/// GPU-rendered brush preview pixel buffers, shared with VelloCallback::prepare().
|
||||
brush_preview_pixels: std::sync::Arc<std::sync::Mutex<Vec<(u32, u32, Vec<u8>)>>>,
|
||||
// Audio engine integration
|
||||
|
|
@ -1098,37 +1068,7 @@ impl EditorApp {
|
|||
draw_simplify_mode: lightningbeam_core::tool::SimplifyMode::Smooth, // Default to smooth curves
|
||||
rdp_tolerance: 10.0, // Default RDP tolerance
|
||||
schneider_max_error: 30.0, // Default Schneider max error
|
||||
brush_radius: 10.0,
|
||||
brush_opacity: 1.0,
|
||||
brush_hardness: 0.5,
|
||||
brush_spacing: 0.1,
|
||||
brush_use_fg: true,
|
||||
active_brush_settings: lightningbeam_core::brush_settings::BrushSettings::default(),
|
||||
eraser_radius: 10.0,
|
||||
eraser_opacity: 1.0,
|
||||
eraser_hardness: 0.5,
|
||||
eraser_spacing: 0.1,
|
||||
active_eraser_settings: lightningbeam_core::brush_settings::bundled_brushes()
|
||||
.iter()
|
||||
.find(|p| p.name == "Brush")
|
||||
.map(|p| p.settings.clone())
|
||||
.unwrap_or_default(),
|
||||
smudge_radius: 15.0,
|
||||
smudge_hardness: 0.8,
|
||||
smudge_spacing: 8.0,
|
||||
smudge_strength: 1.0,
|
||||
pattern_type: 0,
|
||||
pattern_scale: 32.0,
|
||||
dodge_burn_radius: 30.0,
|
||||
dodge_burn_hardness: 0.5,
|
||||
dodge_burn_spacing: 3.0,
|
||||
dodge_burn_exposure: 0.5,
|
||||
dodge_burn_mode: 0,
|
||||
sponge_radius: 30.0,
|
||||
sponge_hardness: 0.5,
|
||||
sponge_spacing: 3.0,
|
||||
sponge_flow: 0.5,
|
||||
sponge_mode: 0,
|
||||
raster_settings: tools::RasterToolSettings::default(),
|
||||
brush_preview_pixels: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
||||
audio_stream,
|
||||
audio_controller,
|
||||
|
|
@ -5582,33 +5522,7 @@ impl eframe::App for EditorApp {
|
|||
draw_simplify_mode: &mut self.draw_simplify_mode,
|
||||
rdp_tolerance: &mut self.rdp_tolerance,
|
||||
schneider_max_error: &mut self.schneider_max_error,
|
||||
brush_radius: &mut self.brush_radius,
|
||||
brush_opacity: &mut self.brush_opacity,
|
||||
brush_hardness: &mut self.brush_hardness,
|
||||
brush_spacing: &mut self.brush_spacing,
|
||||
brush_use_fg: &mut self.brush_use_fg,
|
||||
active_brush_settings: &mut self.active_brush_settings,
|
||||
eraser_radius: &mut self.eraser_radius,
|
||||
eraser_opacity: &mut self.eraser_opacity,
|
||||
eraser_hardness: &mut self.eraser_hardness,
|
||||
eraser_spacing: &mut self.eraser_spacing,
|
||||
active_eraser_settings: &mut self.active_eraser_settings,
|
||||
smudge_radius: &mut self.smudge_radius,
|
||||
smudge_hardness: &mut self.smudge_hardness,
|
||||
smudge_spacing: &mut self.smudge_spacing,
|
||||
smudge_strength: &mut self.smudge_strength,
|
||||
pattern_type: &mut self.pattern_type,
|
||||
pattern_scale: &mut self.pattern_scale,
|
||||
dodge_burn_radius: &mut self.dodge_burn_radius,
|
||||
dodge_burn_hardness: &mut self.dodge_burn_hardness,
|
||||
dodge_burn_spacing: &mut self.dodge_burn_spacing,
|
||||
dodge_burn_exposure: &mut self.dodge_burn_exposure,
|
||||
dodge_burn_mode: &mut self.dodge_burn_mode,
|
||||
sponge_radius: &mut self.sponge_radius,
|
||||
sponge_hardness: &mut self.sponge_hardness,
|
||||
sponge_spacing: &mut self.sponge_spacing,
|
||||
sponge_flow: &mut self.sponge_flow,
|
||||
sponge_mode: &mut self.sponge_mode,
|
||||
raster_settings: &mut self.raster_settings,
|
||||
audio_controller: self.audio_controller.as_ref(),
|
||||
video_manager: &self.video_manager,
|
||||
playback_time: &mut self.playback_time,
|
||||
|
|
|
|||
|
|
@ -169,12 +169,8 @@ impl InfopanelPane {
|
|||
.and_then(|id| shared.action_executor.document().get_layer(&id))
|
||||
.map_or(false, |l| matches!(l, AnyLayer::Raster(_)));
|
||||
|
||||
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::HealingBrush | Tool::PatternStamp
|
||||
| Tool::DodgeBurn | Tool::Sponge
|
||||
);
|
||||
let raster_tool_def = active_is_raster.then(|| crate::tools::raster_tool_def(&tool)).flatten();
|
||||
let is_raster_paint_tool = raster_tool_def.is_some();
|
||||
|
||||
// Only show tool options for tools that have options
|
||||
let is_vector_tool = !active_is_raster && matches!(
|
||||
|
|
@ -191,20 +187,9 @@ impl InfopanelPane {
|
|||
return;
|
||||
}
|
||||
|
||||
let header_label = if is_raster_paint_tool {
|
||||
match tool {
|
||||
Tool::Erase => "Eraser",
|
||||
Tool::Smudge => "Smudge",
|
||||
Tool::CloneStamp => "Clone Stamp",
|
||||
Tool::HealingBrush => "Healing Brush",
|
||||
Tool::PatternStamp => "Pattern Stamp",
|
||||
Tool::DodgeBurn => "Dodge / Burn",
|
||||
Tool::Sponge => "Sponge",
|
||||
_ => "Brush",
|
||||
}
|
||||
} else {
|
||||
"Tool Options"
|
||||
};
|
||||
let header_label = raster_tool_def
|
||||
.map(|d| d.header_label())
|
||||
.unwrap_or("Tool Options");
|
||||
|
||||
egui::CollapsingHeader::new(header_label)
|
||||
.id_salt(("tool_options", path))
|
||||
|
|
@ -218,6 +203,14 @@ impl InfopanelPane {
|
|||
ui.add_space(2.0);
|
||||
}
|
||||
|
||||
// Raster paint tool: delegate to per-tool impl.
|
||||
if let Some(def) = raster_tool_def {
|
||||
def.render_ui(ui, shared.raster_settings);
|
||||
if def.show_brush_preset_picker() {
|
||||
self.render_raster_tool_options(ui, shared, def.is_eraser());
|
||||
}
|
||||
}
|
||||
|
||||
match tool {
|
||||
Tool::Draw if !is_raster_paint_tool => {
|
||||
// Stroke width
|
||||
|
|
@ -328,124 +321,6 @@ impl InfopanelPane {
|
|||
});
|
||||
}
|
||||
|
||||
// Raster paint tools
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
|
||||
| Tool::Erase | Tool::CloneStamp | Tool::HealingBrush if is_raster_paint_tool => {
|
||||
self.render_raster_tool_options(ui, shared, matches!(tool, Tool::Erase));
|
||||
}
|
||||
|
||||
Tool::PatternStamp if is_raster_paint_tool => {
|
||||
const PATTERN_NAMES: &[&str] = &[
|
||||
"Checkerboard", "Dots", "H-Lines", "V-Lines", "Diagonal \\", "Diagonal /", "Crosshatch",
|
||||
];
|
||||
let selected_name = PATTERN_NAMES
|
||||
.get(*shared.pattern_type as usize)
|
||||
.copied()
|
||||
.unwrap_or("Checkerboard");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Pattern:");
|
||||
egui::ComboBox::from_id_salt("pattern_type")
|
||||
.selected_text(selected_name)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, name) in PATTERN_NAMES.iter().enumerate() {
|
||||
ui.selectable_value(shared.pattern_type, i as u32, *name);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scale:");
|
||||
ui.add(egui::Slider::new(shared.pattern_scale, 4.0_f32..=256.0)
|
||||
.logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
self.render_raster_tool_options(ui, shared, false);
|
||||
}
|
||||
|
||||
Tool::DodgeBurn if is_raster_paint_tool => {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(*shared.dodge_burn_mode == 0, "Dodge").clicked() {
|
||||
*shared.dodge_burn_mode = 0;
|
||||
}
|
||||
if ui.selectable_label(*shared.dodge_burn_mode == 1, "Burn").clicked() {
|
||||
*shared.dodge_burn_mode = 1;
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(egui::Slider::new(shared.dodge_burn_radius, 1.0_f32..=500.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Exposure:");
|
||||
ui.add(egui::Slider::new(shared.dodge_burn_exposure, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
ui.add(egui::Slider::new(shared.dodge_burn_hardness, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Spacing:");
|
||||
ui.add(egui::Slider::new(shared.dodge_burn_spacing, 0.5_f32..=20.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.1}", v)));
|
||||
});
|
||||
}
|
||||
|
||||
Tool::Sponge if is_raster_paint_tool => {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(*shared.sponge_mode == 0, "Saturate").clicked() {
|
||||
*shared.sponge_mode = 0;
|
||||
}
|
||||
if ui.selectable_label(*shared.sponge_mode == 1, "Desaturate").clicked() {
|
||||
*shared.sponge_mode = 1;
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(egui::Slider::new(shared.sponge_radius, 1.0_f32..=500.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Flow:");
|
||||
ui.add(egui::Slider::new(shared.sponge_flow, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
ui.add(egui::Slider::new(shared.sponge_hardness, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Spacing:");
|
||||
ui.add(egui::Slider::new(shared.sponge_spacing, 0.5_f32..=20.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.1}", v)));
|
||||
});
|
||||
}
|
||||
|
||||
Tool::Smudge if is_raster_paint_tool => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(egui::Slider::new(shared.smudge_radius, 1.0_f32..=200.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Strength:");
|
||||
ui.add(egui::Slider::new(shared.smudge_strength, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
ui.add(egui::Slider::new(shared.smudge_hardness, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Spacing:");
|
||||
ui.add(egui::Slider::new(shared.smudge_spacing, 0.5_f32..=20.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.1}", v)));
|
||||
});
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
|
@ -464,17 +339,19 @@ impl InfopanelPane {
|
|||
self.render_brush_preset_grid(ui, shared, is_eraser);
|
||||
ui.add_space(2.0);
|
||||
|
||||
let rs = &mut shared.raster_settings;
|
||||
|
||||
if !is_eraser {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Color:");
|
||||
ui.selectable_value(shared.brush_use_fg, true, "FG");
|
||||
ui.selectable_value(shared.brush_use_fg, false, "BG");
|
||||
ui.selectable_value(&mut rs.brush_use_fg, true, "FG");
|
||||
ui.selectable_value(&mut rs.brush_use_fg, false, "BG");
|
||||
});
|
||||
}
|
||||
|
||||
macro_rules! field {
|
||||
($eraser:ident, $brush:ident) => {
|
||||
if is_eraser { &mut *shared.$eraser } else { &mut *shared.$brush }
|
||||
if is_eraser { &mut rs.$eraser } else { &mut rs.$brush }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -603,16 +480,17 @@ impl InfopanelPane {
|
|||
selected = Some(idx);
|
||||
expanded = false;
|
||||
let s = &preset.settings;
|
||||
let rs = &mut shared.raster_settings;
|
||||
if is_eraser {
|
||||
*shared.eraser_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
*shared.eraser_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
*shared.eraser_spacing = s.dabs_per_radius;
|
||||
*shared.active_eraser_settings = s.clone();
|
||||
rs.eraser_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
rs.eraser_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
rs.eraser_spacing = s.dabs_per_radius;
|
||||
rs.active_eraser_settings = s.clone();
|
||||
} else {
|
||||
*shared.brush_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
*shared.brush_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
*shared.brush_spacing = s.dabs_per_radius;
|
||||
*shared.active_brush_settings = s.clone();
|
||||
rs.brush_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
rs.brush_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
rs.brush_spacing = s.dabs_per_radius;
|
||||
rs.active_brush_settings = s.clone();
|
||||
// If the user was on a preset-backed tool (Pencil/Pen/Airbrush)
|
||||
// and manually picked a different brush, revert to the generic tool.
|
||||
if matches!(*shared.selected_tool, Tool::Pencil | Tool::Pen | Tool::Airbrush) {
|
||||
|
|
|
|||
|
|
@ -187,45 +187,8 @@ pub struct SharedPaneState<'a> {
|
|||
pub draw_simplify_mode: &'a mut lightningbeam_core::tool::SimplifyMode,
|
||||
pub rdp_tolerance: &'a mut f64,
|
||||
pub schneider_max_error: &'a mut f64,
|
||||
/// Raster paint brush settings
|
||||
pub brush_radius: &'a mut f32,
|
||||
pub brush_opacity: &'a mut f32,
|
||||
pub brush_hardness: &'a mut f32,
|
||||
pub brush_spacing: &'a mut f32,
|
||||
/// Whether the brush paints with the foreground (fill) color (true) or background (stroke) color (false)
|
||||
pub brush_use_fg: &'a mut bool,
|
||||
/// Full brush settings for the active paint preset (carries elliptical, jitter, slow_tracking, etc.)
|
||||
pub active_brush_settings: &'a mut lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Raster eraser brush settings (separate from paint brush)
|
||||
pub eraser_radius: &'a mut f32,
|
||||
pub eraser_opacity: &'a mut f32,
|
||||
pub eraser_hardness: &'a mut f32,
|
||||
pub eraser_spacing: &'a mut f32,
|
||||
/// Full brush settings for the active eraser preset
|
||||
pub active_eraser_settings: &'a mut lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Raster smudge tool settings (no preset picker)
|
||||
pub smudge_radius: &'a mut f32,
|
||||
pub smudge_hardness: &'a mut f32,
|
||||
pub smudge_spacing: &'a mut f32,
|
||||
pub smudge_strength: &'a mut f32,
|
||||
/// Pattern stamp: selected procedural pattern type (0=Checkerboard..5=Crosshatch)
|
||||
pub pattern_type: &'a mut u32,
|
||||
/// Pattern stamp: tile size in pixels
|
||||
pub pattern_scale: &'a mut f32,
|
||||
/// Dodge/Burn tool settings
|
||||
pub dodge_burn_radius: &'a mut f32,
|
||||
pub dodge_burn_hardness: &'a mut f32,
|
||||
pub dodge_burn_spacing: &'a mut f32,
|
||||
pub dodge_burn_exposure: &'a mut f32,
|
||||
/// 0 = dodge (lighten), 1 = burn (darken)
|
||||
pub dodge_burn_mode: &'a mut u32,
|
||||
/// Sponge tool settings
|
||||
pub sponge_radius: &'a mut f32,
|
||||
pub sponge_hardness: &'a mut f32,
|
||||
pub sponge_spacing: &'a mut f32,
|
||||
pub sponge_flow: &'a mut f32,
|
||||
/// 0 = saturate, 1 = desaturate
|
||||
pub sponge_mode: &'a mut u32,
|
||||
/// All per-tool raster paint settings (replaces 20+ individual fields).
|
||||
pub raster_settings: &'a mut crate::tools::RasterToolSettings,
|
||||
/// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety)
|
||||
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
||||
/// Video manager for video decoding and frame caching
|
||||
|
|
|
|||
|
|
@ -614,11 +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,
|
||||
pattern_type: 0,
|
||||
pattern_scale: 32.0,
|
||||
dodge_burn_mode: 0,
|
||||
sponge_mode: 0,
|
||||
tool_params: [0.0; 4],
|
||||
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 },
|
||||
|
|
@ -2487,8 +2483,6 @@ 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)>,
|
||||
|
|
@ -2616,7 +2610,6 @@ 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,
|
||||
|
|
@ -4841,6 +4834,27 @@ impl StagePane {
|
|||
/// `self.pending_raster_dabs` for dispatch by `VelloCallback::prepare()`.
|
||||
///
|
||||
/// The actual pixel rendering happens on the GPU (compute shader). The CPU
|
||||
/// Build the `tool_params: [f32; 4]` for a StrokeRecord.
|
||||
/// For clone/healing: [offset_x, offset_y, 0, 0] (computed from clone_stroke_offset).
|
||||
/// For all other tools: delegates to def.tool_params().
|
||||
fn make_tool_params(
|
||||
&self,
|
||||
def: &dyn crate::tools::RasterToolDef,
|
||||
shared: &SharedPaneState,
|
||||
) -> [f32; 4] {
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
match def.blend_mode() {
|
||||
RasterBlendMode::CloneStamp | RasterBlendMode::Healing => {
|
||||
if let Some((ox, oy)) = self.clone_stroke_offset {
|
||||
[ox, oy, 0.0, 0.0]
|
||||
} else {
|
||||
[0.0; 4]
|
||||
}
|
||||
}
|
||||
_ => def.tool_params(shared.raster_settings),
|
||||
}
|
||||
}
|
||||
|
||||
/// only does dab placement arithmetic (cheap). On stroke end a readback is
|
||||
/// requested so the undo system can capture the final pixel state.
|
||||
fn handle_raster_stroke_tool(
|
||||
|
|
@ -4848,9 +4862,10 @@ impl StagePane {
|
|||
ui: &mut egui::Ui,
|
||||
response: &egui::Response,
|
||||
world_pos: egui::Vec2,
|
||||
blend_mode: lightningbeam_core::raster_layer::RasterBlendMode,
|
||||
def: &'static dyn crate::tools::RasterToolDef,
|
||||
shared: &mut SharedPaneState,
|
||||
) {
|
||||
let blend_mode = def.blend_mode();
|
||||
use lightningbeam_core::tool::ToolState;
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
use lightningbeam_core::raster_layer::StrokePoint;
|
||||
|
|
@ -4869,46 +4884,10 @@ impl StagePane {
|
|||
if !is_raster { return; }
|
||||
|
||||
let brush = {
|
||||
// Start from the active preset for this tool, then override the
|
||||
// user-controlled slider values.
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
let (base_settings, radius, opacity, hardness, spacing) = match blend_mode {
|
||||
RasterBlendMode::Erase => (
|
||||
shared.active_eraser_settings.clone(),
|
||||
*shared.eraser_radius,
|
||||
*shared.eraser_opacity,
|
||||
*shared.eraser_hardness,
|
||||
*shared.eraser_spacing,
|
||||
),
|
||||
RasterBlendMode::Smudge => (
|
||||
lightningbeam_core::brush_settings::BrushSettings::default(),
|
||||
*shared.smudge_radius,
|
||||
1.0, // opacity fixed at 1.0; strength is a separate smudge_dist multiplier
|
||||
*shared.smudge_hardness,
|
||||
*shared.smudge_spacing,
|
||||
),
|
||||
RasterBlendMode::DodgeBurn => (
|
||||
lightningbeam_core::brush_settings::BrushSettings::default(),
|
||||
*shared.dodge_burn_radius,
|
||||
*shared.dodge_burn_exposure,
|
||||
*shared.dodge_burn_hardness,
|
||||
*shared.dodge_burn_spacing,
|
||||
),
|
||||
RasterBlendMode::Sponge => (
|
||||
lightningbeam_core::brush_settings::BrushSettings::default(),
|
||||
*shared.sponge_radius,
|
||||
*shared.sponge_flow,
|
||||
*shared.sponge_hardness,
|
||||
*shared.sponge_spacing,
|
||||
),
|
||||
_ => (
|
||||
shared.active_brush_settings.clone(),
|
||||
*shared.brush_radius,
|
||||
*shared.brush_opacity,
|
||||
*shared.brush_hardness,
|
||||
*shared.brush_spacing,
|
||||
),
|
||||
};
|
||||
// Delegate brush parameter extraction to the tool definition.
|
||||
let bp = def.brush_params(shared.raster_settings);
|
||||
let (base_settings, radius, opacity, hardness, spacing) =
|
||||
(bp.base_settings, bp.radius, bp.opacity, bp.hardness, bp.spacing);
|
||||
let mut b = base_settings;
|
||||
// Compensate for pressure_radius_gain so that the UI-chosen radius is the
|
||||
// actual rendered radius at our fixed mouse pressure of 1.0.
|
||||
|
|
@ -4918,12 +4897,12 @@ impl StagePane {
|
|||
b.hardness = hardness;
|
||||
b.opaque = opacity;
|
||||
b.dabs_per_radius = spacing;
|
||||
if matches!(blend_mode, RasterBlendMode::Smudge) {
|
||||
if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Smudge) {
|
||||
// Zero dabs_per_actual_radius so the spacing slider is the sole density control.
|
||||
b.dabs_per_actual_radius = 0.0;
|
||||
// strength controls how far behind the stroke to sample (smudge_dist multiplier).
|
||||
// smudge_dist = radius * exp(smudge_radius_log), so log(strength) gives the ratio.
|
||||
b.smudge_radius_log = *shared.smudge_strength; // linear [0,1] strength
|
||||
b.smudge_radius_log = shared.raster_settings.smudge_strength; // linear [0,1] strength
|
||||
}
|
||||
b
|
||||
};
|
||||
|
|
@ -4931,7 +4910,7 @@ impl StagePane {
|
|||
let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) {
|
||||
[1.0f32, 1.0, 1.0, 1.0]
|
||||
} else {
|
||||
let c = if *shared.brush_use_fg { *shared.stroke_color } else { *shared.fill_color };
|
||||
let c = if shared.raster_settings.brush_use_fg { *shared.stroke_color } else { *shared.fill_color };
|
||||
let s2l = |v: u8| -> f32 {
|
||||
let f = v as f32 / 255.0;
|
||||
if f <= 0.04045 { f / 12.92 } else { ((f + 0.055) / 1.055).powf(2.4) }
|
||||
|
|
@ -4956,7 +4935,7 @@ impl StagePane {
|
|||
// This is constant for the entire stroke and used in every StrokeRecord below.
|
||||
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 = shared.raster_settings.clone_source.map(|s| (
|
||||
s.x - world_pos.x, s.y - world_pos.y,
|
||||
));
|
||||
} else {
|
||||
|
|
@ -4991,11 +4970,7 @@ impl StagePane {
|
|||
brush_settings: brush.clone(),
|
||||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
dodge_burn_mode: *shared.dodge_burn_mode,
|
||||
sponge_mode: *shared.sponge_mode,
|
||||
tool_params: self.make_tool_params(def, shared),
|
||||
points: vec![first_pt.clone()],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0);
|
||||
|
|
@ -5084,11 +5059,7 @@ impl StagePane {
|
|||
brush_settings: brush.clone(),
|
||||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
dodge_burn_mode: *shared.dodge_burn_mode,
|
||||
sponge_mode: *shared.sponge_mode,
|
||||
tool_params: self.make_tool_params(def, shared),
|
||||
points: vec![first_pt.clone()],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0);
|
||||
|
|
@ -5130,6 +5101,7 @@ impl StagePane {
|
|||
// Mouse drag: compute dabs for this segment
|
||||
// ----------------------------------------------------------------
|
||||
if self.rsp_dragged(response) {
|
||||
let tool_params = self.make_tool_params(def, shared);
|
||||
if let Some((layer_id, time, ref mut stroke_state, _)) = self.raster_stroke_state {
|
||||
if let Some(prev_pt) = self.raster_last_point.take() {
|
||||
// Get canvas info and float offset now (used for both distance check
|
||||
|
|
@ -5168,16 +5140,11 @@ 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,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
dodge_burn_mode: *shared.dodge_burn_mode,
|
||||
sponge_mode: *shared.sponge_mode,
|
||||
tool_params,
|
||||
points: vec![prev_pt, curr_local],
|
||||
};
|
||||
let current_time = ui.input(|i| i.time);
|
||||
|
|
@ -5214,6 +5181,7 @@ impl StagePane {
|
|||
if self.raster_last_compute_time > 0.0 {
|
||||
let dt = (current_time - self.raster_last_compute_time).clamp(0.0, 0.1) as f32;
|
||||
self.raster_last_compute_time = current_time;
|
||||
let tool_params = self.make_tool_params(def, shared);
|
||||
|
||||
if let Some((layer_id, time, ref mut stroke_state, _)) = self.raster_stroke_state {
|
||||
let canvas_info = if self.painting_float {
|
||||
|
|
@ -5239,11 +5207,7 @@ impl StagePane {
|
|||
brush_settings: brush.clone(),
|
||||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
dodge_burn_mode: *shared.dodge_burn_mode,
|
||||
sponge_mode: *shared.sponge_mode,
|
||||
tool_params,
|
||||
points: vec![pt],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, stroke_state, dt);
|
||||
|
|
@ -7541,16 +7505,18 @@ impl StagePane {
|
|||
});
|
||||
}
|
||||
|
||||
// Clone stamp / healing brush: Alt+click sets the source point regardless of the alt-pan guard below.
|
||||
// Alt+click: set source point for clone/healing tools.
|
||||
{
|
||||
use lightningbeam_core::tool::Tool;
|
||||
if matches!(*shared.selected_tool, Tool::CloneStamp | Tool::HealingBrush)
|
||||
let tool_uses_alt = crate::tools::raster_tool_def(shared.selected_tool)
|
||||
.map_or(false, |d| d.uses_alt_click());
|
||||
if tool_uses_alt
|
||||
&& alt_held
|
||||
&& self.rsp_primary_pressed(ui)
|
||||
&& response.hovered()
|
||||
{
|
||||
eprintln!("[clone/healing] set clone source to ({:.1}, {:.1})", world_pos.x, world_pos.y);
|
||||
self.clone_source = Some(world_pos);
|
||||
shared.raster_settings.clone_source = Some(world_pos);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -7584,37 +7550,14 @@ impl StagePane {
|
|||
shared.action_executor.document().get_layer(&id)
|
||||
}).map_or(false, |l| matches!(l, lightningbeam_core::layer::AnyLayer::Raster(_)));
|
||||
if is_raster {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Normal, shared);
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, &crate::tools::paint::PAINT, shared);
|
||||
} else {
|
||||
self.handle_draw_tool(ui, &response, world_pos, shared);
|
||||
}
|
||||
}
|
||||
Tool::Pencil | Tool::Pen | Tool::Airbrush => {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Normal, shared);
|
||||
}
|
||||
Tool::Erase => {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Erase, shared);
|
||||
}
|
||||
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::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::PatternStamp => {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::PatternStamp, shared);
|
||||
}
|
||||
Tool::DodgeBurn => {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::DodgeBurn, shared);
|
||||
}
|
||||
Tool::Sponge => {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Sponge, shared);
|
||||
tool if crate::tools::raster_tool_def(&tool).is_some() => {
|
||||
let def = crate::tools::raster_tool_def(&tool).unwrap();
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, def, shared);
|
||||
}
|
||||
Tool::SelectLasso => {
|
||||
self.handle_raster_lasso_tool(ui, &response, world_pos, shared);
|
||||
|
|
@ -8092,20 +8035,25 @@ impl StagePane {
|
|||
use lightningbeam_core::tool::Tool;
|
||||
|
||||
// Compute semi-axes (world pixels) and dab rotation angle.
|
||||
let (a_world, b_world, dab_angle_rad) = match *shared.selected_tool {
|
||||
Tool::Erase => (*shared.eraser_radius, *shared.eraser_radius, 0.0_f32),
|
||||
Tool::Smudge
|
||||
| Tool::BlurSharpen => (*shared.smudge_radius, *shared.smudge_radius, 0.0_f32),
|
||||
Tool::DodgeBurn => (*shared.dodge_burn_radius, *shared.dodge_burn_radius, 0.0_f32),
|
||||
Tool::Sponge => (*shared.sponge_radius, *shared.sponge_radius, 0.0_f32),
|
||||
_ => {
|
||||
let bs = &shared.active_brush_settings;
|
||||
let r = *shared.brush_radius;
|
||||
let (a_world, b_world, dab_angle_rad) = if let Some(def) = crate::tools::raster_tool_def(shared.selected_tool) {
|
||||
let r = def.cursor_radius(shared.raster_settings);
|
||||
// For the standard paint brush, also account for elliptical shape.
|
||||
if matches!(*shared.selected_tool,
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush)
|
||||
{
|
||||
let bs = &shared.raster_settings.active_brush_settings;
|
||||
let ratio = bs.elliptical_dab_ratio.max(1.0);
|
||||
// Expand radius to cover the full jitter extent.
|
||||
let expand = 1.0 + bs.offset_by_random;
|
||||
(r * expand, r * expand / ratio, bs.elliptical_dab_angle.to_radians())
|
||||
} else {
|
||||
(r, r, 0.0_f32)
|
||||
}
|
||||
} else {
|
||||
let bs = &shared.raster_settings.active_brush_settings;
|
||||
let r = shared.raster_settings.brush_radius;
|
||||
let ratio = bs.elliptical_dab_ratio.max(1.0);
|
||||
let expand = 1.0 + bs.offset_by_random;
|
||||
(r * expand, r * expand / ratio, bs.elliptical_dab_angle.to_radians())
|
||||
};
|
||||
|
||||
let a = a_world * self.zoom; // major semi-axis in screen pixels
|
||||
|
|
@ -8726,8 +8674,10 @@ impl PaneRenderer for StagePane {
|
|||
}
|
||||
|
||||
// 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 tool_uses_alt = crate::tools::raster_tool_def(shared.selected_tool)
|
||||
.map_or(false, |d| d.uses_alt_click());
|
||||
if tool_uses_alt {
|
||||
if let Some(src_world) = shared.raster_settings.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,
|
||||
|
|
|
|||
|
|
@ -136,10 +136,10 @@ impl PaneRenderer for ToolbarPane {
|
|||
if let Some(name) = preset_name {
|
||||
if let Some(preset) = bundled_brushes().iter().find(|p| p.name == name) {
|
||||
let s = &preset.settings;
|
||||
*shared.brush_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
*shared.brush_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
*shared.brush_spacing = s.dabs_per_radius;
|
||||
*shared.active_brush_settings = s.clone();
|
||||
shared.raster_settings.brush_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
shared.raster_settings.brush_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
shared.raster_settings.brush_spacing = s.dabs_per_radius;
|
||||
shared.raster_settings.active_brush_settings = s.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
|
||||
pub struct CloneStampTool;
|
||||
pub static CLONE_STAMP: CloneStampTool = CloneStampTool;
|
||||
|
||||
impl RasterToolDef for CloneStampTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::CloneStamp }
|
||||
fn header_label(&self) -> &'static str { "Clone Stamp" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: s.active_brush_settings.clone(),
|
||||
radius: s.brush_radius,
|
||||
opacity: s.brush_opacity,
|
||||
hardness: s.brush_hardness,
|
||||
spacing: s.brush_spacing,
|
||||
}
|
||||
}
|
||||
/// For Clone Stamp, tool_params are filled by stage.rs at stroke-start time
|
||||
/// (offset = clone_source - stroke_start), not from settings directly.
|
||||
fn tool_params(&self, _s: &RasterToolSettings) -> [f32; 4] { [0.0; 4] }
|
||||
fn uses_alt_click(&self) -> bool { true }
|
||||
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings) {
|
||||
if s.clone_source.is_none() {
|
||||
ui.label("Alt+click to set source point.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::{brush_settings::BrushSettings, raster_layer::RasterBlendMode};
|
||||
|
||||
pub struct DodgeBurnTool;
|
||||
pub static DODGE_BURN: DodgeBurnTool = DodgeBurnTool;
|
||||
|
||||
impl RasterToolDef for DodgeBurnTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::DodgeBurn }
|
||||
fn header_label(&self) -> &'static str { "Dodge / Burn" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: BrushSettings::default(),
|
||||
radius: s.dodge_burn_radius,
|
||||
opacity: s.dodge_burn_exposure,
|
||||
hardness: s.dodge_burn_hardness,
|
||||
spacing: s.dodge_burn_spacing,
|
||||
}
|
||||
}
|
||||
fn tool_params(&self, s: &RasterToolSettings) -> [f32; 4] {
|
||||
[s.dodge_burn_mode as f32, 0.0, 0.0, 0.0]
|
||||
}
|
||||
fn show_brush_preset_picker(&self) -> bool { false }
|
||||
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings) {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(s.dodge_burn_mode == 0, "Dodge").clicked() {
|
||||
s.dodge_burn_mode = 0;
|
||||
}
|
||||
if ui.selectable_label(s.dodge_burn_mode == 1, "Burn").clicked() {
|
||||
s.dodge_burn_mode = 1;
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(egui::Slider::new(&mut s.dodge_burn_radius, 1.0_f32..=500.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Exposure:");
|
||||
ui.add(egui::Slider::new(&mut s.dodge_burn_exposure, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
ui.add(egui::Slider::new(&mut s.dodge_burn_hardness, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Spacing:");
|
||||
ui.add(egui::Slider::new(&mut s.dodge_burn_spacing, 0.5_f32..=20.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.1}", v)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
|
||||
pub struct EraseTool;
|
||||
pub static ERASE: EraseTool = EraseTool;
|
||||
|
||||
impl RasterToolDef for EraseTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::Erase }
|
||||
fn header_label(&self) -> &'static str { "Eraser" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: s.active_eraser_settings.clone(),
|
||||
radius: s.eraser_radius,
|
||||
opacity: s.eraser_opacity,
|
||||
hardness: s.eraser_hardness,
|
||||
spacing: s.eraser_spacing,
|
||||
}
|
||||
}
|
||||
fn tool_params(&self, _s: &RasterToolSettings) -> [f32; 4] { [0.0; 4] }
|
||||
fn is_eraser(&self) -> bool { true }
|
||||
fn render_ui(&self, _ui: &mut egui::Ui, _s: &mut RasterToolSettings) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
|
||||
pub struct HealingBrushTool;
|
||||
pub static HEALING_BRUSH: HealingBrushTool = HealingBrushTool;
|
||||
|
||||
impl RasterToolDef for HealingBrushTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::Healing }
|
||||
fn header_label(&self) -> &'static str { "Healing Brush" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: s.active_brush_settings.clone(),
|
||||
radius: s.brush_radius,
|
||||
opacity: s.brush_opacity,
|
||||
hardness: s.brush_hardness,
|
||||
spacing: s.brush_spacing,
|
||||
}
|
||||
}
|
||||
/// tool_params are filled by stage.rs at stroke-start time (clone offset).
|
||||
fn tool_params(&self, _s: &RasterToolSettings) -> [f32; 4] { [0.0; 4] }
|
||||
fn uses_alt_click(&self) -> bool { true }
|
||||
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings) {
|
||||
if s.clone_source.is_none() {
|
||||
ui.label("Alt+click to set source point.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/// Per-tool module for raster painting tools.
|
||||
///
|
||||
/// Each tool implements `RasterToolDef`. Adding a new tool requires:
|
||||
/// 1. A new file in this directory implementing `RasterToolDef`.
|
||||
/// 2. One entry in `raster_tool_def()` below.
|
||||
/// 3. Core changes: `RasterBlendMode` variant, `brush_engine.rs` constant, WGSL branch.
|
||||
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::{
|
||||
brush_settings::BrushSettings,
|
||||
raster_layer::RasterBlendMode,
|
||||
tool::Tool,
|
||||
};
|
||||
|
||||
pub mod paint;
|
||||
pub mod erase;
|
||||
pub mod smudge;
|
||||
pub mod clone_stamp;
|
||||
pub mod healing_brush;
|
||||
pub mod pattern_stamp;
|
||||
pub mod dodge_burn;
|
||||
pub mod sponge;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared settings struct (replaces 20+ individual SharedPaneState / EditorApp fields)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// All per-tool settings for raster painting. Owned by `EditorApp`; borrowed
|
||||
/// by `SharedPaneState` as a single `&'a mut RasterToolSettings`.
|
||||
pub struct RasterToolSettings {
|
||||
// --- Paint brush ---
|
||||
pub brush_radius: f32,
|
||||
pub brush_opacity: f32,
|
||||
pub brush_hardness: f32,
|
||||
pub brush_spacing: f32,
|
||||
/// true = paint with FG (stroke) color, false = BG (fill) color
|
||||
pub brush_use_fg: bool,
|
||||
pub active_brush_settings: BrushSettings,
|
||||
// --- Eraser ---
|
||||
pub eraser_radius: f32,
|
||||
pub eraser_opacity: f32,
|
||||
pub eraser_hardness: f32,
|
||||
pub eraser_spacing: f32,
|
||||
pub active_eraser_settings: BrushSettings,
|
||||
// --- Smudge ---
|
||||
pub smudge_radius: f32,
|
||||
pub smudge_hardness: f32,
|
||||
pub smudge_spacing: f32,
|
||||
pub smudge_strength: f32,
|
||||
// --- Clone / Healing ---
|
||||
/// World-space source point set by Alt+click.
|
||||
pub clone_source: Option<egui::Vec2>,
|
||||
// --- Pattern stamp ---
|
||||
pub pattern_type: u32,
|
||||
pub pattern_scale: f32,
|
||||
// --- Dodge / Burn ---
|
||||
pub dodge_burn_radius: f32,
|
||||
pub dodge_burn_hardness: f32,
|
||||
pub dodge_burn_spacing: f32,
|
||||
pub dodge_burn_exposure: f32,
|
||||
/// 0 = dodge (lighten), 1 = burn (darken)
|
||||
pub dodge_burn_mode: u32,
|
||||
// --- Sponge ---
|
||||
pub sponge_radius: f32,
|
||||
pub sponge_hardness: f32,
|
||||
pub sponge_spacing: f32,
|
||||
pub sponge_flow: f32,
|
||||
/// 0 = saturate, 1 = desaturate
|
||||
pub sponge_mode: u32,
|
||||
}
|
||||
|
||||
impl Default for RasterToolSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
brush_radius: 10.0,
|
||||
brush_opacity: 1.0,
|
||||
brush_hardness: 0.5,
|
||||
brush_spacing: 0.1,
|
||||
brush_use_fg: true,
|
||||
active_brush_settings: BrushSettings::default(),
|
||||
eraser_radius: 10.0,
|
||||
eraser_opacity: 1.0,
|
||||
eraser_hardness: 0.5,
|
||||
eraser_spacing: 0.1,
|
||||
active_eraser_settings: lightningbeam_core::brush_settings::bundled_brushes()
|
||||
.iter()
|
||||
.find(|p| p.name == "Brush")
|
||||
.map(|p| p.settings.clone())
|
||||
.unwrap_or_default(),
|
||||
smudge_radius: 15.0,
|
||||
smudge_hardness: 0.8,
|
||||
smudge_spacing: 8.0,
|
||||
smudge_strength: 1.0,
|
||||
clone_source: None,
|
||||
pattern_type: 0,
|
||||
pattern_scale: 32.0,
|
||||
dodge_burn_radius: 30.0,
|
||||
dodge_burn_hardness: 0.5,
|
||||
dodge_burn_spacing: 3.0,
|
||||
dodge_burn_exposure: 0.5,
|
||||
dodge_burn_mode: 0,
|
||||
sponge_radius: 30.0,
|
||||
sponge_hardness: 0.5,
|
||||
sponge_spacing: 3.0,
|
||||
sponge_flow: 0.5,
|
||||
sponge_mode: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brush parameters extracted per-tool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct BrushParams {
|
||||
pub base_settings: BrushSettings,
|
||||
pub radius: f32,
|
||||
pub opacity: f32,
|
||||
pub hardness: f32,
|
||||
pub spacing: f32,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RasterToolDef trait
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub trait RasterToolDef: Send + Sync {
|
||||
fn blend_mode(&self) -> RasterBlendMode;
|
||||
fn header_label(&self) -> &'static str;
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams;
|
||||
/// Encode tool-specific state into the 4-float `StrokeRecord::tool_params`.
|
||||
fn tool_params(&self, s: &RasterToolSettings) -> [f32; 4];
|
||||
/// Cursor display radius (world pixels).
|
||||
fn cursor_radius(&self, s: &RasterToolSettings) -> f32 {
|
||||
self.brush_params(s).radius
|
||||
}
|
||||
/// Render tool-specific controls in the infopanel (called before preset picker if any).
|
||||
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings);
|
||||
/// Whether to show the brush preset picker after `render_ui`.
|
||||
fn show_brush_preset_picker(&self) -> bool { true }
|
||||
/// Whether this tool is the eraser (drives preset picker + color UI visibility).
|
||||
fn is_eraser(&self) -> bool { false }
|
||||
/// Whether Alt+click sets a source point for this tool.
|
||||
fn uses_alt_click(&self) -> bool { false }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lookup: Tool → &'static dyn RasterToolDef
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn raster_tool_def(tool: &Tool) -> Option<&'static dyn RasterToolDef> {
|
||||
match tool {
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush => Some(&paint::PAINT),
|
||||
Tool::Erase => Some(&erase::ERASE),
|
||||
Tool::Smudge => Some(&smudge::SMUDGE),
|
||||
Tool::CloneStamp => Some(&clone_stamp::CLONE_STAMP),
|
||||
Tool::HealingBrush => Some(&healing_brush::HEALING_BRUSH),
|
||||
Tool::PatternStamp => Some(&pattern_stamp::PATTERN_STAMP),
|
||||
Tool::DodgeBurn => Some(&dodge_burn::DODGE_BURN),
|
||||
Tool::Sponge => Some(&sponge::SPONGE),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
|
||||
pub struct PaintTool;
|
||||
pub static PAINT: PaintTool = PaintTool;
|
||||
|
||||
impl RasterToolDef for PaintTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::Normal }
|
||||
fn header_label(&self) -> &'static str { "Brush" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: s.active_brush_settings.clone(),
|
||||
radius: s.brush_radius,
|
||||
opacity: s.brush_opacity,
|
||||
hardness: s.brush_hardness,
|
||||
spacing: s.brush_spacing,
|
||||
}
|
||||
}
|
||||
fn tool_params(&self, _s: &RasterToolSettings) -> [f32; 4] { [0.0; 4] }
|
||||
fn render_ui(&self, _ui: &mut egui::Ui, _s: &mut RasterToolSettings) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
|
||||
pub struct PatternStampTool;
|
||||
pub static PATTERN_STAMP: PatternStampTool = PatternStampTool;
|
||||
|
||||
const PATTERN_NAMES: &[&str] = &[
|
||||
"Checkerboard", "Dots", "H-Lines", "V-Lines", "Diagonal \\", "Diagonal /", "Crosshatch",
|
||||
];
|
||||
|
||||
impl RasterToolDef for PatternStampTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::PatternStamp }
|
||||
fn header_label(&self) -> &'static str { "Pattern Stamp" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: s.active_brush_settings.clone(),
|
||||
radius: s.brush_radius,
|
||||
opacity: s.brush_opacity,
|
||||
hardness: s.brush_hardness,
|
||||
spacing: s.brush_spacing,
|
||||
}
|
||||
}
|
||||
fn tool_params(&self, s: &RasterToolSettings) -> [f32; 4] {
|
||||
[s.pattern_type as f32, s.pattern_scale, 0.0, 0.0]
|
||||
}
|
||||
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings) {
|
||||
let selected_name = PATTERN_NAMES
|
||||
.get(s.pattern_type as usize)
|
||||
.copied()
|
||||
.unwrap_or("Checkerboard");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Pattern:");
|
||||
egui::ComboBox::from_id_salt("pattern_type")
|
||||
.selected_text(selected_name)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, name) in PATTERN_NAMES.iter().enumerate() {
|
||||
ui.selectable_value(&mut s.pattern_type, i as u32, *name);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scale:");
|
||||
ui.add(egui::Slider::new(&mut s.pattern_scale, 4.0_f32..=256.0)
|
||||
.logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::{brush_settings::BrushSettings, raster_layer::RasterBlendMode};
|
||||
|
||||
pub struct SmudgeTool;
|
||||
pub static SMUDGE: SmudgeTool = SmudgeTool;
|
||||
|
||||
impl RasterToolDef for SmudgeTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::Smudge }
|
||||
fn header_label(&self) -> &'static str { "Smudge" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: BrushSettings::default(),
|
||||
radius: s.smudge_radius,
|
||||
opacity: 1.0, // strength is a separate smudge_dist multiplier
|
||||
hardness: s.smudge_hardness,
|
||||
spacing: s.smudge_spacing,
|
||||
}
|
||||
}
|
||||
fn tool_params(&self, _s: &RasterToolSettings) -> [f32; 4] { [0.0; 4] }
|
||||
fn show_brush_preset_picker(&self) -> bool { false }
|
||||
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(egui::Slider::new(&mut s.smudge_radius, 1.0_f32..=200.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Strength:");
|
||||
ui.add(egui::Slider::new(&mut s.smudge_strength, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
ui.add(egui::Slider::new(&mut s.smudge_hardness, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Spacing:");
|
||||
ui.add(egui::Slider::new(&mut s.smudge_spacing, 0.5_f32..=20.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.1}", v)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
use super::{BrushParams, RasterToolDef, RasterToolSettings};
|
||||
use eframe::egui;
|
||||
use lightningbeam_core::{brush_settings::BrushSettings, raster_layer::RasterBlendMode};
|
||||
|
||||
pub struct SpongeTool;
|
||||
pub static SPONGE: SpongeTool = SpongeTool;
|
||||
|
||||
impl RasterToolDef for SpongeTool {
|
||||
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::Sponge }
|
||||
fn header_label(&self) -> &'static str { "Sponge" }
|
||||
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
|
||||
BrushParams {
|
||||
base_settings: BrushSettings::default(),
|
||||
radius: s.sponge_radius,
|
||||
opacity: s.sponge_flow,
|
||||
hardness: s.sponge_hardness,
|
||||
spacing: s.sponge_spacing,
|
||||
}
|
||||
}
|
||||
fn tool_params(&self, s: &RasterToolSettings) -> [f32; 4] {
|
||||
[s.sponge_mode as f32, 0.0, 0.0, 0.0]
|
||||
}
|
||||
fn show_brush_preset_picker(&self) -> bool { false }
|
||||
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings) {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.selectable_label(s.sponge_mode == 0, "Saturate").clicked() {
|
||||
s.sponge_mode = 0;
|
||||
}
|
||||
if ui.selectable_label(s.sponge_mode == 1, "Desaturate").clicked() {
|
||||
s.sponge_mode = 1;
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(egui::Slider::new(&mut s.sponge_radius, 1.0_f32..=500.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Flow:");
|
||||
ui.add(egui::Slider::new(&mut s.sponge_flow, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
ui.add(egui::Slider::new(&mut s.sponge_hardness, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Spacing:");
|
||||
ui.add(egui::Slider::new(&mut s.sponge_spacing, 0.5_f32..=20.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.1}", v)));
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue