feat: Add theme and font management for egui GUI
- Introduced `theme.rs` to handle visual themes, font discovery, and application of theme colors. - Implemented system font registration for Linux, macOS, and Windows. - Added `apply_theme` function to configure egui visuals based on selected theme presets. feat: Implement interactive tool state management - Created `tool_state.rs` to manage runtime tool, selection, and transform states. - Defined `ToolState` struct to encapsulate active tool, color settings, drawing states, and AI panel states. - Included functionality for managing brush presets, text editing, and selection transformations. feat: Add utility functions for image and file handling - Developed `utils.rs` with stateless helper functions for color formatting, drawing icons, and cropping images. - Implemented save dialog builders and error handling for file operations. - Added flood fill functionality for composite RGBA pixels. test: Add unit test for bitmap brush stamp functionality - Created `bitmap_brush_stamp.rs` to verify the behavior of bitmap brush stamping in the engine. - Ensured that the painted area matches expected dimensions and characteristics.
This commit is contained in:
@@ -419,11 +419,32 @@ pub fn draw_dab(
|
||||
opacity: f32,
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
draw_dab_with_stamp(pixels, width, height, cx, cy, size, hardness, color, opacity, is_eraser, mask, None);
|
||||
}
|
||||
|
||||
/// Apply a single brush dab, optionally using a precomputed bitmap stamp.
|
||||
/// When `stamp` is provided it overrides the procedural circle/square shape.
|
||||
/// `stamp_diameter` is the width/height of the square stamp buffer.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_dab_with_stamp(
|
||||
pixels: &mut [u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
cx: f32,
|
||||
cy: f32,
|
||||
size: f32,
|
||||
hardness: f32,
|
||||
color: [u8; 4],
|
||||
opacity: f32,
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
stamp: Option<&[u8]>,
|
||||
) {
|
||||
let r = (size / 2.0).max(0.5);
|
||||
let r_sq = r * r;
|
||||
let hard_sq = (r * hardness).powi(2);
|
||||
let soft_range_inv = 1.0 / (r_sq - hard_sq + 1e-6);
|
||||
let soft_range_inv = 1.0 / (r_sq - hard_sq + 1e-6);
|
||||
|
||||
let x_min = ((cx - r).floor() as i32).max(0).min(width as i32 - 1);
|
||||
let x_max = ((cx + r).ceil() as i32).max(0).min(width as i32 - 1);
|
||||
@@ -432,6 +453,10 @@ pub fn draw_dab(
|
||||
|
||||
let [fr, fg, fb, _] = color;
|
||||
|
||||
// If a stamp is supplied, scale its local coordinates to the requested dab size.
|
||||
let stamp_d = if let Some(s) = stamp { (s.len() as f32).sqrt().round() as usize } else { 0 };
|
||||
let stamp_f = stamp_d as f32;
|
||||
|
||||
for py in y_min..=y_max {
|
||||
for px in x_min..=x_max {
|
||||
let dx = px as f32 - cx;
|
||||
@@ -449,12 +474,27 @@ pub fn draw_dab(
|
||||
};
|
||||
if mask_val == 0 { continue; }
|
||||
|
||||
let alpha_factor = if d_sq <= hard_sq {
|
||||
let alpha_factor = if let Some(s) = stamp {
|
||||
if stamp_d == 0 {
|
||||
0.0
|
||||
} else {
|
||||
// Map dab footprint back into stamp coordinates.
|
||||
let sx = ((dx + r) * stamp_f / size).round() as isize;
|
||||
let sy = ((dy + r) * stamp_f / size).round() as isize;
|
||||
if sx < 0 || sy < 0 || sx as usize >= stamp_d || sy as usize >= stamp_d {
|
||||
0.0
|
||||
} else {
|
||||
s[(sy as usize) * stamp_d + (sx as usize)] as f32 / 255.0
|
||||
}
|
||||
}
|
||||
} else if d_sq <= hard_sq {
|
||||
1.0
|
||||
} else {
|
||||
(1.0 - (d_sq - hard_sq) * soft_range_inv).clamp(0.0, 1.0)
|
||||
};
|
||||
|
||||
if alpha_factor <= 0.0 { continue; }
|
||||
|
||||
let dab_alpha = alpha_factor * opacity * (color[3] as f32 / 255.0) * (mask_val as f32 / 255.0);
|
||||
let brush_alpha = (dab_alpha * 255.0).round() as u8;
|
||||
let i = idx * 4;
|
||||
@@ -586,19 +626,41 @@ pub fn draw_dab_capped(
|
||||
opacity: f32,
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
mut stroke_mask: Option<&mut [u8]>,
|
||||
stroke_mask: Option<&mut [u8]>,
|
||||
bg_pixels: Option<&[u8]>,
|
||||
max_stroke_opacity: f32,
|
||||
) {
|
||||
draw_dab_capped_with_stamp(pixels, width, height, cx, cy, size, hardness, color, opacity, is_eraser, mask, stroke_mask, bg_pixels, max_stroke_opacity, None);
|
||||
}
|
||||
|
||||
/// `draw_dab_capped` variant that uses a pre-generated bitmap stamp for the dab shape.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw_dab_capped_with_stamp(
|
||||
pixels: &mut [u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
cx: f32,
|
||||
cy: f32,
|
||||
size: f32,
|
||||
hardness: f32,
|
||||
color: [u8; 4],
|
||||
opacity: f32,
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
mut stroke_mask: Option<&mut [u8]>,
|
||||
bg_pixels: Option<&[u8]>,
|
||||
max_stroke_opacity: f32,
|
||||
stamp: Option<&[u8]>,
|
||||
) {
|
||||
if stroke_mask.is_none() || bg_pixels.is_none() {
|
||||
draw_dab(pixels, width, height, cx, cy, size, hardness, color, opacity, is_eraser, mask);
|
||||
draw_dab_with_stamp(pixels, width, height, cx, cy, size, hardness, color, opacity, is_eraser, mask, stamp);
|
||||
return;
|
||||
}
|
||||
|
||||
let r = (size / 2.0).max(0.5);
|
||||
let r_sq = r * r;
|
||||
let hard_sq = (r * hardness).powi(2);
|
||||
let soft_range_inv = 1.0 / (r_sq - hard_sq + 1e-6);
|
||||
let soft_range_inv = 1.0 / (r_sq - hard_sq + 1e-6);
|
||||
|
||||
let x_min = ((cx - r).floor() as i32).max(0).min(width as i32 - 1);
|
||||
let x_max = ((cx + r).ceil() as i32).max(0).min(width as i32 - 1);
|
||||
@@ -609,6 +671,10 @@ pub fn draw_dab_capped(
|
||||
let stroke_mask_ref = stroke_mask.as_mut().unwrap();
|
||||
let bg = bg_pixels.unwrap();
|
||||
|
||||
// Bitmap stamp support: if a stamp is provided, use it as the per-pixel alpha mask.
|
||||
let stamp_d = if let Some(s) = stamp { (s.len() as f32).sqrt().round() as usize } else { 0 };
|
||||
let stamp_f = stamp_d as f32;
|
||||
|
||||
for py in y_min..=y_max {
|
||||
for px in x_min..=x_max {
|
||||
let dx = px as f32 - cx;
|
||||
@@ -626,17 +692,31 @@ pub fn draw_dab_capped(
|
||||
};
|
||||
if mask_val == 0 { continue; }
|
||||
|
||||
let alpha_factor = if d_sq <= hard_sq {
|
||||
let alpha_factor = if let Some(s) = stamp {
|
||||
if stamp_d == 0 {
|
||||
0.0
|
||||
} else {
|
||||
let sx = ((dx + r) * stamp_f / size).round() as isize;
|
||||
let sy = ((dy + r) * stamp_f / size).round() as isize;
|
||||
if sx < 0 || sy < 0 || sx as usize >= stamp_d || sy as usize >= stamp_d {
|
||||
0.0
|
||||
} else {
|
||||
s[(sy as usize) * stamp_d + (sx as usize)] as f32 / 255.0
|
||||
}
|
||||
}
|
||||
} else if d_sq <= hard_sq {
|
||||
1.0
|
||||
} else {
|
||||
(1.0 - (d_sq - hard_sq) * soft_range_inv).clamp(0.0, 1.0)
|
||||
};
|
||||
|
||||
if alpha_factor <= 0.0 { continue; }
|
||||
|
||||
let dab_alpha = alpha_factor * opacity * (color[3] as f32 / 255.0) * (mask_val as f32 / 255.0);
|
||||
let i = idx * 4;
|
||||
|
||||
let current_stroke_a = stroke_mask_ref[idx] as f32 / 255.0;
|
||||
|
||||
|
||||
// Blend dab alpha into the accumulated stroke mask (capped at max_stroke_opacity)
|
||||
let new_stroke_a = (current_stroke_a + dab_alpha * (1.0 - current_stroke_a)).min(max_stroke_opacity);
|
||||
stroke_mask_ref[idx] = (new_stroke_a * 255.0).round() as u8;
|
||||
@@ -649,7 +729,7 @@ pub fn draw_dab_capped(
|
||||
let bg_pixel = [bg[i], bg[i+1], bg[i+2], bg[i+3]];
|
||||
let brush_stamp = [fr, fg, fb, (new_stroke_a * 255.0).round() as u8];
|
||||
let out = alpha_blend(bg_pixel, brush_stamp);
|
||||
|
||||
|
||||
pixels[i] = out[0];
|
||||
pixels[i+1] = out[1];
|
||||
pixels[i+2] = out[2];
|
||||
|
||||
Reference in New Issue
Block a user