feat: Add digital, dry, ink, and paint brush presets; implement regression testing hooks
- Introduced new crates for digital brushes, dry media brushes, ink brushes, and paint brushes, each containing various presets. - Updated the menus to include "Paste Special" and "Paste as New Layer" options. - Enhanced the feature scorecard tests to validate selection texture encoding. - Added a regression gate workflow for automated testing on push and pull requests. - Implemented Git hooks for pre-commit checks to ensure code formatting and run tests before commits. - Created a settings file for local configurations.
This commit is contained in:
@@ -162,8 +162,8 @@ pub fn draw_brush_stroke(
|
||||
color_variant: tip.color_variant,
|
||||
variant_amount: tip.variant_amount,
|
||||
density: tip.density,
|
||||
drawing_angle: false,
|
||||
rotation_random: 0.0,
|
||||
drawing_angle: tip.drawing_angle,
|
||||
rotation_random: tip.rotation_random,
|
||||
};
|
||||
hcie_draw::draw_brush_stroke(
|
||||
layer,
|
||||
@@ -198,6 +198,13 @@ pub fn draw_specialized_stroke(
|
||||
color_variant: bool,
|
||||
variant_amount: f32,
|
||||
density: f32,
|
||||
jitter_amount: f32,
|
||||
scatter_amount: f32,
|
||||
angle: f32,
|
||||
roundness: f32,
|
||||
rotation_random: f32,
|
||||
drawing_angle: bool,
|
||||
include_first_dab: bool,
|
||||
) {
|
||||
let style = map_brush_style(brush_style);
|
||||
hcie_brush_engine::draw_specialized_stroke(
|
||||
@@ -221,6 +228,13 @@ pub fn draw_specialized_stroke(
|
||||
color_variant,
|
||||
variant_amount,
|
||||
density,
|
||||
jitter_amount,
|
||||
scatter_amount,
|
||||
angle,
|
||||
roundness,
|
||||
rotation_random,
|
||||
drawing_angle,
|
||||
include_first_dab,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,20 @@ use crate::Engine;
|
||||
use hcie_protocol::{BrushStyle, BrushTip};
|
||||
|
||||
impl Engine {
|
||||
/// Returns a conservative dirty radius including procedural and preset offsets.
|
||||
fn brush_dirty_radius(tip: &BrushTip) -> f32 {
|
||||
let procedural = match tip.style {
|
||||
BrushStyle::Star | BrushStyle::Spray => 1.8,
|
||||
BrushStyle::Meadow | BrushStyle::Leaf => 3.0,
|
||||
BrushStyle::Clouds | BrushStyle::Tree => 2.5,
|
||||
BrushStyle::Watercolor => 1.4,
|
||||
BrushStyle::WetPaint => 1.2,
|
||||
_ => 1.0,
|
||||
};
|
||||
let offset = tip.jitter_amount.max(0.0) * 0.5 + tip.scatter_amount.max(0.0) * 0.3;
|
||||
(tip.size * (procedural + offset)).max(2.0)
|
||||
}
|
||||
|
||||
/// Interpolate a brush property along the stroke using accumulated distance.
|
||||
///
|
||||
/// `t` is the normalized stroke progress (0.0 at start, 1.0 at end). If the
|
||||
@@ -137,20 +151,19 @@ impl Engine {
|
||||
tip.color_variant,
|
||||
tip.variant_amount,
|
||||
tip.density,
|
||||
tip.jitter_amount,
|
||||
tip.scatter_amount,
|
||||
tip.angle,
|
||||
tip.roundness,
|
||||
tip.rotation_random,
|
||||
tip.drawing_angle,
|
||||
false,
|
||||
);
|
||||
layer.dirty = true;
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
*layer.effects_cache.lock().unwrap() = None;
|
||||
}
|
||||
let dirty_r = match tip.style {
|
||||
// Star/Spray: spread was reduced to 0.65× so 1.8× covers the footprint.
|
||||
BrushStyle::Star | BrushStyle::Spray => tip.size * 1.8,
|
||||
// Meadow/Leaf draw blades/leaves upward from center — asymmetric extent.
|
||||
BrushStyle::Meadow | BrushStyle::Leaf => tip.size * 3.0,
|
||||
// Clouds/Tree scatter particles in a wide area.
|
||||
BrushStyle::Clouds | BrushStyle::Tree => tip.size * 2.5,
|
||||
_ => tip.size.max(2.0),
|
||||
};
|
||||
let dirty_r = Self::brush_dirty_radius(&tip);
|
||||
self.document.expand_dirty(lx as u32, ly as u32, dirty_r);
|
||||
self.document.expand_dirty(x as u32, y as u32, dirty_r);
|
||||
self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r);
|
||||
@@ -276,6 +289,13 @@ impl Engine {
|
||||
tip.color_variant,
|
||||
tip.variant_amount,
|
||||
tip.density,
|
||||
tip.jitter_amount,
|
||||
tip.scatter_amount,
|
||||
tip.angle,
|
||||
tip.roundness,
|
||||
tip.rotation_random,
|
||||
tip.drawing_angle,
|
||||
true,
|
||||
);
|
||||
layer.dirty = true;
|
||||
} else {
|
||||
@@ -297,15 +317,7 @@ impl Engine {
|
||||
}
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
let dirty_r = match tip.style {
|
||||
// Star/Spray: spread was reduced to 0.65× so 1.8× covers the footprint.
|
||||
BrushStyle::Star | BrushStyle::Spray => tip.size * 1.8,
|
||||
// Meadow/Leaf draw blades/leaves upward from center — asymmetric extent.
|
||||
BrushStyle::Meadow | BrushStyle::Leaf => tip.size * 3.0,
|
||||
// Clouds/Tree scatter particles in a wide area.
|
||||
BrushStyle::Clouds | BrushStyle::Tree => tip.size * 2.5,
|
||||
_ => tip.size.max(2.0),
|
||||
};
|
||||
let dirty_r = Self::brush_dirty_radius(&tip);
|
||||
for &(px, py, _) in points {
|
||||
if px >= 0.0
|
||||
&& py >= 0.0
|
||||
@@ -322,7 +334,8 @@ impl Engine {
|
||||
"Brush Stroke ({})",
|
||||
crate::stroke_brush::brush_style_label(tip.style)
|
||||
);
|
||||
self.document.push_draw_snapshot(layer_idx, before, None, desc);
|
||||
self.document
|
||||
.push_draw_snapshot(layer_idx, before, None, desc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +487,11 @@ impl Engine {
|
||||
| BrushStyle::WetPaint
|
||||
| BrushStyle::Leaf
|
||||
| BrushStyle::Mixer
|
||||
| BrushStyle::Blender
|
||||
| BrushStyle::Noise
|
||||
| BrushStyle::Texture
|
||||
| BrushStyle::Pen
|
||||
| BrushStyle::InkPen
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -519,3 +537,21 @@ pub fn brush_style_label(style: BrushStyle) -> &'static str {
|
||||
BrushStyle::Bitmap => "Bitmap",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dirty_radius_tests {
|
||||
use super::*;
|
||||
|
||||
/// Ensures partial uploads include procedural watercolor spread and preset offsets.
|
||||
#[test]
|
||||
fn watercolor_dirty_radius_covers_jitter_and_scatter() {
|
||||
let tip = BrushTip {
|
||||
style: BrushStyle::Watercolor,
|
||||
size: 40.0,
|
||||
jitter_amount: 0.4,
|
||||
scatter_amount: 0.9,
|
||||
..BrushTip::default()
|
||||
};
|
||||
assert!((Engine::brush_dirty_radius(&tip) - 74.8).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ fn first_vector_shape_is_one_atomic_layer_transaction() {
|
||||
assert_eq!(engine.history_len(), history_before + 1);
|
||||
assert_eq!(
|
||||
engine.history_description(history_before).as_deref(),
|
||||
Some("Add Vector Shape")
|
||||
Some("Add Vector Shape (Rect)")
|
||||
);
|
||||
assert_eq!(
|
||||
engine
|
||||
|
||||
Reference in New Issue
Block a user