Graphite/node-graph/nodes/brush/src/brush_stroke.rs

106 lines
2.9 KiB
Rust

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