From 589ff9a2d3af5529f98126a0b58c21d0ca781e39 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Tue, 11 Apr 2023 10:35:21 +0200 Subject: [PATCH] Implement the Brush tool (#1099) * Implement Brush Node * Add color Input * Add VectorPointsNode * Add Erase Node * Adapt compilation infrastructure to allow non Image Frame inputs * Remove debug output from TransformNode * Fix transform calculation * Fix Blending by making the brush texture use associated alpha * Code improvements and UX polish * Rename Opacity to Flow * Add erase option to brush node + fix freehand tool * Fix crash * Revert erase implementation * Fix flattening id calculation * Fix some transformation issues * Fix changing the pivot location * Fix vector data modify bounds * Minor fn name cleanup * Fix some tests * Fix tests --------- Co-authored-by: Keavon Chambers Co-authored-by: hypercube <0hypercube@gmail.com> --- .../messages/input_mapper/default_mapping.rs | 6 + .../document/document_message_handler.rs | 50 ++- .../graph_operation_message_handler.rs | 15 +- .../document_node_types.rs | 65 ++- .../node_properties.rs | 10 + editor/src/messages/prelude.rs | 1 + .../graph_modification_utils.rs | 8 +- editor/src/messages/tool/tool_message.rs | 8 +- .../src/messages/tool/tool_message_handler.rs | 3 + .../messages/tool/tool_messages/brush_tool.rs | 290 ++++++++++++ .../tool/tool_messages/freehand_tool.rs | 6 - editor/src/messages/tool/tool_messages/mod.rs | 1 + .../tool/tool_messages/spline_tool.rs | 6 - editor/src/messages/tool/utility_types.rs | 11 +- editor/src/node_graph_executor.rs | 7 +- libraries/dyn-any/src/lib.rs | 21 + node-graph/gcore/Cargo.toml | 25 +- node-graph/gcore/src/lib.rs | 2 +- node-graph/gcore/src/ops.rs | 2 +- node-graph/gcore/src/raster.rs | 29 +- node-graph/gcore/src/raster/adjustments.rs | 76 ++-- node-graph/gcore/src/raster/color.rs | 50 ++- node-graph/gcore/src/transform.rs | 100 +++-- node-graph/gcore/src/types.rs | 75 +++- node-graph/gcore/src/value.rs | 12 + node-graph/gpu-compiler/Cargo.lock | 41 +- node-graph/graph-craft/src/document.rs | 417 +++++++++++------- node-graph/graph-craft/src/document/value.rs | 9 + node-graph/graph-craft/src/proto.rs | 56 ++- node-graph/gstd/src/any.rs | 2 +- node-graph/gstd/src/brush.rs | 215 +++++++++ node-graph/gstd/src/lib.rs | 2 + node-graph/gstd/src/raster.rs | 96 +++- .../interpreted-executor/src/executor.rs | 9 + .../interpreted-executor/src/node_registry.rs | 183 ++++++-- node-graph/node-macro/src/lib.rs | 24 +- 36 files changed, 1527 insertions(+), 406 deletions(-) create mode 100644 editor/src/messages/tool/tool_messages/brush_tool.rs create mode 100644 node-graph/gstd/src/brush.rs diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index 06418e13..ed317c90 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -205,6 +205,11 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftPointerDown), entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightPointerDown), // + // BrushToolMessage + entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove), + entry!(KeyDown(Lmb); action_dispatch=BrushToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=BrushToolMessage::DragStop), + // // ToolMessage entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect), entry!(KeyDown(KeyZ); action_dispatch=ToolMessage::ActivateToolNavigate), @@ -219,6 +224,7 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle), entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse), entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape), + entry!(KeyDown(KeyB); action_dispatch=ToolMessage::ActivateToolBrush), entry!(KeyDown(KeyX); modifiers=[Shift, Accel], action_dispatch=ToolMessage::ResetColors), entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors), entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=ToolMessage::SelectRandomPrimaryColor), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 2880c5e4..38f239cd 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -32,7 +32,9 @@ use document_legacy::layers::style::{Fill, RenderData, ViewMode}; use document_legacy::layers::text_layer::Font; use document_legacy::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation}; use graph_craft::document::NodeId; +use graph_craft::{concrete, Type, TypeDescriptor}; use graphene_core::raster::{Color, ImageFrame}; +use graphene_core::Cow; use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; @@ -1050,39 +1052,39 @@ impl DocumentMessageHandler { return None; }; - // Skip processing under node graph frame input if not connected - if !node_network.connected_to_output(node_network.inputs[0], false) { - return Some( - PortfolioMessage::ProcessNodeGraphFrame { + // Find the primary input type of the node graph + let primary_input_type = node_network.input_types().next().clone(); + let response = match primary_input_type { + // Only calclate the frame if the primary input is an image + Some(ty) if ty == concrete!(ImageFrame) => { + // Calculate the size of the region to be exported + let old_transforms = self.remove_document_transform(); + let transform = self.document_legacy.multiply_transforms(&layer_path).unwrap(); + let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); + + let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); + self.restore_document_transform(old_transforms); + + FrontendMessage::TriggerNodeGraphFrameGenerate { document_id, layer_path, - image_data: Default::default(), - size: (0, 0), + svg, + size, imaginate_node, } - .into(), - ); - } - - // Calculate the size of the region to be exported - - let old_transforms = self.remove_document_transform(); - let transform = self.document_legacy.multiply_transforms(&layer_path).unwrap(); - let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); - - let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); - self.restore_document_transform(old_transforms); - - Some( - FrontendMessage::TriggerNodeGraphFrameGenerate { + .into() + } + // Skip processing under node graph frame input if not connected + _ => PortfolioMessage::ProcessNodeGraphFrame { document_id, layer_path, - svg, - size, + image_data: Default::default(), + size: (0, 0), imaginate_node, } .into(), - ) + }; + Some(response) } /// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index f65aaa64..90bd86ee 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -119,7 +119,7 @@ impl<'a> ModifyInputsContext<'a> { }); } - fn transform_change(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) { + fn transform_change(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, bounds: LayerBounds, skip_rerender: bool) { self.modify_inputs("Transform", skip_rerender, |inputs| { let layer_transform = transform_utils::get_current_transform(inputs); let to = match transform_in { @@ -127,7 +127,8 @@ impl<'a> ModifyInputsContext<'a> { TransformIn::Scope { scope } => scope * parent_transform, TransformIn::Viewport => parent_transform, }; - let transform = to.inverse() * transform * to * layer_transform; + let pivot = DAffine2::from_translation(bounds.layerspace_pivot(transform_utils::get_current_normalized_pivot(inputs))); + let transform = pivot.inverse() * to.inverse() * transform * to * pivot * layer_transform; transform_utils::update_transform(inputs, transform); }); } @@ -140,7 +141,7 @@ impl<'a> ModifyInputsContext<'a> { TransformIn::Viewport => parent_transform, }; let pivot = DAffine2::from_translation(bounds.layerspace_pivot(transform_utils::get_current_normalized_pivot(inputs))); - let transform = to.inverse() * transform * pivot; + let transform = pivot.inverse() * to.inverse() * transform * pivot; transform_utils::update_transform(inputs, transform); }); } @@ -150,7 +151,7 @@ impl<'a> ModifyInputsContext<'a> { let layer_transform = transform_utils::get_current_transform(inputs); let old_pivot_transform = DAffine2::from_translation(bounds.local_pivot(transform_utils::get_current_normalized_pivot(inputs))); let new_pivot_transform = DAffine2::from_translation(bounds.local_pivot(new_pivot)); - let transform = layer_transform * old_pivot_transform.inverse() * new_pivot_transform; + let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform; transform_utils::update_transform(inputs, transform); inputs[5] = NodeInput::value(TaggedValue::DVec2(new_pivot), false); }); @@ -184,6 +185,7 @@ impl<'a> ModifyInputsContext<'a> { [new_bounds_min, new_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths); }); + self.modify_inputs("Transform", false, |inputs| { let layer_transform = transform_utils::get_current_transform(inputs); let normalized_pivot = transform_utils::get_current_normalized_pivot(inputs); @@ -193,7 +195,7 @@ impl<'a> ModifyInputsContext<'a> { let new_pivot_transform = DAffine2::from_translation(new_layerspace_pivot); let old_pivot_transform = DAffine2::from_translation(old_layerspace_pivot); - let transform = layer_transform * old_pivot_transform.inverse() * new_pivot_transform; + let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform; transform_utils::update_transform(inputs, transform); }); } @@ -225,8 +227,9 @@ impl MessageHandler { let parent_transform = document.multiply_transforms(&layer[..layer.len() - 1]).unwrap_or_default(); + let bounds = LayerBounds::new(document, &layer); if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) { - modify_inputs.transform_change(transform, transform_in, parent_transform, skip_rerender); + modify_inputs.transform_change(transform, transform_in, parent_transform, bounds, skip_rerender); } let transform = transform.to_cols_array(); 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 1d3c6685..d7f10eba 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::*; use graph_craft::imaginate_input::ImaginateSamplingMethod; use graph_craft::NodeIdentifier; use graphene_core::raster::{BlendMode, Color, Image, ImageFrame, LuminanceCalculation}; +use graphene_core::vector::VectorData; use graphene_core::*; use once_cell::sync::Lazy; @@ -448,6 +449,35 @@ fn static_nodes() -> Vec { }], properties: node_properties::blur_image_properties, }, + DocumentNodeType { + name: "Brush", + category: "Brush", + identifier: NodeImplementation::proto("graphene_std::brush::BrushNode"), + inputs: vec![ + DocumentInputType::value("None", TaggedValue::None, false), + DocumentInputType::value("Trace", TaggedValue::VecDVec2((0..2).map(|x| DVec2::new(x as f64 * 10., 0.)).collect()), true), + DocumentInputType::value("Diameter", TaggedValue::F64(40.), false), + DocumentInputType::value("Hardness", TaggedValue::F64(50.), false), + DocumentInputType::value("Flow", TaggedValue::F64(100.), false), + DocumentInputType::value("Color", TaggedValue::Color(Color::BLACK), false), + ], + outputs: vec![DocumentOutputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + }], + properties: node_properties::brush_node_properties, + }, + DocumentNodeType { + name: "Extract Vector Points", + category: "Brush", + identifier: NodeImplementation::proto("graphene_std::brush::VectorPointsNode"), + inputs: vec![DocumentInputType::value("VectorData", TaggedValue::VectorData(VectorData::empty()), true)], + outputs: vec![DocumentOutputType { + name: "Vector Points", + data_type: FrontendGraphDataType::General, + }], + properties: node_properties::no_properties, + }, DocumentNodeType { name: "Cache", category: "Structural", @@ -490,6 +520,14 @@ fn static_nodes() -> Vec { outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: |_document_node, _node_id, _context| node_properties::string_properties("A bitmap image embedded in this node"), }, + DocumentNodeType { + name: "Ref", + category: "Structural", + identifier: NodeImplementation::proto("graphene_std::memo::CacheNode"), + inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::no_properties, + }, #[cfg(feature = "gpu")] DocumentNodeType { name: "GpuImage", @@ -841,8 +879,18 @@ impl DocumentNodeType { } pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork { + // if the network has no inputs, it doesn't need to be wrapped in a scope + if network.inputs.is_empty() { + return network; + } + assert_eq!(network.inputs.len(), 1, "Networks wrapped in scope must have exactly one input"); - let input_type = network.nodes[&network.inputs[0]].inputs.iter().find(|&i| matches!(i, NodeInput::Network(_))).unwrap().clone(); + let input = network.nodes[&network.inputs[0]].inputs.iter().find(|&i| matches!(i, NodeInput::Network(_))).cloned(); + + // if the network has no network inputs, it doesn't need to be wrapped in a scope either + let Some(input_type) = input else { + return network; + }; let inner_network = DocumentNode { name: "Scope".to_string(), @@ -850,6 +898,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork { inputs: vec![NodeInput::node(0, 1)], metadata: DocumentNodeMetadata::default(), }; + // wrap the inner network in a scope let nodes = vec![ resolve_document_node_type("Begin Scope") @@ -890,7 +939,6 @@ pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetw } pub fn new_vector_network(subpaths: Vec>) -> NodeNetwork { - let input = resolve_document_node_type("Input Frame").expect("Input Frame 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"); @@ -905,15 +953,14 @@ pub fn new_vector_network(subpaths: Vec Vec { + let color = color_widget(document_node, node_id, 5, "Color", ColorInput::default(), true); + + let size = number_widget(document_node, node_id, 2, "Diameter", NumberInput::default().min(0.).max(100.).unit(" px"), true); + let hardness = number_widget(document_node, node_id, 3, "Hardness", NumberInput::default().min(0.).max(100.).unit("%"), true); + let flow = number_widget(document_node, node_id, 4, "Flow", NumberInput::default().min(1.).max(100.).unit("%"), true); + + vec![color, LayoutGroup::Row { widgets: size }, LayoutGroup::Row { widgets: hardness }, LayoutGroup::Row { widgets: flow }] +} + pub fn adjust_threshold_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let thereshold_min = number_widget(document_node, node_id, 1, "Min Luminance", NumberInput::default().min(0.).max(100.).unit("%"), true); let thereshold_max = number_widget(document_node, node_id, 2, "Max Luminance", NumberInput::default().min(0.).max(100.).unit("%"), true); diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 056edf8f..e440815f 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -32,6 +32,7 @@ pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscrimin pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant}; pub use crate::messages::message::{Message, MessageDiscriminant}; pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant}; +pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::ellipse_tool::{EllipseToolMessage, EllipseToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMessage, EyedropperToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant}; diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 798b8ec1..11a8e8b8 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -3,6 +3,7 @@ use crate::messages::prelude::*; use bezier_rs::Subpath; use document_legacy::{LayerId, Operation}; +use graph_craft::document::NodeNetwork; use graphene_core::uuid::ManipulatorGroupId; use glam::DAffine2; @@ -10,9 +11,12 @@ use std::collections::VecDeque; /// Create a new vector layer from a vector of [`bezier_rs::Subpath`]. pub fn new_vector_layer(subpaths: Vec>, layer_path: Vec, responses: &mut VecDeque) { - responses.push_back(DocumentMessage::DeselectAllLayers.into()); - let network = node_graph::new_vector_network(subpaths); + new_custom_layer(network, layer_path, responses); +} + +pub fn new_custom_layer(network: NodeNetwork, layer_path: Vec, responses: &mut VecDeque) { + responses.push_back(DocumentMessage::DeselectAllLayers.into()); responses.push_back( Operation::AddNodeGraphFrame { path: layer_path.clone(), diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index a775a7e6..1f811f9c 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -61,9 +61,9 @@ pub enum ToolMessage { #[child] Text(TextToolMessage), - // #[remain::unsorted] - // #[child] - // Brush(BrushToolMessage), + #[remain::unsorted] + #[child] + Brush(BrushToolMessage), // #[remain::unsorted] // #[child] // Heal(HealToolMessage), @@ -119,6 +119,8 @@ pub enum ToolMessage { #[remain::unsorted] ActivateToolShape, + #[remain::unsorted] + ActivateToolBrush, #[remain::unsorted] ActivateToolImaginate, #[remain::unsorted] diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 70303e6e..ab973b65 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -69,6 +69,8 @@ impl MessageHandler responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }.into()), + #[remain::unsorted] + ToolMessage::ActivateToolBrush => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }.into()), #[remain::unsorted] ToolMessage::ActivateToolImaginate => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Imaginate }.into()), @@ -276,6 +278,7 @@ impl MessageHandler Self { + Self { + diameter: 40., + hardness: 50., + flow: 100., + } + } +} + +#[remain::sorted] +#[impl_message(Message, ToolMessage, Brush)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] +pub enum BrushToolMessage { + // Standard messages + #[remain::unsorted] + Abort, + + // Tool-specific messages + DragStart, + DragStop, + PointerMove, + UpdateOptions(BrushToolMessageOptionsUpdate), +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] +pub enum BrushToolMessageOptionsUpdate { + Diameter(f64), + Flow(f64), + Hardness(f64), +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum BrushToolFsmState { + #[default] + Ready, + Drawing, +} + +impl ToolMetadata for BrushTool { + fn icon_name(&self) -> String { + "RasterBrushTool".into() + } + fn tooltip(&self) -> String { + "Brush Tool".into() + } + fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { + ToolType::Brush + } +} + +impl PropertyHolder for BrushTool { + fn properties(&self) -> Layout { + let diameter = NumberInput::new(Some(self.options.diameter)) + .label("Diameter") + .min(1.) + .unit(" px") + .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap())).into()) + .widget_holder(); + let hardness = NumberInput::new(Some(self.options.hardness)) + .label("Hardness") + .min(0.) + .max(100.) + .unit("%") + .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap())).into()) + .widget_holder(); + let flow = NumberInput::new(Some(self.options.flow)) + .label("Flow") + .min(1.) + .max(100.) + .unit("%") + .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into()) + .widget_holder(); + + let separator = Separator::new(SeparatorDirection::Horizontal, SeparatorType::Related).widget_holder(); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { + widgets: vec![diameter, separator.clone(), hardness, separator, flow], + }])) + } +} + +impl<'a> MessageHandler> for BrushTool { + fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { + if let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message { + match action { + BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter, + BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness, + BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow, + } + return; + } + + self.fsm_state.process_event(message, &mut self.data, tool_data, &self.options, responses, true); + } + + fn actions(&self) -> ActionList { + use BrushToolFsmState::*; + + match self.fsm_state { + Ready => actions!(BrushToolMessageDiscriminant; + DragStart, + DragStop, + Abort, + ), + Drawing => actions!(BrushToolMessageDiscriminant; + DragStop, + PointerMove, + Abort, + ), + } + } +} + +impl ToolTransition for BrushTool { + fn event_to_message_map(&self) -> EventToMessageMap { + EventToMessageMap { + document_dirty: None, + tool_abort: Some(BrushToolMessage::Abort.into()), + selection_changed: None, + } + } +} + +#[derive(Clone, Debug, Default)] +struct BrushToolData { + points: Vec, + diameter: f64, + hardness: f64, + flow: f64, + path: Option>, +} + +impl Fsm for BrushToolFsmState { + type ToolData = BrushToolData; + type ToolOptions = BrushOptions; + + fn transition( + self, + event: ToolMessage, + tool_data: &mut Self::ToolData, + ToolActionHandlerData { + document, global_tool_data, input, .. + }: &mut ToolActionHandlerData, + tool_options: &Self::ToolOptions, + responses: &mut VecDeque, + ) -> Self { + use BrushToolFsmState::*; + use BrushToolMessage::*; + + let transform = document.document_legacy.root.transform; + + if let ToolMessage::Brush(event) = event { + match (self, event) { + (Ready, DragStart) => { + responses.push_back(DocumentMessage::StartTransaction.into()); + responses.push_back(DocumentMessage::DeselectAllLayers.into()); + tool_data.path = Some(document.get_path_for_new_layer()); + + let pos = transform.inverse().transform_point2(input.mouse.position); + + tool_data.points.push(pos); + + tool_data.diameter = tool_options.diameter; + tool_data.hardness = tool_options.hardness; + tool_data.flow = tool_options.flow; + + add_polyline(tool_data, global_tool_data, responses); + + Drawing + } + (Drawing, PointerMove) => { + let pos = transform.inverse().transform_point2(input.mouse.position); + + if tool_data.points.last() != Some(&pos) { + tool_data.points.push(pos); + } + + add_polyline(tool_data, global_tool_data, responses); + + Drawing + } + (Drawing, DragStop) | (Drawing, Abort) => { + if !tool_data.points.is_empty() { + responses.push_back(remove_preview(tool_data)); + add_brush_render(tool_data, global_tool_data, responses); + responses.push_back(DocumentMessage::CommitTransaction.into()); + } else { + responses.push_back(DocumentMessage::AbortTransaction.into()); + } + + tool_data.path = None; + tool_data.points.clear(); + + Ready + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + BrushToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polyline")])]), + BrushToolFsmState::Drawing => HintData(vec![]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into()); + } +} + +fn remove_preview(data: &BrushToolData) -> Message { + Operation::DeleteLayer { path: data.path.clone().unwrap() }.into() +} + +fn add_polyline(data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque) { + let layer_path = data.path.clone().unwrap(); + let subpath = bezier_rs::Subpath::from_anchors(data.points.iter().copied(), false); + graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses); + + responses.add(GraphOperationMessage::StrokeSet { + layer: layer_path, + stroke: Stroke::new(tool_data.primary_color.apply_opacity(data.flow as f32 / 100.), data.diameter), + }); +} + +fn add_brush_render(data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque) { + let layer_path = data.path.clone().unwrap(); + let brush_node = DocumentNode { + name: "Brush".to_string(), + inputs: vec![ + NodeInput::ShortCircut(concrete!(())), + NodeInput::value(TaggedValue::VecDVec2(data.points.clone()), false), + // Diameter + NodeInput::value(TaggedValue::F64(data.diameter), false), + // Hardness + NodeInput::value(TaggedValue::F64(data.hardness), false), + // Flow + NodeInput::value(TaggedValue::F64(data.flow), false), + // Color + NodeInput::value(TaggedValue::Color(tool_data.primary_color), false), + ], + implementation: DocumentNodeImplementation::Unresolved("graphene_std::brush::BrushNode".into()), + metadata: graph_craft::document::DocumentNodeMetadata { position: (8, 4).into() }, + }; + let mut network = NodeNetwork::value_network(brush_node); + network.push_output_node(); + graph_modification_utils::new_custom_layer(network, layer_path.clone(), responses); +} diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index 0810be27..d050942d 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -226,10 +226,4 @@ fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData, responses layer: layer_path.clone(), stroke: Stroke::new(tool_data.primary_color, data.weight), }); - responses.add(GraphOperationMessage::TransformSet { - layer: layer_path, - transform: DAffine2::from_translation(position), - transform_in: TransformIn::Local, - skip_rerender: false, - }); } diff --git a/editor/src/messages/tool/tool_messages/mod.rs b/editor/src/messages/tool/tool_messages/mod.rs index 6e7e4d19..3bbcc8a6 100644 --- a/editor/src/messages/tool/tool_messages/mod.rs +++ b/editor/src/messages/tool/tool_messages/mod.rs @@ -1,4 +1,5 @@ pub mod artboard_tool; +pub mod brush_tool; pub mod ellipse_tool; pub mod eyedropper_tool; pub mod fill_tool; diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 2806357b..321dcfed 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -270,10 +270,4 @@ fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, s layer: layer_path.clone(), stroke: Stroke::new(global_tool_data.primary_color, tool_data.weight), }); - responses.add(GraphOperationMessage::TransformSet { - layer: layer_path, - transform: glam::DAffine2::from_translation(position), - transform_in: TransformIn::Local, - skip_rerender: false, - }) } diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 4cec8886..97614cc7 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -410,12 +410,7 @@ fn list_tools_in_groups() -> Vec> { // Raster tool group ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), - ToolAvailability::ComingSoon(ToolEntry { - tool_type: ToolType::Brush, - icon_name: "RasterBrushTool".into(), - tooltip: "Coming Soon: Brush Tool (B)".into(), - tooltip_shortcut: None, - }), + ToolAvailability::Available(Box::::default()), ToolAvailability::ComingSoon(ToolEntry { tool_type: ToolType::Heal, icon_name: "RasterHealTool".into(), @@ -472,7 +467,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType { ToolMessage::Text(_) => ToolType::Text, // Raster tool group - // ToolMessage::Brush(_) => ToolType::Brush, + ToolMessage::Brush(_) => ToolType::Brush, // ToolMessage::Heal(_) => ToolType::Heal, // ToolMessage::Clone(_) => ToolType::Clone, // ToolMessage::Patch(_) => ToolType::Patch, @@ -509,7 +504,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis ToolType::Text => ToolMessageDiscriminant::ActivateToolText, // Raster tool group - // ToolType::Brush => ToolMessageDiscriminant::ActivateToolBrush, + ToolType::Brush => ToolMessageDiscriminant::ActivateToolBrush, // ToolType::Heal => ToolMessageDiscriminant::ActivateToolHeal, // ToolType::Clone => ToolMessageDiscriminant::ActivateToolClone, // ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch, diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 2ad574e9..55bdf8b9 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -9,6 +9,7 @@ use document_legacy::{LayerId, Operation}; use dyn_any::DynAny; use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork, NodeOutput}; use graph_craft::executor::Compiler; +use graph_craft::{concrete, Type, TypeDescriptor}; use graphene_core::raster::{Image, ImageFrame}; use graphene_core::vector::VectorData; use interpreted_executor::executor::DynamicExecutor; @@ -42,7 +43,11 @@ impl NodeGraphExecutor { use dyn_any::IntoDynAny; use graph_craft::executor::Executor; - self.executor.execute(image_frame.into_dyn()).map_err(|e| e.to_string()) + match self.executor.input_type() { + Some(t) if t == concrete!(ImageFrame) => self.executor.execute(image_frame.into_dyn()).map_err(|e| e.to_string()), + Some(t) if t == concrete!(()) => self.executor.execute(().into_dyn()).map_err(|e| e.to_string()), + _ => Err("Invalid input type".to_string()), + } } /// Computes an input for a node in the graph diff --git a/libraries/dyn-any/src/lib.rs b/libraries/dyn-any/src/lib.rs index bb32d07e..94662ccd 100644 --- a/libraries/dyn-any/src/lib.rs +++ b/libraries/dyn-any/src/lib.rs @@ -159,6 +159,27 @@ impl StaticType for *mut [T] { impl<'a, T: StaticTypeSized> StaticType for &'a [T] { type Static = &'static [::Static]; } +macro_rules! impl_slice { + ($($id:ident),*) => { + $( + impl<'a, T: StaticTypeSized> StaticType for $id<'a, T> { + type Static = $id<'static, ::Static>; + } + )* + }; +} + +mod slice { + use super::*; + use core::slice::*; + impl_slice!(Iter, IterMut, Chunks, ChunksMut, RChunks, RChunksMut, Windows); +} + +#[cfg(feature = "alloc")] +impl<'a, T: StaticTypeSized> StaticType for Box + 'a + Send + Sync> { + type Static = Box + Send + Sync>; +} + impl<'a> StaticType for &'a str { type Static = &'static str; } diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index 3efb245d..2cfaaeb2 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -12,7 +12,7 @@ license = "MIT OR Apache-2.0" std = ["dyn-any", "dyn-any/std", "alloc", "glam/std", "specta"] default = ["async", "serde", "kurbo", "log", "std"] log = ["dep:log"] -serde = ["dep:serde", "glam/serde"] +serde = ["dep:serde", "glam/serde", "bezier-rs/serde"] gpu = ["spirv-std", "bytemuck", "glam/bytemuck", "dyn-any", "glam/libm"] async = ["async-trait", "alloc"] nightly = [] @@ -20,13 +20,18 @@ alloc = ["dyn-any", "bezier-rs", "once_cell"] type_id_logging = [] [dependencies] -dyn-any = {path = "../../libraries/dyn-any", features = ["derive", "glam"], optional = true, default-features = false } +dyn-any = { path = "../../libraries/dyn-any", features = [ + "derive", + "glam", +], optional = true, default-features = false } -spirv-std = { version = "0.5", features = ["glam"] , optional = true} -bytemuck = {version = "1.8", features = ["derive"], optional = true} -async-trait = {version = "0.1", optional = true} -serde = {version = "1.0", features = ["derive"], optional = true, default-features = false } -log = {version = "0.4", optional = true} +spirv-std = { version = "0.5", features = ["glam"], optional = true } +bytemuck = { version = "1.8", features = ["derive"], optional = true } +async-trait = { version = "0.1", optional = true } +serde = { version = "1.0", features = [ + "derive", +], optional = true, default-features = false } +log = { version = "0.4", optional = true } bezier-rs = { path = "../../libraries/bezier-rs", optional = true } kurbo = { git = "https://github.com/linebender/kurbo.git", features = [ @@ -34,8 +39,10 @@ kurbo = { git = "https://github.com/linebender/kurbo.git", features = [ ], optional = true } rand_chacha = "0.3.1" spin = "0.9.2" -glam = { version = "^0.22", default-features = false, features = ["scalar-math"]} -node-macro = {path = "../node-macro"} +glam = { version = "^0.22", default-features = false, features = [ + "scalar-math", +] } +node-macro = { path = "../node-macro" } specta.workspace = true specta.optional = true once_cell = { version = "1.17.0", default-features = false, optional = true } diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index d0e8a9bd..b3cdba05 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -60,7 +60,7 @@ where core::any::type_name::() } #[cfg(feature = "alloc")] - fn to_node_io(&self, parameters: Vec<(Type, Type)>) -> NodeIOTypes { + fn to_node_io(&self, parameters: Vec) -> NodeIOTypes { NodeIOTypes { input: concrete!(::Static), output: concrete!(::Static), diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 13a27612..663856e3 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -23,7 +23,7 @@ pub struct AddParameterNode { } #[node_macro::node_fn(AddParameterNode)] -fn flat_map(first: U, second: T) -> >::Output +fn add_parameter(first: U, second: T) -> >::Output where U: Add, { diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index 7500fa4c..9664dc8d 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -239,12 +239,11 @@ fn brighten_color_node(color: Color, brightness: f32) -> Color { } #[derive(Debug)] -pub struct ForEachNode { +pub struct ForEachNode { map_node: MapNode, - _iter: PhantomData, } -#[node_macro::node_fn(ForEachNode<_Iter>)] +#[node_macro::node_fn(ForEachNode)] fn map_node<_Iter: Iterator, MapNode>(input: _Iter, map_node: &'any_input MapNode) -> () where MapNode: for<'any_input> Node<'any_input, _Iter::Item, Output = ()> + 'input, @@ -359,6 +358,15 @@ mod image { data: Vec::new(), } } + + pub fn new(width: u32, height: u32, color: Color) -> Self { + Self { + width, + height, + data: vec![color; (width * height) as usize], + } + } + pub fn as_slice(&self) -> ImageSlice { ImageSlice { width: self.width, @@ -366,6 +374,15 @@ mod image { data: self.data.as_slice(), } } + + pub fn get_mut(&mut self, x: u32, y: u32) -> Option<&mut Color> { + self.data.get_mut((y * self.width + x) as usize) + } + + pub fn get(&self, x: u32, y: u32) -> Option<&Color> { + self.data.get((y * self.width + x) as usize) + } + /// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array) pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self { let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8(v[0], v[1], v[2], v[3])).collect(); @@ -451,6 +468,12 @@ mod image { } } + impl AsRef for ImageFrame { + fn as_ref(&self) -> &ImageFrame { + self + } + } + impl Hash for ImageFrame { fn hash(&self, state: &mut H) { self.image.hash(state); diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 2ee7dbec..41d89ac0 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -300,7 +300,7 @@ pub struct InvertRGBNode; #[node_macro::node_fn(InvertRGBNode)] fn invert_image(color: Color) -> Color { - color.map_rgb(|c| 1. - c) + color.map_rgb(|c| color.a() - c) } #[derive(Debug, Clone, Copy)] @@ -339,43 +339,55 @@ pub struct BlendNode { opacity: Opacity, } +impl StaticType for BlendNode { + type Static = BlendNode; +} + #[node_macro::node_fn(BlendNode)] fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Color { - let (source_color, backdrop) = input; - let actual_opacity = 1. - (opacity / 100.) as f32; - return match blend_mode { - BlendMode::Normal => backdrop.blend_rgb(source_color, Color::blend_normal), - BlendMode::Multiply => backdrop.blend_rgb(source_color, Color::blend_multiply), - BlendMode::Darken => backdrop.blend_rgb(source_color, Color::blend_darken), - BlendMode::ColorBurn => backdrop.blend_rgb(source_color, Color::blend_color_burn), - BlendMode::LinearBurn => backdrop.blend_rgb(source_color, Color::blend_linear_burn), - BlendMode::DarkerColor => backdrop.blend_darker_color(source_color), + let opacity = opacity / 100.; - BlendMode::Screen => backdrop.blend_rgb(source_color, Color::blend_screen), - BlendMode::Lighten => backdrop.blend_rgb(source_color, Color::blend_lighten), - BlendMode::ColorDodge => backdrop.blend_rgb(source_color, Color::blend_color_dodge), - BlendMode::LinearDodge => backdrop.blend_rgb(source_color, Color::blend_linear_dodge), - BlendMode::LighterColor => backdrop.blend_lighter_color(source_color), + let (foreground, background) = input; + let foreground = foreground.to_linear_srgb(); + let background = background.to_linear_srgb(); - BlendMode::Overlay => source_color.blend_rgb(backdrop, Color::blend_hardlight), - BlendMode::SoftLight => backdrop.blend_rgb(source_color, Color::blend_softlight), - BlendMode::HardLight => backdrop.blend_rgb(source_color, Color::blend_hardlight), - BlendMode::VividLight => backdrop.blend_rgb(source_color, Color::blend_vivid_light), - BlendMode::LinearLight => backdrop.blend_rgb(source_color, Color::blend_linear_light), - BlendMode::PinLight => backdrop.blend_rgb(source_color, Color::blend_pin_light), - BlendMode::HardMix => backdrop.blend_rgb(source_color, Color::blend_hard_mix), + let target_color = match blend_mode { + BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal), + BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply), + BlendMode::Darken => background.blend_rgb(foreground, Color::blend_darken), + BlendMode::ColorBurn => background.blend_rgb(foreground, Color::blend_color_burn), + BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn), + BlendMode::DarkerColor => background.blend_darker_color(foreground), - BlendMode::Difference => backdrop.blend_rgb(source_color, Color::blend_exclusion), - BlendMode::Exclusion => backdrop.blend_rgb(source_color, Color::blend_exclusion), - BlendMode::Subtract => backdrop.blend_rgb(source_color, Color::blend_subtract), - BlendMode::Divide => backdrop.blend_rgb(source_color, Color::blend_divide), + BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen), + BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten), + BlendMode::ColorDodge => background.blend_rgb(foreground, Color::blend_color_dodge), + BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge), + BlendMode::LighterColor => background.blend_lighter_color(foreground), - BlendMode::Hue => backdrop.blend_hue(source_color), - BlendMode::Saturation => backdrop.blend_saturation(source_color), - BlendMode::Color => backdrop.blend_color(source_color), - BlendMode::Luminosity => backdrop.blend_luminosity(source_color), - } - .lerp(backdrop, actual_opacity); + BlendMode::Overlay => foreground.blend_rgb(background, Color::blend_hardlight), + BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight), + BlendMode::HardLight => background.blend_rgb(foreground, Color::blend_hardlight), + BlendMode::VividLight => background.blend_rgb(foreground, Color::blend_vivid_light), + BlendMode::LinearLight => background.blend_rgb(foreground, Color::blend_linear_light), + BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light), + BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix), + + BlendMode::Difference => background.blend_rgb(foreground, Color::blend_exclusion), + BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion), + BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract), + BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide), + + BlendMode::Hue => background.blend_hue(foreground), + BlendMode::Saturation => background.blend_saturation(foreground), + BlendMode::Color => background.blend_color(foreground), + BlendMode::Luminosity => background.blend_luminosity(foreground), + }; + + let multiplied_target_color = target_color.to_associated_alpha(opacity as f32); + let blended = background.alpha_blend(multiplied_target_color); + + blended.to_gamma_srgb() } #[derive(Debug, Clone, Copy)] diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index e54c196b..8fb33ce7 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -83,6 +83,11 @@ impl Color { Color { red, green, blue, alpha } } + /// Return an opaque `Color` from given `f32` RGB channels. + pub fn from_unassociated_alpha(red: f32, green: f32, blue: f32, alpha: f32) -> Color { + Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha) + } + /// Return an opaque SDR `Color` given RGB channels from `0` to `255`. /// /// # Examples @@ -602,14 +607,49 @@ impl Color { pub fn map_rgb f32>(&self, f: F) -> Self { Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a()) } - pub fn blend_rgb f32>(&self, other: Color, f: F) -> Self { - Color { - red: f(self.red, other.red).clamp(0., 1.), - green: f(self.green, other.green).clamp(0., 1.), - blue: f(self.blue, other.blue).clamp(0., 1.), + + pub fn apply_opacity(&self, opacity: f32) -> Self { + Self::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), self.a() * opacity) + } + + pub fn to_associated_alpha(&self, alpha: f32) -> Self { + Self { + red: self.red * alpha, + green: self.green * alpha, + blue: self.blue * alpha, + alpha: self.alpha * alpha, + } + } + + pub fn to_unassociated_alpha(&self) -> Self { + let unmultiply = 1. / self.alpha; + Self { + red: self.red * unmultiply, + green: self.green * unmultiply, + blue: self.blue * unmultiply, alpha: self.alpha, } } + + pub fn blend_rgb f32>(&self, other: Color, f: F) -> Self { + let background = self.to_unassociated_alpha(); + Color { + red: f(background.red, other.red).clamp(0., 1.), + green: f(background.green, other.green).clamp(0., 1.), + blue: f(background.blue, other.blue).clamp(0., 1.), + alpha: other.alpha, + } + } + + pub fn alpha_blend(&self, other: Color) -> Self { + let inv_alpha = 1. - other.alpha; + Self { + red: self.red * inv_alpha + other.red, + green: self.green * inv_alpha + other.green, + blue: self.blue * inv_alpha + other.blue, + alpha: self.alpha * inv_alpha + other.alpha, + } + } } #[test] diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs index 0c148bb5..3964bb67 100644 --- a/node-graph/gcore/src/transform.rs +++ b/node-graph/gcore/src/transform.rs @@ -6,6 +6,61 @@ use crate::raster::ImageFrame; use crate::vector::VectorData; use crate::Node; +pub trait Transform { + fn transform(&self) -> DAffine2; + fn local_pivot(&self, pivot: DVec2) -> DVec2 { + pivot + } +} + +pub trait TransformMut: Transform { + fn transform_mut(&mut self) -> &mut DAffine2; + fn translate(&mut self, offset: DVec2) { + *self.transform_mut() = DAffine2::from_translation(offset) * self.transform(); + } +} + +impl Transform for ImageFrame { + fn transform(&self) -> DAffine2 { + self.transform + } +} +impl Transform for &ImageFrame { + fn transform(&self) -> DAffine2 { + self.transform + } +} +impl TransformMut for ImageFrame { + fn transform_mut(&mut self) -> &mut DAffine2 { + &mut self.transform + } +} + +impl Transform for VectorData { + fn transform(&self) -> DAffine2 { + self.transform + } + fn local_pivot(&self, pivot: DVec2) -> DVec2 { + self.local_pivot(pivot) + } +} +impl TransformMut for VectorData { + fn transform_mut(&mut self) -> &mut DAffine2 { + &mut self.transform + } +} + +impl Transform for DAffine2 { + fn transform(&self) -> DAffine2 { + *self + } +} +impl TransformMut for DAffine2 { + fn transform_mut(&mut self) -> &mut DAffine2 { + self + } +} + #[derive(Debug, Clone, Copy)] pub struct TransformNode { pub(crate) translate: Translation, @@ -16,45 +71,12 @@ pub struct TransformNode { } #[node_macro::node_fn(TransformNode)] -pub(crate) fn transform_vector_data(mut vector_data: VectorData, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2, pivot: DVec2) -> VectorData { - let pivot = DAffine2::from_translation(vector_data.local_pivot(pivot)); +pub(crate) fn transform_vector_data(mut data: Data, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2, pivot: DVec2) -> Data { + let pivot = DAffine2::from_translation(data.local_pivot(pivot)); - let modification = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse(); - vector_data.transform = modification * vector_data.transform; + let modification = pivot * DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse(); + let data_transform = data.transform_mut(); + *data_transform = modification * (*data_transform); - vector_data -} - -impl<'input, Translation: 'input, Rotation: 'input, Scale: 'input, Shear: 'input, Pivot: 'input> Node<'input, ImageFrame> for TransformNode -where - Translation: for<'any_input> Node<'any_input, (), Output = DVec2>, - Rotation: for<'any_input> Node<'any_input, (), Output = f64>, - Scale: for<'any_input> Node<'any_input, (), Output = DVec2>, - Shear: for<'any_input> Node<'any_input, (), Output = DVec2>, - Pivot: for<'any_input> Node<'any_input, (), Output = DVec2>, -{ - type Output = ImageFrame; - #[inline] - fn eval<'node: 'input>(&'node self, mut image_frame: ImageFrame) -> Self::Output { - let translate = self.translate.eval(()); - let rotate = self.rotate.eval(()); - let scale = self.scale.eval(()); - let shear = self.shear.eval(()); - let pivot = self.pivot.eval(()); - - let pivot = DAffine2::from_translation(pivot); - let modification = pivot * DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse(); - image_frame.transform = modification * image_frame.transform; - - image_frame - } -} - -// Generates a transform matrix that rotates around the center of the image -fn generate_transform(shear: DVec2, transform: &DAffine2, scale: DVec2, rotate: f64, translate: DVec2) -> DAffine2 { - let shear_matrix = DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]); - let pivot = transform.transform_point2(DVec2::splat(0.5)); - let translate_to_center = DAffine2::from_translation(-pivot); - - translate_to_center.inverse() * DAffine2::from_scale_angle_translation(scale, rotate, translate) * shear_matrix * translate_to_center + data } diff --git a/node-graph/gcore/src/types.rs b/node-graph/gcore/src/types.rs index a3c181f4..b5878222 100644 --- a/node-graph/gcore/src/types.rs +++ b/node-graph/gcore/src/types.rs @@ -9,13 +9,17 @@ pub use std::borrow::Cow; pub struct NodeIOTypes { pub input: Type, pub output: Type, - pub parameters: Vec<(Type, Type)>, + pub parameters: Vec, } impl NodeIOTypes { - pub fn new(input: Type, output: Type, parameters: Vec<(Type, Type)>) -> Self { + pub fn new(input: Type, output: Type, parameters: Vec) -> Self { Self { input, output, parameters } } + + pub fn ty(&self) -> Type { + Type::Fn(Box::new(self.input.clone()), Box::new(self.output.clone())) + } } #[macro_export] @@ -34,6 +38,20 @@ macro_rules! generic { }}; } +#[macro_export] +macro_rules! fn_type { + ($input:ty, $output:ty) => { + Type::Fn(Box::new(concrete!($input)), Box::new(concrete!($output))) + }; +} + +#[macro_export] +macro_rules! value_fn { + ($output:ty) => { + Type::Fn(Box::new(concrete!(())), Box::new(concrete!($output))) + }; +} + #[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeIdentifier { @@ -72,16 +90,62 @@ impl PartialEq for TypeDescriptor { pub enum Type { Generic(Cow<'static, str>), Concrete(TypeDescriptor), + Fn(Box, Box), +} + +impl Type { + pub fn is_generic(&self) -> bool { + matches!(self, Type::Generic(_)) + } + + pub fn is_concrete(&self) -> bool { + matches!(self, Type::Concrete(_)) + } + + pub fn is_fn(&self) -> bool { + matches!(self, Type::Fn(_, _)) + } + + pub fn is_value(&self) -> bool { + matches!(self, Type::Fn(_, _) | Type::Concrete(_)) + } + + pub fn is_unit(&self) -> bool { + matches!(self, Type::Fn(_, _) | Type::Concrete(_)) + } + + pub fn is_generic_or_fn(&self) -> bool { + matches!(self, Type::Fn(_, _) | Type::Generic(_)) + } + + pub fn fn_input(&self) -> Option<&Type> { + match self { + Type::Fn(first, _) => Some(first), + _ => None, + } + } + + pub fn fn_output(&self) -> Option<&Type> { + match self { + Type::Fn(_, second) => Some(second), + _ => None, + } + } + + pub fn function(input: &Type, output: &Type) -> Type { + Type::Fn(Box::new(input.clone()), Box::new(output.clone())) + } } impl core::fmt::Debug for Type { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Generic(arg0) => f.write_fmt(format_args!("Generic({})", arg0)), + Self::Generic(arg0) => write!(f, "Generic({})", arg0), #[cfg(feature = "type_id_logging")] - Self::Concrete(arg0) => f.write_fmt(format_args!("Concrete({}, {:?}))", arg0.name, arg0.id)), + Self::Concrete(arg0) => write!(f, "Concrete({}, {:?})", arg0.name, arg0.id), #[cfg(not(feature = "type_id_logging"))] - Self::Concrete(arg0) => f.write_fmt(format_args!("Concrete({})", arg0.name)), + Self::Concrete(arg0) => write!(f, "Concrete({})", arg0.name), + Self::Fn(arg0, arg1) => write!(f, "({:?} -> {:?})", arg0, arg1), } } } @@ -91,6 +155,7 @@ impl std::fmt::Display for Type { match self { Type::Generic(name) => write!(f, "{}", name), Type::Concrete(ty) => write!(f, "{}", ty.name), + Type::Fn(input, output) => write!(f, "({} -> {})", input, output), } } } diff --git a/node-graph/gcore/src/value.rs b/node-graph/gcore/src/value.rs index ff715b84..9890f0d2 100644 --- a/node-graph/gcore/src/value.rs +++ b/node-graph/gcore/src/value.rs @@ -1,4 +1,5 @@ use core::marker::PhantomData; +use dyn_any::{DynAny, StaticType, StaticTypeSized}; use crate::Node; @@ -15,6 +16,10 @@ impl<'i, const N: u32> Node<'i, ()> for IntNode { #[derive(Default, Debug)] pub struct ValueNode(pub T); +impl StaticType for ValueNode { + type Static = ValueNode; +} + impl<'i, T: 'i> Node<'i, ()> for ValueNode { type Output = &'i T; fn eval<'s: 'i>(&'s self, _input: ()) -> Self::Output { @@ -43,6 +48,13 @@ impl Copy for ValueNode {} #[derive(Clone)] pub struct ClonedNode(pub T); +impl StaticType for ClonedNode +where + T::Static: Clone, +{ + type Static = ClonedNode; +} + impl<'i, T: Clone + 'i> Node<'i, ()> for ClonedNode { type Output = T; fn eval<'s: 'i>(&'s self, _input: ()) -> Self::Output { diff --git a/node-graph/gpu-compiler/Cargo.lock b/node-graph/gpu-compiler/Cargo.lock index 225808c3..81f63fb7 100644 --- a/node-graph/gpu-compiler/Cargo.lock +++ b/node-graph/gpu-compiler/Cargo.lock @@ -80,9 +80,11 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bezier-rs" -version = "0.1.0" +version = "0.2.0" dependencies = [ + "dyn-any", "glam", + "serde", ] [[package]] @@ -421,7 +423,6 @@ dependencies = [ "log", "num-traits", "nvtx", - "rand_chacha", "serde", "serde_json", "spirv-builder", @@ -435,6 +436,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "bezier-rs", "bytemuck", "dyn-any", "dyn-clone", @@ -442,9 +444,9 @@ dependencies = [ "graphene-core", "log", "num-traits", - "rand_chacha", "serde", "specta", + "xxhash-rust", ] [[package]] @@ -459,8 +461,10 @@ dependencies = [ "log", "node-macro", "once_cell", + "rand_chacha", "serde", "specta", + "spin", ] [[package]] @@ -620,6 +624,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -963,6 +977,12 @@ dependencies = [ "regex", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "scratch" version = "1.0.3" @@ -1061,6 +1081,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "spin" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34" +dependencies = [ + "lock_api", +] + [[package]] name = "spirt" version = "0.1.0" @@ -1404,3 +1433,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xxhash-rust" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 8e5c8c6e..ee6d1662 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -47,7 +47,7 @@ impl DocumentNode { .inputs .iter() .enumerate() - .filter(|(_, input)| matches!(input, NodeInput::Network(_))) + .filter(|(_, input)| matches!(input, NodeInput::Network(_) | NodeInput::ShortCircut(_))) .nth(offset) .unwrap_or_else(|| panic!("no network input found for {self:#?} and offset: {offset}")); @@ -72,6 +72,7 @@ impl DocumentNode { NodeInput::ShortCircut(ty) => (ProtoNodeInput::ShortCircut(ty), ConstructionArgs::Nodes(vec![])), }; assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Network(_))), "recieved non resolved parameter"); + assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::ShortCircut(_))), "recieved non resolved parameter"); assert!( !self.inputs.iter().any(|input| matches!(input, NodeInput::Value { .. })), "recieved value as parameter. inupts: {:#?}, construction_args: {:#?}", @@ -218,6 +219,12 @@ pub enum DocumentNodeImplementation { Unresolved(NodeIdentifier), } +impl Default for DocumentNodeImplementation { + fn default() -> Self { + Self::Unresolved(NodeIdentifier::new("graphene_cored::ops::IdNode")) + } +} + impl DocumentNodeImplementation { pub fn get_network(&self) -> Option<&NodeNetwork> { if let DocumentNodeImplementation::Network(n) = self { @@ -260,6 +267,227 @@ pub struct NodeNetwork { pub previous_outputs: Option>, } +/// Graph modification functions +impl NodeNetwork { + /// Get the original output nodes of this network, ignoring any preview node + pub fn original_outputs(&self) -> &Vec { + self.previous_outputs.as_ref().unwrap_or(&self.outputs) + } + + pub fn input_types<'a>(&'a self) -> impl Iterator + 'a { + self.inputs.iter().map(move |id| self.nodes[id].inputs.get(0).map(|i| i.ty().clone()).unwrap_or(concrete!(()))) + } + + /// An empty graph + pub fn value_network(node: DocumentNode) -> Self { + Self { + inputs: vec![0], + outputs: vec![NodeOutput::new(0, 0)], + nodes: [(0, node)].into_iter().collect(), + disabled: vec![], + previous_outputs: None, + } + } + /// A graph with just an input node + pub fn new_network() -> Self { + Self { + inputs: vec![0], + outputs: vec![NodeOutput::new(0, 0)], + nodes: [( + 0, + DocumentNode { + name: "Input Frame".into(), + inputs: vec![NodeInput::ShortCircut(concrete!(u32))], + implementation: DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()), + metadata: DocumentNodeMetadata { position: (8, 4).into() }, + }, + )] + .into_iter() + .collect(), + ..Default::default() + } + } + + /// Appends a new node to the network after the output node and sets it as the new output + pub fn push_node(&mut self, mut node: DocumentNode) -> NodeId { + let id = self.nodes.len().try_into().expect("Too many nodes in network"); + // Set the correct position for the new node + if let Some(pos) = self.nodes.get(&self.original_outputs()[0].node_id).map(|n| n.metadata.position) { + node.metadata.position = pos + IVec2::new(8, 0); + } + if self.outputs.is_empty() { + self.outputs.push(NodeOutput::new(id, 0)); + } + let input = NodeInput::node(self.outputs[0].node_id, self.outputs[0].node_output_index); + if node.inputs.is_empty() { + node.inputs.push(input); + } else { + node.inputs[0] = input; + } + self.nodes.insert(id, node); + self.outputs = vec![NodeOutput::new(id, 0)]; + id + } + + /// Adds a output identity node to the network + pub fn push_output_node(&mut self) -> NodeId { + let node = DocumentNode { + name: "Output".into(), + inputs: vec![], + implementation: DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()), + metadata: DocumentNodeMetadata { position: (0, 0).into() }, + }; + self.push_node(node) + } + + /// Adds a Cache and a Clone node to the network + pub fn push_cache_node(&mut self, ty: Type) -> NodeId { + let node = DocumentNode { + name: "Cache".into(), + inputs: vec![], + implementation: DocumentNodeImplementation::Network(NodeNetwork { + inputs: vec![0], + outputs: vec![NodeOutput::new(1, 0)], + nodes: vec![ + ( + 0, + DocumentNode { + name: "CacheNode".to_string(), + inputs: vec![NodeInput::ShortCircut(concrete!(())), NodeInput::Network(ty)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_std::memo::CacheNode")), + metadata: Default::default(), + }, + ), + ( + 1, + DocumentNode { + name: "CloneNode".to_string(), + inputs: vec![NodeInput::node(0, 0)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::CloneNode<_>")), + metadata: Default::default(), + }, + ), + ] + .into_iter() + .collect(), + ..Default::default() + }), + metadata: DocumentNodeMetadata { position: (0, 0).into() }, + }; + self.push_node(node) + } + + /// Get the nested network given by the path of node ids + pub fn nested_network(&self, nested_path: &[NodeId]) -> Option<&Self> { + let mut network = Some(self); + + for segment in nested_path { + network = network.and_then(|network| network.nodes.get(segment)).and_then(|node| node.implementation.get_network()); + } + network + } + + /// Get the mutable nested network given by the path of node ids + pub fn nested_network_mut(&mut self, nested_path: &[NodeId]) -> Option<&mut Self> { + let mut network = Some(self); + + for segment in nested_path { + network = network.and_then(|network| network.nodes.get_mut(segment)).and_then(|node| node.implementation.get_network_mut()); + } + network + } + + /// Check if the specified node id is connected to the output + pub fn connected_to_output(&self, target_node_id: NodeId, ignore_imaginate: bool) -> bool { + // If the node is the output then return true + if self.outputs.iter().any(|&NodeOutput { node_id, .. }| node_id == target_node_id) { + return true; + } + // Get the outputs + let Some(mut stack) = self.outputs.iter().map(|&output| self.nodes.get(&output.node_id)).collect::>>() else { + return false; + }; + let mut already_visited = HashSet::new(); + already_visited.extend(self.outputs.iter().map(|output| output.node_id)); + + while let Some(node) = stack.pop() { + // Skip the imaginate node inputs + if ignore_imaginate && node.name == "Imaginate" { + continue; + } + + for input in &node.inputs { + if let &NodeInput::Node { node_id: ref_id, .. } = input { + // Skip if already viewed + if already_visited.contains(&ref_id) { + continue; + } + // If the target node is used as input then return true + if ref_id == target_node_id { + return true; + } + // Add the referenced node to the stack + let Some(ref_node) = self.nodes.get(&ref_id) else { + continue; + }; + already_visited.insert(ref_id); + stack.push(ref_node); + } + } + } + + false + } + + /// Is the node being used directly as an output? + pub fn outputs_contain(&self, node_id: NodeId) -> bool { + self.outputs.iter().any(|output| output.node_id == node_id) + } + + /// Is the node being used directly as an original output? + pub fn original_outputs_contain(&self, node_id: NodeId) -> bool { + self.original_outputs().iter().any(|output| output.node_id == node_id) + } + + /// Is the node being used directly as a previous output? + pub fn previous_outputs_contain(&self, node_id: NodeId) -> Option { + self.previous_outputs.as_ref().map(|outputs| outputs.iter().any(|output| output.node_id == node_id)) + } + + /// A iterator of all nodes connected by primary inputs. + /// + /// Used for the properties panel and tools. + pub fn primary_flow(&self) -> impl Iterator { + struct FlowIter<'a> { + stack: Vec, + network: &'a NodeNetwork, + } + impl<'a> Iterator for FlowIter<'a> { + type Item = (&'a DocumentNode, NodeId); + fn next(&mut self) -> Option { + loop { + let node_id = self.stack.pop()?; + if let Some(document_node) = self.network.nodes.get(&node_id) { + self.stack.extend( + document_node + .inputs + .iter() + .take(1) // Only show the primary input + .filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }), + ); + return Some((document_node, node_id)); + }; + } + } + } + FlowIter { + stack: self.outputs.iter().map(|output| output.node_id).collect(), + network: &self, + } + } +} + +/// Functions for compiling the network impl NodeNetwork { pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) { self.inputs.iter_mut().for_each(|id| *id = f(*id)); @@ -405,6 +633,31 @@ impl NodeNetwork { self.nodes.insert(id, node); return; } + // replace value inputs with value nodes + for input in &mut node.inputs { + let mut dummy_input = NodeInput::ShortCircut(concrete!(())); + std::mem::swap(&mut dummy_input, input); + if let NodeInput::Value { tagged_value, exposed } = dummy_input { + let value_node_id = gen_id(); + let value_node_id = map_ids(id, value_node_id); + self.nodes.insert( + value_node_id, + DocumentNode { + name: "Value".into(), + inputs: vec![NodeInput::Value { tagged_value, exposed }], + implementation: DocumentNodeImplementation::Unresolved("graphene_core::value::ValueNode".into()), + metadata: DocumentNodeMetadata::default(), + }, + ); + *input = NodeInput::Node { + node_id: value_node_id, + output_index: 0, + lambda: false, + }; + } else { + *input = dummy_input; + } + } match node.implementation { DocumentNodeImplementation::Network(mut inner_network) => { @@ -423,20 +676,6 @@ impl NodeNetwork { let network_input = self.nodes.get_mut(network_input).unwrap(); network_input.populate_first_network_input(node_id, output_index, *offset, lambda); } - NodeInput::Value { tagged_value, exposed } => { - let name = "Value".to_string(); - let new_id = map_ids(id, gen_id()); - let value_node = DocumentNode { - name, - inputs: vec![NodeInput::Value { tagged_value, exposed }], - implementation: DocumentNodeImplementation::Unresolved("graphene_core::value::ValueNode".into()), - metadata: DocumentNodeMetadata::default(), - }; - assert!(!self.nodes.contains_key(&new_id)); - self.nodes.insert(new_id, value_node); - let network_input = self.nodes.get_mut(network_input).unwrap(); - network_input.populate_first_network_input(new_id, 0, *offset, false); - } NodeInput::Network(_) => { *network_offsets.get_mut(network_input).unwrap() += 1; if let Some(index) = self.inputs.iter().position(|i| *i == id) { @@ -444,6 +683,7 @@ impl NodeNetwork { } } NodeInput::ShortCircut(_) => (), + NodeInput::Value { .. } => unreachable!("Value inputs should have been replaced with value nodes"), } } node.implementation = DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()); @@ -461,7 +701,7 @@ impl NodeNetwork { self.flatten_with_fns(node_id, map_ids, gen_id); } } - DocumentNodeImplementation::Unresolved(_) => (), + DocumentNodeImplementation::Unresolved(_) => {} } assert!(!self.nodes.contains_key(&id), "Trying to insert a node into the network caused an id conflict"); self.nodes.insert(id, node); @@ -478,151 +718,6 @@ impl NodeNetwork { nodes: nodes.clone(), }) } - - /// Get the original output nodes of this network, ignoring any preview node - pub fn original_outputs(&self) -> &Vec { - self.previous_outputs.as_ref().unwrap_or(&self.outputs) - } - - /// A graph with just an input and output node - pub fn new_network(output_offset: i32, output_node_id: NodeId) -> Self { - Self { - inputs: vec![0], - outputs: vec![NodeOutput::new(1, 0)], - nodes: [ - ( - 0, - DocumentNode { - name: "Input Frame".into(), - inputs: vec![NodeInput::Network(concrete!(u32))], - implementation: DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()), - metadata: DocumentNodeMetadata { position: (8, 4).into() }, - }, - ), - ( - 1, - DocumentNode { - name: "Output".into(), - inputs: vec![NodeInput::node(output_node_id, 0)], - implementation: DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()), - metadata: DocumentNodeMetadata { position: (output_offset, 4).into() }, - }, - ), - ] - .into_iter() - .collect(), - ..Default::default() - } - } - - /// Get the nested network given by the path of node ids - pub fn nested_network(&self, nested_path: &[NodeId]) -> Option<&Self> { - let mut network = Some(self); - - for segment in nested_path { - network = network.and_then(|network| network.nodes.get(segment)).and_then(|node| node.implementation.get_network()); - } - network - } - - /// Get the mutable nested network given by the path of node ids - pub fn nested_network_mut(&mut self, nested_path: &[NodeId]) -> Option<&mut Self> { - let mut network = Some(self); - - for segment in nested_path { - network = network.and_then(|network| network.nodes.get_mut(segment)).and_then(|node| node.implementation.get_network_mut()); - } - network - } - - /// Check if the specified node id is connected to the output - pub fn connected_to_output(&self, target_node_id: NodeId, ignore_imaginate: bool) -> bool { - // If the node is the output then return true - if self.outputs.iter().any(|&NodeOutput { node_id, .. }| node_id == target_node_id) { - return true; - } - // Get the outputs - let Some(mut stack) = self.outputs.iter().map(|&output| self.nodes.get(&output.node_id)).collect::>>() else { - return false; - }; - let mut already_visited = HashSet::new(); - already_visited.extend(self.outputs.iter().map(|output| output.node_id)); - - while let Some(node) = stack.pop() { - // Skip the imaginate node inputs - if ignore_imaginate && node.name == "Imaginate" { - continue; - } - - for input in &node.inputs { - if let &NodeInput::Node { node_id: ref_id, .. } = input { - // Skip if already viewed - if already_visited.contains(&ref_id) { - continue; - } - // If the target node is used as input then return true - if ref_id == target_node_id { - return true; - } - // Add the referenced node to the stack - let Some(ref_node) = self.nodes.get(&ref_id) else { - continue; - }; - already_visited.insert(ref_id); - stack.push(ref_node); - } - } - } - - false - } - - /// Is the node being used directly as an output? - pub fn outputs_contain(&self, node_id: NodeId) -> bool { - self.outputs.iter().any(|output| output.node_id == node_id) - } - - /// Is the node being used directly as an original output? - pub fn original_outputs_contain(&self, node_id: NodeId) -> bool { - self.original_outputs().iter().any(|output| output.node_id == node_id) - } - - /// Is the node being used directly as a previous output? - pub fn previous_outputs_contain(&self, node_id: NodeId) -> Option { - self.previous_outputs.as_ref().map(|outputs| outputs.iter().any(|output| output.node_id == node_id)) - } - - /// A iterator of all nodes connected by primary inputs. - /// - /// Used for the properties panel and tools. - pub fn primary_flow(&self) -> impl Iterator { - struct FlowIter<'a> { - stack: Vec, - network: &'a NodeNetwork, - } - impl<'a> Iterator for FlowIter<'a> { - type Item = (&'a DocumentNode, NodeId); - fn next(&mut self) -> Option { - loop { - let node_id = self.stack.pop()?; - if let Some(document_node) = self.network.nodes.get(&node_id) { - self.stack.extend( - document_node - .inputs - .iter() - .take(1) // Only show the primary input - .filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }), - ); - return Some((document_node, node_id)); - }; - } - } - } - FlowIter { - stack: self.outputs.iter().map(|output| output.node_id).collect(), - network: &self, - } - } } #[cfg(test)] diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index d793a6df..62d6543b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -47,6 +47,7 @@ pub enum TaggedValue { Quantization(graphene_core::quantization::QuantizationChannels), OptionalColor(Option), ManipulatorGroupIds(Vec), + VecDVec2(Vec), } #[allow(clippy::derived_hash_with_manual_eq)] @@ -104,6 +105,12 @@ impl Hash for TaggedValue { Self::Quantization(quantized_image) => quantized_image.hash(state), Self::OptionalColor(color) => color.hash(state), Self::ManipulatorGroupIds(mirror) => mirror.hash(state), + Self::VecDVec2(vec_dvec2) => { + vec_dvec2.len().hash(state); + for dvec2 in vec_dvec2 { + dvec2.to_array().iter().for_each(|x| x.to_bits().hash(state)); + } + } } } } @@ -145,6 +152,7 @@ impl<'a> TaggedValue { TaggedValue::Quantization(x) => Box::new(x), TaggedValue::OptionalColor(x) => Box::new(x), TaggedValue::ManipulatorGroupIds(x) => Box::new(x), + TaggedValue::VecDVec2(x) => Box::new(x), } } @@ -185,6 +193,7 @@ impl<'a> TaggedValue { TaggedValue::Quantization(_) => concrete!(graphene_core::quantization::QuantizationChannels), TaggedValue::OptionalColor(_) => concrete!(Option), TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec), + TaggedValue::VecDVec2(_) => concrete!(Vec), } } } diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 948e97da..34e32259 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -403,6 +403,11 @@ impl TypingContext { self.constructor.get(&node_id).copied() } + /// Returns the type of a given node id if it exists + pub fn type_of(&self, node_id: NodeId) -> Option<&NodeIOTypes> { + self.inferred.get(&node_id) + } + /// Returns the inferred types for a given node id. pub fn infer(&mut self, node_id: NodeId, node: &ProtoNode) -> Result { let identifier = node.identifier.name.clone(); @@ -416,7 +421,8 @@ impl TypingContext { // If the node has a value parameter we can infer the return type from it ConstructionArgs::Value(ref v) => { assert!(matches!(node.input, ProtoNodeInput::None)); - let types = NodeIOTypes::new(concrete!(()), v.ty(), vec![]); + // TODO: This should return a reference to the value + let types = NodeIOTypes::new(concrete!(()), v.ty(), vec![v.ty()]); self.inferred.insert(node_id, types.clone()); return Ok(types); } @@ -427,9 +433,9 @@ impl TypingContext { self.inferred .get(id) .ok_or(format!("Inferring type of {node_id} depends on {id} which is not present in the typing context")) - .map(|node| (node.input.clone(), node.output.clone())) + .map(|node| node.ty()) }) - .collect::, String>>()?, + .collect::, String>>()?, }; // Get the node input type from the proto node declaration @@ -450,26 +456,27 @@ impl TypingContext { if matches!(input, Type::Generic(_)) { return Err(format!("Generic types are not supported as inputs yet {:?} occured in {:?}", &input, node.identifier)); } - if parameters.iter().any(|p| matches!(p.1, Type::Generic(_))) { + if parameters.iter().any(|p| match p { + Type::Fn(_, b) if matches!(b.as_ref(), Type::Generic(_)) => true, + _ => false, + }) { return Err(format!("Generic types are not supported in parameters: {:?} occured in {:?}", parameters, node.identifier)); } - let covariant = |output, input| match (&output, &input) { - (Type::Concrete(t1), Type::Concrete(t2)) => t1 == t2, - (Type::Concrete(_), Type::Generic(_)) => true, - // TODO: relax this requirement when allowing generic types as inputs - (Type::Generic(_), _) => false, - }; + fn covariant(from: &Type, to: &Type) -> bool { + match (from, to) { + (Type::Concrete(t1), Type::Concrete(t2)) => t1 == t2, + (Type::Fn(a1, b1), Type::Fn(a2, b2)) => covariant(a1, a2) && covariant(b1, b2), + // TODO: relax this requirement when allowing generic types as inputs + (Type::Generic(_), _) => false, + (_, Type::Generic(_)) => true, + _ => false, + } + } // List of all implementations that match the input and parameter types let valid_output_types = impls .keys() - .filter(|node_io| { - covariant(input.clone(), node_io.input.clone()) - && parameters - .iter() - .zip(node_io.parameters.iter()) - .all(|(p1, p2)| covariant(p1.0.clone(), p2.0.clone()) && covariant(p1.1.clone(), p2.1.clone())) - }) + .filter(|node_io| covariant(&input, &node_io.input) && parameters.iter().zip(node_io.parameters.iter()).all(|(p1, p2)| covariant(p1, p2) && covariant(p1, p2))) .collect::>(); // Attempt to substitute generic types with concrete types and save the list of results @@ -517,7 +524,7 @@ impl TypingContext { /// Returns a list of all generic types used in the node fn collect_generics(types: &NodeIOTypes) -> Vec> { - let inputs = [&types.input].into_iter().chain(types.parameters.iter().map(|(_, x)| x)); + let inputs = [&types.input].into_iter().chain(types.parameters.iter().flat_map(|x| x.fn_output())); let mut generics = inputs .filter_map(|t| match t { Type::Generic(out) => Some(out.clone()), @@ -532,15 +539,16 @@ fn collect_generics(types: &NodeIOTypes) -> Vec> { } /// Checks if a generic type can be substituted with a concrete type and returns the concrete type -fn check_generic(types: &NodeIOTypes, input: &Type, parameters: &[(Type, Type)], generic: &str) -> Result { - let inputs = [(&types.input, input)] +fn check_generic(types: &NodeIOTypes, input: &Type, parameters: &[Type], generic: &str) -> Result { + let inputs = [(Some(&types.input), Some(input))] .into_iter() - .chain(types.parameters.iter().map(|(_, x)| x).zip(parameters.iter().map(|(_, x)| x))); - let mut concrete_inputs = inputs.filter(|(ni, _)| matches!(ni, Type::Generic(input) if generic == input)); - let (_, out_ty) = concrete_inputs + .chain(types.parameters.iter().map(|x| x.fn_output()).zip(parameters.iter().map(|x| x.fn_output()))); + let concrete_inputs = inputs.filter(|(ni, _)| matches!(ni, Some(Type::Generic(input)) if generic == input)); + let mut outputs = concrete_inputs.flat_map(|(_, out)| out); + let out_ty = outputs .next() .ok_or_else(|| format!("Generic output type {generic} is not dependent on input {input:?} or parameters {parameters:?}",))?; - if concrete_inputs.any(|(_, ty)| ty != out_ty) { + if outputs.any(|ty| ty != out_ty) { return Err(format!("Generic output type {generic} is dependent on multiple inputs or parameters",)); } Ok(out_ty.clone()) diff --git a/node-graph/gstd/src/any.rs b/node-graph/gstd/src/any.rs index 2f0a549a..87fbbb35 100644 --- a/node-graph/gstd/src/any.rs +++ b/node-graph/gstd/src/any.rs @@ -93,7 +93,7 @@ impl Clone for DowncastNode { impl Copy for DowncastNode {} #[node_macro::node_fn(DowncastNode<_O>)] -fn downcast(input: Any<'input>, node: &'input N) -> _O +fn downcast(input: Any<'input>, node: &'input N) -> _O where N: for<'any_input> Node<'any_input, Any<'any_input>, Output = Any<'any_input>> + 'input, { diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs new file mode 100644 index 00000000..be6fb1a3 --- /dev/null +++ b/node-graph/gstd/src/brush.rs @@ -0,0 +1,215 @@ +use std::marker::PhantomData; + +use glam::{DAffine2, DVec2}; +use graphene_core::raster::{Color, Image, ImageFrame}; +use graphene_core::transform::TransformMut; +use graphene_core::vector::VectorData; +use graphene_core::Node; +use node_macro::node_fn; + +// Spacing is a consistent 0.2 apart, even when tiled across pixels (from 0.9 to the neighboring 0.1), to avoid bias +const MULTISAMPLE_GRID: [(f64, f64); 25] = [ + // Row 1 + (0.1, 0.1), + (0.1, 0.3), + (0.1, 0.5), + (0.1, 0.7), + (0.1, 0.9), + // Row 2 + (0.3, 0.1), + (0.3, 0.3), + (0.3, 0.5), + (0.3, 0.7), + (0.3, 0.9), + // Row 3 + (0.5, 0.1), + (0.5, 0.3), + (0.5, 0.5), + (0.5, 0.7), + (0.5, 0.9), + // Row 4 + (0.7, 0.1), + (0.7, 0.3), + (0.7, 0.5), + (0.7, 0.7), + (0.7, 0.9), + // Row 5 + (0.9, 0.1), + (0.9, 0.3), + (0.9, 0.5), + (0.9, 0.7), + (0.9, 0.9), +]; + +#[derive(Clone, Debug, PartialEq)] +pub struct ReduceNode { + pub initial: Initial, + pub lambda: Lambda, +} + +#[node_fn(ReduceNode)] +fn reduce(iter: I, initial: T, lambda: &'any_input Lambda) -> T +where + Lambda: for<'a> Node<'a, (T, I::Item), Output = T>, +{ + iter.fold(initial, |a, x| lambda.eval((a, x))) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct IntoIterNode { + _t: PhantomData, +} + +#[node_fn(IntoIterNode<_T>)] +fn into_iter<'i: 'input, _T: Send + Sync>(vec: &'i Vec<_T>) -> Box + Send + Sync + 'i> { + Box::new(vec.iter()) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct VectorPointsNode; + +#[node_fn(VectorPointsNode)] +fn vector_points(vector: VectorData) -> Vec { + vector.subpaths.iter().flat_map(|subpath| subpath.manipulator_groups().iter().map(|group| group.anchor)).collect() +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BrushTextureNode { + pub color: ColorNode, + pub hardness: Hardness, + pub flow: Flow, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct EraseNode { + flow: Flow, +} + +#[node_fn(EraseNode)] +fn erase(input: (Color, Color), flow: f64) -> Color { + let (input, brush) = input; + let alpha = input.a() * (1.0 - flow as f32 * brush.a()); + Color::from_unassociated_alpha(input.r(), input.g(), input.b(), alpha) +} + +#[node_fn(BrushTextureNode)] +fn brush_texture(diameter: f64, color: Color, hardness: f64, flow: f64) -> ImageFrame { + // Diameter + let radius = diameter / 2.; + // TODO: Remove the 4px padding after figuring out why the brush stamp gets randomly offset by 1px up/down/left/right when clicking with the Brush tool + let dimension = diameter.ceil() as u32 + 4; + let center = DVec2::splat(radius + (dimension as f64 - diameter) / 2.); + + // Hardness + let hardness = hardness / 100.; + let feather_exponent = 1. / (1. - hardness); + + // Flow + let flow = flow / 100.; + + // Color + let color = color.apply_opacity(flow as f32); + + // Initial transparent image + let mut image = Image::new(dimension, dimension, Color::TRANSPARENT); + + for y in 0..dimension { + for x in 0..dimension { + let summation = MULTISAMPLE_GRID.iter().fold(0., |acc, (offset_x, offset_y)| { + let position = DVec2::new(x as f64 + offset_x, y as f64 + offset_y); + let distance = (position - center).length(); + + if distance < radius { + acc + (1. - (distance / radius).powf(feather_exponent)).clamp(0., 1.) + } else { + acc + } + }); + + let pixel_fill = summation / MULTISAMPLE_GRID.len() as f64; + + let pixel = image.get_mut(x, y).unwrap(); + *pixel = color.apply_opacity(pixel_fill as f32); + } + } + + ImageFrame { + image, + transform: DAffine2::from_scale_angle_translation(DVec2::splat(dimension as f64), 0., -DVec2::splat(radius)), + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TranslateNode { + translatable: Translatable, +} + +#[node_fn(TranslateNode)] +fn translate_node(offset: DVec2, mut translatable: Data) -> Data { + translatable.translate(offset); + translatable +} + +#[cfg(test)] +mod test { + use super::*; + use crate::raster::*; + use glam::DAffine2; + use graphene_core::ops::{AddNode, CloneNode}; + use graphene_core::raster::*; + use graphene_core::structural::Then; + use graphene_core::transform::{Transform, TransformMut}; + use graphene_core::value::{ClonedNode, ValueNode}; + + #[test] + fn test_translate_node() { + let image = Image::new(10, 10, Color::TRANSPARENT); + let mut image = ImageFrame { image, transform: DAffine2::IDENTITY }; + image.translate(DVec2::new(1.0, 2.0)); + let translate_node = TranslateNode::new(ClonedNode::new(image)); + let image = translate_node.eval(DVec2::new(1.0, 2.0)); + assert_eq!(image.transform(), DAffine2::from_translation(DVec2::new(2.0, 4.0))); + } + + #[test] + fn test_reduce() { + let reduce_node = ReduceNode::new(ClonedNode::new(0u32), ValueNode::new(AddNode)); + let sum = reduce_node.eval(vec![1, 2, 3, 4, 5].into_iter()); + assert_eq!(sum, 15); + } + + #[test] + fn test_brush_texture() { + let brush_texture_node = BrushTextureNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(100.), ClonedNode::new(100.)); + let size = 20.; + let image = brush_texture_node.eval(size); + assert_eq!(image.image.width, size.ceil() as u32 + 4); + assert_eq!(image.image.height, size.ceil() as u32 + 4); + assert_eq!(image.transform, DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil() + 4.), 0., -DVec2::splat(size / 2.))); + // center pixel should be BLACK + assert_eq!(image.image.get(11, 11), Some(&Color::BLACK)); + } + + #[test] + fn test_brush() { + let brush_texture_node = BrushTextureNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(1.0), ClonedNode::new(1.0)); + let image = brush_texture_node.eval(20.); + let trace = vec![DVec2::new(0.0, 0.0), DVec2::new(10.0, 0.0)]; + let trace = ClonedNode::new(trace.into_iter()); + let translate_node = TranslateNode::new(ClonedNode::new(image)); + let frames = MapNode::new(ValueNode::new(translate_node)); + let frames = trace.then(frames).eval(()).collect::>(); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].image.width, 24); + let background_bounds = ReduceNode::new(ClonedNode::new(None), ValueNode::new(MergeBoundingBoxNode::new())); + let background_bounds = background_bounds.eval(frames.clone().into_iter()); + let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform()); + let background_image = background_bounds.then(EmptyImageNode::new(ClonedNode::new(Color::TRANSPARENT))); + let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(1.0)); + let final_image = ReduceNode::new(background_image, ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node)))); + let final_image = final_image.eval(frames.into_iter()); + assert_eq!(final_image.image.height, 24); + assert_eq!(final_image.image.width, 34); + drop(final_image); + } +} diff --git a/node-graph/gstd/src/lib.rs b/node-graph/gstd/src/lib.rs index 1f3121a4..681ed9fd 100644 --- a/node-graph/gstd/src/lib.rs +++ b/node-graph/gstd/src/lib.rs @@ -19,3 +19,5 @@ pub mod executor; pub mod quantization; pub use graphene_core::*; + +pub mod brush; diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index e7a8c5c1..85ac2431 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -1,9 +1,12 @@ -use dyn_any::{DynAny, StaticType}; +use dyn_any::{DynAny, StaticType, StaticTypeSized}; use glam::{BVec2, DAffine2, DVec2}; use graphene_core::raster::{Color, Image, ImageFrame}; +use graphene_core::transform::Transform; +use graphene_core::value::{ClonedNode, ValueNode}; use graphene_core::Node; +use std::marker::PhantomData; use std::path::Path; #[derive(Debug, DynAny)] @@ -139,6 +142,10 @@ pub struct MapImageFrameNode { map_fn: MapFn, } +impl StaticType for MapImageFrameNode { + type Static = MapImageFrameNode; +} + #[node_macro::node_fn(MapImageFrameNode)] fn map_image(mut image_frame: ImageFrame, map_fn: &'any_input MapFn) -> ImageFrame where @@ -151,12 +158,37 @@ where image_frame } -#[derive(Debug, Clone)] -struct AxisAlignedBbox { +#[derive(Debug, Clone, DynAny)] +pub struct AxisAlignedBbox { start: DVec2, end: DVec2, } +impl AxisAlignedBbox { + pub fn size(&self) -> DVec2 { + self.end - self.start + } + + pub fn to_transform(&self) -> DAffine2 { + DAffine2::from_translation(self.start) * DAffine2::from_scale(self.size()) + } + + pub fn contains(&self, point: DVec2) -> bool { + point.x >= self.start.x && point.x <= self.end.x && point.y >= self.start.y && point.y <= self.end.y + } + + pub fn intersects(&self, other: &AxisAlignedBbox) -> bool { + other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y + } + + pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox { + AxisAlignedBbox { + start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)), + end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)), + } + } +} + #[derive(Debug, Clone)] struct Bbox { top_left: DVec2, @@ -228,18 +260,42 @@ fn mask_image(mut image: ImageFrame, mask: ImageFrame) -> ImageFrame { image } +#[derive(Debug, Clone, Copy)] +pub struct BlendImageTupleNode { + map_fn: MapFn, +} + +impl StaticType for BlendImageTupleNode { + type Static = BlendImageTupleNode; +} + +#[node_macro::node_fn(BlendImageTupleNode)] +fn blend_image_tuple(images: (ImageFrame, ImageFrame), map_fn: &'any_input MapFn) -> ImageFrame +where + MapFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color> + 'input + Clone, +{ + let (mut background, foreground) = images; + let node = BlendImageNode::new(ClonedNode::new(background), ValueNode::new(map_fn.clone())); + node.eval(foreground) +} + #[derive(Debug, Clone, Copy)] pub struct BlendImageNode { background: Background, map_fn: MapFn, } +impl StaticType for BlendImageNode { + type Static = BlendImageNode; +} + // TODO: Implement proper blending #[node_macro::node_fn(BlendImageNode)] -fn blend_image(foreground: ImageFrame, mut background: ImageFrame, map_fn: &'any_input MapFn) -> ImageFrame +fn blend_image>(foreground: Frame, mut background: ImageFrame, map_fn: &'any_input MapFn) -> ImageFrame where MapFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color> + 'input, { + let foreground = foreground.as_ref(); let foreground_size = DVec2::new(foreground.image.width as f64, foreground.image.height as f64); let background_size = DVec2::new(background.image.width as f64, background.image.height as f64); @@ -271,6 +327,38 @@ where background } +#[derive(Clone, Debug, PartialEq)] +pub struct MergeBoundingBoxNode { + _data: PhantomData, +} + +#[node_macro::node_fn(MergeBoundingBoxNode<_Data>)] +fn merge_bounding_box_node<_Data: Transform>(input: (Option, _Data)) -> Option { + let (initial_aabb, data) = input; + + let snd_aabb = compute_transformed_bounding_box(data.transform()).axis_aligned_bbox(); + + if let Some(fst_aabb) = initial_aabb { + Some(fst_aabb.union(&snd_aabb)) + } else { + Some(snd_aabb) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct EmptyImageNode { + pub color: FillColor, +} + +#[node_macro::node_fn(EmptyImageNode)] +fn empty_image(transform: DAffine2, color: Color) -> ImageFrame { + let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32; + let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32; + + let image = Image::new(width, height, color); + ImageFrame { image, transform } +} + #[derive(Debug, Clone, Copy)] pub struct ImaginateNode { cached: E, diff --git a/node-graph/interpreted-executor/src/executor.rs b/node-graph/interpreted-executor/src/executor.rs index eb48745a..a499bada 100644 --- a/node-graph/interpreted-executor/src/executor.rs +++ b/node-graph/interpreted-executor/src/executor.rs @@ -7,6 +7,7 @@ use graph_craft::document::value::UpcastNode; use graph_craft::document::NodeId; use graph_craft::executor::Executor; use graph_craft::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, TypingContext}; +use graph_craft::{Type, TypeDescriptor}; use graphene_std::any::{Any, TypeErasedPinned, TypeErasedPinnedRef}; use crate::node_registry; @@ -48,6 +49,14 @@ impl DynamicExecutor { } Ok(()) } + + pub fn input_type(&self) -> Option { + self.typing_context.type_of(self.output).map(|node_io| node_io.input.clone()) + } + + pub fn output_type(&self) -> Option { + self.typing_context.type_of(self.output).map(|node_io| node_io.output.clone()) + } } impl Executor for DynamicExecutor { diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index cdb9a0e5..6ea2a302 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -10,6 +10,8 @@ use graphene_core::raster::*; use graphene_core::structural::Then; use graphene_core::value::{ClonedNode, ForgetNode, ValueNode}; use graphene_core::{Node, NodeIO, NodeIOTypes}; +use graphene_std::brush::*; +use graphene_std::raster::*; use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DowncastBothRefNode, DynAnyInRefNode, DynAnyNode, DynAnyRefNode, IntoTypeErasedNode, TypeErasedPinnedRef}; @@ -17,8 +19,9 @@ use graphene_core::{Cow, NodeIdentifier, Type, TypeDescriptor}; use graph_craft::proto::NodeConstructor; -use graphene_core::{concrete, generic}; +use graphene_core::{concrete, fn_type, generic, value_fn}; use graphene_std::memo::{CacheNode, LetNode}; +use graphene_std::raster::{BlendImageTupleNode, MapImageFrameNode}; use crate::executor::NodeContainer; @@ -55,7 +58,7 @@ macro_rules! register_node { let node = <$path>::new($( graphene_std::any::input_node::<$type>(_node) ),*); - let params = vec![$((concrete!(()), concrete!($type))),*]; + let params = vec![$(value_fn!($type)),*]; let mut node_io = <$path as NodeIO<'_, $input>>::to_node_io(&node, params); node_io.input = concrete!(<$input as StaticType>::Static); node_io @@ -75,7 +78,7 @@ macro_rules! raster_node { Box::pin(any) }, { - let params = vec![$((concrete!(()), concrete!($type))),*]; + let params = vec![$(value_fn!($type)),*]; NodeIOTypes::new(concrete!(Color), concrete!(Color), params) }, ), @@ -88,7 +91,7 @@ macro_rules! raster_node { Box::pin(any) }, { - let params = vec![$((concrete!(()), concrete!($type))),*]; + let params = vec![$(value_fn!($type)),*]; NodeIOTypes::new(concrete!(Image), concrete!(Image), params) }, ), @@ -101,7 +104,7 @@ macro_rules! raster_node { Box::pin(any) }, { - let params = vec![$((concrete!(()), concrete!($type))),*]; + let params = vec![$(value_fn!($type)),*]; NodeIOTypes::new(concrete!(ImageFrame), concrete!(ImageFrame), params) }, ) @@ -137,6 +140,7 @@ fn node_registry() -> HashMap, input: ImageFrame, params: [ImageFrame]), + register_node!(graphene_std::raster::EmptyImageNode<_>, input: DAffine2, params: [Color]), #[cfg(feature = "gpu")] register_node!(graphene_std::executor::MapGpuSingleImageNode<_>, input: Image, params: [String]), vec![( @@ -145,30 +149,100 @@ fn node_registry() -> HashMap, input: core::slice::Iter, params: [ImageFrame, &ValueNode, ClonedNode>>>>]), + //register_node!(graphene_std::brush::ReduceNode<_, _>, input: core::slice::Iter, params: [AxisAlignedBbox, &MergeBoundingBoxNode]), + register_node!(graphene_std::brush::IntoIterNode<_>, input: &Vec, params: []), + vec![( + NodeIdentifier::new("graphene_std::brush::BrushNode"), + |args| { + use graphene_std::brush::*; + + let trace: DowncastBothNode<(), Vec> = DowncastBothNode::new(args[0]); + let diameter: DowncastBothNode<(), f64> = DowncastBothNode::new(args[1]); + let hardness: DowncastBothNode<(), f64> = DowncastBothNode::new(args[2]); + let flow: DowncastBothNode<(), f64> = DowncastBothNode::new(args[3]); + let color: DowncastBothNode<(), Color> = DowncastBothNode::new(args[4]); + + let stamp = BrushTextureNode::new(color, ClonedNode::new(hardness.eval(())), ClonedNode::new(flow.eval(()))); + let stamp = stamp.eval(diameter.eval(())); + + let frames = TranslateNode::new(ClonedNode::new(stamp)); + let frames = MapNode::new(ValueNode::new(frames)); + let frames = frames.eval(trace.eval(()).into_iter()).collect::>(); + + let background_bounds = ReduceNode::new(ClonedNode::new(None), ValueNode::new(MergeBoundingBoxNode::new())); + let background_bounds = background_bounds.eval(frames.clone().into_iter()); + let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform()); + + let background_image = background_bounds.then(EmptyImageNode::new(ClonedNode::new(Color::TRANSPARENT))); + + let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(100.)); + + let final_image = ReduceNode::new(background_image, ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node)))); + let final_image = final_image.eval(frames.into_iter()); + let final_image = ClonedNode::new(final_image); + + let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(final_image)); + Box::pin(any) + }, + NodeIOTypes::new( + concrete!(()), + concrete!(ImageFrame), + vec![value_fn!(Vec), value_fn!(f64), value_fn!(f64), value_fn!(f64), value_fn!(Color)], + ), + )], + vec![( + NodeIdentifier::new("graphene_std::brush::ReduceNode<_, _>"), + |args| { + let acc: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[0]); + let image = acc.eval(()); + let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(1.0)); + let _ = &blend_node as &dyn for<'i> Node<'i, (Color, Color), Output = Color>; + let node = ReduceNode::new(ClonedNode::new(image), ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node)))); + //let _ = &node as &dyn for<'i> Node<'i, core::slice::Iter, Output = ImageFrame>; + let any: DynAnyNode + Sync + Send>, _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(node)); + Box::pin(any) + }, + NodeIOTypes::new(concrete!(Box + Sync + Send>), concrete!(ImageFrame), vec![value_fn!(ImageFrame)]), )], // Filters raster_node!(graphene_core::raster::LuminanceNode<_>, params: [LuminanceCalculation]), raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f64, f64, f64, f64, f64]), - vec![( - NodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"), - |args| { - use graphene_core::Node; - let image: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[0]); - let blend_mode: DowncastBothNode<(), BlendMode> = DowncastBothNode::new(args[1]); - let opacity: DowncastBothNode<(), f64> = DowncastBothNode::new(args[2]); - let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(blend_mode.eval(())), ClonedNode::new(opacity.eval(()))); - let node = graphene_std::raster::BlendImageNode::new(image, ValueNode::new(blend_node)); - let _ = &node as &dyn for<'i> Node<'i, ImageFrame, Output = ImageFrame>; - let any: DynAnyNode = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node)); - any.into_type_erased() - }, - NodeIOTypes::new( - concrete!(ImageFrame), - concrete!(ImageFrame), - vec![(concrete!(()), concrete!(ImageFrame)), (concrete!(()), concrete!(BlendMode)), (concrete!(()), concrete!(f64))], + vec![ + ( + NodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"), + |args| { + let image: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[0]); + let blend_mode: DowncastBothNode<(), BlendMode> = DowncastBothNode::new(args[1]); + let opacity: DowncastBothNode<(), f64> = DowncastBothNode::new(args[2]); + let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(blend_mode.eval(())), ClonedNode::new(opacity.eval(()))); + let node = graphene_std::raster::BlendImageNode::new(image, ValueNode::new(blend_node)); + let _ = &node as &dyn for<'i> Node<'i, ImageFrame, Output = ImageFrame>; + let any: DynAnyNode = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node)); + any.into_type_erased() + }, + NodeIOTypes::new(concrete!(ImageFrame), concrete!(ImageFrame), vec![value_fn!(ImageFrame), value_fn!(BlendMode), value_fn!(f64)]), ), - )], + ( + NodeIdentifier::new("graphene_core::raster::EraseNode<_, _>"), + |args| { + let image: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[0]); + let opacity: DowncastBothNode<(), f64> = DowncastBothNode::new(args[1]); + let blend_node = graphene_std::brush::EraseNode::new(ClonedNode::new(opacity.eval(()))); + let node = graphene_std::raster::BlendImageNode::new(image, ValueNode::new(blend_node)); + let _ = &node as &dyn for<'i> Node<'i, ImageFrame, Output = ImageFrame>; + let any: DynAnyNode = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node)); + any.into_type_erased() + }, + NodeIOTypes::new(concrete!(ImageFrame), concrete!(ImageFrame), vec![value_fn!(ImageFrame), value_fn!(f64)]), + ), + ], raster_node!(graphene_core::raster::GrayscaleNode<_, _, _, _, _, _, _>, params: [Color, f64, f64, f64, f64, f64, f64]), raster_node!(graphene_core::raster::HueSaturationNode<_, _, _>, params: [f64, f64, f64]), raster_node!(graphene_core::raster::InvertRGBNode, params: []), @@ -196,7 +270,7 @@ fn node_registry() -> HashMap = graphene_std::any::DynAnyInRefNode::new(node); any.into_type_erased() }, - NodeIOTypes::new(generic!(T), concrete!(ImageFrame), vec![(concrete!(()), concrete!(ImageFrame))]), + NodeIOTypes::new(generic!(T), concrete!(ImageFrame), vec![value_fn!(ImageFrame)]), ), ( NodeIdentifier::new("graphene_std::memo::EndLetNode<_>"), @@ -206,7 +280,7 @@ fn node_registry() -> HashMap = graphene_std::any::DynAnyInRefNode::new(node); any.into_type_erased() }, - NodeIOTypes::new(generic!(T), concrete!(ImageFrame), vec![(concrete!(()), concrete!(VectorData))]), + NodeIOTypes::new(generic!(T), concrete!(ImageFrame), vec![value_fn!(VectorData)]), ), ( NodeIdentifier::new("graphene_std::memo::RefNode<_, _>"), @@ -240,24 +314,24 @@ fn node_registry() -> HashMap)), - (concrete!(()), concrete!(f64)), - (concrete!(()), concrete!(ImaginateSamplingMethod)), - (concrete!(()), concrete!(f64)), - (concrete!(()), concrete!(String)), - (concrete!(()), concrete!(String)), - (concrete!(()), concrete!(bool)), - (concrete!(()), concrete!(f64)), - (concrete!(()), concrete!(Option>)), - (concrete!(()), concrete!(bool)), - (concrete!(()), concrete!(f64)), - (concrete!(()), concrete!(ImaginateMaskStartingFill)), - (concrete!(()), concrete!(bool)), - (concrete!(()), concrete!(bool)), - (concrete!(()), concrete!(Option>)), - (concrete!(()), concrete!(f64)), - (concrete!(()), concrete!(ImaginateStatus)), + value_fn!(f64), + value_fn!(Option), + value_fn!(f64), + value_fn!(ImaginateSamplingMethod), + value_fn!(f64), + value_fn!(String), + value_fn!(String), + value_fn!(bool), + value_fn!(f64), + value_fn!(Option>), + value_fn!(bool), + value_fn!(f64), + value_fn!(ImaginateMaskStartingFill), + value_fn!(bool), + value_fn!(bool), + value_fn!(Option>), + value_fn!(f64), + value_fn!(ImaginateStatus), ], ), ), @@ -296,7 +370,7 @@ fn node_registry() -> HashMap = DynAnyNode::new(ValueNode::new(new_image)); node.into_type_erased() }, - NodeIOTypes::new(concrete!(Image), concrete!(Image), vec![(concrete!(()), concrete!(u32)), (concrete!(()), concrete!(f64))]), + NodeIOTypes::new(concrete!(Image), concrete!(Image), vec![value_fn!(u32), value_fn!(f64)]), ), //register_node!(graphene_std::memo::CacheNode<_>, input: Image, params: []), ( @@ -307,7 +381,7 @@ fn node_registry() -> HashMap HashMap HashMap HashMap> = DowncastBothNode::new(args[0]); + let node: CacheNode, _> = graphene_std::memo::CacheNode::new(input); + let any = DynAnyRefNode::new(node); + any.into_type_erased() + }, + NodeIOTypes::new(concrete!(()), concrete!(&Vec), vec![value_fn!(Vec)]), ), ], register_node!(graphene_core::structural::ConsNode<_, _>, input: Image, params: [&str]), @@ -357,6 +441,7 @@ fn node_registry() -> HashMap>, params: [Vec] ), + register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []), ]; let mut map: HashMap> = HashMap::new(); for (id, c, types) in node_types.into_iter().flatten() { diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 839b295d..fb853b1d 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -2,8 +2,8 @@ use proc_macro::TokenStream; use proc_macro2::Span; use quote::{format_ident, ToTokens}; use syn::{ - parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, GenericParam, Ident, ItemFn, Lifetime, Pat, PatIdent, PathArguments, PredicateType, ReturnType, Token, TraitBound, Type, TypeParam, - TypeParamBound, WhereClause, WherePredicate, + parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, GenericParam, Ident, ItemFn, Lifetime, Pat, PatIdent, PatType, PathArguments, PredicateType, ReturnType, Token, TraitBound, Type, + TypeParam, TypeParamBound, WhereClause, WherePredicate, }; #[proc_macro_attribute] @@ -46,6 +46,20 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { panic!("Expected ident as primary input."); }; let primary_input_ty = &primary_input.ty; + let aux_type_generics = type_generics + .iter() + .filter(|gen| { + if let GenericParam::Type(ty) = gen { + !function.sig.inputs.iter().take(1).any(|param_ty| match param_ty { + FnArg::Typed(pat_ty) => pat_ty.ty.to_token_stream().to_string() == ty.ident.to_string(), + _ => false, + }) + } else { + false + } + }) + .cloned() + .collect::>(); let body = function.block; @@ -101,6 +115,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { } }); let generics = type_generics.into_iter().chain(node_generics.iter().cloned()).collect::>(); + let new_fn_generics = aux_type_generics.into_iter().chain(node_generics.iter().cloned()).collect::>(); // Bindings for all of the above generics to a node with an input of `()` and an output of the type in the function let extra_where_clause = parameter_inputs .iter() @@ -122,7 +137,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { }) }) .collect::>(); - where_clause.predicates.extend(extra_where_clause); + where_clause.predicates.extend(extra_where_clause.clone()); quote::quote! { @@ -140,7 +155,8 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { } } - impl <#(#args),*> #node_name<#(#args),*> + impl <'input, #new_fn_generics> #node_name<#(#args),*> + where #(#extra_where_clause),* { pub const fn new(#(#parameter_idents: #struct_generics_iter),*) -> Self{ Self{