diff --git a/Cargo.lock b/Cargo.lock index fb9976d56f..45dbbe2b91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,10 +370,12 @@ dependencies = [ name = "brush-nodes" version = "0.1.0" dependencies = [ + "brush-types", "core-types", "dyn-any", "glam", "graphene-hash", + "graphic-types", "node-macro", "raster-nodes", "raster-types", @@ -381,6 +383,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "brush-types" +version = "0.1.0" +dependencies = [ + "core-types", + "dyn-any", + "glam", + "graphene-hash", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -2208,6 +2221,7 @@ dependencies = [ name = "graphic-nodes" version = "0.1.0" dependencies = [ + "brush-types", "core-types", "dyn-any", "glam", @@ -2223,6 +2237,7 @@ dependencies = [ name = "graphic-types" version = "0.1.0" dependencies = [ + "brush-types", "core-types", "dyn-any", "glam", @@ -4871,6 +4886,7 @@ name = "rendering" version = "0.1.0" dependencies = [ "base64", + "brush-types", "core-types", "dyn-any", "glam", diff --git a/Cargo.toml b/Cargo.toml index d7e04973ee..8b79b129ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ no-std-types = { path = "node-graph/libraries/no-std-types" } raster-types = { path = "node-graph/libraries/raster-types" } vector-types = { path = "node-graph/libraries/vector-types" } graphic-types = { path = "node-graph/libraries/graphic-types" } +brush-types = { path = "node-graph/libraries/brush-types" } rendering = { path = "node-graph/libraries/rendering" } brush-nodes = { path = "node-graph/nodes/brush" } blending-nodes = { path = "node-graph/nodes/blending" } diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 9220ef5028..332a02d6c5 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -492,6 +492,15 @@ impl TableItemLayout for Artboard { } } +impl TableItemLayout for graphene_std::brush::Stroke { + fn type_name() -> &'static str { + "Stroke" + } + fn identifier(&self) -> String { + format!("Stroke ({} {})", self.len(), if self.len() == 1 { "sample" } else { "samples" }) + } +} + impl TableItemLayout for DashPattern { fn type_name() -> &'static str { "DashPattern" @@ -605,6 +614,7 @@ impl TableItemLayout for Graphic { Self::ColorList(list) => list.identifier(), Self::GradientList(list) => list.identifier(), Self::TextList(list) => list.identifier(), + Self::StrokeList(list) => list.identifier(), } } // Don't put a breadcrumb for Graphic @@ -629,6 +639,7 @@ impl TableItemLayout for Graphic { Self::ColorList(list) => list.layout_with_breadcrumb(data), Self::GradientList(list) => list.layout_with_breadcrumb(data), Self::TextList(list) => list.layout_with_breadcrumb(data), + Self::StrokeList(list) => list.layout_with_breadcrumb(data), } } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index d717a2f3ff..66e05c0e5e 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -2,6 +2,7 @@ use super::DocumentNode; use crate::application_io::PlatformEditorApi; use crate::application_io::resource::Resource; use crate::proto::{Any as DAny, FutureAny}; +use brush_nodes::Stroke; use brush_nodes::brush_stroke::{BrushStroke, BrushTrace}; use core_types::color::SRGBA8; use core_types::list::{Item, List, NodeIdPath}; @@ -97,6 +98,7 @@ macro_rules! tagged_value { #[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this document upgrade code #[serde(alias = "BrushStrokeTable")] BrushStrokes(Vec), + Strokes(Vec), // ======================= // AUTO-GENERATED VARIANTS // ======================= @@ -140,6 +142,7 @@ macro_rules! tagged_value { Self::BoxCorners(values) => values.cache_hash(state), Self::GradientRamp(ramp) => ramp.cache_hash(state), Self::BrushStrokes(strokes) => strokes.cache_hash(state), + Self::Strokes(strokes) => strokes.cache_hash(state), // ======================= // NON-SERIALIZED VARIANTS // ======================= @@ -203,6 +206,10 @@ macro_rules! tagged_value { Self::BoxCorners(values) => Box::new(Item::new_from_element(BoxCorners::from(values))), Self::GradientRamp(ramp) => Box::new(Item::::from(ramp)), Self::BrushStrokes(strokes) => Box::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))), + Self::Strokes(strokes) => { + let list: List = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); + Box::new(list) + } // ======================= // AUTO-GENERATED VARIANTS // ======================= @@ -266,6 +273,10 @@ macro_rules! tagged_value { Self::BoxCorners(values) => Arc::new(Item::new_from_element(BoxCorners::from(values))), Self::GradientRamp(ramp) => Arc::new(Item::::from(ramp)), Self::BrushStrokes(strokes) => Arc::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))), + Self::Strokes(strokes) => { + let list: List = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); + Arc::new(list) + } // ======================= // AUTO-GENERATED VARIANTS // ======================= @@ -295,6 +306,7 @@ macro_rules! tagged_value { Self::BoxCorners(_) => item!(BoxCorners), Self::GradientRamp(_) => item!(Gradient), Self::BrushStrokes(_) => item!(BrushTrace), + Self::Strokes(_) => list!(Stroke), // ======================= // AUTO-GENERATED VARIANTS // ======================= @@ -335,6 +347,7 @@ macro_rules! tagged_value { x if x == TypeId::of::>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(&*downcast::>(input).unwrap()))), x if x == TypeId::of::>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())), x if x == TypeId::of::>() => Ok(TaggedValue::BrushStrokes(downcast::>(input).unwrap().into_element().0.iter_element_values().cloned().collect())), + x if x == TypeId::of::>() => Ok(TaggedValue::Strokes(downcast::>(input).unwrap().into_iter().map(Item::into_element).collect())), // ======================= // AUTO-GENERATED VARIANTS // ======================= @@ -369,6 +382,7 @@ macro_rules! tagged_value { x if x == TypeId::of::>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(input.downcast_ref::>().unwrap()))), x if x == TypeId::of::>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::>().unwrap().clone())), x if x == TypeId::of::>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::>().unwrap().element().0.iter_element_values().cloned().collect())), + x if x == TypeId::of::>() => Ok(TaggedValue::Strokes(input.downcast_ref::>().unwrap().iter_element_values().cloned().collect())), // ======================= // AUTO-GENERATED VARIANTS // ======================= @@ -397,6 +411,7 @@ macro_rules! tagged_value { if name == std::any::type_name::() { return Some(TaggedValue::BoxCorners(Vec::new())) } $( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )* if name == std::any::type_name::() { return Some(TaggedValue::BrushStrokes(Vec::new())) } + if name == std::any::type_name::>() { return Some(TaggedValue::Strokes(Vec::new())) } // Unranked types without a variant route through `TypeDefault`, with `to_dynany`/`to_any` constructing the actual default at execution time macro_rules! check_bare { ($type_default:ty) => { @@ -423,6 +438,9 @@ macro_rules! tagged_value { if **element == concrete!(f64) { return Some(TaggedValue::F64Array(Vec::new())); } + if **element == concrete!(Stroke) { + return Some(TaggedValue::Strokes(Vec::new())); + } macro_rules! check { ($type_default:ty) => { if **element == concrete!($type_default) { return Some(TaggedValue::TypeDefault(input.clone())); } @@ -450,6 +468,7 @@ macro_rules! tagged_value { Self::BoxCorners(values) => format!("BoxCorners({values:?})"), Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"), Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"), + Self::Strokes(strokes) => format!("Strokes({strokes:?})"), // ======================= // AUTO-GENERATED VARIANTS // ======================= diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 85970e91b0..db58d1b779 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -1059,7 +1059,7 @@ mod test { // If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them. assert_eq!( ids, - vec![NodeId(8464972237805743576), NodeId(3528778906331798968), NodeId(1126597937993520391), NodeId(17582929706900579130)] + vec![NodeId(12331852515109999872), NodeId(5084548161767585362), NodeId(14635346976242256925), NodeId(16015195863711239715)] ); } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index da018f3ab5..327b7e532a 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -6,6 +6,7 @@ use graph_craft::document::value::RenderOutput; use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graphene_std::animation::RealTimeMode; use graphene_std::any::DynAnyNode; +use graphene_std::brush::Stroke; use graphene_std::brush::brush_stroke::BrushTrace; use graphene_std::extract_xy::XY; use graphene_std::gradient::Gradient; @@ -82,6 +83,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), // Context nullification #[cfg(feature = "gpu")] async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item]), @@ -145,6 +147,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => List>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&wgpu_executor::WgpuExecutor>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item>]), @@ -353,6 +356,7 @@ fn node_registry() -> HashMap"] +license = "MIT OR Apache-2.0" + +[features] +default = ["serde"] +serde = ["dep:serde", "core-types/serde"] + +[dependencies] +# Local dependencies +core-types = { workspace = true } +graphene-hash = { workspace = true } + +# Workspace dependencies +dyn-any = { workspace = true } +glam = { workspace = true } + +# Optional workspace dependencies +serde = { workspace = true, optional = true } diff --git a/node-graph/libraries/brush-types/src/lib.rs b/node-graph/libraries/brush-types/src/lib.rs new file mode 100644 index 0000000000..023fb29176 --- /dev/null +++ b/node-graph/libraries/brush-types/src/lib.rs @@ -0,0 +1,131 @@ +use core_types::CacheHash; +use core_types::bounds::{BoundingBox, RenderBoundingBox}; +use core_types::render_complexity::RenderComplexity; +use dyn_any::DynAny; +use glam::{DAffine2, DVec2, Vec2}; +use std::f32::consts::{PI, TAU}; + +#[derive(Clone, Debug, PartialEq, CacheHash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Channel { + Uniform(T), + Samples(Vec), +} + +impl Channel { + pub fn get(&self, index: usize) -> T { + match self { + Self::Uniform(value) => *value, + Self::Samples(values) => values[index], + } + } + + fn len(&self) -> Option { + match self { + Self::Uniform(_) => None, + Self::Samples(values) => Some(values.len()), + } + } +} + +unsafe impl dyn_any::StaticType for Channel { + type Static = Channel; +} + +#[derive(Clone, Debug, PartialEq, CacheHash, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Stroke { + pub position: Vec, + pub pressure: Channel, + pub tilt: Channel, + pub twist: Channel, + pub time: Channel, + pub seed: u64, +} + +impl Default for Stroke { + fn default() -> Self { + Self { + position: Vec::new(), + pressure: Channel::Uniform(1.), + tilt: Channel::Uniform(Vec2::ZERO), + twist: Channel::Uniform(0.), + time: Channel::Uniform(0.), + seed: 0, + } + } +} + +impl Stroke { + pub fn len(&self) -> usize { + self.position.len() + } + + pub fn is_empty(&self) -> bool { + self.position.is_empty() + } + + pub fn is_valid(&self) -> bool { + let n = self.len(); + [self.pressure.len(), self.tilt.len(), self.twist.len(), self.time.len()].into_iter().flatten().all(|len| len == n) + } + + pub fn sample(&self, index: usize) -> Sample { + Sample { + position: self.position[index], + pressure: self.pressure.get(index), + tilt: self.tilt.get(index), + twist: self.twist.get(index), + time: self.time.get(index), + } + } + + pub fn sample_lerp(&self, index: usize, t: f32) -> Sample { + let a = self.sample(index); + let b = self.sample((index + 1).min(self.len().saturating_sub(1))); + Sample { + position: a.position.lerp(b.position, t as f64), + pressure: a.pressure + (b.pressure - a.pressure) * t, + tilt: a.tilt.lerp(b.tilt, t), + twist: { + let delta = (b.twist - a.twist).rem_euclid(TAU); + let delta = if delta > PI { delta - TAU } else { delta }; + a.twist + delta * t + }, + time: a.time + (b.time - a.time) * t as f64, + } + } + + pub fn samples(&self) -> impl Iterator + '_ { + (0..self.len()).map(|index| self.sample(index)) + } +} + +impl BoundingBox for Stroke { + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + let Some(first) = self.position.first() else { return RenderBoundingBox::None }; + let (min, max) = self.position.iter().fold((*first, *first), |(min, max), &point| (min.min(point), max.max(point))); + let corners = [min, DVec2::new(max.x, min.y), max, DVec2::new(min.x, max.y)].map(|corner| transform.transform_point2(corner)); + let (min, max) = corners.iter().fold((corners[0], corners[0]), |(min, max), &point| (min.min(point), max.max(point))); + RenderBoundingBox::Rectangle([min, max]) + } + + fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + self.bounding_box(transform, include_stroke) + } +} + +impl RenderComplexity for Stroke { + fn render_complexity(&self) -> usize { + self.len() + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Sample { + pub position: DVec2, + pub pressure: f32, + pub tilt: Vec2, + pub twist: f32, + pub time: f64, +} diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index e34bf699ff..37acdad0a4 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -97,6 +97,14 @@ pub const ATTR_JOIN: &str = "join"; pub const ATTR_JOIN_MITER_LIMIT: &str = "join_miter_limit"; /// Stroke coverage's `StrokeAlign` (implicit default `Center`), on the `Item` inside a `Coverage`. pub const ATTR_ALIGN: &str = "align"; +/// Brush stroke item's `Color` its strokes are painted with. +pub const ATTR_COLOR: &str = "color"; +/// Brush stroke item's tip diameter in document-space units (`f64`). +pub const ATTR_DIAMETER: &str = "diameter"; +/// Brush stroke item's edge hardness from `0.` (softest) to `1.` (hardest) (`f64`). +pub const ATTR_HARDNESS: &str = "hardness"; +/// Brush stroke item's per-pass paint coverage from `0.` to `1.` (`f64`). +pub const ATTR_FLOW: &str = "flow"; /// Text item's font size in document-space units (`f64`, implicit default `24.`). pub const ATTR_FONT_SIZE: &str = "font_size"; /// Text item's font, as a `Resource` of the loaded font file. diff --git a/node-graph/libraries/graphic-types/Cargo.toml b/node-graph/libraries/graphic-types/Cargo.toml index 6c9e0c23c4..0762710a89 100644 --- a/node-graph/libraries/graphic-types/Cargo.toml +++ b/node-graph/libraries/graphic-types/Cargo.toml @@ -20,6 +20,7 @@ wasm = [ # Local dependencies core-types = { workspace = true } graphene-hash = { workspace = true } +brush-types = { workspace = true } raster-types = { workspace = true, features = ["wgpu"] } vector-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 34103385fc..c6848c0dc1 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,4 +1,5 @@ use crate::appearance::{Appearance, Cover, Coverage}; +use brush_types::Stroke; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::graphene_hash::CacheHash; use core_types::list::{ATTR_APPEARANCE, ATTR_PAINT, Item, ItemAttributeValues, List, NodeIdPath}; @@ -34,6 +35,7 @@ pub enum Graphic { ColorList(List), GradientList(List), TextList(List), + StrokeList(List), } impl Default for Graphic { @@ -140,7 +142,19 @@ impl From> for Graphic { } } -// String +// Stroke +impl From for Graphic { + fn from(stroke: Stroke) -> Self { + Graphic::StrokeList(List::new_from_element(stroke)) + } +} +impl From> for Graphic { + fn from(stroke: List) -> Self { + Graphic::StrokeList(stroke) + } +} + +// Text impl From for Graphic { fn from(text: String) -> Self { Graphic::Text(Item::new_from_element(text)) @@ -310,6 +324,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA Graphic::RasterGPUList(list) => bake_list_transform(list, transform), Graphic::GradientList(list) => bake_list_transform(list, transform), Graphic::TextList(list) => bake_list_transform(list, transform), + Graphic::StrokeList(list) => bake_list_transform(list, transform), // A color has no spatial extent, so there is no placement for a transform to move Graphic::None(_) | Graphic::NoneList(_) | Graphic::Color(_) | Graphic::ColorList(_) => {} } @@ -380,6 +395,12 @@ impl TryFromGraphic for String { } } +impl TryFromGraphic for Stroke { + fn try_from_graphic(graphic: Graphic) -> Option> { + if let Graphic::StrokeList(t) = graphic { Some(t) } else { None } + } +} + // Local trait to convert types to List (avoids orphan rule issues) pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static { fn into_graphic_list(self) -> List; @@ -431,6 +452,17 @@ impl IntoGraphicList for List { } } +impl IntoGraphicList for List { + fn into_graphic_list(self) -> List { + let layer_path: NodeIdPath = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); + let mut graphic_list = List::new_from_element(Graphic::StrokeList(self)); + if !layer_path.0.is_empty() { + graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); + } + graphic_list + } +} + impl IntoGraphicList for List { fn into_graphic_list(self) -> List { List::new_from_element(Graphic::TextList(self)) @@ -542,6 +574,7 @@ impl Graphic { Graphic::ColorList(list) => all_clipped(list), Graphic::GradientList(list) => all_clipped(list), Graphic::TextList(list) => all_clipped(list), + Graphic::StrokeList(list) => all_clipped(list), } } @@ -595,6 +628,7 @@ impl Graphic { }) } Graphic::Text(_) | Graphic::TextList(_) => false, + Graphic::StrokeList(_) => false, } } @@ -627,6 +661,7 @@ impl Graphic { Graphic::RasterCPUList(list) => every_item_has_zero_opacity(list), Graphic::RasterGPUList(list) => every_item_has_zero_opacity(list), Graphic::TextList(list) => every_item_has_zero_opacity(list), + Graphic::StrokeList(list) => every_item_has_zero_opacity(list), } } @@ -648,6 +683,7 @@ impl Graphic { Graphic::RasterCPUList(list) => list.is_empty(), Graphic::RasterGPUList(list) => list.is_empty(), Graphic::TextList(list) => list.is_empty(), + Graphic::StrokeList(list) => list.is_empty(), } } } @@ -802,6 +838,7 @@ impl BoundingBox for Graphic { Graphic::ColorList(list) => list.bounding_box(transform, include_stroke), Graphic::GradientList(list) => list.bounding_box(transform, include_stroke), Graphic::TextList(list) => list.bounding_box(transform, include_stroke), + Graphic::StrokeList(list) => list.bounding_box(transform, include_stroke), } } @@ -822,6 +859,7 @@ impl BoundingBox for Graphic { Graphic::ColorList(color) => color.thumbnail_bounding_box(transform, include_stroke), Graphic::GradientList(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), Graphic::TextList(list) => list.thumbnail_bounding_box(transform, include_stroke), + Graphic::StrokeList(list) => list.thumbnail_bounding_box(transform, include_stroke), } } } @@ -844,6 +882,7 @@ impl RenderComplexity for Graphic { Self::ColorList(list) => list.render_complexity(), Self::GradientList(list) => list.render_complexity(), Self::TextList(list) => list.render_complexity(), + Self::StrokeList(list) => list.render_complexity(), } } } diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index 13facc359c..7da33a4eb0 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -8,12 +8,13 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde"] +serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde", "brush-types/serde"] [dependencies] # Local dependencies dyn-any = { workspace = true } core-types = { workspace = true } +brush-types = { workspace = true } graphene-hash = { workspace = true } graphene-resource = { workspace = true } text-nodes = { workspace = true } diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index f7d1ea08f5..c0207ff279 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -292,7 +292,8 @@ impl RenderExt for Graphic { | Graphic::RasterGPUList(_) | Graphic::GraphicList(_) | Graphic::GradientList(_) - | Graphic::TextList(_) => { + | Graphic::TextList(_) + | Graphic::StrokeList(_) => { let bounds = if target == PaintTarget::Stroke { // To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly. let inverse = |len: f64| if len > 0. { 1. / len } else { 0. }; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index bbb18a12ec..6ae5a80ae0 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1066,6 +1066,7 @@ impl Render for Graphic { Graphic::ColorList(list) => list.render_svg(render, render_params), Graphic::GradientList(list) => list.render_svg(render, render_params), Graphic::TextList(list) => list.render_svg(render, render_params), + Graphic::StrokeList(_) => (), } } @@ -1093,6 +1094,7 @@ impl Render for Graphic { Graphic::ColorList(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::GradientList(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::TextList(list) => list.render_to_vello(scene, transform, context, render_params), + Graphic::StrokeList(_) => (), } } @@ -1163,6 +1165,14 @@ impl Render for Graphic { Graphic::TextList(list) => { metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + } + } + Graphic::StrokeList(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first item if !list.is_empty() { metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); @@ -1187,6 +1197,7 @@ impl Render for Graphic { Graphic::ColorList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance), Graphic::GradientList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance), Graphic::TextList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance), + Graphic::StrokeList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance), } } @@ -1207,6 +1218,7 @@ impl Render for Graphic { Graphic::ColorList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance), Graphic::GradientList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance), Graphic::TextList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance), + Graphic::StrokeList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance), } } @@ -1227,6 +1239,7 @@ impl Render for Graphic { Graphic::ColorList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance), Graphic::GradientList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance), Graphic::TextList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance), + Graphic::StrokeList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance), } } @@ -1836,7 +1849,8 @@ fn render_vector_item_to_vello( | Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::GraphicList(_) - | Graphic::TextList(_) => { + | Graphic::TextList(_) + | Graphic::StrokeList(_) => { scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path); paint.render_to_vello(scene, multiplied_transform, context, paint_render_params); scene.pop_layer(); @@ -1928,7 +1942,8 @@ fn render_vector_item_to_vello( | Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::GraphicList(_) - | Graphic::TextList(_) => { + | Graphic::TextList(_) + | Graphic::StrokeList(_) => { let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01); scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &stroked); @@ -2557,6 +2572,12 @@ fn render_raster_gpu_item_to_vello(item: ItemRef<'_, Raster>, scene: &mut S } } +impl Render for List { + fn render_svg(&self, _render: &mut SvgRender, _render_params: &RenderParams) {} + + fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {} +} + // Since colors and gradients are technically infinitely big, we have to implement // workarounds for rendering them correctly in a way which still allows us // to cache the intermediate render data (SVG string/Vello scene). diff --git a/node-graph/nodes/brush/Cargo.toml b/node-graph/nodes/brush/Cargo.toml index e48a52e85a..f9b467b57f 100644 --- a/node-graph/nodes/brush/Cargo.toml +++ b/node-graph/nodes/brush/Cargo.toml @@ -13,8 +13,10 @@ serde = ["dep:serde", "core-types/serde", "raster-types/serde", "raster-nodes/se [dependencies] # Local dependencies dyn-any = { workspace = true } +brush-types = { workspace = true } core-types = { workspace = true } graphene-hash = { workspace = true } +graphic-types = { workspace = true } raster-types = { workspace = true } raster-nodes = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/brush/src/lib.rs b/node-graph/nodes/brush/src/lib.rs index cc69608117..2ff3208596 100644 --- a/node-graph/nodes/brush/src/lib.rs +++ b/node-graph/nodes/brush/src/lib.rs @@ -1,7 +1,33 @@ +use core_types::list::{ATTR_COLOR, ATTR_DIAMETER, ATTR_FLOW, ATTR_HARDNESS, Item, List}; +use core_types::registry::types::Percentage; +use core_types::{Color, Ctx}; +use graphic_types::Graphic; + pub mod brush; mod brush_cache; pub mod brush_stroke; +pub use brush_types::*; + +#[node_macro::node(category("Raster: Brush"))] +fn brush_strokes( + _: impl Ctx, + strokes: List, + color: List, + #[default(40.)] diameter: Item, + #[default(0.)] hardness: Item, + #[default(100.)] flow: Item, +) -> List { + let (diameter, hardness, flow) = (diameter.into_element(), hardness.into_element(), flow.into_element()); + List::new_from_item( + Item::new_from_element(Graphic::from(strokes)) + .with_attribute(ATTR_COLOR, color.element(0).copied().unwrap_or_default()) + .with_attribute(ATTR_DIAMETER, diameter.max(0.)) + .with_attribute(ATTR_HARDNESS, (hardness / 100.).clamp(0., 1.)) + .with_attribute(ATTR_FLOW, (flow / 100.).clamp(0., 1.)), + ) +} + pub mod migrations { use crate::brush_stroke::BrushStroke; diff --git a/node-graph/nodes/graphic/Cargo.toml b/node-graph/nodes/graphic/Cargo.toml index 5b2594a116..2fc815c2e3 100644 --- a/node-graph/nodes/graphic/Cargo.toml +++ b/node-graph/nodes/graphic/Cargo.toml @@ -8,6 +8,7 @@ authors.workspace = true [dependencies] # Local dependencies core-types = { workspace = true } +brush-types = { workspace = true } graphic-types = { workspace = true } vector-types = { workspace = true } raster-types = { workspace = true } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 01168afec9..a4d6de3db2 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -1,3 +1,4 @@ +use brush_types::Stroke; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath}; use core_types::registry::types::{Angle, SeedValue, SignedInteger}; @@ -870,6 +871,7 @@ pub async fn extend( List, List, List, + List, )] base: List, /// The list whose items will appear at the end of the extended list. @@ -890,6 +892,7 @@ pub async fn extend( List, List, List, + List, )] new: List, ) -> List { @@ -942,6 +945,7 @@ pub async fn into_group + 'n>( List, List, Item, // TODO: Remove this + List, )] content: T, ) -> Item { diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 907071f700..fdef837044 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -268,6 +268,8 @@ fn flatten_vector(graphic_list: &List) -> List { // Rasters, colors, and gradients bound no region, so they contribute no operand Graphic::None(_) | Graphic::NoneList(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Color(_) | Graphic::Gradient(_) => Vec::new(), Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::ColorList(_) | Graphic::GradientList(_) => Vec::new(), + // Strokes have no vector outline representation; a brush node renders them to rasters + Graphic::StrokeList(_) => Vec::new(), // Normalized to GraphicList above Graphic::Graphic(_) => Vec::new(), }