| 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 |
| } |
| } |
|
|