diff --git a/Cargo.lock b/Cargo.lock index b90912b1..d43e803b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1624,6 +1624,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "bezier-rs", "bytemuck", "dyn-any", "dyn-clone", diff --git a/document-legacy/src/document.rs b/document-legacy/src/document.rs index 8dfe543e..2d41670b 100644 --- a/document-legacy/src/document.rs +++ b/document-legacy/src/document.rs @@ -21,7 +21,7 @@ use std::hash::{Hash, Hasher}; /// This does not technically need to be unique globally, only within a folder. pub type LayerId = u64; -#[derive(Debug, Clone, Deserialize, Serialize, specta::Type)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Document { /// The root layer, usually a [FolderLayer](layers::folder_layer::FolderLayer) that contains all other [Layers](layers::layer_info::Layer). pub root: Layer, diff --git a/document-legacy/src/layers/folder_layer.rs b/document-legacy/src/layers/folder_layer.rs index 7b15ac69..d68fbc62 100644 --- a/document-legacy/src/layers/folder_layer.rs +++ b/document-legacy/src/layers/folder_layer.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; /// A layer that encapsulates other layers, including potentially more folders. /// The contained layers are rendered in the same order they are /// stored in the [layers](FolderLayer::layers) field. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default, specta::Type)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)] pub struct FolderLayer { /// The ID that will be assigned to the next layer that is added to the folder next_assignment_id: LayerId, diff --git a/document-legacy/src/layers/layer_info.rs b/document-legacy/src/layers/layer_info.rs index d1c7a207..62441f77 100644 --- a/document-legacy/src/layers/layer_info.rs +++ b/document-legacy/src/layers/layer_info.rs @@ -15,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2}; use serde::{Deserialize, Serialize}; use std::fmt::Write; -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, specta::Type)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] /// Represents different types of layers. pub enum LayerDataType { /// A layer that wraps a [FolderLayer] struct. @@ -213,7 +213,7 @@ fn return_true() -> bool { true } -#[derive(Debug, PartialEq, Deserialize, Serialize, specta::Type)] +#[derive(Debug, PartialEq, Deserialize, Serialize)] pub struct Layer { /// Whether the layer is currently visible or hidden. pub visible: bool, diff --git a/document-legacy/src/layers/nodegraph_layer.rs b/document-legacy/src/layers/nodegraph_layer.rs index 4ce26c3b..f8b8ea84 100644 --- a/document-legacy/src/layers/nodegraph_layer.rs +++ b/document-legacy/src/layers/nodegraph_layer.rs @@ -9,7 +9,7 @@ use kurbo::{Affine, BezPath, Shape as KurboShape}; use serde::{Deserialize, Serialize}; use std::fmt::Write; -#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, specta::Type)] +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] pub struct NodeGraphFrameLayer { // Image stored in layer after generation completes pub mime: String, diff --git a/document-legacy/src/operation.rs b/document-legacy/src/operation.rs index aae0ed73..dbf84b77 100644 --- a/document-legacy/src/operation.rs +++ b/document-legacy/src/operation.rs @@ -13,7 +13,7 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; #[repr(C)] -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, specta::Type)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] // TODO: Rename all instances of `path` to `layer_path` /// Operations that can be performed to mutate the document. pub enum Operation { diff --git a/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs b/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs index 47c35510..61a81245 100644 --- a/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs +++ b/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs @@ -12,7 +12,7 @@ use graphene_core::raster::color::Color; use glam::DAffine2; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, specta::Type)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] pub struct ArtboardMessageHandler { pub artboards_document: DocumentLegacy, pub artboard_ids: Vec, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 10d43f4c..d371dee9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -46,7 +46,7 @@ impl FrontendGraphDataType { TaggedValue::Image(_) => Self::Raster, TaggedValue::ImageFrame(_) => Self::Raster, TaggedValue::Color(_) => Self::Color, - TaggedValue::RcSubpath(_) | TaggedValue::Subpath(_) => Self::Subpath, + TaggedValue::RcSubpath(_) | TaggedValue::Subpath(_) | TaggedValue::VectorData(_) => Self::Subpath, _ => Self::General, } } 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 d4022b9d..ecdedd84 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 @@ -32,7 +32,7 @@ impl DocumentInputType { Self { name, data_type, default } } - pub const fn _none() -> Self { + pub const fn none() -> Self { Self { name: "None", data_type: FrontendGraphDataType::General, @@ -589,18 +589,10 @@ fn static_nodes() -> Vec { properties: node_properties::add_properties, }, (*IMAGINATE_NODE).clone(), - /*DocumentNodeType { + DocumentNodeType { name: "Unit Circle Generator", category: "Vector", - identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::UnitCircleGenerator", &[]), - inputs: vec![DocumentInputType::none()], - outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], - properties: node_properties::no_properties, - }, - DocumentNodeType { - name: "Unit Square Generator", - category: "Vector", - identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::UnitSquareGenerator", &[]), + identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::UnitCircleGenerator"), inputs: vec![DocumentInputType::none()], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::no_properties, @@ -608,40 +600,63 @@ fn static_nodes() -> Vec { DocumentNodeType { name: "Path Generator", category: "Vector", - identifier: NodeImplementation::proto("graphene_core::ops::IdNode"), + identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator"), inputs: vec![DocumentInputType { name: "Path Data", data_type: FrontendGraphDataType::Subpath, - default: NodeInput::value(TaggedValue::Subpath(Subpath::new()), false), + default: NodeInput::value(TaggedValue::Subpath(bezier_rs::Subpath::new(Vec::new(), false)), false), }], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::no_properties, }, DocumentNodeType { - name: "Transform Subpath", + name: "Transform", category: "Vector", - identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::TransformSubpathNode", &[]), + identifier: NodeImplementation::proto("graphene_core::vector::TransformNode<_, _, _, _>"), inputs: vec![ - DocumentInputType::new("Subpath", TaggedValue::Subpath(Subpath::empty()), true), - DocumentInputType::new("Translation", TaggedValue::DVec2(DVec2::ZERO), false), - DocumentInputType::new("Rotation", TaggedValue::F64(0.), false), - DocumentInputType::new("Scale", TaggedValue::DVec2(DVec2::ONE), false), - DocumentInputType::new("Skew", TaggedValue::DVec2(DVec2::ZERO), false), + DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), + DocumentInputType::value("Translation", TaggedValue::DVec2(DVec2::ZERO), false), + DocumentInputType::value("Rotation", TaggedValue::F64(0.), false), + DocumentInputType::value("Scale", TaggedValue::DVec2(DVec2::ONE), false), + DocumentInputType::value("Skew", TaggedValue::DVec2(DVec2::ZERO), false), ], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::transform_properties, }, DocumentNodeType { - name: "Blit Subpath", + name: "Fill", category: "Vector", - identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::BlitSubpath", &[]), + identifier: NodeImplementation::proto("graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>"), inputs: vec![ - DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), - DocumentInputType::new("Subpath", TaggedValue::Subpath(Subpath::empty()), true), + DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), + DocumentInputType::value("Fill Type", TaggedValue::FillType(vector::style::FillType::Solid), false), + DocumentInputType::value("Solid Color", TaggedValue::Color(Color::BLACK), false), + DocumentInputType::value("Gradient Type", TaggedValue::GradientType(vector::style::GradientType::Linear), false), + DocumentInputType::value("Start", TaggedValue::DVec2(DVec2::new(0., 0.5)), false), + DocumentInputType::value("End", TaggedValue::DVec2(DVec2::new(1., 0.5)), false), + DocumentInputType::value("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), false), + DocumentInputType::value("Positions", TaggedValue::GradientPositions(vec![(0., Some(Color::BLACK)), (1., Some(Color::WHITE))]), false), ], - outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Raster)], - properties: node_properties::no_properties, - },*/ + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], + properties: node_properties::fill_properties, + }, + DocumentNodeType { + name: "Stroke", + category: "Vector", + identifier: NodeImplementation::proto("graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>"), + inputs: vec![ + DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), + DocumentInputType::value("Color", TaggedValue::Color(Color::BLACK), false), + DocumentInputType::value("Weight", TaggedValue::F64(0.), false), + DocumentInputType::value("Dash Lengths", TaggedValue::VecF32(Vec::new()), false), + DocumentInputType::value("Dash Offset", TaggedValue::F64(0.), false), + DocumentInputType::value("Line Cap", TaggedValue::LineCap(graphene_core::vector::style::LineCap::Butt), false), + DocumentInputType::value("Line Join", TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::Miter), false), + DocumentInputType::value("Miter Limit", TaggedValue::F64(4.), false), + ], + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], + properties: node_properties::stroke_properties, + }, ] } @@ -718,14 +733,25 @@ impl DocumentNodeType { DocumentNodeImplementation::Network(inner_network) } + /// Converts the [DocumentNodeType] type to a [DocumentNode], based on the inputs from the graph (which must be the correct length) and the metadata pub fn to_document_node(&self, inputs: impl IntoIterator, metadata: graph_craft::document::DocumentNodeMetadata) -> DocumentNode { + let inputs: Vec<_> = inputs.into_iter().collect(); + assert_eq!(inputs.len(), self.inputs.len(), "Inputs passed from the graph must be equal to the number required"); DocumentNode { name: self.name.to_string(), - inputs: inputs.into_iter().collect(), + inputs, implementation: self.generate_implementation(), metadata, } } + + /// Converts the [DocumentNodeType] type to a [DocumentNode], using the provided `input_override` and falling back to the default inputs. + /// `input_override` does not have to be the correct length. + pub fn to_document_node_default_inputs(&self, input_override: impl IntoIterator>, metadata: graph_craft::document::DocumentNodeMetadata) -> DocumentNode { + let mut input_override = input_override.into_iter(); + let inputs = self.inputs.iter().map(|default| input_override.next().unwrap_or_default().unwrap_or_else(|| default.default.clone())); + self.to_document_node(inputs, metadata) + } } pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork { @@ -764,7 +790,7 @@ pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetw nodes: [ resolve_document_node_type("Input") .expect("Input node does not exist") - .to_document_node([NodeInput::Network(concrete!(ImageFrame))], DocumentNodeMetadata::position((8, 4))), + .to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))), resolve_document_node_type("Output") .expect("Output node does not exist") .to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))), @@ -776,3 +802,37 @@ pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetw ..Default::default() } } + +pub fn new_vector_network(subpath: bezier_rs::Subpath) -> NodeNetwork { + let input = resolve_document_node_type("Input").expect("Input node does not exist"); + let path_generator = resolve_document_node_type("Path Generator").expect("Path Generator node does not exist"); + let transform = resolve_document_node_type("Transform").expect("Transform node does not exist"); + let fill = resolve_document_node_type("Fill").expect("Fill node does not exist"); + let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist"); + let output = resolve_document_node_type("Output").expect("Output node does not exist"); + + let mut pos = 0; + let mut next_pos = || { + let node_pos = DocumentNodeMetadata::position((pos, 4)); + pos += 8; + node_pos + }; + + NodeNetwork { + inputs: vec![0], + outputs: vec![NodeOutput::new(5, 0)], + nodes: [ + input.to_document_node_default_inputs([], next_pos()), + path_generator.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpath(subpath), false))], next_pos()), + transform.to_document_node_default_inputs([Some(NodeInput::node(1, 0))], next_pos()), + fill.to_document_node_default_inputs([Some(NodeInput::node(2, 0))], next_pos()), + stroke.to_document_node_default_inputs([Some(NodeInput::node(3, 0))], next_pos()), + output.to_document_node_default_inputs([Some(NodeInput::node(4, 0))], next_pos()), + ] + .into_iter() + .enumerate() + .map(|(id, node)| (id as NodeId, node)) + .collect(), + ..Default::default() + } +} diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 7ae4609c..b6f614bd 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -9,6 +9,7 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::imaginate_input::*; use graphene_core::raster::{BlendMode, Color, LuminanceCalculation}; +use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin}; use super::document_node_types::NodePropertiesContext; use super::{FrontendGraphDataType, IMAGINATE_NODE}; @@ -18,17 +19,20 @@ pub fn string_properties(text: impl Into) -> Vec { vec![LayoutGroup::Row { widgets: vec![widget] }] } -fn update_value(value: impl Fn(&T) -> TaggedValue + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync { +fn optionally_update_value(value: impl Fn(&T) -> Option + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync { move |input_value: &T| { - NodeGraphMessage::SetInputValue { - node_id, - input_index, - value: value(input_value), + if let Some(value) = value(input_value) { + NodeGraphMessage::SetInputValue { node_id, input_index, value }.into() + } else { + Message::NoOp } - .into() } } +fn update_value(value: impl Fn(&T) -> TaggedValue + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync { + optionally_update_value(move |v| Some(value(v)), node_id, input_index) +} + fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder { ParameterExposeButton::new() .exposed(exposed) @@ -45,6 +49,15 @@ fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType .widget_holder() } +fn add_blank_assist(widgets: &mut Vec) { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), // TODO: This last one is the separator after the 24px assist. + ]); +} + fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec { let input = document_node.inputs.get(index).unwrap(); let mut widgets = vec![ @@ -53,12 +66,7 @@ fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, na WidgetHolder::text_widget(name), ]; if blank_assist { - widgets.extend_from_slice(&[ - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. - WidgetHolder::unrelated_separator(), // TODO: This last one is the separator after the 24px assist. - ]); + add_blank_assist(&mut widgets); } widgets } @@ -117,6 +125,35 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name widgets } +fn vec_f32_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Color, blank_assist); + + let from_string = |string: &str| { + string + .split(&[',', ' ']) + .filter(|x| !x.is_empty()) + .map(str::parse::) + .collect::, _>>() + .ok() + .map(TaggedValue::VecF32) + }; + + if let NodeInput::Value { + tagged_value: TaggedValue::VecF32(x), + exposed: false, + } = &document_node.inputs[index] + { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + text_props + .value(x.iter().map(|v| v.to_string()).collect::>().join(", ")) + .on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, index)) + .widget_holder(), + ]) + } + widgets +} + fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, number_props: NumberInput, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); @@ -191,6 +228,172 @@ fn luminance_calculation(document_node: &DocumentNode, node_id: u64, index: usiz LayoutGroup::Row { widgets }.with_tooltip("Formula used to calculate the luminance of a pixel") } +fn line_cap_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::LineCap(line_cap), + exposed: false, + } = &document_node.inputs[index] + { + let entries = [("Butt", LineCap::Butt), ("Round", LineCap::Round), ("Square", LineCap::Square)] + .into_iter() + .map(|(name, val)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::LineCap(val), node_id, index))) + .collect(); + + widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), RadioInput::new(entries).selected_index(line_cap as u32).widget_holder()]); + } + LayoutGroup::Row { widgets } +} + +fn line_join_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::LineJoin(line_join), + exposed: false, + } = &document_node.inputs[index] + { + let entries = [("Miter", LineJoin::Miter), ("Bevel", LineJoin::Bevel), ("Round", LineJoin::Round)] + .into_iter() + .map(|(name, val)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::LineJoin(val), node_id, index))) + .collect(); + + widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), RadioInput::new(entries).selected_index(line_join as u32).widget_holder()]); + } + LayoutGroup::Row { widgets } +} + +fn gradient_type_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::GradientType(gradient_type), + exposed: false, + } = &document_node.inputs[index] + { + let entries = [("Linear", GradientType::Linear), ("Radial", GradientType::Radial)] + .into_iter() + .map(|(name, val)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::GradientType(val), node_id, index))) + .collect(); + + widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), RadioInput::new(entries).selected_index(gradient_type as u32).widget_holder()]); + } + LayoutGroup::Row { widgets } +} + +fn gradient_row(row: &mut Vec, positions: &Vec<(f64, Option)>, index: usize, node_id: NodeId, input_index: usize) { + let label = TextLabel::new(format!("Gradient: {:.0}%", positions[index].0 * 100.)).tooltip("Adjustable by dragging the gradient stops in the viewport with the Gradient tool active"); + row.push(label.widget_holder()); + let on_update = { + let positions = positions.clone(); + move |color_input: &ColorInput| { + let mut new_positions = positions.clone(); + new_positions[index].1 = color_input.value; + TaggedValue::GradientPositions(new_positions) + } + }; + let color = ColorInput::new(positions[index].1).on_update(update_value(on_update, node_id, input_index)); + add_blank_assist(row); + row.push(WidgetHolder::unrelated_separator()); + row.push(color.widget_holder()); + + let mut skip_separator = false; + // Remove button + if positions.len() != index + 1 && index != 0 { + let on_update = { + let in_positions = positions.clone(); + move |_: &IconButton| { + let mut new_positions = in_positions.clone(); + new_positions.remove(index); + TaggedValue::GradientPositions(new_positions) + } + }; + + skip_separator = true; + row.push(WidgetHolder::related_separator()); + row.push( + IconButton::new("Remove", 16) + .tooltip("Remove this gradient stop") + .on_update(update_value(on_update, node_id, input_index)) + .widget_holder(), + ); + } + // Add button + if positions.len() != index + 1 { + let on_update = { + let positions = positions.clone(); + move |_: &IconButton| { + let mut new_positions = positions.clone(); + + // Blend linearly between the two colours. + let get_color = |index: usize| match (new_positions[index].1, new_positions.get(index + 1).and_then(|x| x.1)) { + (Some(a), Some(b)) => Color::from_rgbaf32((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)), + (Some(v), _) | (_, Some(v)) => Some(v), + _ => Some(Color::WHITE), + }; + let get_pos = |index: usize| (new_positions[index].0 + new_positions.get(index + 1).map(|v| v.0).unwrap_or(1.)) / 2.; + + new_positions.push((get_pos(index), get_color(index))); + + new_positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + + TaggedValue::GradientPositions(new_positions) + } + }; + + if !skip_separator { + row.push(WidgetHolder::related_separator()); + } + row.push( + IconButton::new("Add", 16) + .tooltip("Add a gradient stop after this") + .on_update(update_value(on_update, node_id, input_index)) + .widget_holder(), + ); + } +} + +fn gradient_positions(rows: &mut Vec, document_node: &DocumentNode, name: &str, node_id: u64, input_index: usize) { + let mut widgets = vec![expose_widget(node_id, input_index, FrontendGraphDataType::General, document_node.inputs[input_index].is_exposed())]; + widgets.push(WidgetHolder::unrelated_separator()); + if let NodeInput::Value { + tagged_value: TaggedValue::GradientPositions(gradient_positions), + exposed: false, + } = &document_node.inputs[input_index] + { + for index in 0..gradient_positions.len() { + gradient_row(&mut widgets, gradient_positions, index, node_id, input_index); + + let widgets = std::mem::take(&mut widgets); + rows.push(LayoutGroup::Row { widgets }); + } + let on_update = { + let gradient_positions = gradient_positions.clone(); + move |_: &TextButton| { + let mut new_positions = gradient_positions.clone(); + new_positions = new_positions.iter().map(|(distance, color)| (1. - distance, *color)).collect(); + new_positions.reverse(); + TaggedValue::GradientPositions(new_positions) + } + }; + let invert = TextButton::new("Invert") + .icon(Some("Swap".into())) + .tooltip("Reverse the order of each color stop") + .on_update(update_value(on_update, node_id, input_index)) + .widget_holder(); + + if widgets.is_empty() { + widgets.push(TextLabel::new("").widget_holder()); + add_blank_assist(&mut widgets); + } + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(invert); + + rows.push(LayoutGroup::Row { widgets }); + } else { + widgets.push(TextLabel::new(name).widget_holder()); + rows.push(LayoutGroup::Row { widgets }) + } +} + fn color_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, color_props: ColorInput, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); @@ -365,11 +568,11 @@ pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: & vec![operand("Input", 0), operand("Addend", 1)] } -pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext, blank_assist: bool) -> Vec { +pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let translation = { let index = 1; - let mut widgets = start_widgets(document_node, node_id, index, "Translation", FrontendGraphDataType::Vector, blank_assist); + let mut widgets = start_widgets(document_node, node_id, index, "Translation", FrontendGraphDataType::Vector, true); if let NodeInput::Value { tagged_value: TaggedValue::DVec2(vec2), @@ -383,7 +586,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con .unit(" px") .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index)) .widget_holder(), - WidgetHolder::unrelated_separator(), + WidgetHolder::related_separator(), NumberInput::new(Some(vec2.y)) .label("Y") .unit(" px") @@ -398,7 +601,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con let rotation = { let index = 2; - let mut widgets = start_widgets(document_node, node_id, index, "Rotation", FrontendGraphDataType::Number, blank_assist); + let mut widgets = start_widgets(document_node, node_id, index, "Rotation", FrontendGraphDataType::Number, true); if let NodeInput::Value { tagged_value: TaggedValue::F64(val), @@ -423,7 +626,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con let scale = { let index = 3; - let mut widgets = start_widgets(document_node, node_id, index, "Scale", FrontendGraphDataType::Vector, blank_assist); + let mut widgets = start_widgets(document_node, node_id, index, "Scale", FrontendGraphDataType::Vector, true); if let NodeInput::Value { tagged_value: TaggedValue::DVec2(vec2), @@ -436,7 +639,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con .label("X") .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index)) .widget_holder(), - WidgetHolder::unrelated_separator(), + WidgetHolder::related_separator(), NumberInput::new(Some(vec2.y)) .label("Y") .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index)) @@ -996,3 +1199,86 @@ pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId, c }; LayoutGroup::Section { name, layout } } + +pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let color_index = 1; + let weight_index = 2; + let dash_lengths_index = 3; + let dash_offset_index = 4; + let line_cap_index = 5; + let line_join_index = 6; + let miter_limit_index = 7; + + let color = color_widget(document_node, node_id, color_index, "Color", ColorInput::default(), true); + let weight = number_widget(document_node, node_id, weight_index, "Weight", NumberInput::default().unit("px").min(0.), true); + let dash_lengths = vec_f32_input(document_node, node_id, dash_lengths_index, "Dash Lengths", TextInput::default().centered(true), true); + let dash_offset = number_widget(document_node, node_id, dash_offset_index, "Dash Offset", NumberInput::default().unit("px").min(0.), true); + let line_cap = line_cap_widget(document_node, node_id, line_cap_index, "Line Cap", true); + let line_join = line_join_widget(document_node, node_id, line_join_index, "Line Join", true); + let miter_limit = number_widget(document_node, node_id, miter_limit_index, "Miter Limit", NumberInput::default().min(0.), true); + + vec![ + color, + LayoutGroup::Row { widgets: weight }, + LayoutGroup::Row { widgets: dash_lengths }, + LayoutGroup::Row { widgets: dash_offset }, + line_cap, + line_join, + LayoutGroup::Row { widgets: miter_limit }, + ] +} + +pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let fill_type_index = 1; + let solid_color_index = 2; + let gradient_type_index = 3; + let positions_index = 7; + + let fill_type = if let &NodeInput::Value { + tagged_value: TaggedValue::FillType(fill_type), + .. + } = &document_node.inputs[fill_type_index] + { + Some(fill_type) + } else { + None + }; + + let mut widgets = Vec::new(); + let gradient = fill_type == Some(graphene_core::vector::style::FillType::Gradient); + let solid = fill_type == Some(graphene_core::vector::style::FillType::Solid); + if fill_type.is_none() || solid { + let solid_color = color_widget(document_node, node_id, solid_color_index, "Color", ColorInput::default(), true); + widgets.push(solid_color); + } + + if fill_type.is_none() || gradient { + let gradient_type = gradient_type_widget(document_node, node_id, gradient_type_index, "Gradient Type", true); + widgets.push(gradient_type); + gradient_positions(&mut widgets, document_node, "Gradient Positions", node_id, positions_index); + } + + if gradient || solid { + let new_fill_type = if gradient { FillType::Solid } else { FillType::Gradient }; + let switch_button = TextButton::new(if gradient { "Use Solid Color" } else { "Use Gradient" }) + .tooltip(if gradient { + "Change this fill from a gradient to a solid color, keeping the 0% stop color" + } else { + "Change this fill from a solid color to a gradient" + }) + .on_update(update_value(move |_| TaggedValue::FillType(new_fill_type), node_id, fill_type_index)); + + widgets.push(LayoutGroup::Row { + widgets: { + let mut widgets = Vec::new(); + widgets.push(TextLabel::new("").widget_holder()); + add_blank_assist(&mut widgets); + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(switch_button.widget_holder()); + widgets + }, + }); + } + + widgets +} diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index da2f9f2f..d74976e2 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -1,15 +1,15 @@ +use crate::uuid::ManipulatorGroupId; +use crate::vector::VectorData; use crate::Node; -use glam::{DAffine2, DVec2}; -use super::subpath::Subpath; - -type VectorData = Subpath; +use bezier_rs::Subpath; +use glam::DVec2; pub struct UnitCircleGenerator; #[node_macro::node_fn(UnitCircleGenerator)] fn unit_circle(_input: ()) -> VectorData { - Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE) + super::VectorData::from_subpath(Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE)) } #[derive(Debug, Clone, Copy)] @@ -17,19 +17,17 @@ pub struct UnitSquareGenerator; #[node_macro::node_fn(UnitSquareGenerator)] fn unit_square(_input: ()) -> VectorData { - Subpath::new_rect(DVec2::ZERO, DVec2::ONE) + super::VectorData::from_subpath(Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE)) } // TODO: I removed the Arc requirement we shouuld think about when it makes sense to use its // vs making a generic value node #[derive(Debug, Clone)] -pub struct PathGenerator

