Update Imaginate to output bitmap data to the graph via Image Frame node (#1001)

* Multiple node outputs

* Add new nodes

* gcore use std by default to allow for testing

* Allow multiple node outputs

* Multiple outputs to frontend

* Add ImageFrameNode to node registry

* Minor cleanup

* Basic transform implementation

* Add some logging to image encoding

* Fix ImageFrameNode

* Add transform input to Imaginate node (#1014)

* Add transform input to imaginate node

* Force the resolution to be edited with no transform

* Add transform to imaginate generation

* Fix compilation

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>

* Add labels to node outputs

* Fix seed; disable mask when transform is disconnected; add Imaginate tooltips

* Rename 'Input Multiple' node to 'Input'

* Code review

* Replicate to Svelte

* Show only the primary input chain in the Properties panel

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2023-02-11 08:56:31 +00:00 committed by Keavon Chambers
parent a709a772d5
commit 1b0e1b9bdf
35 changed files with 1172 additions and 553 deletions

View file

@ -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;

View file

@ -107,6 +107,7 @@ pub enum DocumentMessage {
},
NodeGraphFrameImaginateRandom {
imaginate_node: Vec<NodeId>,
then_generate: bool,
},
NodeGraphFrameImaginateTerminate {
layer_path: Vec<LayerId>,

View file

@ -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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler)> for DocumentMessageHandler {
impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler, &mut NodeGraphExecutor)> for DocumentMessageHandler {
#[remain::check]
fn process_message(
&mut self,
message: DocumentMessage,
responses: &mut VecDeque<Message>,
(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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
artboard_document: &self.artboard_message_handler.artboards_document,
selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice())),
node_graph_message_handler: &self.node_graph_handler,
executor,
};
self.properties_panel_message_handler
.process_message(message, responses, (persistent_data, properties_panel_message_handler_data));
@ -527,18 +529,22 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
responses.push_back(message);
}
}
NodeGraphFrameImaginateRandom { imaginate_node } => {
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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
responses.push_back(DocumentMessage::StartTransaction.into());
let path = vec![generate_uuid()];
let image_node_id = 2;
let mut network = graph_craft::document::NodeNetwork::new_network(32, image_node_id);
let image_node_id = 100;
let mut network = crate::messages::portfolio::document::node_graph::new_image_network(32, image_node_id);
let Some(image_node_type) = crate::messages::portfolio::document::node_graph::resolve_document_node_type("Image") else {
warn!("Image node should be in registry");
@ -609,12 +615,10 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
network.nodes.insert(
image_node_id,
graph_craft::document::DocumentNode {
name: image_node_type.name.to_string(),
inputs: vec![graph_craft::document::NodeInput::value(graph_craft::document::value::TaggedValue::Image(image), false)],
implementation: image_node_type.generate_implementation(),
metadata: graph_craft::document::DocumentNodeMetadata { position: (20, 4).into() },
},
image_node_type.to_document_node(
[graph_craft::document::NodeInput::value(graph_craft::document::value::TaggedValue::Image(image), false)],
graph_craft::document::DocumentNodeMetadata::position((20, 4)),
),
);
responses.push_back(

View file

@ -12,6 +12,7 @@ pub enum NodeGraphMessage {
CloseNodeGraph,
ConnectNodesByLink {
output_node: u64,
output_node_connector_index: usize,
input_node: u64,
input_node_connector_index: usize,
},

View file

@ -9,7 +9,7 @@ use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
use document_legacy::layers::nodegraph_layer::NodeGraphFrameLayer;
use document_legacy::LayerId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, NodeOutput};
mod document_node_types;
mod node_properties;
@ -39,7 +39,7 @@ impl FrontendGraphDataType {
pub const fn with_tagged_value(value: &TaggedValue) -> 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<FrontendGraphDataType>,
#[serde(rename = "exposedInputs")]
pub exposed_inputs: Vec<NodeGraphInput>,
pub outputs: Vec<FrontendGraphDataType>,
pub outputs: Vec<NodeGraphOutput>, // 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<LayoutGroup>) {
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::<Vec<_>>();
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<NodeId, NodeId>) -> impl Iterator<Item = (NodeId, DocumentNode)> + '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<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
}
NodeGraphMessage::ConnectNodesByLink {
output_node,
output_node_connector_index,
input_node,
input_node_connector_index,
} => {
@ -422,7 +448,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
responses.push_back(DocumentMessage::StartTransaction.into());
let input = NodeInput::Node(output_node);
let input = NodeInput::node(output_node, output_node_connector_index);
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
let should_rerender = network.connected_to_output(node_id);
@ -454,12 +480,10 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
responses.push_back(DocumentMessage::StartTransaction.into());
let document_node = DocumentNode {
name: node_type.clone(),
inputs: document_node_type.inputs.iter().map(|input| input.default.clone()).collect(),
implementation: document_node_type.generate_implementation(),
metadata: graph_craft::document::DocumentNodeMetadata { position: (x, y).into() },
};
let document_node = document_node_type.to_document_node(
document_node_type.inputs.iter().map(|input| input.default.clone()),
graph_craft::document::DocumentNodeMetadata::position((x, y)),
);
responses.push_back(NodeGraphMessage::InsertNode { node_id, document_node }.into());
responses.push_back(NodeGraphMessage::SendGraph { should_rerender: false }.into());
@ -782,7 +806,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
.get(&node_id)
.map_or(&Vec::new(), |node| &node.inputs)
.iter()
.filter_map(|input| if let NodeInput::Node(previous_id) = input { Some(*previous_id) } else { None })
.filter_map(|input| if let NodeInput::Node { node_id: previous_id, .. } = input { Some(*previous_id) } else { None })
.collect::<Vec<_>>();
for input_node in inputs {
@ -812,9 +836,11 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
// Remove all selected nodes from the disabled list
network.disabled.retain(|id| !self.selected_nodes.contains(id));
} else {
let original_output = network.original_output();
let original_outputs = network.original_outputs().iter().map(|output| output.node_id).collect::<Vec<_>>();
// 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<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
}
NodeGraphMessage::TogglePreviewImpl { node_id } => {
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;
}

View file

@ -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<crate::messages::prelude::Message>,
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<LayoutGroup>,
}
fn document_node_types() -> Vec<DocumentNodeType> {
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<DocumentNodeType> {
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<DocumentNodeType> {
.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<std::sync::Arc<Image>>")]),
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<Item = NodeInput>, 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()
}
}

View file

@ -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<LayoutGroup> {
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.)",
)
};

View file

@ -28,6 +28,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
artboard_document,
selected_layers,
node_graph_message_handler,
executor,
} = data;
let get_document = |document_selector: TargetDocument| match document_selector {
TargetDocument::Artboard => artboard_document,
@ -166,7 +167,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
let layer = document.layer(&path).unwrap();
match target_document {
TargetDocument::Artboard => 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),
}
}
}

