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:
2026-07-10 01:23:47 +03:00
parent e88a2be508
commit 07a9b35208
14 changed files with 2250 additions and 1523 deletions
+88 -8
View File
@@ -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];