{ - path_data: P, -} +pub struct PathGenerator; #[node_macro::node_fn(PathGenerator)] -fn generate_path(_input: (), path_data: Subpath) -> VectorData { - path_data +fn generate_path(path_data: Subpath) -> super::VectorData { + super::VectorData::from_subpath(path_data) } use crate::raster::Image; @@ -40,7 +38,7 @@ pub struct BlitSubpath

{ } #[node_macro::node_fn(BlitSubpath)] -fn bilt_subpath(base_image: Image, path_data: Subpath) -> Image { +fn bilt_subpath(base_image: Image, path_data: VectorData) -> Image { log::info!("Blitting subpath {path_data:#?}"); // TODO: Get forma to compile /*use forma::prelude::*; @@ -58,20 +56,3 @@ fn bilt_subpath(base_image: Image, path_data: Subpath) -> Image { base_image } - -#[derive(Debug, Clone, Copy)] -pub struct TransformSubpathNode { - translate: Translation, - rotate: Rotation, - scale: Scale, - shear: Shear, -} - -#[node_macro::node_fn(TransformSubpathNode)] -fn transform_subpath(subpath: Subpath, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2) -> VectorData { - let (sin, cos) = rotate.sin_cos(); - - let mut subpath = subpath; - subpath.apply_affine(DAffine2::from_cols_array(&[scale.x + cos, shear.y + sin, shear.x - sin, scale.y + cos, translate.x, translate.y])); - subpath -} diff --git a/node-graph/gcore/src/vector/manipulator_point.rs b/node-graph/gcore/src/vector/manipulator_point.rs index 1bc8481e..06fb9acd 100644 --- a/node-graph/gcore/src/vector/manipulator_point.rs +++ b/node-graph/gcore/src/vector/manipulator_point.rs @@ -28,7 +28,7 @@ impl Default for ManipulatorPoint { } } -#[allow(clippy::derive_hash_xor_eq)] +#[allow(clippy::derived_hash_with_manual_eq)] impl Hash for ManipulatorPoint { fn hash(&self, state: &mut H) { self.position.to_array().iter().for_each(|x| x.to_bits().hash(state)); diff --git a/node-graph/gcore/src/vector/mod.rs b/node-graph/gcore/src/vector/mod.rs index 3ac05c9d..eb8a58bb 100644 --- a/node-graph/gcore/src/vector/mod.rs +++ b/node-graph/gcore/src/vector/mod.rs @@ -14,3 +14,8 @@ pub use vector_data::VectorData; mod id_vec; pub use id_vec::IdBackedVec; + +mod vector_nodes; +pub use vector_nodes::*; + +pub use bezier_rs; diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 62b245f1..68da3ae6 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -3,6 +3,7 @@ use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; use crate::Color; +use dyn_any::{DynAny, StaticType}; use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display, Write}; @@ -19,7 +20,7 @@ fn format_opacity(name: &str, opacity: f32) -> String { } } -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize, specta::Type)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize, DynAny, specta::Type)] pub enum GradientType { #[default] Linear, @@ -30,30 +31,41 @@ pub enum GradientType { /// /// Contains the start and end points, along with the colors at varying points along the length. #[repr(C)] -#[derive(Debug, PartialEq, Default, Serialize, Deserialize, specta::Type)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, DynAny, specta::Type)] pub struct Gradient { pub start: DVec2, pub end: DVec2, pub transform: DAffine2, pub positions: Vec<(f64, Option)>, - uuid: u64, pub gradient_type: GradientType, } +impl core::hash::Hash for Gradient { + fn hash(&self, state: &mut H) { + self.positions.len().hash(state); + [].iter() + .chain(self.start.to_array().iter()) + .chain(self.end.to_array().iter()) + .chain(self.transform.to_cols_array().iter()) + .chain(self.positions.iter().map(|(position, _)| position)) + .for_each(|x| x.to_bits().hash(state)); + self.positions.iter().for_each(|(_, color)| color.hash(state)); + self.gradient_type.hash(state); + } +} impl Gradient { /// Constructs a new gradient with the colors at 0 and 1 specified. - pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, uuid: u64, gradient_type: GradientType) -> Self { + pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, _uuid: u64, gradient_type: GradientType) -> Self { Gradient { start, end, positions: vec![(0., Some(start_color)), (1., Some(end_color))], transform, - uuid, gradient_type, } } - /// Adds the gradient def with the uuid specified - fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) { + /// Adds the gradient def, returning the gradient id + fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 { let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]); let updated_transform = multiplied_transform * bound_transform; @@ -78,12 +90,13 @@ impl Gradient { .map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," }) .collect::(); + let gradient_id = crate::uuid::generate_uuid(); match self.gradient_type { GradientType::Linear => { let _ = write!( svg_defs, r#"{}"#, - self.uuid, start.x, end.x, start.y, end.y, transform, positions + gradient_id, start.x, end.x, start.y, end.y, transform, positions ); } GradientType::Radial => { @@ -91,10 +104,12 @@ impl Gradient { let _ = write!( svg_defs, r#"{}"#, - self.uuid, start.x, start.y, radius, transform, positions + gradient_id, start.x, start.y, radius, transform, positions ); } } + + gradient_id } /// Insert a stop into the gradient, the index if successful @@ -137,26 +152,11 @@ impl Gradient { } } -impl Clone for Gradient { - /// Clones the gradient, with the cloned gradient having the new uuid. - /// If multiple gradients have the same id then only one gradient will be shown in the final svg output. - fn clone(&self) -> Self { - Self { - start: self.start, - end: self.end, - transform: self.transform, - positions: self.positions.clone(), - uuid: crate::uuid::generate_uuid(), - gradient_type: self.gradient_type, - } - } -} - /// Describes the fill of a layer. /// /// Can be None, a solid [Color], a linear [Gradient], a radial [Gradient] or potentially some sort of image or pattern in the future #[repr(C)] -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, specta::Type)] +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, DynAny, Hash, specta::Type)] pub enum Fill { #[default] None, @@ -186,8 +186,8 @@ impl Fill { Self::None => r#" fill="none""#.to_string(), Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a())), Self::Gradient(gradient) => { - gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds); - format!(r##" fill="url('#{}')""##, gradient.uuid) + let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds); + format!(r##" fill="url('#{}')""##, gradient_id) } } } @@ -207,9 +207,18 @@ impl Fill { } } +/// Enum describing the type of [Fill] +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, DynAny, Hash, specta::Type)] +pub enum FillType { + None, + Solid, + Gradient, +} + /// The stroke (outline) style of an SVG element. #[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, specta::Type)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, DynAny, specta::Type)] pub enum LineCap { Butt, Round, @@ -227,7 +236,7 @@ impl Display for LineCap { } #[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, specta::Type)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, DynAny, specta::Type)] pub enum LineJoin { Miter, Bevel, @@ -245,25 +254,42 @@ impl Display for LineJoin { } #[repr(C)] -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, specta::Type)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, DynAny, specta::Type)] pub struct Stroke { /// Stroke color - color: Option, + pub color: Option, /// Line thickness - weight: f64, - dash_lengths: Vec, - dash_offset: f64, - line_cap: LineCap, - line_join: LineJoin, - line_join_miter_limit: f64, + pub weight: f64, + pub dash_lengths: Vec, + pub dash_offset: f64, + pub line_cap: LineCap, + pub line_join: LineJoin, + pub line_join_miter_limit: f64, +} + +impl core::hash::Hash for Stroke { + fn hash(&self, state: &mut H) { + self.color.hash(state); + self.weight.to_bits().hash(state); + self.dash_lengths.len().hash(state); + self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state)); + self.dash_offset.to_bits().hash(state); + self.line_cap.hash(state); + self.line_join.hash(state); + self.line_join_miter_limit.to_bits().hash(state); + } } impl Stroke { - pub fn new(color: Color, weight: f64) -> Self { + pub const fn new(color: Color, weight: f64) -> Self { Self { color: Some(color), weight, - ..Default::default() + dash_lengths: Vec::new(), + dash_offset: 0., + line_cap: LineCap::Butt, + line_join: LineJoin::Miter, + line_join_miter_limit: 4., } } @@ -278,7 +304,11 @@ impl Stroke { } pub fn dash_lengths(&self) -> String { - self.dash_lengths.iter().map(|v| v.to_string()).collect::>().join(", ") + if self.dash_lengths.is_empty() { + "none".to_string() + } else { + self.dash_lengths.iter().map(|v| v.to_string()).collect::>().join(", ") + } } pub fn dash_offset(&self) -> f64 { @@ -367,7 +397,7 @@ impl Default for Stroke { Self { weight: 0., color: Some(Color::from_rgba8(0, 0, 0, 255)), - dash_lengths: vec![0.], + dash_lengths: Vec::new(), dash_offset: 0., line_cap: LineCap::Butt, line_join: LineJoin::Miter, @@ -377,14 +407,14 @@ impl Default for Stroke { } #[repr(C)] -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, specta::Type)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, DynAny, Hash, specta::Type)] pub struct PathStyle { stroke: Option, fill: Fill, } impl PathStyle { - pub fn new(stroke: Option, fill: Fill) -> Self { + pub const fn new(stroke: Option, fill: Fill) -> Self { Self { stroke, fill } } @@ -508,7 +538,7 @@ impl PathStyle { } /// Represents different ways of rendering an object -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, specta::Type)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, DynAny, specta::Type)] pub enum ViewMode { /// Render with normal coloration at the current viewport resolution #[default] diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index cb9a07e4..69e94679 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -1,12 +1,33 @@ -use glam::DAffine2; +use super::style::{PathStyle, Stroke}; +use crate::{uuid::ManipulatorGroupId, Color}; -use super::style::PathStyle; -use crate::uuid::ManipulatorGroupId; +use dyn_any::{DynAny, StaticType}; +use glam::DAffine2; /// [VectorData] is passed between nodes. /// It contains a list of subpaths (that may be open or closed), a transform and some style information. +#[derive(Clone, Debug, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct VectorData { pub subpaths: Vec>, pub transform: DAffine2, pub style: PathStyle, } + +impl VectorData { + pub const fn empty() -> Self { + Self { + subpaths: Vec::new(), + transform: DAffine2::IDENTITY, + style: PathStyle::new(Some(Stroke::new(Color::BLACK, 0.)), super::style::Fill::Solid(Color::BLACK)), + } + } + + pub fn from_subpath(subpath: bezier_rs::Subpath) -> Self { + super::VectorData { + subpaths: vec![subpath], + transform: DAffine2::IDENTITY, + style: PathStyle::default(), + } + } +} diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs new file mode 100644 index 00000000..30e2a983 --- /dev/null +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -0,0 +1,90 @@ +use super::style::{Fill, FillType, Gradient, GradientType, Stroke}; +use super::VectorData; +use crate::{Color, Node}; +use glam::{DAffine2, DVec2}; + +#[derive(Debug, Clone, Copy)] +pub struct TransformNode { + translate: Translation, + rotate: Rotation, + scale: Scale, + shear: Shear, +} + +#[node_macro::node_fn(TransformNode)] +fn transform_vector_data(mut vector_data: VectorData, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2) -> VectorData { + let (sin, cos) = rotate.sin_cos(); + + vector_data.transform = vector_data.transform * DAffine2::from_cols_array(&[scale.x + cos, shear.y + sin, shear.x - sin, scale.y + cos, translate.x, translate.y]); + vector_data +} + +#[derive(Debug, Clone, Copy)] +pub struct SetFillNode { + fill_type: FillType, + solid_color: SolidColor, + gradient_type: GradientType, + start: Start, + end: End, + transform: Transform, + positions: Positions, +} + +#[node_macro::node_fn(SetFillNode)] +fn set_vector_data_fill( + mut vector_data: VectorData, + fill_type: FillType, + solid_color: Color, + gradient_type: GradientType, + start: DVec2, + end: DVec2, + transform: DAffine2, + positions: Vec<(f64, Option)>, +) -> VectorData { + vector_data.style.set_fill(match fill_type { + FillType::None => Fill::None, + FillType::Solid => Fill::Solid(solid_color), + FillType::Gradient => Fill::Gradient(Gradient { + start, + end, + transform, + positions, + gradient_type, + }), + }); + vector_data +} + +#[derive(Debug, Clone, Copy)] +pub struct SetStrokeNode { + color: Color, + weight: Weight, + dash_lengths: DashLengths, + dash_offset: DashOffset, + line_cap: LineCap, + line_join: LineJoin, + miter_limit: MiterLimit, +} + +#[node_macro::node_fn(SetStrokeNode)] +fn set_vector_data_stroke( + mut vector_data: VectorData, + color: crate::Color, + weight: f64, + dash_lengths: Vec, + dash_offset: f64, + line_cap: super::style::LineCap, + line_join: super::style::LineJoin, + miter_limit: f64, +) -> VectorData { + vector_data.style.set_stroke(Stroke { + color: Some(color), + weight, + dash_lengths, + dash_offset, + line_cap, + line_join, + line_join_miter_limit: miter_limit, + }); + vector_data +} diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index 4f5c9989..09dacf06 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0" [features] default = [] -serde = ["dep:serde", "graphene-core/serde", "glam/serde"] +serde = ["dep:serde", "graphene-core/serde", "glam/serde", "bezier-rs/serde"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -19,6 +19,7 @@ log = "0.4" serde = { version = "1", features = ["derive", "rc"], optional = true } glam = { version = "0.22" } base64 = "0.13" +bezier-rs = { path = "../../libraries/bezier-rs", features = ["dyn-any"] } specta.workspace = true bytemuck = {version = "1.8" } diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 70dea30b..7e6f6842 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -33,7 +33,7 @@ impl DocumentNodeMetadata { } } -#[derive(Clone, Debug, PartialEq, specta::Type)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DocumentNode { pub name: String, @@ -122,7 +122,7 @@ impl DocumentNode { } } -#[derive(Debug, Clone, PartialEq, Hash, specta::Type)] +#[derive(Debug, Clone, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum NodeInput { Node { node_id: NodeId, output_index: usize, lambda: bool }, @@ -165,7 +165,7 @@ impl NodeInput { } } -#[derive(Clone, Debug, PartialEq, specta::Type)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum DocumentNodeImplementation { Network(NodeNetwork), @@ -202,7 +202,7 @@ impl NodeOutput { } } -#[derive(Clone, Debug, Default, PartialEq, DynAny, specta::Type)] +#[derive(Clone, Debug, Default, PartialEq, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeNetwork { pub inputs: Vec, diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 39e500c6..fa760466 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,7 +11,7 @@ use crate::executor::Any; pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus}; /// A type that is known, allowing serialization (serde::Deserialize is not object safe) -#[derive(Clone, Debug, PartialEq, specta::Type)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TaggedValue { None, @@ -27,17 +27,26 @@ pub enum TaggedValue { RcImage(Option>), ImageFrame(graphene_core::raster::ImageFrame), Color(graphene_core::raster::color::Color), - Subpath(graphene_core::vector::subpath::Subpath), - RcSubpath(Arc), + Subpath(bezier_rs::Subpath), + RcSubpath(Arc>), BlendMode(BlendMode), LuminanceCalculation(LuminanceCalculation), ImaginateSamplingMethod(ImaginateSamplingMethod), ImaginateMaskStartingFill(ImaginateMaskStartingFill), ImaginateStatus(ImaginateStatus), LayerPath(Option>), + VectorData(graphene_core::vector::VectorData), + Fill(graphene_core::vector::style::Fill), + Stroke(graphene_core::vector::style::Stroke), + VecF32(Vec), + LineCap(graphene_core::vector::style::LineCap), + LineJoin(graphene_core::vector::style::LineJoin), + FillType(graphene_core::vector::style::FillType), + GradientType(graphene_core::vector::style::GradientType), + GradientPositions(Vec<(f64, Option)>), } -#[allow(clippy::derive_hash_xor_eq)] +#[allow(clippy::derived_hash_with_manual_eq)] impl Hash for TaggedValue { fn hash(&self, state: &mut H) { match self { @@ -120,10 +129,52 @@ impl Hash for TaggedValue { p.hash(state) } Self::ImageFrame(i) => { - 20.hash(state); + 21.hash(state); i.image.hash(state); i.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) } + Self::VectorData(vector_data) => { + 22.hash(state); + vector_data.subpaths.hash(state); + vector_data.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); + vector_data.style.hash(state); + } + Self::Fill(fill) => { + 23.hash(state); + fill.hash(state); + } + Self::Stroke(stroke) => { + 24.hash(state); + stroke.hash(state); + } + Self::VecF32(vec_f32) => { + 25.hash(state); + vec_f32.iter().for_each(|val| val.to_bits().hash(state)); + } + Self::LineCap(line_cap) => { + 26.hash(state); + line_cap.hash(state); + } + Self::LineJoin(line_join) => { + 27.hash(state); + line_join.hash(state); + } + Self::FillType(fill_type) => { + 28.hash(state); + fill_type.hash(state); + } + Self::GradientType(gradient_type) => { + 29.hash(state); + gradient_type.hash(state); + } + Self::GradientPositions(gradient_positions) => { + 30.hash(state); + gradient_positions.len().hash(state); + for (position, color) in gradient_positions { + position.to_bits().hash(state); + color.hash(state); + } + } } } } @@ -153,6 +204,15 @@ impl<'a> TaggedValue { TaggedValue::ImaginateMaskStartingFill(x) => Box::new(x), TaggedValue::ImaginateStatus(x) => Box::new(x), TaggedValue::LayerPath(x) => Box::new(x), + TaggedValue::VectorData(x) => Box::new(x), + TaggedValue::Fill(x) => Box::new(x), + TaggedValue::Stroke(x) => Box::new(x), + TaggedValue::VecF32(x) => Box::new(x), + TaggedValue::LineCap(x) => Box::new(x), + TaggedValue::LineJoin(x) => Box::new(x), + TaggedValue::FillType(x) => Box::new(x), + TaggedValue::GradientType(x) => Box::new(x), + TaggedValue::GradientPositions(x) => Box::new(x), } } @@ -172,8 +232,8 @@ impl<'a> TaggedValue { TaggedValue::RcImage(_) => concrete!(Option>), TaggedValue::ImageFrame(_) => concrete!(graphene_core::raster::ImageFrame), TaggedValue::Color(_) => concrete!(graphene_core::raster::Color), - TaggedValue::Subpath(_) => concrete!(graphene_core::vector::subpath::Subpath), - TaggedValue::RcSubpath(_) => concrete!(Arc), + TaggedValue::Subpath(_) => concrete!(bezier_rs::Subpath), + TaggedValue::RcSubpath(_) => concrete!(Arc>), TaggedValue::BlendMode(_) => concrete!(BlendMode), TaggedValue::ImaginateSamplingMethod(_) => concrete!(ImaginateSamplingMethod), TaggedValue::ImaginateMaskStartingFill(_) => concrete!(ImaginateMaskStartingFill), @@ -181,6 +241,15 @@ impl<'a> TaggedValue { TaggedValue::LayerPath(_) => concrete!(Option>), TaggedValue::DAffine2(_) => concrete!(DAffine2), TaggedValue::LuminanceCalculation(_) => concrete!(LuminanceCalculation), + TaggedValue::VectorData(_) => concrete!(graphene_core::vector::VectorData), + TaggedValue::Fill(_) => concrete!(graphene_core::vector::style::Fill), + TaggedValue::Stroke(_) => concrete!(graphene_core::vector::style::Stroke), + TaggedValue::VecF32(_) => concrete!(Vec), + TaggedValue::LineCap(_) => concrete!(graphene_core::vector::style::LineCap), + TaggedValue::LineJoin(_) => concrete!(graphene_core::vector::style::LineJoin), + TaggedValue::FillType(_) => concrete!(graphene_core::vector::style::FillType), + TaggedValue::GradientType(_) => concrete!(graphene_core::vector::style::GradientType), + TaggedValue::GradientPositions(_) => concrete!(Vec<(f64, Option)>), } } } diff --git a/node-graph/graph-craft/src/imaginate_input.rs b/node-graph/graph-craft/src/imaginate_input.rs index 163b86ed..206bbc52 100644 --- a/node-graph/graph-craft/src/imaginate_input.rs +++ b/node-graph/graph-craft/src/imaginate_input.rs @@ -14,7 +14,7 @@ pub enum ImaginateStatus { Terminated, } -#[allow(clippy::derive_hash_xor_eq)] +#[allow(clippy::derived_hash_with_manual_eq)] impl core::hash::Hash for ImaginateStatus { fn hash(&self, state: &mut H) { match self { diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index ce1dbe8c..867a0690 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -1,6 +1,7 @@ use glam::{DAffine2, DVec2}; use graph_craft::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus}; use graphene_core::ops::{CloneNode, IdNode, TypeNode}; +use graphene_core::vector::VectorData; use once_cell::sync::Lazy; use std::collections::HashMap; @@ -294,6 +295,15 @@ fn node_registry() -> HashMap, input: Image, params: [&str]), register_node!(graphene_std::raster::ImageFrameNode<_>, input: Image, params: [DAffine2]), + register_node!(graphene_core::vector::TransformNode<_, _, _, _>, input: VectorData, params: [DVec2, f64, DVec2, DVec2]), + register_node!(graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>, input: VectorData, params: [ graphene_core::vector::style::FillType, graphene_core::Color, graphene_core::vector::style::GradientType, DVec2, DVec2, DAffine2, Vec<(f64, Option)>]), + register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [graphene_core::Color, f64, Vec, f64, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f64]), + register_node!(graphene_core::vector::generator_nodes::UnitCircleGenerator, input: (), params: []), + register_node!( + graphene_core::vector::generator_nodes::PathGenerator, + input: graphene_core::vector::bezier_rs::Subpath, + params: [] + ), /* (NodeIdentifier::new("graphene_std::raster::ImageNode", &[concrete!("&str")]), |_proto_node, stack| { stack.push_fn(|_nodes| { diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index ae163a41..839b295d 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -42,7 +42,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { // Extract primary input as first argument let primary_input = function_inputs.next().expect("Primary input required - set to `()` if not needed."); - let Pat::Ident(PatIdent{ident: primary_input_ident,..} ) =&*primary_input.pat else { + let Pat::Ident(PatIdent{ident: primary_input_ident, mutability: primary_input_mutability,..} ) =&*primary_input.pat else { panic!("Expected ident as primary input."); }; let primary_input_ty = &primary_input.ty; @@ -51,13 +51,15 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { // Extract secondary inputs as all other arguments let parameter_inputs = function_inputs.collect::>(); - let parameter_idents = parameter_inputs + let parameter_pat_ident_patterns = parameter_inputs .iter() .map(|input| { - let Pat::Ident(PatIdent { ident: primary_input_ident,.. }) = &*input.pat else { panic!("Expected ident for secondary input."); }; - primary_input_ident + let Pat::Ident(pat_ident) = &*input.pat else { panic!("Expected ident for secondary input."); }; + pat_ident }) .collect::>(); + let parameter_idents = parameter_pat_ident_patterns.iter().map(|pat_ident| &pat_ident.ident).collect::>(); + let parameter_mutability = parameter_pat_ident_patterns.iter().map(|pat_ident| &pat_ident.mutability); // Extract the output type of the entire node - `()` by default let output = if let ReturnType::Type(_, ty) = &function.sig.output { @@ -129,9 +131,9 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { { type Output = #output; #[inline] - fn eval<'node: 'input>(&'node self, #primary_input_ident: #primary_input_ty) -> Self::Output { + fn eval<'node: 'input>(&'node self, #primary_input_mutability #primary_input_ident: #primary_input_ty) -> Self::Output { #( - let #parameter_idents = self.#parameter_idents.eval(()); + let #parameter_mutability #parameter_idents = self.#parameter_idents.eval(()); )* #body