diff --git a/editor/src/lib.rs b/editor/src/lib.rs index b971d86f89..0fe3b2a3f4 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -11,5 +11,6 @@ pub mod application; pub mod consts; pub mod dispatcher; pub mod messages; +pub mod node_graph_executor; pub mod test_utils; pub mod utility_traits; diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 80a14a77d3..4ed9de6ded 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -107,6 +107,7 @@ pub enum DocumentMessage { }, NodeGraphFrameImaginateRandom { imaginate_node: Vec, + then_generate: bool, }, NodeGraphFrameImaginateTerminate { layer_path: Vec, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 65415fe86b..3b5d551311 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -21,6 +21,7 @@ use crate::messages::portfolio::document::utility_types::vectorize_layer_metadat use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; use crate::messages::tool::utility_types::ToolType; +use crate::node_graph_executor::NodeGraphExecutor; use document_legacy::boolean_ops::BooleanOperationError; use document_legacy::document::Document as DocumentLegacy; @@ -104,13 +105,13 @@ impl Default for DocumentMessageHandler { } } -impl MessageHandler for DocumentMessageHandler { +impl MessageHandler for DocumentMessageHandler { #[remain::check] fn process_message( &mut self, message: DocumentMessage, responses: &mut VecDeque, - (document_id, ipp, persistent_data, preferences): (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler), + (document_id, ipp, persistent_data, preferences, executor): (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler, &mut NodeGraphExecutor), ) { use DocumentMessage::*; @@ -203,6 +204,7 @@ impl MessageHandler { + NodeGraphFrameImaginateRandom { imaginate_node, then_generate } => { // Set a random seed input responses.push_back( NodeGraphMessage::SetInputValue { node_id: *imaginate_node.last().unwrap(), - input_index: 1, + // Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeType` in `document_node_type.rs` + input_index: 2, value: graph_craft::document::value::TaggedValue::F64((generate_uuid() >> 1) as f64), } .into(), ); + // Generate the image - responses.push_back(DocumentMessage::NodeGraphFrameImaginate { imaginate_node }.into()); + if then_generate { + responses.push_back(DocumentMessage::NodeGraphFrameImaginate { imaginate_node }.into()); + } } NodeGraphFrameImaginateTerminate { layer_path, node_path } => { responses.push_back( @@ -599,8 +605,8 @@ impl MessageHandler Self { match value { TaggedValue::String(_) => Self::Text, - TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::U32(_) => Self::Number, + TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::U32(_) | TaggedValue::DAffine2(_) => Self::Number, TaggedValue::Bool(_) => Self::Boolean, TaggedValue::DVec2(_) => Self::Vector, TaggedValue::Image(_) => Self::Raster, @@ -57,6 +57,13 @@ pub struct NodeGraphInput { name: String, } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct NodeGraphOutput { + #[serde(rename = "dataType")] + data_type: FrontendGraphDataType, + name: String, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNode { pub id: graph_craft::document::NodeId, @@ -66,10 +73,10 @@ pub struct FrontendNode { pub primary_input: Option, #[serde(rename = "exposedInputs")] pub exposed_inputs: Vec, - pub outputs: Vec, + pub outputs: Vec, // TODO: Break this apart into `primary_output` and `exposed_outputs` pub position: (i32, i32), pub disabled: bool, - pub output: bool, + pub previewed: bool, } // (link_start, link_end, link_end_input_index) @@ -77,6 +84,8 @@ pub struct FrontendNode { pub struct FrontendNodeLink { #[serde(rename = "linkStart")] pub link_start: u64, + #[serde(rename = "linkStartOutputIndex")] + pub link_start_output_index: usize, #[serde(rename = "linkEnd")] pub link_end: u64, #[serde(rename = "linkEndInputIndex")] @@ -176,7 +185,7 @@ impl NodeGraphMessageHandler { let mut widgets = Vec::new(); // Don't allow disabling input or output nodes - let mut selected_nodes = self.selected_nodes.iter().filter(|&&id| !network.inputs.contains(&id) && network.original_output() != id); + let mut selected_nodes = self.selected_nodes.iter().filter(|&&id| !network.inputs.contains(&id) && !network.original_outputs_contain(id)); // If there is at least one other selected node then show the hide or show button if selected_nodes.next().is_some() { @@ -187,13 +196,11 @@ impl NodeGraphMessageHandler { let multiple_nodes = selected_nodes.next().is_some(); // Generate the enable or disable button accordingly - let hide_button = WidgetHolder::new(Widget::TextButton(TextButton { - label: if is_hidden { "Show" } else { "Hide" }.to_string(), - tooltip: if is_hidden { "Show node" } else { "Hide node" }.to_string() + if multiple_nodes { "s" } else { "" }, - tooltip_shortcut: action_keys!(NodeGraphMessageDiscriminant::ToggleHidden), - on_update: WidgetCallback::new(move |_| NodeGraphMessage::ToggleHidden.into()), - ..Default::default() - })); + let hide_button = TextButton::new(if is_hidden { "Show" } else { "Hide" }) + .tooltip(if is_hidden { "Show node" } else { "Hide node" }.to_string() + if multiple_nodes { "s" } else { "" }) + .tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleHidden)) + .on_update(move |_| NodeGraphMessage::ToggleHidden.into()) + .widget_holder(); widgets.push(hide_button); } @@ -201,13 +208,13 @@ impl NodeGraphMessageHandler { if self.selected_nodes.len() == 1 { let node_id = self.selected_nodes[0]; // Is this node the current output - let is_output = network.output == node_id; + let is_output = network.outputs_contain(node_id); // Don't show stop previewing button on the original output node - if !(is_output && network.previous_output.filter(|&id| id != self.selected_nodes[0]).is_none()) { + if !(is_output && network.previous_outputs_contain(node_id).unwrap_or(true)) { let output_button = WidgetHolder::new(Widget::TextButton(TextButton { label: if is_output { "End Preview" } else { "Preview" }.to_string(), - tooltip: if is_output { "Restore preview to Output node" } else { "Preview node" }.to_string() + " (shortcut: Alt+click node)", + tooltip: if is_output { "Restore preview to Output node" } else { "Preview node" }.to_string() + " (Shortcut: Alt-click node)", on_update: WidgetCallback::new(move |_| NodeGraphMessage::TogglePreview { node_id }.into()), ..Default::default() })); @@ -220,6 +227,7 @@ impl NodeGraphMessageHandler { self.send_node_bar_layout(responses); } + /// Collate the properties panel sections for a node graph pub fn collate_properties(&self, node_graph_frame: &NodeGraphFrameLayer, context: &mut NodePropertiesContext, sections: &mut Vec) { let mut network = &node_graph_frame.network; for segment in &self.nested_path { @@ -228,20 +236,27 @@ impl NodeGraphMessageHandler { // If empty, show all nodes in the network starting with the output if self.selected_nodes.is_empty() { - let mut stack = vec![network.output]; + let mut stack = network.outputs.iter().map(|output| output.node_id).collect::>(); let mut nodes = Vec::new(); while let Some(node_id) = stack.pop() { let Some(document_node) = network.nodes.get(&node_id) else { continue; }; - stack.extend(document_node.inputs.iter().filter_map(|input| if let NodeInput::Node(ref_id) = input { Some(*ref_id) } else { None })); + 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 }), + ); nodes.push((document_node, node_id)); } for &(document_node, node_id) in nodes.iter().rev() { sections.push(node_properties::generate_node_properties(document_node, node_id, context)); } } + // Show properties for all selected nodes for node_id in &self.selected_nodes { let Some(document_node) = network.nodes.get(node_id) else { continue; @@ -260,9 +275,14 @@ impl NodeGraphMessageHandler { .iter() .flat_map(|(link_end, node)| node.inputs.iter().filter(|input| input.is_exposed()).enumerate().map(move |(index, input)| (input, link_end, index))) .filter_map(|(input, &link_end, link_end_input_index)| { - if let NodeInput::Node(link_start) = *input { + if let NodeInput::Node { + node_id: link_start, + output_index: link_start_index, + } = *input + { Some(FrontendNodeLink { link_start, + link_start_output_index: link_start_index, link_end, link_end_input_index: link_end_input_index as u64, }) @@ -298,9 +318,16 @@ impl NodeGraphMessageHandler { name: input_type.name.to_string(), }) .collect(), - outputs: node_type.outputs.to_vec(), + outputs: node_type + .outputs + .iter() + .map(|output_type| NodeGraphOutput { + data_type: output_type.data_type, + name: output_type.name.to_string(), + }) + .collect(), position: node.metadata.position.into(), - output: network.output == *id, + previewed: network.outputs_contain(*id), disabled: network.disabled.contains(id), }) } @@ -318,24 +345,24 @@ impl NodeGraphMessageHandler { ); } - fn remove_references_from_network(network: &mut NodeNetwork, node_id: NodeId) -> bool { - if network.inputs.iter().any(|&id| id == node_id) { + fn remove_references_from_network(network: &mut NodeNetwork, deleting_node_id: NodeId) -> bool { + if network.inputs.contains(&deleting_node_id) { warn!("Deleting input node"); return false; } - if network.output == node_id { + if network.outputs_contain(deleting_node_id) { warn!("Deleting the output node!"); return false; } - for (id, node) in network.nodes.iter_mut() { - if *id == node_id { + for (node_id, node) in network.nodes.iter_mut() { + if *node_id == deleting_node_id { continue; } for (input_index, input) in node.inputs.iter_mut().enumerate() { - let NodeInput::Node(id) = input else { + let NodeInput::Node{ node_id, .. } = input else { continue; }; - if *id != node_id { + if *node_id != deleting_node_id { continue; } @@ -344,19 +371,17 @@ impl NodeGraphMessageHandler { return false; }; if let NodeInput::Value { tagged_value, .. } = &node_type.inputs[input_index].default { - *input = NodeInput::Value { - tagged_value: tagged_value.clone(), - exposed: true, - }; + *input = NodeInput::value(tagged_value.clone(), true); } } if let DocumentNodeImplementation::Network(network) = &mut node.implementation { - Self::remove_references_from_network(network, node_id); + Self::remove_references_from_network(network, deleting_node_id); } } true } + /// Tries to remove a node from the network, returning true on success. fn remove_node(&mut self, network: &mut NodeNetwork, node_id: NodeId) -> bool { if Self::remove_references_from_network(network, node_id) { network.nodes.remove(&node_id); @@ -378,7 +403,7 @@ impl NodeGraphMessageHandler { fn copy_nodes<'a>(network: &'a NodeNetwork, new_ids: &'a HashMap) -> impl Iterator + 'a { new_ids .iter() - .filter(|&(&id, _)| id != network.output && !network.inputs.contains(&id)) + .filter(|&(&id, _)| !network.outputs_contain(id) && !network.inputs.contains(&id)) .filter_map(|(&id, &new)| network.nodes.get(&id).map(|node| (new, node.clone()))) .map(move |(new, node)| (new, node.map_ids(Self::default_node_input, new_ids))) } @@ -402,6 +427,7 @@ impl MessageHandler { @@ -422,7 +448,7 @@ impl MessageHandler>(); for input_node in inputs { @@ -812,9 +836,11 @@ impl MessageHandler>(); // Add all selected nodes to the disabled list (excluding input or output nodes) - network.disabled.extend(self.selected_nodes.iter().filter(|&id| !network.inputs.contains(id) && original_output != *id)); + network + .disabled + .extend(self.selected_nodes.iter().filter(|&id| !network.inputs.contains(id) && !original_outputs.contains(id))); } Self::send_graph(network, responses); @@ -831,12 +857,12 @@ impl MessageHandler { if let Some(network) = self.get_active_network_mut(document) { - // Check if the node is not already - if network.output != node_id { - network.previous_output = Some(network.previous_output.unwrap_or(network.output)); - network.output = node_id; - } else if let Some(output) = network.previous_output.take() { - network.output = output + // Check if the node is not already being previewed + if !network.outputs_contain(node_id) { + network.previous_outputs = Some(network.previous_outputs.to_owned().unwrap_or_else(|| network.outputs.clone())); + network.outputs[0] = NodeOutput::new(node_id, 0); + } else if let Some(outputs) = network.previous_outputs.take() { + network.outputs = outputs } else { return; } 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 60da33bbe7..9b72a2ed63 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 @@ -1,5 +1,6 @@ use super::{node_properties, FrontendGraphDataType, FrontendNodeType}; use crate::messages::layout::utility_types::layout_widget::LayoutGroup; +use crate::node_graph_executor::NodeGraphExecutor; use graph_craft::document::value::*; use graph_craft::document::*; @@ -32,12 +33,25 @@ impl DocumentInputType { } } +pub struct DocumentOutputType { + pub name: &'static str, + pub data_type: FrontendGraphDataType, +} + +impl DocumentOutputType { + pub const fn new(name: &'static str, data_type: FrontendGraphDataType) -> Self { + Self { name, data_type } + } +} + pub struct NodePropertiesContext<'a> { pub persistent_data: &'a crate::messages::portfolio::utility_types::PersistentData, pub document: &'a document_legacy::document::Document, pub responses: &'a mut VecDeque, pub layer_path: &'a [document_legacy::LayerId], pub nested_path: &'a [NodeId], + pub executor: &'a mut NodeGraphExecutor, + pub network: &'a NodeNetwork, } #[derive(Clone)] @@ -58,25 +72,24 @@ pub struct DocumentNodeType { pub category: &'static str, pub identifier: NodeImplementation, pub inputs: &'static [DocumentInputType], - pub outputs: &'static [FrontendGraphDataType], + pub outputs: &'static [DocumentOutputType], pub properties: fn(&DocumentNode, NodeId, &mut NodePropertiesContext) -> Vec, } fn document_node_types() -> Vec { let mut vec: Vec<_> = STATIC_NODES.to_vec(); - const INPUTS: &[DocumentInputType] = &[ + const GAUSSIAN_BLUR_NODE_INPUTS: &[DocumentInputType] = &[ DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), DocumentInputType::new("Radius", TaggedValue::U32(3), false), DocumentInputType::new("Sigma", TaggedValue::F64(1.), false), ]; - let blur = DocumentNodeType { name: "Gaussian Blur", category: "Image Filters", identifier: NodeImplementation::DocumentNode(NodeNetwork { inputs: vec![0, 1, 1], - output: 1, + outputs: vec![NodeOutput::new(1, 0)], nodes: vec![ ( 0, @@ -91,7 +104,7 @@ fn document_node_types() -> Vec { 1, DocumentNode { name: "BlurNode".to_string(), - inputs: vec![NodeInput::Node(0), NodeInput::Network, NodeInput::Network, NodeInput::Node(0)], + inputs: vec![NodeInput::node(0, 0), NodeInput::Network, NodeInput::Network, NodeInput::node(0, 0)], implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::raster::BlurNode", &[concrete!("Image")])), metadata: Default::default(), }, @@ -101,11 +114,64 @@ fn document_node_types() -> Vec { .collect(), ..Default::default() }), - inputs: INPUTS, - outputs: &[FrontendGraphDataType::Raster], + inputs: GAUSSIAN_BLUR_NODE_INPUTS, + outputs: &[DocumentOutputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + }], properties: node_properties::blur_image_properties, }; vec.push(blur); + + const INPUT_NODE_INPUTS: &[DocumentInputType] = &[ + DocumentInputType { + name: "In", + data_type: FrontendGraphDataType::General, + default: NodeInput::Network, + }, + DocumentInputType::new("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), false), + ]; + let input = DocumentNodeType { + name: "Input", + category: "Ignore", + identifier: NodeImplementation::DocumentNode(NodeNetwork { + inputs: vec![0, 1], + outputs: vec![NodeOutput::new(0, 0), NodeOutput::new(1, 0)], + nodes: [ + DocumentNode { + name: "Identity".to_string(), + inputs: vec![NodeInput::Network], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])), + metadata: Default::default(), + }, + DocumentNode { + name: "Identity".to_string(), + inputs: vec![NodeInput::Network], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])), + metadata: Default::default(), + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (id as NodeId, node)) + .collect(), + ..Default::default() + }), + inputs: INPUT_NODE_INPUTS, + outputs: &[ + DocumentOutputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + }, + DocumentOutputType { + name: "Transform", + data_type: FrontendGraphDataType::Number, + }, + ], + properties: node_properties::input_properties, + }; + vec.push(input); + vec } @@ -122,9 +188,9 @@ static STATIC_NODES: &[DocumentNodeType] = &[ inputs: &[DocumentInputType { name: "In", data_type: FrontendGraphDataType::General, - default: NodeInput::Node(0), + default: NodeInput::node(0, 0), }], - outputs: &[FrontendGraphDataType::General], + outputs: &[DocumentOutputType::new("Out", FrontendGraphDataType::General)], properties: |_document_node, _node_id, _context| node_properties::string_properties("The identity node simply returns the input"), }, DocumentNodeType { @@ -132,21 +198,21 @@ static STATIC_NODES: &[DocumentNodeType] = &[ category: "Ignore", identifier: NodeImplementation::proto("graphene_core::ops::IdNode", &[generic!("T")]), inputs: &[DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), false)], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: |_document_node, _node_id, _context| node_properties::string_properties("A bitmap image embedded in this node"), }, - DocumentNodeType { - name: "Input", - category: "Ignore", - identifier: NodeImplementation::proto("graphene_core::ops::IdNode", &[generic!("T")]), - inputs: &[DocumentInputType { - name: "In", - data_type: FrontendGraphDataType::Raster, - default: NodeInput::Network, - }], - outputs: &[FrontendGraphDataType::Raster], - properties: node_properties::input_properties, - }, + // DocumentNodeType { + // name: "Input", + // category: "Ignore", + // identifier: NodeImplementation::proto("graphene_core::ops::IdNode", &[generic!("T")]), + // inputs: &[DocumentInputType { + // name: "In", + // data_type: FrontendGraphDataType::Raster, + // default: NodeInput::Network, + // }], + // outputs: &[DocumentOutputType::new("Out", FrontendGraphDataType::Raster)], + // properties: node_properties::input_properties, + // }, DocumentNodeType { name: "Output", category: "Ignore", @@ -157,7 +223,18 @@ static STATIC_NODES: &[DocumentNodeType] = &[ default: NodeInput::value(TaggedValue::Image(Image::empty()), true), }], outputs: &[], - properties: |_document_node, _node_id, _context| node_properties::string_properties("The graph's output is rendered into the frame".to_string()), + properties: |_document_node, _node_id, _context| node_properties::string_properties("The graph's output is rendered into the frame"), + }, + DocumentNodeType { + name: "Image Frame", + category: "General", + identifier: NodeImplementation::proto("graphene_std::raster::ImageFrameNode<_>", &[concrete!("Image"), concrete!("DAffine2")]), + inputs: &[ + DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), + DocumentInputType::new("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), true), + ], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: |_document_node, _node_id, _context| node_properties::string_properties("Creates an embedded image with the given transform"), }, DocumentNodeType { name: "Grayscale", @@ -217,7 +294,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ default: NodeInput::value(TaggedValue::F64(50.), false), }, ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::grayscale_properties, }, DocumentNodeType { @@ -228,7 +305,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), DocumentInputType::new("Luma Calculation", TaggedValue::LuminanceCalculation(LuminanceCalculation::SRGB), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::luminance_properties, }, #[cfg(feature = "gpu")] @@ -244,7 +321,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ default: NodeInput::value(TaggedValue::String(String::new()), false), }, ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::gpu_map_properties, }, #[cfg(feature = "quantization")] @@ -269,7 +346,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ default: NodeInput::value(TaggedValue::U32(0), false), }, ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::quantize_properties, }, DocumentNodeType { @@ -277,7 +354,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ category: "Structural", identifier: NodeImplementation::proto("graphene_std::memo::CacheNode", &[concrete!("Image")]), inputs: &[DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true)], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::no_properties, }, DocumentNodeType { @@ -285,7 +362,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ category: "Image Adjustments", identifier: NodeImplementation::proto("graphene_core::raster::InvertRGBNode", &[concrete!("Image")]), inputs: &[DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true)], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::no_properties, }, DocumentNodeType { @@ -301,7 +378,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Saturation Shift", TaggedValue::F64(0.), false), DocumentInputType::new("Lightness Shift", TaggedValue::F64(0.), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::adjust_hsl_properties, }, DocumentNodeType { @@ -313,7 +390,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Brightness", TaggedValue::F64(0.), false), DocumentInputType::new("Contrast", TaggedValue::F64(0.), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::brighten_image_properties, }, DocumentNodeType { @@ -325,7 +402,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Luma Calculation", TaggedValue::LuminanceCalculation(LuminanceCalculation::SRGB), false), DocumentInputType::new("Threshold", TaggedValue::F64(50.), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::adjust_threshold_properties, }, DocumentNodeType { @@ -336,7 +413,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), DocumentInputType::new("Vibrance", TaggedValue::F64(0.), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::adjust_vibrance_properties, }, DocumentNodeType { @@ -347,7 +424,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), DocumentInputType::new("Factor", TaggedValue::F64(100.), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::multiply_opacity, }, DocumentNodeType { @@ -358,7 +435,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), DocumentInputType::new("Value", TaggedValue::F64(4.), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::posterize_properties, }, DocumentNodeType { @@ -374,7 +451,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Offset", TaggedValue::F64(0.), false), DocumentInputType::new("Gamma Correction", TaggedValue::F64(1.), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::exposure_properties, }, IMAGINATE_NODE, @@ -386,7 +463,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Input", TaggedValue::F64(0.), true), DocumentInputType::new("Addend", TaggedValue::F64(0.), true), ], - outputs: &[FrontendGraphDataType::Number], + outputs: &[DocumentOutputType::new("Output", FrontendGraphDataType::Number)], properties: node_properties::add_properties, }, /*DocumentNodeType { @@ -394,7 +471,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ category: "Vector", identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::UnitCircleGenerator", &[]), inputs: &[DocumentInputType::none()], - outputs: &[FrontendGraphDataType::Subpath], + outputs: &[DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::no_properties, }, DocumentNodeType { @@ -402,7 +479,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ category: "Vector", identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::UnitSquareGenerator", &[]), inputs: &[DocumentInputType::none()], - outputs: &[FrontendGraphDataType::Subpath], + outputs: &[DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::no_properties, }, DocumentNodeType { @@ -414,7 +491,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ data_type: FrontendGraphDataType::Subpath, default: NodeInput::value(TaggedValue::Subpath(Subpath::new()), false), }], - outputs: &[FrontendGraphDataType::Subpath], + outputs: &[DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::no_properties, }, DocumentNodeType { @@ -428,7 +505,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Scale", TaggedValue::DVec2(DVec2::ONE), false), DocumentInputType::new("Skew", TaggedValue::DVec2(DVec2::ZERO), false), ], - outputs: &[FrontendGraphDataType::Subpath], + outputs: &[DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::transform_properties, }, DocumentNodeType { @@ -439,7 +516,7 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), DocumentInputType::new("Subpath", TaggedValue::Subpath(Subpath::empty()), true), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Vector", FrontendGraphDataType::Raster)], properties: node_properties::no_properties, },*/ ]; @@ -450,11 +527,12 @@ pub const IMAGINATE_NODE: DocumentNodeType = DocumentNodeType { identifier: NodeImplementation::proto("graphene_std::raster::ImaginateNode<_>", &[concrete!("Image"), concrete!("Option>")]), inputs: &[ DocumentInputType::new("Input Image", TaggedValue::Image(Image::empty()), true), - DocumentInputType::new("Seed", TaggedValue::F64(0.), false), + DocumentInputType::new("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), true), + DocumentInputType::new("Seed", TaggedValue::F64(0.), false), // Remember to keep index used in `NodeGraphFrameImaginateRandom` updated with this entry's index DocumentInputType::new("Resolution", TaggedValue::OptionalDVec2(None), false), DocumentInputType::new("Samples", TaggedValue::F64(30.), false), DocumentInputType::new("Sampling Method", TaggedValue::ImaginateSamplingMethod(ImaginateSamplingMethod::EulerA), false), - DocumentInputType::new("Prompt Guidance", TaggedValue::F64(10.), false), + DocumentInputType::new("Prompt Guidance", TaggedValue::F64(7.5), false), DocumentInputType::new("Prompt", TaggedValue::String(String::new()), false), DocumentInputType::new("Negative Prompt", TaggedValue::String(String::new()), false), DocumentInputType::new("Adapt Input Image", TaggedValue::Bool(false), false), @@ -470,7 +548,7 @@ pub const IMAGINATE_NODE: DocumentNodeType = DocumentNodeType { DocumentInputType::new("Percent Complete", TaggedValue::F64(0.), false), DocumentInputType::new("Status", TaggedValue::ImaginateStatus(ImaginateStatus::Idle), false), ], - outputs: &[FrontendGraphDataType::Raster], + outputs: &[DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::imaginate_properties, }; @@ -495,7 +573,7 @@ impl DocumentNodeType { NodeImplementation::ProtoNode(ident) => { NodeNetwork { inputs: (0..num_inputs).map(|_| 0).collect(), - output: 0, + outputs: vec![NodeOutput::new(0, 0)], nodes: [( 0, DocumentNode { @@ -515,4 +593,37 @@ impl DocumentNodeType { }; DocumentNodeImplementation::Network(inner_network) } + + pub fn to_document_node(&self, inputs: impl IntoIterator, metadata: graph_craft::document::DocumentNodeMetadata) -> DocumentNode { + DocumentNode { + name: self.name.to_string(), + inputs: inputs.into_iter().collect(), + implementation: self.generate_implementation(), + metadata, + } + } +} + +pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetwork { + NodeNetwork { + inputs: vec![0], + outputs: vec![NodeOutput::new(1, 0)], + nodes: [ + resolve_document_node_type("Input").expect("Input node does not exist").to_document_node( + [NodeInput::Network, NodeInput::value(TaggedValue::DAffine2(DAffine2::IDENTITY), false)], + DocumentNodeMetadata::position((8, 4)), + ), + resolve_document_node_type("Output") + .expect("Output node does not exist") + .to_document_node([NodeInput::node(2, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))), + resolve_document_node_type("Image Frame") + .expect("Image frame node does not exist") + .to_document_node([NodeInput::node(output_node_id, 0), NodeInput::node(0, 1)], DocumentNodeMetadata::position((output_offset, 4))), + ] + .into_iter() + .enumerate() + .map(|(id, node)| (id as NodeId, node)) + .collect(), + ..Default::default() + } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 424b23457e..42f446e5f3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -6,7 +6,7 @@ use document_legacy::layers::layer_info::LayerDataTypeDiscriminant; use document_legacy::Operation; use glam::DVec2; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput}; +use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::imaginate_input::*; use graphene_core::raster::{Color, LuminanceCalculation}; @@ -407,10 +407,10 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { let imaginate_node = [context.nested_path, &[node_id]].concat(); - let imaginate_node_1 = imaginate_node.clone(); let layer_path = context.layer_path.to_vec(); let resolve_input = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); + let transform_index = resolve_input("Transform"); let seed_index = resolve_input("Seed"); let resolution_index = resolve_input("Resolution"); let samples_index = resolve_input("Samples"); @@ -478,6 +478,9 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte } else { true }; + + let transform_not_connected = matches!(document_node.inputs[transform_index], NodeInput::Value { .. }); + let progress = { // Since we don't serialize the status, we need to derive from other state whether the Idle state is actually supposed to be the Terminated state let mut interpreted_status = imaginate_status; @@ -527,12 +530,15 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte widgets.push( TextButton::new("Terminate") .tooltip("Cancel the in-progress image generation and keep the latest progress") - .on_update(move |_| { - DocumentMessage::NodeGraphFrameImaginateTerminate { - layer_path: layer_path.clone(), - node_path: imaginate_node.clone(), + .on_update({ + let imaginate_node = imaginate_node.clone(); + move |_| { + DocumentMessage::NodeGraphFrameImaginateTerminate { + layer_path: layer_path.clone(), + node_path: imaginate_node.clone(), + } + .into() } - .into() }) .widget_holder(), ); @@ -549,21 +555,28 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte ImaginateStatus::Idle | ImaginateStatus::Terminated => widgets.extend_from_slice(&[ IconButton::new("Random", 24) .tooltip("Generate with a new random seed") - .on_update(move |_| { - DocumentMessage::NodeGraphFrameImaginateRandom { - imaginate_node: imaginate_node.clone(), + .on_update({ + let imaginate_node = imaginate_node.clone(); + move |_| { + DocumentMessage::NodeGraphFrameImaginateRandom { + imaginate_node: imaginate_node.clone(), + then_generate: true, + } + .into() } - .into() }) .widget_holder(), WidgetHolder::unrelated_separator(), TextButton::new("Generate") .tooltip("Fill layer frame by generating a new image") - .on_update(move |_| { - DocumentMessage::NodeGraphFrameImaginate { - imaginate_node: imaginate_node_1.clone(), + .on_update({ + let imaginate_node = imaginate_node.clone(); + move |_| { + DocumentMessage::NodeGraphFrameImaginate { + imaginate_node: imaginate_node.clone(), + } + .into() } - .into() }) .widget_holder(), WidgetHolder::related_separator(), @@ -590,7 +603,16 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte WidgetHolder::unrelated_separator(), IconButton::new("Regenerate", 24) .tooltip("Set a new random seed") - .on_update(update_value(move |_| TaggedValue::F64((generate_uuid() >> 1) as f64), node_id, seed_index)) + .on_update({ + let imaginate_node = imaginate_node.clone(); + move |_| { + DocumentMessage::NodeGraphFrameImaginateRandom { + imaginate_node: imaginate_node.clone(), + then_generate: false, + } + .into() + } + }) .widget_holder(), WidgetHolder::unrelated_separator(), NumberInput::new(Some(seed)) @@ -604,6 +626,16 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte LayoutGroup::Row { widgets }.with_tooltip("Seed determines the random outcome, enabling limitless unique variations") }; + // Get the existing layer transform + let transform = context.document.root.transform.inverse() * context.document.multiply_transforms(context.layer_path).unwrap(); + // Create the input to the graph using an empty image + let image_frame = std::borrow::Cow::Owned(graphene_core::raster::ImageFrame { + image: graphene_core::raster::Image::empty(), + transform, + }); + // Compute the transform input to the node graph frame + let transform: glam::DAffine2 = context.executor.compute_input(context.network, &imaginate_node, 1, image_frame).unwrap_or_default(); + let resolution = { use document_legacy::document::pick_safe_imaginate_resolution; @@ -621,7 +653,6 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte { let dimensions_is_auto = vec2.is_none(); let vec2 = vec2.unwrap_or_else(|| { - let transform = context.document.root.transform.inverse() * context.document.multiply_transforms(context.layer_path).unwrap(); let w = transform.transform_vector2(DVec2::new(1., 0.)).length(); let h = transform.transform_vector2(DVec2::new(0., 1.)).length(); @@ -644,9 +675,21 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte }) .widget_holder(), WidgetHolder::unrelated_separator(), - CheckboxInput::new(!dimensions_is_auto) + CheckboxInput::new(!dimensions_is_auto || transform_not_connected) .icon("Edit") - .tooltip("Set a custom resolution instead of using the frame's rounded dimensions") + .tooltip({ + let message = "Set a custom resolution instead of using the frame's rounded dimensions"; + let manual_message = "Set a custom resolution instead of using the frame's rounded dimensions.\n\ + \n\ + (Resolution must be set manually while the 'Transform' input is disconnected.)"; + + if transform_not_connected { + manual_message + } else { + message + } + }) + .disabled(transform_not_connected) .on_update(update_value( move |checkbox_input: &CheckboxInput| { if checkbox_input.checked { @@ -663,7 +706,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte NumberInput::new(Some(vec2.x)) .label("W") .unit(" px") - .disabled(dimensions_is_auto) + .disabled(dimensions_is_auto && !transform_not_connected) .on_update(update_value( move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(number_input.value.unwrap(), vec2.y))), node_id, @@ -674,7 +717,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte NumberInput::new(Some(vec2.y)) .label("H") .unit(" px") - .disabled(dimensions_is_auto) + .disabled(dimensions_is_auto && !transform_not_connected) .on_update(update_value( move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(vec2.x, number_input.value.unwrap()))), node_id, @@ -778,18 +821,22 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte let layer_reference_input_layer_name = layer_reference_input_layer.as_ref().map(|(layer_name, _)| layer_name); let layer_reference_input_layer_type = layer_reference_input_layer.as_ref().map(|(_, layer_type)| layer_type); - widgets.extend_from_slice(&[ - WidgetHolder::unrelated_separator(), - LayerReferenceInput::new(layer_path.clone(), layer_reference_input_layer_name.cloned(), layer_reference_input_layer_type.cloned()) - .disabled(!use_base_image) - .on_update(update_value(|input: &LayerReferenceInput| TaggedValue::LayerPath(input.value.clone()), node_id, mask_index)) - .widget_holder(), - ]); + widgets.push(WidgetHolder::unrelated_separator()); + if !transform_not_connected { + widgets.push( + LayerReferenceInput::new(layer_path.clone(), layer_reference_input_layer_name.cloned(), layer_reference_input_layer_type.cloned()) + .disabled(!use_base_image) + .on_update(update_value(|input: &LayerReferenceInput| TaggedValue::LayerPath(input.value.clone()), node_id, mask_index)) + .widget_holder(), + ); + } else { + widgets.push(TextLabel::new("Requires Transform Input").italic(true).widget_holder()); + } } LayoutGroup::Row { widgets }.with_tooltip( "Reference to a layer or folder which masks parts of the input image. Image generation is constrained to masked areas.\n\ \n\ - Black shapes represent the masked regions. Lighter shades of gray act as a partial mask, and colors become grayscale.", + Black shapes represent the masked regions. Lighter shades of gray act as a partial mask, and colors become grayscale. (This is the reverse of traditional masks because it is easier to draw black shapes; this will be changed later when the mask input is a bitmap.)", ) }; diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index b91ec5df72..3552716a3c 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -28,6 +28,7 @@ impl<'a> MessageHandler artboard_document, @@ -166,7 +167,7 @@ impl<'a> MessageHandler register_artboard_layer_properties(layer, responses, persistent_data), - TargetDocument::Artwork => register_artwork_layer_properties(document, path, layer, responses, persistent_data, node_graph_message_handler), + TargetDocument::Artwork => register_artwork_layer_properties(document, path, layer, responses, persistent_data, node_graph_message_handler, executor), } } } diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index 5d2ea0c097..c78044470e 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -8,6 +8,7 @@ use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInp use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, TextLabel}; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; +use crate::node_graph_executor::NodeGraphExecutor; use document_legacy::document::Document; use document_legacy::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant}; @@ -246,6 +247,7 @@ pub fn register_artwork_layer_properties( responses: &mut VecDeque, persistent_data: &PersistentData, node_graph_message_handler: &NodeGraphMessageHandler, + executor: &mut NodeGraphExecutor, ) { let options_bar = vec![LayoutGroup::Row { widgets: vec![ @@ -323,6 +325,8 @@ pub fn register_artwork_layer_properties( responses, nested_path: &node_graph_message_handler.nested_path, layer_path: &layer_path, + executor, + network: &node_graph_frame.network, }; node_graph_message_handler.collate_properties(node_graph_frame, &mut context, &mut properties_sections); diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_types.rs b/editor/src/messages/portfolio/document/properties_panel/utility_types.rs index beea3be43c..a4a6395393 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_types.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_types.rs @@ -3,13 +3,14 @@ use document_legacy::LayerId; use serde::{Deserialize, Serialize}; -use crate::messages::prelude::NodeGraphMessageHandler; +use crate::{messages::prelude::NodeGraphMessageHandler, node_graph_executor::NodeGraphExecutor}; pub struct PropertiesPanelMessageHandlerData<'a> { pub artwork_document: &'a DocumentLegacy, pub artboard_document: &'a DocumentLegacy, pub selected_layers: &'a mut dyn Iterator, pub node_graph_message_handler: &'a NodeGraphMessageHandler, + pub executor: &'a mut NodeGraphExecutor, } #[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize, specta::Type)] diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 3ada78c16c..e84b197397 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -2,37 +2,29 @@ use super::utility_types::PersistentData; use crate::application::generate_uuid; use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION}; use crate::messages::dialog::simple_dialogs; -use crate::messages::frontend::utility_types::{FrontendDocumentDetails, FrontendImageData}; +use crate::messages::frontend::utility_types::FrontendDocumentDetails; use crate::messages::layout::utility_types::layout_widget::PropertyHolder; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; -use crate::messages::portfolio::document::utility_types::misc::DocumentRenderMode; use crate::messages::portfolio::utility_types::ImaginateServerStatus; use crate::messages::prelude::*; - use crate::messages::tool::utility_types::{HintData, HintGroup}; -use document_legacy::document::pick_safe_imaginate_resolution; -use document_legacy::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant}; +use crate::node_graph_executor::NodeGraphExecutor; + +use document_legacy::layers::layer_info::LayerDataTypeDiscriminant; use document_legacy::layers::style::RenderData; use document_legacy::layers::text_layer::Font; -use document_legacy::{LayerId, Operation as DocumentOperation}; +use document_legacy::Operation as DocumentOperation; use graph_craft::document::value::TaggedValue; -use graph_craft::document::NodeId; -use graph_craft::document::{NodeInput, NodeNetwork}; -use graph_craft::executor::Compiler; use graphene_core::raster::Image; -use glam::DVec2; -use interpreted_executor::executor::DynamicExecutor; -use std::borrow::Cow; - #[derive(Debug, Clone, Default)] pub struct PortfolioMessageHandler { menu_bar_message_handler: MenuBarMessageHandler, documents: HashMap, document_ids: Vec, - executor: interpreted_executor::executor::DynamicExecutor, + executor: NodeGraphExecutor, active_document_id: Option, copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], pub persistent_data: PersistentData, @@ -50,7 +42,7 @@ impl MessageHandler { if let Some(document_id) = self.active_document_id { if let Some(document) = self.documents.get_mut(&document_id) { - document.process_message(message, responses, (document_id, ipp, &self.persistent_data, preferences)) + document.process_message(message, responses, (document_id, ipp, &self.persistent_data, preferences, &mut self.executor)) } } } @@ -59,7 +51,7 @@ impl MessageHandler { if let Some(document) = self.documents.get_mut(&document_id) { - document.process_message(message, responses, (document_id, ipp, &self.persistent_data, preferences)) + document.process_message(message, responses, (document_id, ipp, &self.persistent_data, preferences, &mut self.executor)) } } PortfolioMessage::AutoSaveActiveDocument => { @@ -489,7 +481,14 @@ impl MessageHandler { - if let Err(description) = self.evaluate_node_graph(document_id, layer_path, (image_data, size), imaginate_node, preferences, responses) { + if let Err(description) = self.executor.evaluate_node_graph( + (document_id, &mut self.documents), + layer_path, + (image_data, size), + imaginate_node, + (preferences, &self.persistent_data), + responses, + ) { responses.push_back( DialogMessage::DisplayDialogError { title: "Unable to update node graph".to_string(), @@ -681,227 +680,4 @@ impl PortfolioMessageHandler { fn document_index(&self, document_id: u64) -> usize { self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids") } - - /// Execute the network by flattening it and creating a borrow stack. Casts the output to the generic `T`. - fn execute_network(executor: &mut DynamicExecutor, network: NodeNetwork, image: Image) -> Result { - let c = Compiler {}; - let proto_network = c.compile(network, true); - - assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?"); - executor.update(proto_network); - - use dyn_any::IntoDynAny; - use graph_craft::executor::Executor; - - let boxed = executor.execute(image.into_dyn()).map_err(|e| e.to_string())?; - - dyn_any::downcast::(boxed).map(|v| *v) - } - - /// Computes an input for a node in the graph - fn compute_input(executor: &mut DynamicExecutor, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image: Cow) -> Result { - let mut network = old_network.clone(); - // Adjust the output of the graph so we find the relevant output - 'outer: for end in (0..node_path.len()).rev() { - let mut inner_network = &mut network; - for &node_id in &node_path[..end] { - inner_network.output = node_id; - - let Some(new_inner) = inner_network.nodes.get_mut(&node_id).and_then(|node| node.implementation.get_network_mut()) else { - return Err("Failed to find network".to_string()); - }; - inner_network = new_inner; - } - match &inner_network.nodes.get(&node_path[end]).unwrap().inputs[input_index] { - // If the input is from a parent network then adjust the input index and continue iteration - NodeInput::Network => { - input_index = inner_network - .inputs - .iter() - .enumerate() - .filter(|&(_index, &id)| id == node_path[end]) - .nth(input_index) - .ok_or_else(|| "Invalid network input".to_string())? - .0; - } - // If the input is just a value, return that value - NodeInput::Value { tagged_value, .. } => return dyn_any::downcast::(tagged_value.clone().to_any()).map(|v| *v), - // If the input is from a node, set the node to be the output (so that is what is evaluated) - NodeInput::Node(n) => { - inner_network.output = *n; - break 'outer; - } - } - } - - Self::execute_network(executor, network, image.into_owned()) - } - - /// Encodes an image into a format using the image crate - fn encode_img(image: Image, resize: Option, format: image::ImageOutputFormat) -> Result<(Vec, (u32, u32)), String> { - use image::{ImageBuffer, Rgba}; - use std::io::Cursor; - - let mut image_data: Vec = Vec::new(); - let [image_width, image_height] = [image.width, image.height]; - let size_estimate = (image_width * image_height * 4) as usize; - - let mut result_bytes = Vec::with_capacity(size_estimate); - result_bytes.extend(image.data.into_iter().flat_map(|color| color.to_rgba8())); - let mut output: ImageBuffer, _> = image::ImageBuffer::from_raw(image_width, image_height, result_bytes).ok_or_else(|| "Invalid image size".to_string())?; - if let Some(size) = resize { - let size = size.as_uvec2(); - if size.x > 0 && size.y > 0 { - output = image::imageops::resize(&output, size.x, size.y, image::imageops::Triangle); - } - } - let size = output.dimensions(); - output.write_to(&mut Cursor::new(&mut image_data), format).map_err(|e| e.to_string())?; - Ok::<_, String>((image_data, size)) - } - - /// Evaluates a node graph, computing either the imaginate node or the entire graph - fn evaluate_node_graph( - &mut self, - document_id: u64, - layer_path: Vec, - (image_data, (width, height)): (Vec, (u32, u32)), - imaginate_node: Option>, - preferences: &PreferencesMessageHandler, - responses: &mut VecDeque, - ) -> Result<(), String> { - // Reformat the input image data into an f32 image - let image = graphene_core::raster::Image::from_image_data(&image_data, width, height); - - // Get the node graph layer - let document = self.documents.get_mut(&document_id).ok_or_else(|| "Invalid document".to_string())?; - let layer = document.document_legacy.layer(&layer_path).map_err(|e| format!("No layer: {e:?}"))?; - let node_graph_frame = match &layer.data { - LayerDataType::NodeGraphFrame(frame) => Ok(frame), - _ => Err("Invalid layer type".to_string()), - }?; - let network = node_graph_frame.network.clone(); - - // Execute the node graph - if let Some(imaginate_node) = imaginate_node { - use graph_craft::imaginate_input::*; - - let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); - - let resolution: Option = Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Resolution"), Cow::Borrowed(&image))?; - let resolution = resolution.unwrap_or_else(|| { - let transform = document.document_legacy.root.transform.inverse() * document.document_legacy.multiply_transforms(&layer_path).unwrap(); - let (x, y) = pick_safe_imaginate_resolution((transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length())); - DVec2::new(x as f64, y as f64) - }); - - let transform = document.document_legacy.root.transform.inverse() * document.document_legacy.multiply_transforms(&layer_path).unwrap(); - let parameters = ImaginateGenerationParameters { - seed: Self::compute_input::(&mut self.executor, &network, &imaginate_node, get("Seed"), Cow::Borrowed(&image))? as u64, - resolution: resolution.as_uvec2().into(), - samples: Self::compute_input::(&mut self.executor, &network, &imaginate_node, get("Samples"), Cow::Borrowed(&image))? as u32, - sampling_method: Self::compute_input::(&mut self.executor, &network, &imaginate_node, get("Sampling Method"), Cow::Borrowed(&image))? - .api_value() - .to_string(), - text_guidance: Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Prompt Guidance"), Cow::Borrowed(&image))?, - text_prompt: Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Prompt"), Cow::Borrowed(&image))?, - negative_prompt: Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Negative Prompt"), Cow::Borrowed(&image))?, - image_creativity: Some(Self::compute_input::(&mut self.executor, &network, &imaginate_node, get("Image Creativity"), Cow::Borrowed(&image))? / 100.), - restore_faces: Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Improve Faces"), Cow::Borrowed(&image))?, - tiling: Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Tiling"), Cow::Borrowed(&image))?, - }; - let use_base_image = Self::compute_input::(&mut self.executor, &network, &imaginate_node, get("Adapt Input Image"), Cow::Borrowed(&image))?; - - let base_image = if use_base_image { - let image: Image = Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Input Image"), Cow::Borrowed(&image))?; - // Only use if has size - if image.width > 0 && image.height > 0 { - let (image_data, size) = Self::encode_img(image, Some(resolution), image::ImageOutputFormat::Png)?; - let size = DVec2::new(size.0 as f64, size.1 as f64); - let mime = "image/png".to_string(); - Some(ImaginateBaseImage { image_data, size, mime }) - } else { - None - } - } else { - None - }; - - let mask_image = - if base_image.is_some() { - let mask_path: Option> = Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Masking Layer"), Cow::Borrowed(&image))?; - - // Calculate the size of the node graph frame - let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); - - // Render the masking layer within the node graph frame - let old_transforms = document.remove_document_transform(); - let mask_is_some = mask_path.is_some(); - let mask_image = mask_path.filter(|mask_layer_path| document.document_legacy.layer(mask_layer_path).is_ok()).map(|mask_layer_path| { - let render_mode = DocumentRenderMode::LayerCutout(&mask_layer_path, graphene_core::raster::color::Color::WHITE); - let svg = document.render_document(size, transform.inverse(), &self.persistent_data, render_mode); - - ImaginateMaskImage { svg, size } - }); - - if mask_is_some && mask_image.is_none() { - return Err("Imagination masking layer is missing.\nIt may have been deleted or moved. Please drag a new layer reference\ninto the 'Masking Layer' parameter input, then generate again.".to_string()); - } - - document.restore_document_transform(old_transforms); - mask_image - } else { - None - }; - - responses.push_back( - FrontendMessage::TriggerImaginateGenerate { - parameters: Box::new(parameters), - base_image: base_image.map(Box::new), - mask_image: mask_image.map(Box::new), - mask_paint_mode: if Self::compute_input::(&mut self.executor, &network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image))? { - ImaginateMaskPaintMode::Inpaint - } else { - ImaginateMaskPaintMode::Outpaint - }, - mask_blur_px: Self::compute_input::(&mut self.executor, &network, &imaginate_node, get("Mask Blur"), Cow::Borrowed(&image))? as u32, - imaginate_mask_starting_fill: Self::compute_input(&mut self.executor, &network, &imaginate_node, get("Mask Starting Fill"), Cow::Borrowed(&image))?, - hostname: preferences.imaginate_server_hostname.clone(), - refresh_frequency: preferences.imaginate_refresh_frequency, - document_id, - layer_path, - node_path: imaginate_node, - } - .into(), - ); - } else { - let mut image: Image = Self::execute_network(&mut self.executor, network, image)?; - - // If no image was generated, use the input image - if image.width == 0 || image.height == 0 { - image = graphene_core::raster::Image::from_image_data(&image_data, width, height); - } - - let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?; - - responses.push_back( - DocumentOperation::SetNodeGraphFrameImageData { - layer_path: layer_path.clone(), - image_data: image_data.clone(), - } - .into(), - ); - let mime = "image/bmp".to_string(); - let image_data = std::sync::Arc::new(image_data); - responses.push_back( - FrontendMessage::UpdateImageData { - document_id, - image_data: vec![FrontendImageData { path: layer_path, image_data, mime }], - } - .into(), - ); - } - - Ok(()) - } } diff --git a/editor/src/messages/tool/tool_messages/imaginate_tool.rs b/editor/src/messages/tool/tool_messages/imaginate_tool.rs index b9de5e8249..3beb44b1a1 100644 --- a/editor/src/messages/tool/tool_messages/imaginate_tool.rs +++ b/editor/src/messages/tool/tool_messages/imaginate_tool.rs @@ -1,7 +1,7 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; use crate::messages::layout::utility_types::layout_widget::PropertyHolder; -use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; +use crate::messages::portfolio::document::node_graph::{self, IMAGINATE_NODE}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; @@ -123,18 +123,14 @@ impl Fsm for ImaginateToolFsmState { let imaginate_node_type = IMAGINATE_NODE; let mut imaginate_inputs: Vec = imaginate_node_type.inputs.iter().map(|input| input.default.clone()).collect(); - imaginate_inputs[0] = NodeInput::Node(0); + imaginate_inputs[0] = NodeInput::node(0, 0); + imaginate_inputs[1] = NodeInput::node(0, 1); - let imaginate_node_id = 2; - let mut network = NodeNetwork::new_network(32, imaginate_node_id); + let imaginate_node_id = 100; + let mut network = node_graph::new_image_network(32, imaginate_node_id); network.nodes.insert( imaginate_node_id, - DocumentNode { - name: imaginate_node_type.name.to_string(), - inputs: imaginate_inputs, - implementation: imaginate_node_type.generate_implementation(), - metadata: graph_craft::document::DocumentNodeMetadata { position: (20, 4).into() }, - }, + imaginate_node_type.to_document_node(imaginate_inputs, graph_craft::document::DocumentNodeMetadata::position((20, 3))), ); responses.push_back( diff --git a/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs b/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs index 4d5f93d91d..ebb390f751 100644 --- a/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs +++ b/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs @@ -1,6 +1,7 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; use crate::messages::layout::utility_types::layout_widget::PropertyHolder; +use crate::messages::portfolio::document::node_graph; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; @@ -117,7 +118,7 @@ impl Fsm for NodeGraphToolFsmState { shape_data.path = Some(document.get_path_for_new_layer()); responses.push_back(DocumentMessage::DeselectAllLayers.into()); - let network = graph_craft::document::NodeNetwork::new_network(20, 0); + let network = node_graph::new_image_network(20, 0); responses.push_back( Operation::AddNodeGraphFrame { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs new file mode 100644 index 0000000000..6c8d658548 --- /dev/null +++ b/editor/src/node_graph_executor.rs @@ -0,0 +1,277 @@ +use crate::messages::frontend::utility_types::FrontendImageData; +use crate::messages::portfolio::document::utility_types::misc::DocumentRenderMode; +use crate::messages::portfolio::utility_types::PersistentData; +use crate::messages::prelude::*; + +use document_legacy::{document::pick_safe_imaginate_resolution, layers::layer_info::LayerDataType}; +use document_legacy::{LayerId, Operation}; +use graph_craft::document::{generate_uuid, value::TaggedValue, NodeId, NodeInput, NodeNetwork, NodeOutput}; +use graph_craft::executor::Compiler; +use graphene_core::raster::{Image, ImageFrame}; +use interpreted_executor::executor::DynamicExecutor; + +use glam::{DAffine2, DVec2}; +use std::borrow::Cow; + +#[derive(Debug, Clone, Default)] +pub struct NodeGraphExecutor { + executor: DynamicExecutor, +} + +impl NodeGraphExecutor { + /// Sets the transform property on the input node + fn set_input_transform(network: &mut NodeNetwork, transform: DAffine2) { + let Some(input_node) = network.nodes.get_mut(&network.inputs[0]) else { + return; + }; + input_node.inputs[1] = NodeInput::value(TaggedValue::DAffine2(transform), false); + } + + /// Execute the network by flattening it and creating a borrow stack. Casts the output to the generic `T`. + fn execute_network(&mut self, mut network: NodeNetwork, image_frame: ImageFrame) -> Result { + Self::set_input_transform(&mut network, image_frame.transform); + network.duplicate_outputs(&mut generate_uuid); + network.remove_dead_nodes(); + + // We assume only one output + assert_eq!(network.outputs.len(), 1, "Graph with multiple outputs not yet handled"); + let c = Compiler {}; + let proto_network = c.compile_single(network, true)?; + + assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?"); + self.executor.update(proto_network); + + use dyn_any::IntoDynAny; + use graph_craft::executor::Executor; + + let boxed = self.executor.execute(image_frame.image.into_dyn()).map_err(|e| e.to_string())?; + + dyn_any::downcast::(boxed).map(|v| *v) + } + + /// Computes an input for a node in the graph + pub fn compute_input(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image_frame: Cow) -> Result { + let mut network = old_network.clone(); + // Adjust the output of the graph so we find the relevant output + 'outer: for end in (0..node_path.len()).rev() { + let mut inner_network = &mut network; + for &node_id in &node_path[..end] { + inner_network.outputs[0] = NodeOutput::new(node_id, 0); + + let Some(new_inner) = inner_network.nodes.get_mut(&node_id).and_then(|node| node.implementation.get_network_mut()) else { + return Err("Failed to find network".to_string()); + }; + inner_network = new_inner; + } + match &inner_network.nodes.get(&node_path[end]).unwrap().inputs[input_index] { + // If the input is from a parent network then adjust the input index and continue iteration + NodeInput::Network => { + input_index = inner_network + .inputs + .iter() + .enumerate() + .filter(|&(_index, &id)| id == node_path[end]) + .nth(input_index) + .ok_or_else(|| "Invalid network input".to_string())? + .0; + } + // If the input is just a value, return that value + NodeInput::Value { tagged_value, .. } => return dyn_any::downcast::(tagged_value.clone().to_any()).map(|v| *v), + // If the input is from a node, set the node to be the output (so that is what is evaluated) + NodeInput::Node { node_id, output_index } => { + inner_network.outputs[0] = NodeOutput::new(*node_id, *output_index); + break 'outer; + } + } + } + + self.execute_network(network, image_frame.into_owned()) + } + + /// Encodes an image into a format using the image crate + fn encode_img(image: Image, resize: Option, format: image::ImageOutputFormat) -> Result<(Vec, (u32, u32)), String> { + use image::{ImageBuffer, Rgba}; + use std::io::Cursor; + + let (result_bytes, width, height) = image.as_flat_u8(); + + let mut output: ImageBuffer, _> = image::ImageBuffer::from_raw(width, height, result_bytes).ok_or_else(|| "Invalid image size".to_string())?; + if let Some(size) = resize { + let size = size.as_uvec2(); + if size.x > 0 && size.y > 0 { + output = image::imageops::resize(&output, size.x, size.y, image::imageops::Triangle); + } + } + let size = output.dimensions(); + let mut image_data: Vec = Vec::new(); + output.write_to(&mut Cursor::new(&mut image_data), format).map_err(|e| e.to_string())?; + Ok::<_, String>((image_data, size)) + } + + fn generate_imaginate( + &mut self, + network: NodeNetwork, + imaginate_node: Vec, + (document, document_id): (&mut DocumentMessageHandler, u64), + layer_path: Vec, + image_frame: ImageFrame, + (preferences, persistent_data): (&PreferencesMessageHandler, &PersistentData), + ) -> Result { + use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; + use graph_craft::imaginate_input::*; + + let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); + + let transform: DAffine2 = self.compute_input(&network, &imaginate_node, get("Transform"), Cow::Borrowed(&image_frame))?; + + let resolution: Option = self.compute_input(&network, &imaginate_node, get("Resolution"), Cow::Borrowed(&image_frame))?; + let resolution = resolution.unwrap_or_else(|| { + let (x, y) = pick_safe_imaginate_resolution((transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length())); + DVec2::new(x as f64, y as f64) + }); + + let parameters = ImaginateGenerationParameters { + seed: self.compute_input::(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&image_frame))? as u64, + resolution: resolution.as_uvec2().into(), + samples: self.compute_input::(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&image_frame))? as u32, + sampling_method: self + .compute_input::(&network, &imaginate_node, get("Sampling Method"), Cow::Borrowed(&image_frame))? + .api_value() + .to_string(), + text_guidance: self.compute_input(&network, &imaginate_node, get("Prompt Guidance"), Cow::Borrowed(&image_frame))?, + text_prompt: self.compute_input(&network, &imaginate_node, get("Prompt"), Cow::Borrowed(&image_frame))?, + negative_prompt: self.compute_input(&network, &imaginate_node, get("Negative Prompt"), Cow::Borrowed(&image_frame))?, + image_creativity: Some(self.compute_input::(&network, &imaginate_node, get("Image Creativity"), Cow::Borrowed(&image_frame))? / 100.), + restore_faces: self.compute_input(&network, &imaginate_node, get("Improve Faces"), Cow::Borrowed(&image_frame))?, + tiling: self.compute_input(&network, &imaginate_node, get("Tiling"), Cow::Borrowed(&image_frame))?, + }; + let use_base_image = self.compute_input::(&network, &imaginate_node, get("Adapt Input Image"), Cow::Borrowed(&image_frame))?; + + let base_image = if use_base_image { + let image: Image = self.compute_input(&network, &imaginate_node, get("Input Image"), Cow::Borrowed(&image_frame))?; + // Only use if has size + if image.width > 0 && image.height > 0 { + let (image_data, size) = Self::encode_img(image, Some(resolution), image::ImageOutputFormat::Png)?; + let size = DVec2::new(size.0 as f64, size.1 as f64); + let mime = "image/png".to_string(); + Some(ImaginateBaseImage { image_data, size, mime }) + } else { + None + } + } else { + None + }; + + let mask_image = if base_image.is_some() { + let mask_path: Option> = self.compute_input(&network, &imaginate_node, get("Masking Layer"), Cow::Borrowed(&image_frame))?; + + // Calculate the size of the node graph frame + let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); + + // Render the masking layer within the node graph frame + let old_transforms = document.remove_document_transform(); + let mask_is_some = mask_path.is_some(); + let mask_image = mask_path.filter(|mask_layer_path| document.document_legacy.layer(mask_layer_path).is_ok()).map(|mask_layer_path| { + let render_mode = DocumentRenderMode::LayerCutout(&mask_layer_path, graphene_core::raster::color::Color::WHITE); + let svg = document.render_document(size, transform.inverse(), persistent_data, render_mode); + + ImaginateMaskImage { svg, size } + }); + + if mask_is_some && mask_image.is_none() { + return Err( + "Imagination masking layer is missing.\nIt may have been deleted or moved. Please drag a new layer reference\ninto the 'Masking Layer' parameter input, then generate again." + .to_string(), + ); + } + + document.restore_document_transform(old_transforms); + mask_image + } else { + None + }; + + Ok(FrontendMessage::TriggerImaginateGenerate { + parameters: Box::new(parameters), + base_image: base_image.map(Box::new), + mask_image: mask_image.map(Box::new), + mask_paint_mode: if self.compute_input::(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image_frame))? { + ImaginateMaskPaintMode::Inpaint + } else { + ImaginateMaskPaintMode::Outpaint + }, + mask_blur_px: self.compute_input::(&network, &imaginate_node, get("Mask Blur"), Cow::Borrowed(&image_frame))? as u32, + imaginate_mask_starting_fill: self.compute_input(&network, &imaginate_node, get("Mask Starting Fill"), Cow::Borrowed(&image_frame))?, + hostname: preferences.imaginate_server_hostname.clone(), + refresh_frequency: preferences.imaginate_refresh_frequency, + document_id, + layer_path, + node_path: imaginate_node, + } + .into()) + } + + /// Evaluates a node graph, computing either the imaginate node or the entire graph + pub fn evaluate_node_graph( + &mut self, + (document_id, documents): (u64, &mut HashMap), + layer_path: Vec, + (image_data, (width, height)): (Vec, (u32, u32)), + imaginate_node: Option>, + persistent_data: (&PreferencesMessageHandler, &PersistentData), + responses: &mut VecDeque, + ) -> Result<(), String> { + // Reformat the input image data into an f32 image + let image = graphene_core::raster::Image::from_image_data(&image_data, width, height); + + // Get the node graph layer + let document = documents.get_mut(&document_id).ok_or_else(|| "Invalid document".to_string())?; + let layer = document.document_legacy.layer(&layer_path).map_err(|e| format!("No layer: {e:?}"))?; + + // Construct the input image frame + let transform = layer.transform; + let image_frame = ImageFrame { image, transform }; + + let node_graph_frame = match &layer.data { + LayerDataType::NodeGraphFrame(frame) => Ok(frame), + _ => Err("Invalid layer type".to_string()), + }?; + let network = node_graph_frame.network.clone(); + + // Execute the node graph + if let Some(imaginate_node) = imaginate_node { + responses.push_back(self.generate_imaginate(network, imaginate_node, (document, document_id), layer_path, image_frame, persistent_data)?); + } else { + let ImageFrame { mut image, transform } = self.execute_network(network, image_frame)?; + + // If no image was generated, use the input image + if image.width == 0 || image.height == 0 { + image = graphene_core::raster::Image::from_image_data(&image_data, width, height); + } + + let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?; + + responses.push_back( + Operation::SetNodeGraphFrameImageData { + layer_path: layer_path.clone(), + image_data: image_data.clone(), + } + .into(), + ); + let mime = "image/bmp".to_string(); + let image_data = std::sync::Arc::new(image_data); + let image_data = vec![FrontendImageData { + path: layer_path.clone(), + image_data, + mime, + }]; + responses.push_back(FrontendMessage::UpdateImageData { document_id, image_data }.into()); + + // Update the transform based on the graph output + let transform = transform.to_cols_array(); + responses.push_back(Operation::SetLayerTransform { path: layer_path, transform }.into()); + } + + Ok(()) + } +} diff --git a/frontend-svelte/src/components/panels/NodeGraph.svelte b/frontend-svelte/src/components/panels/NodeGraph.svelte index 2a93f1c11e..60855ba69a 100644 --- a/frontend-svelte/src/components/panels/NodeGraph.svelte +++ b/frontend-svelte/src/components/panels/NodeGraph.svelte @@ -92,12 +92,15 @@ } function resolveLink(link: FrontendNodeLink, containerBounds: HTMLDivElement): { nodePrimaryOutput: HTMLDivElement | undefined; nodePrimaryInput: HTMLDivElement | undefined } { - const connectorIndex = Number(link.linkEndInputIndex); + const outputIndex = Number(link.linkStartOutputIndex); + const inputIndex = Number(link.linkEndInputIndex); - const nodePrimaryOutput = (containerBounds.querySelector(`[data-node="${String(link.linkStart)}"] [data-port="output"]`) || undefined) as HTMLDivElement | undefined; + const nodeOutputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkStart)}"] [data-port="output"]`) || undefined; const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkEnd)}"] [data-port="input"]`) || undefined; - const nodePrimaryInput = nodeInputConnectors?.[connectorIndex] as HTMLDivElement | undefined; + + const nodePrimaryOutput = nodeOutputConnectors?.[outputIndex] as HTMLDivElement | undefined; + const nodePrimaryInput = nodeInputConnectors?.[inputIndex] as HTMLDivElement | undefined; return { nodePrimaryOutput, nodePrimaryInput }; } @@ -263,8 +266,8 @@ const inputIndexInt = BigInt(inputIndex); const links = $nodeGraph.links; const linkIndex = links.findIndex((value) => value.linkEnd === nodeIdInt && value.linkEndInputIndex === inputIndexInt); - const queryString = `[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`; - linkInProgressFromConnector = (nodesContainer.querySelector(queryString) || undefined) as HTMLDivElement | undefined; + const nodeOutputConnectors = nodesContainer.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined; + linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined; const nodeInputConnectors = nodesContainer.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined; linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined; disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex }; @@ -359,13 +362,16 @@ if (outputNode && inputNode && outputConnectedNodeID && inputConnectedNodeID) { const inputNodeInPorts = Array.from(inputNode.querySelectorAll(`[data-port="input"]`)); + const outputNodeInPorts = Array.from(outputNode.querySelectorAll(`[data-port="output"]`)); + const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(linkInProgressToConnector); + const outputNodeConnectionIndexSearch = outputNodeInPorts.indexOf(linkInProgressFromConnector); + const inputNodeConnectionIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined; + const outputNodeConnectionIndex = outputNodeConnectionIndexSearch > -1 ? outputNodeConnectionIndexSearch : undefined; - if (inputNodeConnectionIndex !== undefined) { - // const oneBasedIndex = inputNodeConnectionIndex + 1; - - editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), BigInt(inputConnectedNodeID), inputNodeConnectionIndex); + if (inputNodeConnectionIndex !== undefined && outputNodeConnectionIndex !== undefined) { + editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), outputNodeConnectionIndex, BigInt(inputConnectedNodeID), inputNodeConnectionIndex); } } } else if (draggingNodes) { @@ -411,8 +417,8 @@ }); // If the node has been dragged on top of the link then connect it into the middle. if (link) { - editor.instance.connectNodesByLink(link.linkStart, selectedNodeId, 0); - editor.instance.connectNodesByLink(selectedNodeId, link.linkEnd, Number(link.linkEndInputIndex)); + editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, 0); + editor.instance.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex)); editor.instance.shiftNode(selectedNodeId); } } @@ -482,10 +488,11 @@ {/if}
{#each $nodeGraph.nodes as node (String(node.id))} + {@const exposedInputsOutputs = [...node.exposedInputs, ...node.outputs.slice(1)]}
@@ -519,22 +526,34 @@ {node.displayName}
- {#if node.exposedInputs.length > 0} -
- {#each node.exposedInputs as argument, index (index)} -
+ {#if exposedInputsOutputs.length > 0} +
+ {#each exposedInputsOutputs as parameter, index (index)} +
-
-
-
+ {#if index < node.exposedInputs.length} +
+
+
+ {:else} +
+
+
+ {/if}
- {argument.name} + {parameter.name}
{/each}
@@ -656,7 +675,7 @@ } } - &.output { + &.previewed { outline: 3px solid var(--color-data-vector); } @@ -679,20 +698,28 @@ } } - .arguments { + .parameters { display: flex; flex-direction: column; width: 100%; position: relative; - .argument { + .parameter { position: relative; display: flex; align-items: center; height: 24px; - width: 100%; + width: calc(100% - 24px * 2); margin-left: 24px; margin-right: 24px; + + .text-label { + width: 100%; + + &.output { + text-align: right; + } + } } // Squares to cover up the rounded corners of the primary area and make them have a straight edge diff --git a/frontend-svelte/src/wasm-communication/messages.ts b/frontend-svelte/src/wasm-communication/messages.ts index 52b653d09e..0c7d601ae3 100644 --- a/frontend-svelte/src/wasm-communication/messages.ts +++ b/frontend-svelte/src/wasm-communication/messages.ts @@ -83,6 +83,12 @@ export class NodeGraphInput { readonly name!: string; } +export class NodeGraphOutput { + readonly dataType!: FrontendGraphDataType; + + readonly name!: string; +} + export class FrontendNode { readonly id!: bigint; @@ -92,12 +98,12 @@ export class FrontendNode { readonly exposedInputs!: NodeGraphInput[]; - readonly outputs!: FrontendGraphDataType[]; + readonly outputs!: NodeGraphOutput[]; @TupleToVec2 readonly position!: XY | undefined; - readonly output!: boolean; + readonly previewed!: boolean; readonly disabled!: boolean; } @@ -105,6 +111,8 @@ export class FrontendNode { export class FrontendNodeLink { readonly linkStart!: bigint; + readonly linkStartOutputIndex!: bigint; + readonly linkEnd!: bigint; readonly linkEndInputIndex!: bigint; diff --git a/frontend-svelte/wasm/src/editor_api.rs b/frontend-svelte/wasm/src/editor_api.rs index 7f5c979869..d2e204ff89 100644 --- a/frontend-svelte/wasm/src/editor_api.rs +++ b/frontend-svelte/wasm/src/editor_api.rs @@ -578,9 +578,10 @@ impl JsEditorHandle { /// Notifies the backend that the user connected a node's primary output to one of another node's inputs #[wasm_bindgen(js_name = connectNodesByLink)] - pub fn connect_nodes_by_link(&self, output_node: u64, input_node: u64, input_node_connector_index: usize) { + pub fn connect_nodes_by_link(&self, output_node: u64, output_node_connector_index: usize, input_node: u64, input_node_connector_index: usize) { let message = NodeGraphMessage::ConnectNodesByLink { output_node, + output_node_connector_index, input_node, input_node_connector_index, }; diff --git a/frontend/src/components/panels/NodeGraph.vue b/frontend/src/components/panels/NodeGraph.vue index b4f266a00f..dece0db94d 100644 --- a/frontend/src/components/panels/NodeGraph.vue +++ b/frontend/src/components/panels/NodeGraph.vue @@ -36,7 +36,7 @@ v-for="node in nodes" :key="String(node.id)" class="node" - :class="{ selected: selected.includes(node.id), output: node.output, disabled: node.disabled }" + :class="{ selected: selected.includes(node.id), previewed: node.previewed, disabled: node.disabled }" :style="{ '--offset-left': (node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0), '--offset-top': (node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0), @@ -58,8 +58,8 @@ v-if="node.outputs.length > 0" class="output port" data-port="output" - :data-datatype="node.outputs[0]" - :style="{ '--data-color': `var(--color-data-${node.outputs[0]})`, '--data-color-dim': `var(--color-data-${node.outputs[0]}-dim)` }" + :data-datatype="node.outputs[0].dataType" + :style="{ '--data-color': `var(--color-data-${node.outputs[0].dataType})`, '--data-color-dim': `var(--color-data-${node.outputs[0].dataType}-dim)` }" >
@@ -67,19 +67,32 @@ {{ node.displayName }}
-
-
+
+
+
+
+
- {{ argument.name }} + {{ parameter.name }}
@@ -208,7 +221,7 @@ } } - &.output { + &.previewed { outline: 3px solid var(--color-data-vector); } @@ -231,20 +244,28 @@ } } - .arguments { + .parameters { display: flex; flex-direction: column; width: 100%; position: relative; - .argument { + .parameter { position: relative; display: flex; align-items: center; height: 24px; - width: 100%; + width: calc(100% - 24px * 2); margin-left: 24px; margin-right: 24px; + + .text-label { + width: 100%; + + &.output { + text-align: right; + } + } } // Squares to cover up the rounded corners of the primary area and make them have a straight edge @@ -315,8 +336,6 @@