diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index a131278b..f2d88e24 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -8,6 +8,7 @@ use graph_craft::document::value::*; use graph_craft::document::*; use graph_craft::imaginate_input::ImaginateSamplingMethod; use graph_craft::NodeIdentifier; +use graphene_core::raster::brush_cache::BrushCache; use graphene_core::raster::{BlendMode, Color, Image, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice}; use graphene_core::text::Font; use graphene_core::vector::VectorData; @@ -677,11 +678,12 @@ fn static_nodes() -> Vec { DocumentNodeType { name: "Brush", category: "Brush", - identifier: NodeImplementation::proto("graphene_std::brush::BrushNode<_, _>"), + identifier: NodeImplementation::proto("graphene_std::brush::BrushNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Background", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Bounds", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Trace", TaggedValue::BrushStrokes(Vec::new()), false), + DocumentInputType::value("Cache", TaggedValue::BrushCache(BrushCache::new_proto()), false), ], outputs: vec![DocumentOutputType { name: "Image", diff --git a/frontend/wasm/.cargo/config.toml b/frontend/wasm/.cargo/config.toml index 49f5ff49..8c044195 100644 --- a/frontend/wasm/.cargo/config.toml +++ b/frontend/wasm/.cargo/config.toml @@ -1,11 +1,13 @@ [target.wasm32-unknown-unknown] -#rustflags = ["-C", "target-feature=+simd128,+atomics,+bulk-memory,+mutable-globals","--cfg=web_sys_unstable_apis"] + rustflags = [ # Currently disabled because of https://github.com/GraphiteEditor/Graphite/issues/1262 # The current simd implementation leads to undefined behavior #"-C", #"target-feature=+simd128", "-C", + "target-feature=+bulk-memory", + "-C", "link-arg=--max-memory=4294967296", "--cfg=web_sys_unstable_apis", ] diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index a3511c38..2ca26f94 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -11,6 +11,7 @@ pub mod adjustments; pub mod bbox; #[cfg(not(target_arch = "spirv"))] pub mod brightness_contrast; +pub mod brush_cache; pub mod color; pub mod discrete_srgb; pub use adjustments::*; diff --git a/node-graph/gcore/src/raster/brush_cache.rs b/node-graph/gcore/src/raster/brush_cache.rs new file mode 100644 index 00000000..88884c4b --- /dev/null +++ b/node-graph/gcore/src/raster/brush_cache.rs @@ -0,0 +1,175 @@ +use core::hash::Hash; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::Mutex; + +use dyn_any::{DynAny, StaticType}; + +use crate::raster::Image; +use crate::raster::ImageFrame; +use crate::vector::brush_stroke::BrushStroke; +use crate::vector::brush_stroke::BrushStyle; +use crate::Color; + +#[derive(Clone, Debug, PartialEq, DynAny, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +struct BrushCacheImpl { + // The full previous input that was cached. + prev_input: Vec, + + // The strokes that have been fully processed and blended into the background. + background: ImageFrame, + blended_image: ImageFrame, + last_stroke_texture: ImageFrame, + + // A cache for brush textures. + #[serde(skip)] + brush_texture_cache: HashMap>, +} + +impl BrushCacheImpl { + fn compute_brush_plan(&mut self, mut background: ImageFrame, input: &[BrushStroke]) -> BrushPlan { + // Do background invalidation. + if background.transform != self.background.transform || background.image != self.background.image { + 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 = core::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 = ImageFrame::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 = core::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, blended_image: ImageFrame, last_stroke_texture: ImageFrame) { + self.prev_input = input; + self.blended_image = blended_image; + self.last_stroke_texture = last_stroke_texture; + } +} + +impl Hash for BrushCacheImpl { + // Zero hash. + fn hash(&self, state: &mut H) {} +} + +#[derive(Clone, Debug, Default)] +pub struct BrushPlan { + pub strokes: Vec, + pub background: ImageFrame, + pub first_stroke_texture: ImageFrame, + pub first_stroke_point_skip: usize, +} + +#[derive(Debug, DynAny, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BrushCache { + inner: Arc>, + proto: bool, +} + +// 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(&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: ImageFrame, input: &[BrushStroke]) -> BrushPlan { + let mut inner = self.inner.lock().unwrap(); + inner.compute_brush_plan(background, input) + } + + pub fn cache_results(&self, input: Vec, blended_image: ImageFrame, last_stroke_texture: ImageFrame) { + 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> { + let inner = self.inner.lock().unwrap(); + inner.brush_texture_cache.get(style).cloned() + } + + pub fn store_brush(&self, style: BrushStyle, brush: Image) { + let mut inner = self.inner.lock().unwrap(); + inner.brush_texture_cache.insert(style, brush); + } +} diff --git a/node-graph/gcore/src/vector/brush_stroke.rs b/node-graph/gcore/src/vector/brush_stroke.rs index b2f4fb59..b9f825ea 100644 --- a/node-graph/gcore/src/vector/brush_stroke.rs +++ b/node-graph/gcore/src/vector/brush_stroke.rs @@ -7,7 +7,7 @@ use glam::DVec2; use std::hash::{Hash, Hasher}; /// The style of a brush. -#[derive(Clone, Debug, PartialEq, DynAny)] +#[derive(Clone, Debug, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BrushStyle { pub color: Color, @@ -37,6 +37,20 @@ impl Hash for BrushStyle { self.diameter.to_bits().hash(state); self.hardness.to_bits().hash(state); self.flow.to_bits().hash(state); + self.spacing.to_bits().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 } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 7e649a73..b6f48bf7 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -3,6 +3,7 @@ use crate::graphene_compiler::Any; pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus}; use crate::proto::{Any as DAny, FutureAny}; +use graphene_core::raster::brush_cache::BrushCache; use graphene_core::raster::{BlendMode, LuminanceCalculation}; use graphene_core::{Color, Node, Type}; @@ -54,6 +55,7 @@ pub enum TaggedValue { ManipulatorGroupIds(Vec), Font(graphene_core::text::Font), BrushStrokes(Vec), + BrushCache(BrushCache), Segments(Vec>), DocumentNode(DocumentNode), GraphicGroup(graphene_core::GraphicGroup), @@ -115,6 +117,7 @@ impl Hash for TaggedValue { Self::ManipulatorGroupIds(mirror) => mirror.hash(state), Self::Font(font) => font.hash(state), Self::BrushStrokes(brush_strokes) => brush_strokes.hash(state), + Self::BrushCache(brush_cache) => brush_cache.hash(state), Self::Segments(segments) => { for segment in segments { segment.hash(state) @@ -171,6 +174,7 @@ impl<'a> TaggedValue { TaggedValue::ManipulatorGroupIds(x) => Box::new(x), TaggedValue::Font(x) => Box::new(x), TaggedValue::BrushStrokes(x) => Box::new(x), + TaggedValue::BrushCache(x) => Box::new(x), TaggedValue::Segments(x) => Box::new(x), TaggedValue::DocumentNode(x) => Box::new(x), TaggedValue::GraphicGroup(x) => Box::new(x), @@ -235,6 +239,7 @@ impl<'a> TaggedValue { TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec), TaggedValue::Font(_) => concrete!(graphene_core::text::Font), TaggedValue::BrushStrokes(_) => concrete!(Vec), + TaggedValue::BrushCache(_) => concrete!(BrushCache), TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode>>), TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode), TaggedValue::GraphicGroup(_) => concrete!(graphene_core::GraphicGroup), @@ -287,6 +292,7 @@ impl<'a> TaggedValue { x if x == TypeId::of::>() => Ok(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::Font(*downcast(input).unwrap())), x if x == TypeId::of::>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())), + x if x == TypeId::of::>() => Ok(TaggedValue::BrushCache(*downcast(input).unwrap())), x if x == TypeId::of::>>>() => Ok(TaggedValue::Segments(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::DocumentNode(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::GraphicGroup(*downcast(input).unwrap())), diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs index 6d84040c..574ab4d3 100644 --- a/node-graph/gstd/src/brush.rs +++ b/node-graph/gstd/src/brush.rs @@ -1,7 +1,8 @@ -use crate::raster::{blend_image_closure, BlendImageTupleNode, EmptyImageNode, ExtendImageNode}; +use crate::raster::{blend_image_closure, BlendImageTupleNode, EmptyImageNode, ExtendImageToBoundsNode}; use graphene_core::raster::adjustments::blend_colors; use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox}; +use graphene_core::raster::brush_cache::BrushCache; use graphene_core::raster::{Alpha, Color, Image, ImageFrame, Pixel, Sample}; use graphene_core::raster::{BlendMode, BlendNode}; use graphene_core::transform::{Transform, TransformMut}; @@ -210,7 +211,7 @@ where target } -pub fn create_brush_texture(brush_style: BrushStyle) -> Image { +pub fn create_brush_texture(brush_style: &BrushStyle) -> Image { let stamp = BrushStampGeneratorNode::new(CopiedNode::new(brush_style.color), CopiedNode::new(brush_style.hardness), CopiedNode::new(brush_style.flow)); let stamp = stamp.eval(brush_style.diameter); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.)); @@ -280,16 +281,22 @@ pub fn blend_with_mode(background: ImageFrame, foreground: ImageFrame { +pub struct BrushNode { bounds: Bounds, strokes: Strokes, + cache: Cache, } #[node_macro::node_fn(BrushNode)] -async fn brush(image: ImageFrame, bounds: ImageFrame, strokes: Vec) -> ImageFrame { +async fn brush(image: ImageFrame, bounds: ImageFrame, strokes: Vec, cache: BrushCache) -> ImageFrame { let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO); let image_bbox = Bbox::from_transform(image.transform).to_axis_aligned_bbox(); - let bbox = stroke_bbox.union(&image_bbox); + let bbox = if image_bbox.size().length() < 0.1 { stroke_bbox } else { stroke_bbox.union(&image_bbox) }; + + let mut draw_strokes: Vec<_> = strokes.iter().cloned().filter(|s| !matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).collect(); + let erase_restore_strokes: Vec<_> = strokes.iter().cloned().filter(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).collect(); + + let mut brush_plan = cache.compute_brush_plan(image, &draw_strokes); let mut background_bounds = bbox.to_transform(); @@ -297,68 +304,90 @@ async fn brush(image: ImageFrame, bounds: ImageFrame, strokes: Vec background_bounds = bounds.transform; } - let has_erase_strokes = strokes.iter().any(|s| s.style.blend_mode == BlendMode::Erase); - let blank_image = ImageFrame { - image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::TRANSPARENT), - transform: background_bounds, - }; - let opaque_image = ImageFrame { - image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE), - transform: background_bounds, - }; - let mut erase_restore_mask = has_erase_strokes.then_some(opaque_image); - let mut actual_image = ExtendImageNode::new(OnceCellNode::new(blank_image)).eval(image); - for stroke in strokes { - let normal_blend = BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.)); - + let mut actual_image = ExtendImageToBoundsNode::new(OnceCellNode::new(background_bounds)).eval(brush_plan.background); + let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1); + for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() { // Create brush texture. // TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes. - let brush_texture = create_brush_texture(stroke.style.clone()); + let brush_texture = cache.get_cached_brush(&stroke.style).unwrap_or_else(|| { + let tex = create_brush_texture(&stroke.style); + cache.store_brush(stroke.style.clone(), tex.clone()); + tex + }); // Compute transformation from stroke texture space into layer space, and create the stroke texture. - let positions: Vec<_> = stroke.compute_blit_points().into_iter().collect(); - let mut bbox = stroke.bounding_box(); - bbox.start = bbox.start.floor(); - bbox.end = bbox.end.floor(); - let stroke_size = bbox.size() + DVec2::splat(stroke.style.diameter); - // For numerical stability we want to place the first blit point at a stable, integer offset - // in layer space. - let snap_offset = positions[0].floor() - positions[0]; - let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.); - let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size); + let skip = if idx == 0 { brush_plan.first_stroke_point_skip } else { 0 }; + let positions: Vec<_> = stroke.compute_blit_points().into_iter().skip(skip).collect(); + let stroke_texture = if idx == 0 && positions.len() == 0 { + core::mem::take(&mut brush_plan.first_stroke_texture) + } else { + let mut bbox = stroke.bounding_box(); + bbox.start = bbox.start.floor(); + bbox.end = bbox.end.floor(); + let stroke_size = bbox.size() + DVec2::splat(stroke.style.diameter); + // For numerical stability we want to place the first blit point at a stable, integer offset + // in layer space. + let snap_offset = positions[0].floor() - positions[0]; + let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.0); + let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size); - match stroke.style.blend_mode { - BlendMode::Erase => { - if let Some(mask) = erase_restore_mask { - let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Erase), CopiedNode::new(100.)); - let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params)); - erase_restore_mask = Some(blit_node.eval(mask)); - } - } + let normal_blend = BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.)); + let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(normal_blend)); + let blit_target = if idx == 0 { + let target = core::mem::take(&mut brush_plan.first_stroke_texture); + ExtendImageToBoundsNode::new(CopiedNode::new(stroke_to_layer)).eval(target) + } else { + EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(stroke_to_layer) + }; + blit_node.eval(blit_target) + }; - // Yes, this is essentially the same as the above, but we duplicate to inline the blend mode. - BlendMode::Restore => { - if let Some(mask) = erase_restore_mask { - let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Restore), CopiedNode::new(100.)); - let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params)); - erase_restore_mask = Some(blit_node.eval(mask)); - } - } - - blend_mode => { - let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(normal_blend)); - let empty_stroke_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(stroke_to_layer); - let stroke_texture = blit_node.eval(empty_stroke_texture); - // TODO: Is this the correct way to do opacity in blending? - actual_image = blend_with_mode(actual_image, stroke_texture, blend_mode, stroke.style.color.a() * 100.); - } + // Cache image before doing final blend, and store final stroke texture. + if idx == final_stroke_idx { + cache.cache_results(core::mem::take(&mut draw_strokes), actual_image.clone(), stroke_texture.clone()); } + + // TODO: Is this the correct way to do opacity in blending? + actual_image = blend_with_mode(actual_image, stroke_texture, stroke.style.blend_mode, stroke.style.color.a() * 100.0); } - if let Some(mask) = erase_restore_mask { - let blend_params = BlendNode::new(CopiedNode::new(BlendMode::MultiplyAlpha), CopiedNode::new(100.)); + let has_erase_strokes = strokes.iter().any(|s| s.style.blend_mode == BlendMode::Erase); + if has_erase_strokes { + let opaque_image = ImageFrame { + image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE), + transform: background_bounds, + }; + let mut erase_restore_mask = opaque_image; + + for stroke in erase_restore_strokes { + let brush_texture = cache.get_cached_brush(&stroke.style).unwrap_or_else(|| { + let tex = create_brush_texture(&stroke.style); + cache.store_brush(stroke.style.clone(), tex.clone()); + tex + }); + let positions: Vec<_> = stroke.compute_blit_points().into_iter().collect(); + + match stroke.style.blend_mode { + BlendMode::Erase => { + let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Erase), CopiedNode::new(100.)); + let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params)); + erase_restore_mask = blit_node.eval(erase_restore_mask); + } + + // Yes, this is essentially the same as the above, but we duplicate to inline the blend mode. + BlendMode::Restore => { + let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Restore), CopiedNode::new(100.)); + let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params)); + erase_restore_mask = blit_node.eval(erase_restore_mask); + } + + _ => unreachable!(), + } + } + + let blend_params = BlendNode::new(CopiedNode::new(BlendMode::MultiplyAlpha), CopiedNode::new(100.0)); let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params)); - actual_image = blend_executor.eval((actual_image, mask)); + actual_image = blend_executor.eval((actual_image, erase_restore_mask)); } actual_image } diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index df04eb96..5b260c20 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -336,6 +336,51 @@ fn extend_image_node(foreground: ImageFrame, background: ImageFrame { + bounds: Bounds, +} + +#[node_macro::node_fn(ExtendImageToBoundsNode)] +fn extend_image_to_bounds_node(image: ImageFrame, bounds: DAffine2) -> ImageFrame { + let image_aabb = Bbox::unit().affine_transform(image.transform()).to_axis_aligned_bbox(); + let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox(); + if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) { + return image; + } + + if image.image.width == 0 || image.image.height == 0 { + return EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(bounds); + } + + let orig_image_scale = DVec2::new(image.image.width as f64, image.image.height as f64); + let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * image.transform.inverse(); + let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox(); + + let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO); + let new_end = bounds_in_image_space.end.ceil().max(orig_image_scale); + let new_scale = new_end - new_start; + + // Copy over original image into embiggened image. + let mut new_img = Image::new(new_scale.x as u32, new_scale.y as u32, Color::TRANSPARENT); + let offset_in_new_image = (-new_start).as_uvec2(); + for y in 0..image.image.height { + let old_start = y * image.image.width; + let new_start = (y + offset_in_new_image.y) * new_img.width + offset_in_new_image.x; + let old_row = &image.image.data[old_start as usize..(old_start + image.image.width) as usize]; + let new_row = &mut new_img.data[new_start as usize..(new_start + image.image.width) as usize]; + new_row.copy_from_slice(old_row); + } + + // Compute new transform. + // let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse(); + let new_texture_to_layer_space = image.transform * DAffine2::from_scale(1.0 / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); + ImageFrame { + image: new_img, + transform: new_texture_to_layer_space, + } +} + #[derive(Clone, Debug, PartialEq)] pub struct MergeBoundingBoxNode { _data: PhantomData, diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index a8a2eee3..79cb681e 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -2,6 +2,7 @@ use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graphene_core::ops::IdNode; use graphene_core::quantization::QuantizationChannels; +use graphene_core::raster::brush_cache::BrushCache; use graphene_core::raster::color::Color; use graphene_core::structural::Then; use graphene_core::value::{ClonedNode, CopiedNode, ValueNode}; @@ -299,7 +300,7 @@ fn node_registry() -> HashMap, input: &Vec, params: []), - async_node!(graphene_std::brush::BrushNode<_, _>, input: ImageFrame, output: ImageFrame, params: [ImageFrame, Vec]), + async_node!(graphene_std::brush::BrushNode<_, _, _>, input: ImageFrame, output: ImageFrame, params: [ImageFrame, Vec, BrushCache]), // Filters raster_node!(graphene_core::raster::LuminanceNode<_>, params: [LuminanceCalculation]), raster_node!(graphene_core::raster::ExtractChannelNode<_>, params: [RedGreenBlue]),