View file

@ -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<Message>,
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);

View file

@ -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<Item = &'a [LayerId]>,
pub node_graph_message_handler: &'a NodeGraphMessageHandler,
pub executor: &'a mut NodeGraphExecutor,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize, specta::Type)]

View file

@ -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<u64, DocumentMessageHandler>,
document_ids: Vec<u64>,
executor: interpreted_executor::executor::DynamicExecutor,
executor: NodeGraphExecutor,
active_document_id: Option<u64>,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
pub persistent_data: PersistentData,
@ -50,7 +42,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
PortfolioMessage::Document(message) => {
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<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
#[remain::unsorted]
PortfolioMessage::DocumentPassMessage { document_id, message } => {
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<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
size,
imaginate_node,
} => {
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<T: dyn_any::StaticType>(executor: &mut DynamicExecutor, network: NodeNetwork, image: Image) -> Result<T, String> {
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::<T>(boxed).map(|v| *v)
}
/// Computes an input for a node in the graph
fn compute_input<T: dyn_any::StaticType>(executor: &mut DynamicExecutor, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image: Cow<Image>) -> Result<T, String> {
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::<T>(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<DVec2>, format: image::ImageOutputFormat) -> Result<(Vec<u8>, (u32, u32)), String> {
use image::{ImageBuffer, Rgba};
use std::io::Cursor;
let mut image_data: Vec<u8> = 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<Rgba<u8>, _> = 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<LayerId>,
(image_data, (width, height)): (Vec<u8>, (u32, u32)),
imaginate_node: Option<Vec<NodeId>>,
preferences: &PreferencesMessageHandler,
responses: &mut VecDeque<Message>,
) -> 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<glam::DVec2> = 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::<f64>(&mut self.executor, &network, &imaginate_node, get("Seed"), Cow::Borrowed(&image))? as u64,
resolution: resolution.as_uvec2().into(),
samples: Self::compute_input::<f64>(&mut self.executor, &network, &imaginate_node, get("Samples"), Cow::Borrowed(&image))? as u32,
sampling_method: Self::compute_input::<ImaginateSamplingMethod>(&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::<f64>(&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::<bool>(&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<Vec<LayerId>> = 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::<bool>(&mut self.executor, &network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image))? {
ImaginateMaskPaintMode::Inpaint
} else {
ImaginateMaskPaintMode::Outpaint
},
mask_blur_px: Self::compute_input::<f64>(&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(())
}
}

View file

@ -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<NodeInput> = 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(

View file

@ -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 {

View file

@ -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<T: dyn_any::StaticType>(&mut self, mut network: NodeNetwork, image_frame: ImageFrame) -> Result<T, String> {
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::<T>(boxed).map(|v| *v)
}
/// Computes an input for a node in the graph
pub fn compute_input<T: dyn_any::StaticType>(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image_frame: Cow<ImageFrame>) -> Result<T, String> {
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::<T>(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<DVec2>, format: image::ImageOutputFormat) -> Result<(Vec<u8>, (u32, u32)), String> {
use image::{ImageBuffer, Rgba};
use std::io::Cursor;
let (result_bytes, width, height) = image.as_flat_u8();
let mut output: ImageBuffer<Rgba<u8>, _> = 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<u8> = 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<NodeId>,
(document, document_id): (&mut DocumentMessageHandler, u64),
layer_path: Vec<LayerId>,
image_frame: ImageFrame,
(preferences, persistent_data): (&PreferencesMessageHandler, &PersistentData),
) -> Result<Message, String> {
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<glam::DVec2> = 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::<f64>(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&image_frame))? as u64,
resolution: resolution.as_uvec2().into(),
samples: self.compute_input::<f64>(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&image_frame))? as u32,
sampling_method: self
.compute_input::<ImaginateSamplingMethod>(&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::<f64>(&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::<bool>(&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<Vec<LayerId>> = 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::<bool>(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image_frame))? {
ImaginateMaskPaintMode::Inpaint
} else {
ImaginateMaskPaintMode::Outpaint
},
mask_blur_px: self.compute_input::<f64>(&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<u64, DocumentMessageHandler>),
layer_path: Vec<LayerId>,
(image_data, (width, height)): (Vec<u8>, (u32, u32)),
imaginate_node: Option<Vec<NodeId>>,
persistent_data: (&PreferencesMessageHandler, &PersistentData),
responses: &mut VecDeque<Message>,
) -> 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(())
}
}

View file

@ -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}
<div class="nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}>
{#each $nodeGraph.nodes as node (String(node.id))}
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.outputs.slice(1)]}
<div
class="node"
class:selected={selected.includes(node.id)}
class:output={node.output}
class:previewed={node.previewed}
class:disabled={node.disabled}
style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
style:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
@ -508,9 +515,9 @@
<div
class="output port"
data-port="output"
data-datatype={node.outputs[0]}
style:--data-color={`var(--color-data-${node.outputs[0]})`}
style:--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})`}
style:--data-color-dim={`var(--color-data-${node.outputs[0].dataType}-dim)`}
>
<div />
</div>
@ -519,22 +526,34 @@
<IconLabel icon={nodeIcon(node.displayName)} />
<TextLabel>{node.displayName}</TextLabel>
</div>
{#if node.exposedInputs.length > 0}
<div class="arguments">
{#each node.exposedInputs as argument, index (index)}
<div class="argument">
{#if exposedInputsOutputs.length > 0}
<div class="parameters">
{#each exposedInputsOutputs as parameter, index (index)}
<div class="parameter">
<div class="ports">
<div
class="input port"
data-port="input"
data-datatype={argument.dataType}
style:--data-color={`var(--color-data-${argument.dataType})`}
style:--data-color-dim={`var(--color-data-${argument.dataType}-dim)`}
>
<div />
</div>
{#if index < node.exposedInputs.length}
<div
class="input port"
data-port="input"
data-datatype={parameter.dataType}
style:--data-color={`var(--color-data-${parameter.dataType})`}
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
>
<div />
</div>
{:else}
<div
class="output port"
data-port="output"
data-datatype={parameter.dataType}
style:--data-color={`var(--color-data-${parameter.dataType})`}
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
>
<div />
</div>
{/if}
</div>
<TextLabel>{argument.name}</TextLabel>
<TextLabel class={index < node.exposedInputs.length ? "name" : "output"}>{parameter.name}</TextLabel>
</div>
{/each}
</div>
@ -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

View file

@ -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;

View file

@ -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,
};

View file

@ -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)` }"
>
<div></div>
</div>
@ -67,19 +67,32 @@
<IconLabel :icon="nodeIcon(node.displayName)" />
<TextLabel>{{ node.displayName }}</TextLabel>
</div>
<div v-if="node.exposedInputs.length > 0" class="arguments">
<div v-for="(argument, index) in node.exposedInputs" :key="index" class="argument">
<div v-if="[...node.exposedInputs, ...node.outputs.slice(1)].length > 0" class="parameters">
<div v-for="(parameter, index) in [...node.exposedInputs, ...node.outputs.slice(1)]" :key="index" class="parameter">
<div class="ports">
<div
v-if="index < node.exposedInputs.length"
class="input port"
data-port="input"
:data-datatype="argument.dataType"
:style="{ '--data-color': `var(--color-data-${argument.dataType})`, '--data-color-dim': `var(--color-data-${argument.dataType}-dim)` }"
:data-datatype="parameter.dataType"
:style="{
'--data-color': `var(--color-data-${parameter.dataType})`,
'--data-color-dim': `var(--color-data-${parameter.dataType}-dim)`,
}"
>
<div></div>
</div>
<div
v-else
class="output port"
data-port="output"
:data-datatype="parameter.dataType"
:style="{ '--data-color': `var(--color-data-${parameter.dataType})`, '--data-color-dim': `var(--color-data-${parameter.dataType}-dim)` }"
>
<div></div>
</div>
</div>
<TextLabel>{{ argument.name }}</TextLabel>
<TextLabel :class="index < node.exposedInputs.length ? 'name' : 'output'">{{ parameter.name }}</TextLabel>
</div>
</div>
</div>
@ -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 @@
<script lang="ts">
import { defineComponent, nextTick } from "vue";
// import type { FrontendNode } from "@/wasm-communication/messages";
import type { IconName } from "@/utility-functions/icons";
import { UpdateNodeGraphSelection, type FrontendNodeLink } from "@/wasm-communication/messages";
@ -411,12 +430,15 @@ export default defineComponent({
},
methods: {
resolveLink(link: FrontendNodeLink, containerBounds: HTMLDivElement): { nodePrimaryOutput: HTMLDivElement | undefined; nodePrimaryInput: HTMLDivElement | undefined } {
const connectorIndex = Number(link.linkEndInputIndex);
const nodePrimaryOutput = (containerBounds.querySelector(`[data-node="${String(link.linkStart)}"] [data-port="output"]`) || undefined) as HTMLDivElement | undefined;
const outputIndex = Number(link.linkStartOutputIndex);
const inputIndex = Number(link.linkEndInputIndex);
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 };
},
async refreshLinks(): Promise<void> {
@ -579,8 +601,8 @@ export default defineComponent({
const inputIndexInt = BigInt(inputIndex);
const links = this.nodeGraph.state.links;
const linkIndex = links.findIndex((value) => value.linkEnd === nodeIdInt && value.linkEndInputIndex === inputIndexInt);
const queryString = `[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`;
this.linkInProgressFromConnector = (containerBounds.querySelector(queryString) || undefined) as HTMLDivElement | undefined;
const nodeOutputConnectors = containerBounds.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined;
this.linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined;
const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined;
this.linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined;
this.disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex };
@ -674,13 +696,16 @@ export default defineComponent({
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(this.linkInProgressToConnector);
const outputNodeConnectionIndexSearch = outputNodeInPorts.indexOf(this.linkInProgressFromConnector);
const inputNodeConnectionIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
const outputNodeConnectionIndex = outputNodeConnectionIndexSearch > -1 ? outputNodeConnectionIndexSearch : undefined;
if (inputNodeConnectionIndex !== undefined) {
// const oneBasedIndex = inputNodeConnectionIndex + 1;
this.editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), BigInt(inputConnectedNodeID), inputNodeConnectionIndex);
if (inputNodeConnectionIndex !== undefined && outputNodeConnectionIndex !== undefined) {
this.editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), outputNodeConnectionIndex, BigInt(inputConnectedNodeID), inputNodeConnectionIndex);
}
}
} else if (this.draggingNodes) {
@ -728,8 +753,8 @@ export default defineComponent({
});
// If the node has been dragged on top of the link then connect it into the middle.
if (link) {
this.editor.instance.connectNodesByLink(link.linkStart, selectedNodeId, 0);
this.editor.instance.connectNodesByLink(selectedNodeId, link.linkEnd, Number(link.linkEndInputIndex));
this.editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, 0);
this.editor.instance.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
this.editor.instance.shiftNode(selectedNodeId);
}
}

