use core_types::CacheHash; use core_types::blending::BlendMode; use core_types::color::Color; use core_types::math::bbox::AxisAlignedBbox; use dyn_any::DynAny; use glam::DVec2; /// The style of a brush. #[derive(Clone, Debug, CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushStyle { pub color: Color, pub diameter: f64, pub hardness: f64, pub flow: f64, pub spacing: f64, // Spacing as a fraction of the diameter. pub blend_mode: BlendMode, } impl Default for BrushStyle { fn default() -> Self { Self { color: Color::BLACK, diameter: 40., hardness: 50., flow: 100., spacing: 50., // Percentage of diameter. blend_mode: BlendMode::Normal, } } } 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 } } /// A single sample of brush parameters across the brush stroke. #[derive(Clone, Debug, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushInputSample { pub position: DVec2, } /// The parameters for a single stroke brush. #[derive(Clone, Debug, PartialEq, core_types::CacheHash, Default, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushStroke { pub style: BrushStyle, pub trace: Vec, } 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 { // We always travel in a straight line towards the next user input, // placing a blit point every time we travelled our spacing distance. 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..] { // Travel to the next sample. 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 { // Take a step to the next blit point. cur_pos += dist_until_next_blit * unit_step; dist_left -= dist_until_next_blit; // Blit. result.push(cur_pos); dist_until_next_blit = spacing_dist; } // Take the partial step to land at the sample. dist_until_next_blit -= dist_left; cur_pos = sample.position; } result } }