Extract gbrush (#2784)
Some checks failed
Editor: Dev & CI / build (push) Has been cancelled
Editor: Dev & CI / cargo-deny (push) Has been cancelled

This commit is contained in:
Firestar99 2025-07-01 20:38:47 +02:00 committed by GitHub
parent 602d7e8bd1
commit a182a7347e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 85 additions and 30 deletions

View file

@ -12,7 +12,6 @@ pub mod color {
pub use super::*;
}
pub mod brush_cache;
pub mod image;
pub use self::image::Image;

View file

@ -1,184 +0,0 @@
use crate::instances::Instance;
use crate::raster_types::CPU;
use crate::raster_types::Raster;
use crate::vector::brush_stroke::BrushStroke;
use crate::vector::brush_stroke::BrushStyle;
use dyn_any::DynAny;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
struct BrushCacheImpl {
// The full previous input that was cached.
prev_input: Vec<BrushStroke>,
// The strokes that have been fully processed and blended into the background.
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
background: Instance<Raster<CPU>>,
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
blended_image: Instance<Raster<CPU>>,
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
last_stroke_texture: Instance<Raster<CPU>>,
// A cache for brush textures.
#[serde(skip)]
brush_texture_cache: HashMap<BrushStyle, Raster<CPU>>,
}
impl BrushCacheImpl {
fn compute_brush_plan(&mut self, mut background: Instance<Raster<CPU>>, input: &[BrushStroke]) -> BrushPlan {
// Do background invalidation.
if background != self.background {
self.background = background.clone();
return BrushPlan {
strokes: input.to_vec(),
background,
..Default::default()
};
}
// Do blended_image invalidation.
let blended_strokes = &self.prev_input[..self.prev_input.len().saturating_sub(1)];
let num_blended_strokes = blended_strokes.len();
if input.get(..num_blended_strokes) != Some(blended_strokes) {
return BrushPlan {
strokes: input.to_vec(),
background,
..Default::default()
};
}
// Take our previous blended image (and invalidate the cache).
// Since we're about to replace our cache anyway, this saves a clone.
background = std::mem::take(&mut self.blended_image);
// Check if the first non-blended stroke is an extension of the last one.
let mut first_stroke_texture = Instance {
instance: Raster::<CPU>::default(),
transform: glam::DAffine2::ZERO,
..Default::default()
};
let mut first_stroke_point_skip = 0;
let strokes = input[num_blended_strokes..].to_vec();
if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes {
let last_stroke = &self.prev_input[num_blended_strokes];
let same_style = strokes[0].style == last_stroke.style;
let prev_points = last_stroke.compute_blit_points();
let new_points = strokes[0].compute_blit_points();
let is_point_prefix = new_points.get(..prev_points.len()) == Some(&prev_points);
if same_style && is_point_prefix {
first_stroke_texture = std::mem::take(&mut self.last_stroke_texture);
first_stroke_point_skip = prev_points.len();
}
}
self.prev_input = Vec::new();
BrushPlan {
strokes,
background,
first_stroke_texture,
first_stroke_point_skip,
}
}
pub fn cache_results(&mut self, input: Vec<BrushStroke>, blended_image: Instance<Raster<CPU>>, last_stroke_texture: Instance<Raster<CPU>>) {
self.prev_input = input;
self.blended_image = blended_image;
self.last_stroke_texture = last_stroke_texture;
}
}
impl Hash for BrushCacheImpl {
// Zero hash.
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
}
#[derive(Clone, Debug, Default)]
pub struct BrushPlan {
pub strokes: Vec<BrushStroke>,
pub background: Instance<Raster<CPU>>,
pub first_stroke_texture: Instance<Raster<CPU>>,
pub first_stroke_point_skip: usize,
}
#[derive(Debug, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushCache {
inner: Arc<Mutex<BrushCacheImpl>>,
proto: bool,
}
impl Default for BrushCache {
fn default() -> Self {
Self::new_proto()
}
}
// A bit of a cursed implementation to work around the current node system.
// The original object is a 'prototype' that when cloned gives you a independent
// new object. Any further clones however are all the same underlying cache object.
impl Clone for BrushCache {
fn clone(&self) -> Self {
if self.proto {
let inner_val = self.inner.lock().unwrap();
Self {
inner: Arc::new(Mutex::new(inner_val.clone())),
proto: false,
}
} else {
Self {
inner: Arc::clone(&self.inner),
proto: false,
}
}
}
}
impl PartialEq for BrushCache {
fn eq(&self, other: &Self) -> bool {
if Arc::ptr_eq(&self.inner, &other.inner) {
return true;
}
let s = self.inner.lock().unwrap();
let o = other.inner.lock().unwrap();
*s == *o
}
}
impl Hash for BrushCache {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.inner.lock().unwrap().hash(state);
}
}
impl BrushCache {
pub fn new_proto() -> Self {
Self {
inner: Default::default(),
proto: true,
}
}
pub fn compute_brush_plan(&self, background: Instance<Raster<CPU>>, input: &[BrushStroke]) -> BrushPlan {
let mut inner = self.inner.lock().unwrap();
inner.compute_brush_plan(background, input)
}
pub fn cache_results(&self, input: Vec<BrushStroke>, blended_image: Instance<Raster<CPU>>, last_stroke_texture: Instance<Raster<CPU>>) {
let mut inner = self.inner.lock().unwrap();
inner.cache_results(input, blended_image, last_stroke_texture)
}
pub fn get_cached_brush(&self, style: &BrushStyle) -> Option<Raster<CPU>> {
let inner = self.inner.lock().unwrap();
inner.brush_texture_cache.get(style).cloned()
}
pub fn store_brush(&self, style: BrushStyle, brush: Raster<CPU>) {
let mut inner = self.inner.lock().unwrap();
inner.brush_texture_cache.insert(style, brush);
}
}

View file

@ -1,127 +0,0 @@
use crate::Color;
use crate::math::bbox::AxisAlignedBbox;
use crate::raster::BlendMode;
use dyn_any::DynAny;
use glam::DVec2;
use std::hash::{Hash, Hasher};
/// The style of a brush.
#[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, // 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 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
}
}
/// A single sample of brush parameters across the brush stroke.
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushInputSample {
// The position of the sample in layer space, in pixels.
// The origin of layer space is not specified.
pub position: DVec2,
// Future work: pressure, stylus angle, etc.
}
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);
}
}
/// The parameters for a single stroke brush.
#[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> {
// 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
}
}

View file

@ -1,5 +1,4 @@
pub mod algorithms;
pub mod brush_stroke;
pub mod click_target;
pub mod generator_nodes;
pub mod misc;