View file

@ -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;

View file

@ -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,
};

View file

@ -9,9 +9,9 @@ fn main() {
let network = NodeNetwork {
inputs: vec![0],
output: 0,
outputs: vec![NodeOutput::new(0, 0)],
disabled: vec![],
previous_output: None,
previous_outputs: None,
nodes: [(
0,
DocumentNode {
@ -39,9 +39,9 @@ fn main() {
fn add_network() -> NodeNetwork {
NodeNetwork {
inputs: vec![0, 0],
output: 1,
outputs: vec![NodeOutput::new(1, 0)],
disabled: vec![],
previous_output: None,
previous_outputs: None,
nodes: [
(
0,
@ -56,7 +56,7 @@ fn add_network() -> NodeNetwork {
1,
DocumentNode {
name: "Add".into(),
inputs: vec![NodeInput::Node(0)],
inputs: vec![NodeInput::node(0, 0)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[generic!("T"), generic!("U")])),
},

View file

@ -10,7 +10,7 @@ license = "MIT OR Apache-2.0"
[features]
std = ["dyn-any", "dyn-any/std"]
default = ["async", "serde", "kurbo", "log"]
default = ["async", "serde", "kurbo", "log", "std"]
log = ["dep:log"]
serde = ["dep:serde", "glam/serde"]
gpu = ["spirv-std", "bytemuck", "glam/bytemuck", "dyn-any"]

View file

@ -275,7 +275,6 @@ mod test {
pub fn map_result() {
let value: ClonedNode<Result<&u32, ()>> = ClonedNode(Ok(&4u32));
assert_eq!(value.eval(()), Ok(&4u32));
static clone: &CloneNode<u32> = &CloneNode::new();
//let type_erased_clone = clone as &dyn for<'a> Node<'a, &'a u32, Output = u32>;
let map_result = MapResultNode::new(ValueNode::new(FnNode::new(|x: &u32| x.clone())));
//et type_erased = &map_result as &dyn for<'a> Node<'a, Result<&'a u32, ()>, Output = Result<u32, ()>>;

View file

@ -285,13 +285,14 @@ fn dimensions_node(input: ImageSlice<'input>) -> (u32, u32) {
}
#[cfg(feature = "alloc")]
pub use image::{CollectNode, Image, ImageRefNode, MapImageSliceNode};
pub use image::{CollectNode, Image, ImageFrame, ImageRefNode, MapImageSliceNode};
#[cfg(feature = "alloc")]
mod image {
use super::{Color, ImageSlice};
use crate::Node;
use alloc::vec::Vec;
use dyn_any::{DynAny, StaticType};
use glam::DAffine2;
#[derive(Clone, Debug, PartialEq, DynAny, Default, specta::Type, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -321,6 +322,15 @@ mod image {
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8(v[0], v[1], v[2], v[3])).collect();
Image { width, height, data }
}
/// Flattens each channel cast to a u8
pub fn as_flat_u8(self) -> (Vec<u8>, u32, u32) {
let Image { width, height, data } = self;
let result_bytes = data.into_iter().flat_map(|color| color.to_rgba8()).collect();
(result_bytes, width, height)
}
}
impl IntoIterator for Image {
@ -363,6 +373,12 @@ mod image {
data,
}
}
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
pub struct ImageFrame {
pub image: Image,
pub transform: DAffine2,
}
}
#[cfg(test)]

View file

@ -38,6 +38,12 @@ pub struct DocumentNodeMetadata {
pub position: IVec2,
}
impl DocumentNodeMetadata {
pub fn position(position: impl Into<IVec2>) -> Self {
Self { position: position.into() }
}
}
#[derive(Clone, Debug, PartialEq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNode {
@ -48,7 +54,7 @@ pub struct DocumentNode {
}
impl DocumentNode {
pub fn populate_first_network_input(&mut self, node: NodeId, offset: usize) {
pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize) {
let input = self
.inputs
.iter()
@ -58,7 +64,7 @@ impl DocumentNode {
.expect("no network input");
let index = input.0;
self.inputs[index] = NodeInput::Node(node);
self.inputs[index] = NodeInput::Node { node_id, output_index };
}
fn resolve_proto_node(mut self) -> ProtoNode {
@ -70,7 +76,10 @@ impl DocumentNode {
assert_eq!(self.inputs.len(), 0);
(ProtoNodeInput::None, ConstructionArgs::Value(tagged_value))
}
NodeInput::Node(id) => (ProtoNodeInput::Node(id), ConstructionArgs::Nodes(vec![])),
NodeInput::Node { node_id, output_index } => {
assert_eq!(output_index, 0, "Outputs should be flattened before converting to protonode.");
(ProtoNodeInput::Node(node_id), ConstructionArgs::Nodes(vec![]))
}
NodeInput::Network => (ProtoNodeInput::Network, ConstructionArgs::Nodes(vec![])),
};
assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Network)), "recieved non resolved parameter");
@ -83,7 +92,7 @@ impl DocumentNode {
if let ConstructionArgs::Nodes(nodes) = &mut args {
nodes.extend(self.inputs.iter().map(|input| match input {
NodeInput::Node(id) => *id,
NodeInput::Node { node_id, .. } => *node_id,
_ => unreachable!(),
}));
}
@ -105,11 +114,11 @@ impl DocumentNode {
P: Fn(String, usize) -> Option<NodeInput>,
{
for (index, input) in self.inputs.iter_mut().enumerate() {
let &mut NodeInput::Node(id) = input else {
let &mut NodeInput::Node{node_id: id, output_index} = input else {
continue;
};
if let Some(&new_id) = new_ids.get(&id) {
*input = NodeInput::Node(new_id);
*input = NodeInput::Node { node_id: new_id, output_index };
} else if let Some(new_input) = default_input(self.name.clone(), index) {
*input = new_input;
} else {
@ -123,23 +132,26 @@ impl DocumentNode {
#[derive(Clone, Debug, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeInput {
Node(NodeId),
Node { node_id: NodeId, output_index: usize },
Value { tagged_value: value::TaggedValue, exposed: bool },
Network,
}
impl NodeInput {
pub const fn node(node_id: NodeId, output_index: usize) -> Self {
Self::Node { node_id, output_index }
}
pub const fn value(tagged_value: value::TaggedValue, exposed: bool) -> Self {
Self::Value { tagged_value, exposed }
}
fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId) {
if let NodeInput::Node(id) = self {
*self = NodeInput::Node(f(*id))
if let &mut NodeInput::Node { node_id, output_index } = self {
*self = NodeInput::Node { node_id: f(node_id), output_index }
}
}
pub fn is_exposed(&self) -> bool {
match self {
NodeInput::Node(_) => true,
NodeInput::Node { .. } => true,
NodeInput::Value { exposed, .. } => *exposed,
NodeInput::Network => false,
}
@ -149,7 +161,7 @@ impl NodeInput {
impl PartialEq for NodeInput {
fn eq(&self, other: &Self) -> bool {
match (&self, &other) {
(Self::Node(n1), Self::Node(n2)) => n1 == n2,
(Self::Node { node_id: n0, output_index: o0 }, Self::Node { node_id: n1, output_index: o1 }) => n0 == n1 && o0 == o1,
(Self::Value { tagged_value: v1, .. }, Self::Value { tagged_value: v2, .. }) => v1 == v2,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
@ -181,24 +193,38 @@ impl DocumentNodeImplementation {
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeOutput {
pub node_id: NodeId,
pub node_output_index: usize,
}
impl NodeOutput {
pub fn new(node_id: NodeId, node_output_index: usize) -> Self {
Self { node_id, node_output_index }
}
}
#[derive(Clone, Debug, Default, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeNetwork {
pub inputs: Vec<NodeId>,
pub output: NodeId,
pub outputs: Vec<NodeOutput>,
pub nodes: HashMap<NodeId, DocumentNode>,
/// These nodes are replaced with identity nodes when flattening
pub disabled: Vec<NodeId>,
/// In the case where a new node is chosen as output - what was the origional
pub previous_output: Option<NodeId>,
/// In the case where a new node is chosen as output - what was the original
pub previous_outputs: Option<Vec<NodeOutput>>,
}
impl NodeNetwork {
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) {
self.inputs.iter_mut().for_each(|id| *id = f(*id));
self.output = f(self.output);
self.outputs.iter_mut().for_each(|output| output.node_id = f(output.node_id));
self.disabled.iter_mut().for_each(|id| *id = f(*id));
self.previous_output = self.previous_output.map(f);
self.previous_outputs
.iter_mut()
.for_each(|nodes| nodes.iter_mut().for_each(|output| output.node_id = f(output.node_id)));
let mut empty = HashMap::new();
std::mem::swap(&mut self.nodes, &mut empty);
self.nodes = empty
@ -215,7 +241,7 @@ impl NodeNetwork {
let mut outwards_links: HashMap<u64, Vec<u64>> = HashMap::new();
for (node_id, node) in &self.nodes {
for input in &node.inputs {
if let NodeInput::Node(ref_id) = input {
if let NodeInput::Node { node_id: ref_id, .. } = input {
outwards_links.entry(*ref_id).or_default().push(*node_id)
}
}
@ -223,6 +249,102 @@ impl NodeNetwork {
outwards_links
}
/// When a node has multiple outputs, we actually just duplicate the node and evaluate each output separately
pub fn duplicate_outputs(&mut self, mut gen_id: &mut impl FnMut() -> NodeId) {
let mut duplicating_nodes = HashMap::new();
// Find the nodes where the inputs require duplicating
for node in &mut self.nodes.values_mut() {
// Recursivly duplicate children
if let DocumentNodeImplementation::Network(network) = &mut node.implementation {
network.duplicate_outputs(gen_id);
}
for input in &mut node.inputs {
let &mut NodeInput::Node { node_id, output_index} = input else {
continue;
};
// Use the initial node when getting the first output
if output_index == 0 {
continue;
}
// Get the existing duplicated node id (or create a new one)
let duplicated_node_id = *duplicating_nodes.entry((node_id, output_index)).or_insert_with(&mut gen_id);
// Use the first output from the duplicated node
*input = NodeInput::node(duplicated_node_id, 0);
}
}
// Find the network outputs that require duplicating
for network_output in &mut self.outputs {
// Use the initial node when getting the first output
if network_output.node_output_index == 0 {
continue;
}
// Get the existing duplicated node id (or create a new one)
let duplicated_node_id = *duplicating_nodes.entry((network_output.node_id, network_output.node_output_index)).or_insert_with(&mut gen_id);
// Use the first output from the duplicated node
*network_output = NodeOutput::new(duplicated_node_id, 0);
}
// Duplicate the nodes
for ((original_node_id, output_index), new_node_id) in duplicating_nodes {
let Some(original_node) = self.nodes.get(&original_node_id) else {
continue;
};
let mut new_node = original_node.clone();
// Update the required outputs from a nested network to be just the relevant output
if let DocumentNodeImplementation::Network(network) = &mut new_node.implementation {
if network.outputs.is_empty() {
continue;
}
network.outputs = vec![network.outputs[output_index]];
}
self.nodes.insert(new_node_id, new_node);
}
// Ensure all nodes only have one output
for node in self.nodes.values_mut() {
if let DocumentNodeImplementation::Network(network) = &mut node.implementation {
if network.outputs.is_empty() {
continue;
}
network.outputs = vec![network.outputs[0]];
}
}
}
/// Removes unused nodes from the graph. Returns a list of bools which represent if each of the inputs have been retained
pub fn remove_dead_nodes(&mut self) -> Vec<bool> {
// Take all the nodes out of the nodes list
let mut old_nodes = std::mem::take(&mut self.nodes);
let mut stack = self.outputs.iter().map(|output| output.node_id).collect::<Vec<_>>();
while let Some(node_id) = stack.pop() {
let Some((node_id, mut document_node)) = old_nodes.remove_entry(&node_id) else {
continue;
};
// Remove dead nodes from child networks
if let DocumentNodeImplementation::Network(network) = &mut document_node.implementation {
// Remove inputs to the parent node if they have been removed from the child
let mut retain_inputs = network.remove_dead_nodes().into_iter();
document_node.inputs.retain(|_| retain_inputs.next().unwrap_or(true))
}
// Visit all nodes that this node references
stack.extend(
document_node
.inputs
.iter()
.filter_map(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id) } else { None }),
);
// Add the node back to the list of nodes
self.nodes.insert(node_id, document_node);
}
// Check if inputs are used and store for return value
let are_inputs_used = self.inputs.iter().map(|input| self.nodes.contains_key(input)).collect();
// Remove unused inputs from graph
self.inputs.retain(|input| self.nodes.contains_key(input));
are_inputs_used
}
pub fn flatten(&mut self, node: NodeId) {
self.flatten_with_fns(node, merge_ids, generate_uuid)
}
@ -254,9 +376,9 @@ impl NodeNetwork {
for (document_input, network_input) in node.inputs.into_iter().zip(inner_network.inputs.iter()) {
let offset = network_offsets.entry(network_input).or_insert(0);
match document_input {
NodeInput::Node(node) => {
NodeInput::Node { node_id, output_index } => {
let network_input = self.nodes.get_mut(network_input).unwrap();
network_input.populate_first_network_input(node, *offset);
network_input.populate_first_network_input(node_id, output_index, *offset);
}
NodeInput::Value { tagged_value, exposed } => {
// Skip formatting very large values for seconds in performance speedup
@ -278,7 +400,7 @@ impl NodeNetwork {
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, *offset);
network_input.populate_first_network_input(new_id, 0, *offset);
}
NodeInput::Network => {
*network_offsets.get_mut(network_input).unwrap() += 1;
@ -289,7 +411,14 @@ impl NodeNetwork {
}
}
node.implementation = DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")]));
node.inputs = vec![NodeInput::Node(inner_network.output)];
node.inputs = inner_network
.outputs
.iter()
.map(|&NodeOutput { node_id, node_output_index }| NodeInput::Node {
node_id,
output_index: node_output_index,
})
.collect();
for node_id in new_nodes {
self.flatten_with_fns(node_id, map_ids, gen_id);
}
@ -300,26 +429,28 @@ impl NodeNetwork {
self.nodes.insert(id, node);
}
pub fn into_proto_network(self) -> ProtoNetwork {
pub fn into_proto_networks(self) -> impl Iterator<Item = ProtoNetwork> {
let mut nodes: Vec<_> = self.nodes.into_iter().map(|(id, node)| (id, node.resolve_proto_node())).collect();
nodes.sort_unstable_by_key(|(i, _)| *i);
ProtoNetwork {
inputs: self.inputs,
output: self.output,
nodes,
}
// Create a network to evaluate each output
self.outputs.into_iter().map(move |output| ProtoNetwork {
inputs: self.inputs.clone(),
output: output.node_id,
nodes: nodes.clone(),
})
}
/// Get the original output node of this network, ignoring any preview node
pub fn original_output(&self) -> NodeId {
self.previous_output.unwrap_or(self.output)
/// Get the original output nodes of this network, ignoring any preview node
pub fn original_outputs(&self) -> &Vec<NodeOutput> {
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],
output: 1,
outputs: vec![NodeOutput::new(1, 0)],
nodes: [
(
0,
@ -334,7 +465,7 @@ impl NodeNetwork {
1,
DocumentNode {
name: "Output".into(),
inputs: vec![NodeInput::Node(output_node_id)],
inputs: vec![NodeInput::node(output_node_id, 0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
metadata: DocumentNodeMetadata { position: (output_offset, 4).into() },
},
@ -367,28 +498,27 @@ impl NodeNetwork {
}
/// Check if the specified node id is connected to the output
pub fn connected_to_output(&self, node_id: NodeId) -> bool {
pub fn connected_to_output(&self, target_node_id: NodeId) -> bool {
// If the node is the output then return true
if self.output == node_id {
if self.outputs.iter().any(|&NodeOutput { node_id, .. }| node_id == target_node_id) {
return true;
}
// Get the output
let Some(output_node) = self.nodes.get(&self.output) else {
// Get the outputs
let Some(mut stack) = self.outputs.iter().map(|&output| self.nodes.get(&output.node_id)).collect::<Option<Vec<_>>>() else {
return false;
};
let mut stack = vec![output_node];
let mut already_visited = HashSet::new();
already_visited.insert(self.output);
already_visited.extend(self.outputs.iter().map(|output| output.node_id));
while let Some(node) = stack.pop() {
for input in &node.inputs {
if let &NodeInput::Node(ref_id) = input {
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 == node_id {
if ref_id == target_node_id {
return true;
}
// Add the referenced node to the stack
@ -403,6 +533,21 @@ impl NodeNetwork {
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<bool> {
self.previous_outputs.as_ref().map(|outputs| outputs.iter().any(|output| output.node_id == node_id))
}
}
#[cfg(test)]
@ -421,7 +566,7 @@ mod test {
fn add_network() -> NodeNetwork {
NodeNetwork {
inputs: vec![0, 0],
output: 1,
outputs: vec![NodeOutput::new(1, 0)],
nodes: [
(
0,
@ -436,7 +581,7 @@ mod test {
1,
DocumentNode {
name: "Add".into(),
inputs: vec![NodeInput::Node(0)],
inputs: vec![NodeInput::node(0, 0)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[generic!("T"), generic!("U")])),
},
@ -454,7 +599,7 @@ mod test {
network.map_ids(|id| id + 1);
let maped_add = NodeNetwork {
inputs: vec![1, 1],
output: 2,
outputs: vec![NodeOutput::new(2, 0)],
nodes: [
(
1,
@ -469,7 +614,7 @@ mod test {
2,
DocumentNode {
name: "Add".into(),
inputs: vec![NodeInput::Node(1)],
inputs: vec![NodeInput::node(1, 0)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[generic!("T"), generic!("U")])),
},
@ -486,7 +631,7 @@ mod test {
fn flatten_add() {
let mut network = NodeNetwork {
inputs: vec![1],
output: 1,
outputs: vec![NodeOutput::new(1, 0)],
nodes: [(
1,
DocumentNode {
@ -518,7 +663,7 @@ mod test {
fn resolve_proto_node_add() {
let document_node = DocumentNode {
name: "Cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Node(0)],
inputs: vec![NodeInput::Network, NodeInput::node(0, 0)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[generic!("T"), generic!("U")])),
};
@ -568,23 +713,23 @@ mod test {
.collect(),
};
let network = flat_network();
let resolved_network = network.into_proto_network();
let resolved_network = network.into_proto_networks().collect::<Vec<_>>();
println!("{:#?}", resolved_network);
println!("{:#?}", resolved_network[0]);
println!("{:#?}", construction_network);
assert_eq!(resolved_network, construction_network);
assert_eq!(resolved_network[0], construction_network);
}
fn flat_network() -> NodeNetwork {
NodeNetwork {
inputs: vec![10],
output: 1,
outputs: vec![NodeOutput::new(1, 0)],
nodes: [
(
1,
DocumentNode {
name: "Inc".into(),
inputs: vec![NodeInput::Node(11)],
inputs: vec![NodeInput::node(11, 0)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
},
@ -593,7 +738,7 @@ mod test {
10,
DocumentNode {
name: "Cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Node(14)],
inputs: vec![NodeInput::Network, NodeInput::node(14, 0)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[generic!("T"), generic!("U")])),
},
@ -614,7 +759,7 @@ mod test {
11,
DocumentNode {
name: "Add".into(),
inputs: vec![NodeInput::Node(10)],
inputs: vec![NodeInput::node(10, 0)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[generic!("T"), generic!("U")])),
},
@ -625,4 +770,121 @@ mod test {
..Default::default()
}
}
fn two_node_identity() -> NodeNetwork {
NodeNetwork {
inputs: vec![1, 2],
outputs: vec![NodeOutput::new(1, 0), NodeOutput::new(2, 0)],
nodes: [
(
1,
DocumentNode {
name: "Identity 1".into(),
inputs: vec![NodeInput::Network],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
},
),
(
2,
DocumentNode {
name: "Identity 2".into(),
inputs: vec![NodeInput::Network],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
},
),
]
.into_iter()
.collect(),
..Default::default()
}
}
fn output_duplicate(network_outputs: Vec<NodeOutput>, result_node_input: NodeInput) -> NodeNetwork {
let mut network = NodeNetwork {
inputs: Vec::new(),
outputs: network_outputs,
nodes: [
(
10,
DocumentNode {
name: "Nested network".into(),
inputs: vec![NodeInput::value(TaggedValue::F32(1.), false), NodeInput::value(TaggedValue::F32(2.), false)],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Network(two_node_identity()),
},
),
(
11,
DocumentNode {
name: "Result".into(),
inputs: vec![result_node_input],
metadata: DocumentNodeMetadata::default(),
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
},
),
]
.into_iter()
.collect(),
..Default::default()
};
let mut new_ids = 101..;
network.duplicate_outputs(&mut || new_ids.next().unwrap());
network.remove_dead_nodes();
network
}
#[test]
fn simple_duplicate() {
let result = output_duplicate(vec![NodeOutput::new(10, 1)], NodeInput::node(10, 0));
assert_eq!(result.outputs.len(), 1, "The number of outputs should remain as 1");
assert_eq!(result.outputs[0], NodeOutput::new(101, 0), "The outer network output should be from a duplicated inner network");
assert_eq!(result.nodes.keys().copied().collect::<Vec<_>>(), vec![101], "Should just call nested network");
let nested_network_node = result.nodes.get(&101).unwrap();
assert_eq!(nested_network_node.name, "Nested network".to_string(), "Name should not change");
assert_eq!(nested_network_node.inputs, vec![NodeInput::value(TaggedValue::F32(2.), false)], "Input should be 2");
let inner_network = nested_network_node.implementation.get_network().expect("Implementation should be network");
assert_eq!(inner_network.inputs, vec![2], "The input should be sent to the second node");
assert_eq!(inner_network.outputs, vec![NodeOutput::new(2, 0)], "The output should be node id 2");
assert_eq!(inner_network.nodes.get(&2).unwrap().name, "Identity 2", "The node should be identity 2");
}
#[test]
fn out_of_order_duplicate() {
let result = output_duplicate(vec![NodeOutput::new(10, 1), NodeOutput::new(10, 0)], NodeInput::node(10, 0));
assert_eq!(result.outputs[0], NodeOutput::new(101, 0), "The first network output should be from a duplicated nested network");
assert_eq!(result.outputs[1], NodeOutput::new(10, 0), "The second network output should be from the original nested network");
assert!(
result.nodes.contains_key(&10) && result.nodes.contains_key(&101) && result.nodes.len() == 2,
"Network should contain two duplicated nodes"
);
for (node_id, input_value, inner_id) in [(10, 1., 1), (101, 2., 2)] {
let nested_network_node = result.nodes.get(&node_id).unwrap();
assert_eq!(nested_network_node.name, "Nested network".to_string(), "Name should not change");
assert_eq!(nested_network_node.inputs, vec![NodeInput::value(TaggedValue::F32(input_value), false)], "Input should be stable");
let inner_network = nested_network_node.implementation.get_network().expect("Implementation should be network");
assert_eq!(inner_network.inputs, vec![inner_id], "The input should be sent to the second node");
assert_eq!(inner_network.outputs, vec![NodeOutput::new(inner_id, 0)], "The output should be node id");
assert_eq!(inner_network.nodes.get(&inner_id).unwrap().name, format!("Identity {inner_id}"), "The node should be identity");
}
}
#[test]
fn using_other_node_duplicate() {
let result = output_duplicate(vec![NodeOutput::new(11, 0)], NodeInput::node(10, 1));
assert_eq!(result.outputs, vec![NodeOutput::new(11, 0)], "The network output should be the result node");
assert!(
result.nodes.contains_key(&11) && result.nodes.contains_key(&101) && result.nodes.len() == 2,
"Network should contain a duplicated node and a result node"
);
let result_node = result.nodes.get(&11).unwrap();
assert_eq!(result_node.inputs, vec![NodeInput::node(101, 0)], "Result node should refer to duplicate node as input");
let nested_network_node = result.nodes.get(&101).unwrap();
assert_eq!(nested_network_node.name, "Nested network".to_string(), "Name should not change");
assert_eq!(nested_network_node.inputs, vec![NodeInput::value(TaggedValue::F32(2.), false)], "Input should be 2");
let inner_network = nested_network_node.implementation.get_network().expect("Implementation should be network");
assert_eq!(inner_network.inputs, vec![2], "The input should be sent to the second node");
assert_eq!(inner_network.outputs, vec![NodeOutput::new(2, 0)], "The output should be node id 2");
assert_eq!(inner_network.nodes.get(&2).unwrap().name, "Identity 2", "The node should be identity 2");
}
}

View file

@ -1,7 +1,7 @@
pub use dyn_any::StaticType;
use dyn_any::{DynAny, Upcast};
use dyn_clone::DynClone;
pub use glam::DVec2;
pub use glam::{DAffine2, DVec2};
use graphene_core::raster::LuminanceCalculation;
use graphene_core::Node;
use std::hash::Hash;
@ -22,6 +22,7 @@ pub enum TaggedValue {
Bool(bool),
DVec2(DVec2),
OptionalDVec2(Option<DVec2>),
DAffine2(DAffine2),
Image(graphene_core::raster::Image),
RcImage(Option<Arc<graphene_core::raster::Image>>),
Color(graphene_core::raster::color::Color),
@ -68,44 +69,48 @@ impl Hash for TaggedValue {
8.hash(state);
Self::DVec2(*v).hash(state)
}
Self::Image(i) => {
Self::DAffine2(m) => {
9.hash(state);
i.hash(state)
m.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
Self::RcImage(i) => {
Self::Image(i) => {
10.hash(state);
i.hash(state)
}
Self::Color(c) => {
Self::RcImage(i) => {
11.hash(state);
i.hash(state)
}
Self::Color(c) => {
12.hash(state);
c.hash(state)
}
Self::Subpath(s) => {
12.hash(state);
s.hash(state)
}
Self::RcSubpath(s) => {
13.hash(state);
s.hash(state)
}
Self::LuminanceCalculation(l) => {
Self::RcSubpath(s) => {
14.hash(state);
s.hash(state)
}
Self::LuminanceCalculation(l) => {
15.hash(state);
l.hash(state)
}
Self::ImaginateSamplingMethod(m) => {
15.hash(state);
16.hash(state);
m.hash(state)
}
Self::ImaginateMaskStartingFill(f) => {
16.hash(state);
17.hash(state);
f.hash(state)
}
Self::ImaginateStatus(s) => {
17.hash(state);
18.hash(state);
s.hash(state)
}
Self::LayerPath(p) => {
18.hash(state);
19.hash(state);
p.hash(state)
}
}
@ -124,6 +129,7 @@ impl<'a> TaggedValue {
TaggedValue::Bool(x) => Box::new(x),
TaggedValue::DVec2(x) => Box::new(x),
TaggedValue::OptionalDVec2(x) => Box::new(x),
TaggedValue::DAffine2(x) => Box::new(x),
TaggedValue::Image(x) => Box::new(x),
TaggedValue::RcImage(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),

View file

@ -8,21 +8,29 @@ use crate::proto::ProtoNetwork;
pub struct Compiler {}
impl Compiler {
pub fn compile(&self, mut network: NodeNetwork, resolve_inputs: bool) -> ProtoNetwork {
pub fn compile(&self, mut network: NodeNetwork, resolve_inputs: bool) -> impl Iterator<Item = ProtoNetwork> {
let node_ids = network.nodes.keys().copied().collect::<Vec<_>>();
println!("flattening");
for id in node_ids {
network.flatten(id);
}
let mut proto_network = network.into_proto_network();
if resolve_inputs {
println!("resolving inputs");
proto_network.resolve_inputs();
}
println!("reordering ids");
proto_network.reorder_ids();
proto_network.generate_stable_node_ids();
proto_network
let proto_networks = network.into_proto_networks();
proto_networks.map(move |mut proto_network| {
if resolve_inputs {
println!("resolving inputs");
proto_network.resolve_inputs();
}
proto_network.reorder_ids();
proto_network.generate_stable_node_ids();
proto_network
})
}
pub fn compile_single(&self, network: NodeNetwork, resolve_inputs: bool) -> Result<ProtoNetwork, String> {
assert_eq!(network.outputs.len(), 1, "Graph with multiple outputs not yet handled");
let Some(proto_network) = self.compile(network, resolve_inputs).next() else {
return Err("Failed to convert graph into proto graph".to_string());
};
Ok(proto_network)
}
}
pub type Any<'a> = Box<dyn DynAny<'a> + 'a>;

View file

@ -94,7 +94,7 @@ pub struct ProtoNetwork {
pub nodes: Vec<(NodeId, ProtoNode)>,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum ConstructionArgs {
Value(value::TaggedValue),
Nodes(Vec<NodeId>),
@ -133,7 +133,7 @@ impl ConstructionArgs {
}
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub struct ProtoNode {
pub construction_args: ConstructionArgs,
pub input: ProtoNodeInput,

View file

@ -73,7 +73,7 @@ where
N: Node<'input, Any<'input>, Output = Any<'input>>,
{
let node_name = core::any::type_name::<N>();
let out = dyn_any::downcast(node.eval(input)).unwrap_or_else(|e| panic!("DynAnyNode Input {e} in:\n{node_name}"));
let out = dyn_any::downcast(node.eval(input)).unwrap_or_else(|e| panic!("DowncastNode Input {e} in:\n{node_name}"));
*out
}
@ -91,7 +91,7 @@ impl<'n: 'input, 'input, O: 'input + StaticType, I: 'input + StaticType> Node<'i
fn eval<'node: 'input>(&'node self, input: I) -> Self::Output {
{
let input = Box::new(input);
let out = dyn_any::downcast(self.node.eval(input)).unwrap_or_else(|e| panic!("DynAnyNode Input {e}"));
let out = dyn_any::downcast(self.node.eval(input)).unwrap_or_else(|e| panic!("DowncastBothNode Input {e}"));
*out
}
}
@ -118,7 +118,7 @@ impl<'n: 'input, 'input, O: 'input + StaticType, I: 'input + StaticType> Node<'i
fn eval<'node: 'input>(&'node self, input: I) -> Self::Output {
{
let input = Box::new(input);
let out: Box<&_> = dyn_any::downcast::<&O>(self.node.eval(input)).unwrap_or_else(|e| panic!("DynAnyNode Input {e}"));
let out: Box<&_> = dyn_any::downcast::<&O>(self.node.eval(input)).unwrap_or_else(|e| panic!("DowncastBothRefNode Input {e}"));
*out
}
}

View file

@ -1,5 +1,6 @@
use dyn_any::{DynAny, StaticType};
use glam::DAffine2;
use graphene_core::raster::{Color, Image};
use graphene_core::Node;
@ -117,6 +118,14 @@ fn imaginate(image: Image, cached: Option<std::sync::Arc<graphene_core::raster::
cached.map(|mut x| std::sync::Arc::make_mut(&mut x).clone()).unwrap_or(image)
}
#[derive(Debug, Clone, Copy)]
pub struct ImageFrameNode<Transform> {
transform: Transform,
}
#[node_macro::node_fn(ImageFrameNode)]
fn image_frame(image: Image, transform: DAffine2) -> graphene_core::raster::ImageFrame {
graphene_core::raster::ImageFrame { image, transform }
}
#[cfg(test)]
mod test {

View file

@ -16,7 +16,7 @@ quantization = ["graphene-std/quantization"]
graphene-core = { path = "../gcore", features = ["async", "std" ] }
graphene-std = { path = "../gstd" }
graph-craft = { path = "../graph-craft" }
dyn-any = { path = "../../libraries/dyn-any", features = ["log-bad-types"] }
dyn-any = { path = "../../libraries/dyn-any", features = ["log-bad-types", "glam"] }
num-traits = "0.2"
borrow_stack = { path = "../borrow_stack" }
dyn-clone = "1.0"

View file

@ -26,7 +26,7 @@ impl DynamicExecutor {
pub fn update(&mut self, proto_network: ProtoNetwork) {
self.output = proto_network.output;
info!("setting output to {}", self.output);
trace!("setting output to {}", self.output);
self.tree.update(proto_network);
}
}

View file

@ -52,7 +52,7 @@ mod tests {
fn add_network() -> NodeNetwork {
NodeNetwork {
inputs: vec![0, 0],
output: 1,
outputs: vec![NodeOutput::new(1, 0)],
nodes: [
(
0,
@ -67,7 +67,7 @@ mod tests {
1,
DocumentNode {
name: "Add".into(),
inputs: vec![NodeInput::Node(0)],
inputs: vec![NodeInput::node(0, 0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[concrete!("(u32, u32)")])),
metadata: DocumentNodeMetadata::default(),
},
@ -81,7 +81,7 @@ mod tests {
let network = NodeNetwork {
inputs: vec![0],
output: 0,
outputs: vec![NodeOutput::new(0, 0)],
nodes: [(
0,
DocumentNode {
@ -106,7 +106,7 @@ mod tests {
use graph_craft::executor::{Compiler, Executor};
let compiler = Compiler {};
let protograph = compiler.compile(network, true);
let protograph = compiler.compile_single(network, true).expect("Graph should be generated");
let exec = DynamicExecutor::new(protograph);

View file

@ -1,3 +1,4 @@
use glam::DAffine2;
use graphene_core::ops::{CloneNode, IdNode, TypeNode};
use graphene_core::raster::color::Color;
use graphene_core::raster::*;
@ -83,7 +84,7 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
(
NodeIdentifier::new("graphene_std::raster::ImaginateNode<_>", &[concrete!("Image"), concrete!("Option<std::sync::Arc<Image>>")]),
|args| {
let cached = graphene_std::any::input_node::<Option<std::sync::Arc<Image>>>(args[15]);
let cached = graphene_std::any::input_node::<Option<std::sync::Arc<Image>>>(args[16]);
let node = graphene_std::raster::ImaginateNode::new(cached);
let any = DynAnyNode::new(ValueNode::new(node));
any.into_type_erased()
@ -126,6 +127,7 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
any.into_type_erased()
}),
register_node!(graphene_core::structural::ConsNode<_, _>, input: Image, params: [&str]),
register_node!(graphene_std::raster::ImageFrameNode<_>, input: Image, params: [DAffine2]),
/*
(NodeIdentifier::new("graphene_std::raster::ImageNode", &[concrete!("&str")]), |_proto_node, stack| {
stack.push_fn(|_nodes| {

View file

@ -13,7 +13,7 @@ fn has_attribute(attrs: &[Attribute], target: &str) -> bool {
/// Make setting strings easier by allowing all types that `impl Into<String>`
///
/// Returns the new input type and a conversion to the origional.
/// Returns the new input type and a conversion to the original.
fn easier_string_assignment(field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) {
if let Type::Path(type_path) = field_ty {
if let Some(last_segment) = type_path.path.segments.last() {