|
use dyn_any::DynAny; |
|
use glam::DVec2; |
|
use graphene_core::blending::BlendMode; |
|
use graphene_core::color::Color; |
|
use graphene_core::math::bbox::AxisAlignedBbox; |
|
use std::hash::{Hash, Hasher}; |
|
|
|
|
|
#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)] |
|
pub struct BrushStyle { |
|
pub color: Color, |
|
pub diameter: f64, |
|
pub hardness: f64, |
|
pub flow: f64, |
|
pub spacing: f64, |
|
pub blend_mode: BlendMode, |
|
} |
|
|
|
impl Default for BrushStyle { |
|
fn default() -> Self { |
|
Self { |
|
color: Color::BLACK, |
|
diameter: 40., |
|
hardness: 50., |
|
flow: 100., |
|
spacing: 50., |
|
blend_mode: BlendMode::Normal, |
|
} |
|
} |
|
} |
|
|
|
impl Hash for BrushStyle { |
|
fn hash<H: Hasher>(&self, state: &mut H) { |
|
self.color.hash(state); |
|
self.diameter.to_bits().hash(state); |
|
self.hardness.to_bits().hash(state); |
|
self.flow.to_bits().hash(state); |
|
self.spacing.to_bits().hash(state); |
|
self.blend_mode.hash(state); |
|
} |
|
} |
|
|
|
impl Eq for BrushStyle {} |
|
|
|
impl PartialEq for BrushStyle { |
|
fn eq(&self, other: &Self) -> bool { |
|
self.color == other.color |
|
&& self.diameter.to_bits() == other.diameter.to_bits() |
|
&& self.hardness.to_bits() == other.hardness.to_bits() |
|
&& self.flow.to_bits() == other.flow.to_bits() |
|
&& self.spacing.to_bits() == other.spacing.to_bits() |
|
&& self.blend_mode == other.blend_mode |
|
} |
|
} |
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] |
|
pub struct BrushInputSample { |
|
|
|
|
|
pub position: DVec2, |
|
|
|
} |
|
|
|
impl Hash for BrushInputSample { |
|
fn hash<H: Hasher>(&self, state: &mut H) { |
|
self.position.x.to_bits().hash(state); |
|
self.position.y.to_bits().hash(state); |
|
} |
|
} |
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny, serde::Serialize, serde::Deserialize)] |
|
pub struct BrushStroke { |
|
pub style: BrushStyle, |
|
pub trace: Vec<BrushInputSample>, |
|
} |
|
|
|
impl BrushStroke { |
|
pub fn bounding_box(&self) -> AxisAlignedBbox { |
|
let radius = self.style.diameter / 2.; |
|
self.compute_blit_points() |
|
.iter() |
|
.map(|pos| AxisAlignedBbox { |
|
start: *pos + DVec2::new(-radius, -radius), |
|
end: *pos + DVec2::new(radius, radius), |
|
}) |
|
.reduce(|a, b| a.union(&b)) |
|
.unwrap_or(AxisAlignedBbox::ZERO) |
|
} |
|
|
|
pub fn compute_blit_points(&self) -> Vec<DVec2> { |
|
|
|
|
|
let spacing_dist = self.style.spacing / 100. * self.style.diameter; |
|
|
|
let Some(first_sample) = self.trace.first() else { |
|
return Vec::new(); |
|
}; |
|
|
|
let mut cur_pos = first_sample.position; |
|
let mut result = vec![cur_pos]; |
|
let mut dist_until_next_blit = spacing_dist; |
|
for sample in &self.trace[1..] { |
|
|
|
let delta = sample.position - cur_pos; |
|
let mut dist_left = delta.length(); |
|
let unit_step = delta / dist_left; |
|
|
|
while dist_left >= dist_until_next_blit { |
|
|
|
cur_pos += dist_until_next_blit * unit_step; |
|
dist_left -= dist_until_next_blit; |
|
|
|
|
|
result.push(cur_pos); |
|
dist_until_next_blit = spacing_dist; |
|
} |
|
|
|
|
|
dist_until_next_blit -= dist_left; |
|
cur_pos = sample.position; |
|
} |
|
|
|
result |
|
} |
|
} |
|
|