From 18507b78ac7bef0b1dec772278e49de50019e64a Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sat, 5 Nov 2022 21:38:14 +0000 Subject: [PATCH] Integrate the node graph as a Node Graph Frame layer type (#812) * Add node graph frame tool * Add a brighten * Use the node graph * Fix topological_sort * Update UI * Add icons for the tool and layer type * Avoid serde & use bitmaps to improve performance * Allow serialising a node graph * Fix missing ..Default::default() * Fix incorrect comments * Cache node graph output image * Suppress no-cycle import warning Co-authored-by: Keavon Chambers --- Cargo.lock | 9 + editor/Cargo.toml | 8 + .../src/messages/frontend/frontend_message.rs | 8 + .../messages/input_mapper/default_mapping.rs | 7 + .../portfolio/document/document_message.rs | 3 +- .../document/document_message_handler.rs | 94 +++++-- .../properties_panel/utility_functions.rs | 68 +++++- .../messages/portfolio/portfolio_message.rs | 6 + .../portfolio/portfolio_message_handler.rs | 86 ++++++- editor/src/messages/prelude.rs | 1 + editor/src/messages/tool/tool_message.rs | 5 + editor/src/messages/tool/tool_messages/mod.rs | 1 + .../tool_messages/node_graph_frame_tool.rs | 231 ++++++++++++++++++ editor/src/messages/tool/utility_types.rs | 36 +-- .../assets/icon-16px-solid/node-nodes.svg | 7 + .../icon-24px-two-tone/raster-nodes-tool.svg | 6 + frontend/src/state-providers/portfolio.ts | 11 +- frontend/src/utility-functions/icons.ts | 4 + frontend/src/wasm-communication/editor.ts | 16 ++ frontend/src/wasm-communication/messages.ts | 14 +- frontend/wasm/src/editor_api.rs | 27 ++ graphene/Cargo.toml | 3 + graphene/src/document.rs | 44 +++- graphene/src/layers/layer_info.rs | 10 +- graphene/src/layers/mod.rs | 2 + graphene/src/layers/nodegraph_layer.rs | 152 ++++++++++++ graphene/src/operation.rs | 13 +- node-graph/graph-craft/Cargo.toml | 5 + node-graph/graph-craft/src/document.rs | 28 ++- node-graph/graph-craft/src/document/value.rs | 20 ++ node-graph/graph-craft/src/lib.rs | 13 +- node-graph/graph-craft/src/node_registry.rs | 224 +++++++---------- node-graph/graph-craft/src/proto.rs | 114 +++++---- 33 files changed, 1018 insertions(+), 258 deletions(-) create mode 100644 editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs create mode 100644 frontend/assets/icon-16px-solid/node-nodes.svg create mode 100644 frontend/assets/icon-24px-two-tone/raster-nodes-tool.svg create mode 100644 graphene/src/layers/nodegraph_layer.rs diff --git a/Cargo.lock b/Cargo.lock index 7232e60411..12f27ee5b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,6 +360,7 @@ dependencies = [ "graphene-std", "num-traits", "rand_chacha", + "serde", ] [[package]] @@ -405,11 +406,17 @@ version = "0.0.0" dependencies = [ "bezier-rs", "bitflags", + "borrow_stack", "derivative", + "dyn-any", "env_logger", "glam", + "graph-craft", + "graphene-core", + "graphene-std", "graphite-graphene", "graphite-proc-macros", + "image", "kurbo", "log", "once_cell", @@ -429,6 +436,8 @@ dependencies = [ "base64", "bezier-rs", "glam", + "graph-craft", + "image", "kurbo", "log", "rustybuzz", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 77ea540060..8ba8ce246a 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -28,6 +28,14 @@ remain = "0.2.2" derivative = "2.2.0" once_cell = "1.13.0" # Remove when `core::cell::OnceCell` is stabilized () +# Node graph +image = { version = "0.24", default-features = false, features = ["bmp"] } +graph-craft = { path = "../node-graph/graph-craft" } +borrow_stack = { path = "../node-graph/borrow_stack" } +dyn-any = { path = "../libraries/dyn-any" } +graphene-core = { path = "../node-graph/gcore" } +graphene-std = { path = "../node-graph/gstd" } + [dependencies.graphene] path = "../graphene" package = "graphite-graphene" diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index ef6c962dab..61eab12efc 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -86,6 +86,14 @@ pub enum FrontendMessage { }, TriggerLoadAutoSaveDocuments, TriggerLoadPreferences, + TriggerNodeGraphFrameGenerate { + #[serde(rename = "documentId")] + document_id: u64, + #[serde(rename = "layerPath")] + layer_path: Vec, + svg: String, + size: glam::DVec2, + }, TriggerOpenDocument, TriggerPaste, TriggerRasterDownload { diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index 1810a607be..4e4f73d17d 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -123,6 +123,13 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort), entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }), // + // NodeGraphFrameToolMessage + entry!(KeyDown(Lmb); action_dispatch=NodeGraphFrameToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=NodeGraphFrameToolMessage::DragStop), + entry!(KeyDown(Rmb); action_dispatch=NodeGraphFrameToolMessage::Abort), + entry!(KeyDown(Escape); action_dispatch=NodeGraphFrameToolMessage::Abort), + entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=NodeGraphFrameToolMessage::Resize { center: Alt, lock_ratio: Shift }), + // // EllipseToolMessage entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart), entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 6f8c4bb365..fe382cb0a3 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -73,8 +73,8 @@ pub enum DocumentMessage { FolderChanged { affected_folder_path: Vec, }, + FrameClear, GroupSelectedLayers, - ImaginateClear, ImaginateGenerate, ImaginateTerminate, LayerChanged { @@ -89,6 +89,7 @@ pub enum DocumentMessage { layer_path: Vec, delta: (f64, f64), }, + NodeGraphFrameGenerate, NudgeSelectedLayers { delta_x: f64, delta_y: f64, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 2130b37dda..dcd906dc6e 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -377,6 +377,30 @@ impl MessageHandler { + let mut selected_frame_layers = self + .selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate) + .chain(self.selected_layers_with_type(LayerDataTypeDiscriminant::NodeGraphFrame)); + // Get what is hopefully the only selected Imaginate/NodeGraphFrame layer + let layer_path = selected_frame_layers.next(); + // Abort if we didn't have any Imaginate/NodeGraphFrame layer, or if there are additional ones also selected + if layer_path.is_none() || selected_frame_layers.next().is_some() { + return; + } + let layer_path = layer_path.unwrap(); + + let layer = self.graphene_document.layer(layer_path).expect("Clearing Imaginate/NodeGraphFrame image for invalid layer"); + let previous_blob_url = match &layer.data { + LayerDataType::Imaginate(imaginate) => &imaginate.blob_url, + LayerDataType::NodeGraphFrame(node_graph_frame) => &node_graph_frame.blob_url, + x => panic!("Cannot find blob url for layer type {}", LayerDataTypeDiscriminant::from(x)), + }; + + if let Some(url) = previous_blob_url { + responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into()); + } + responses.push_back(DocumentOperation::ClearBlobURL { path: layer_path.into() }.into()); + } GroupSelectedLayers => { let mut new_folder_path = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec(); @@ -406,24 +430,6 @@ impl MessageHandler { - let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate); - // Get what is hopefully the only selected Imaginate layer - let layer_path = selected_imaginate_layers.next(); - // Abort if we didn't have any Imaginate layer, or if there are additional ones also selected - if layer_path.is_none() || selected_imaginate_layers.next().is_some() { - return; - } - let layer_path = layer_path.unwrap(); - - let layer = self.graphene_document.layer(layer_path).expect("Clearing Imaginate image for invalid layer"); - let previous_blob_url = &layer.as_imaginate().unwrap().blob_url; - - if let Some(url) = previous_blob_url { - responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into()); - } - responses.push_back(DocumentOperation::ImaginateClear { path: layer_path.into() }.into()); - } ImaginateGenerate => { if let Some(message) = self.call_imaginate(document_id, preferences, persistent_data) { // TODO: Eventually remove this after a message system ordering architectural change @@ -505,6 +511,11 @@ impl MessageHandler { + if let Some(message) = self.call_node_graph_frame(document_id, preferences, persistent_data) { + responses.push_back(message); + } + } NudgeSelectedLayers { delta_x, delta_y } => { self.backup(responses); for path in self.selected_layers().map(|path| path.to_vec()) { @@ -705,8 +716,16 @@ impl MessageHandler { + if let Some(url) = &node_graph_frame.blob_url { + responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into()); + } + } LayerDataType::Image(_) => {} - other => panic!("Setting blob URL for invalid layer type, which must be an `Imaginate` or `Image`. Found: `{:?}`", other), + other => panic!( + "Setting blob URL for invalid layer type, which must be an `Imaginate`, `NodeGraphFrame` or `Image`. Found: `{:?}`", + other + ), } responses.push_back( @@ -936,7 +955,7 @@ impl DocumentMessageHandler { }; let base_image = if imaginate_layer.use_img2img { // Calculate the size of the region to be exported - let size = transform.transform_point2(DVec2::ONE) - transform.transform_point2(DVec2::ZERO); + let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); let old_transforms = self.remove_document_transform(); let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); @@ -960,6 +979,32 @@ impl DocumentMessageHandler { ) } + pub fn call_node_graph_frame(&mut self, document_id: u64, _preferences: &PreferencesMessageHandler, persistent_data: &PersistentData) -> Option { + let layer_path = { + let mut selected_nodegraph_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::NodeGraphFrame); + + // Get what is hopefully the only selected nodegraph layer + match selected_nodegraph_layers.next() { + // Continue only if there are no additional nodegraph layers also selected + Some(layer_path) if selected_nodegraph_layers.next().is_none() => layer_path.to_owned(), + _ => return None, + } + }; + + // Prepare the node graph base image base image + + // Calculate the size of the region to be exported + + let old_transforms = self.remove_document_transform(); + let transform = self.graphene_document.multiply_transforms(&layer_path).unwrap(); + let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); + + let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); + self.restore_document_transform(old_transforms); + + Some(FrontendMessage::TriggerNodeGraphFrameGenerate { document_id, layer_path, svg, size }.into()) + } + /// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end fn remove_document_transform(&mut self) -> [DAffine2; 2] { let old_artwork_transform = self.graphene_document.root.transform; @@ -1474,6 +1519,15 @@ impl DocumentMessageHandler { }); } } + LayerDataType::NodeGraphFrame(node_graph_frame) => { + if let Some(data) = &node_graph_frame.image_data { + image_data.push(FrontendImageData { + path: path.clone(), + image_data: data.image_data.clone(), + mime: node_graph_frame.mime.clone(), + }); + } + } _ => {} } } diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index d27f0f05f7..02622c22d3 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -14,7 +14,8 @@ use crate::messages::prelude::*; use graphene::color::Color; use graphene::document::pick_layer_safe_imaginate_resolution; use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus}; -use graphene::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant}; +use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant}; +use graphene::layers::nodegraph_layer::NodeGraphFrameLayer; use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; use graphene::layers::text_layer::{FontCache, TextLayer}; @@ -251,6 +252,11 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque tooltip: "Imaginate".into(), ..Default::default() })), + LayerDataType::NodeGraphFrame(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { + icon: "NodeNodes".into(), + tooltip: "Node Graph Frame".into(), + ..Default::default() + })), }, WidgetHolder::new(Widget::Separator(Separator { separator_type: SeparatorType::Related, @@ -307,6 +313,9 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque LayerDataType::Imaginate(imaginate) => { vec![node_section_transform(layer, persistent_data), node_section_imaginate(imaginate, layer, persistent_data, responses)] } + LayerDataType::NodeGraphFrame(node_graph_frame) => { + vec![node_section_transform(layer, persistent_data), node_section_node_graph_frame(node_graph_frame)] + } LayerDataType::Folder(_) => { vec![node_section_transform(layer, persistent_data)] } @@ -659,8 +668,8 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi WidgetHolder::new(Widget::TextButton(TextButton { label: "Clear".into(), tooltip: "Remove generated image from the layer frame".into(), - disabled: imaginate_layer.blob_url == None, - on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateClear.into()), + disabled: imaginate_layer.blob_url.is_none(), + on_update: WidgetCallback::new(|_| DocumentMessage::FrameClear.into()), ..Default::default() })), ], @@ -1009,6 +1018,55 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi } } +fn node_section_node_graph_frame(node_graph_frame: &NodeGraphFrameLayer) -> LayoutGroup { + LayoutGroup::Section { + name: "Node Graph Frame".into(), + layout: vec![ + LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Temporary layer that applies a greyscale to the layers below it.".into(), + ..TextLabel::default() + }))], + }, + LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Powered by the node graph :)".into(), + ..TextLabel::default() + }))], + }, + LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextButton(TextButton { + label: "Open Node Graph UI (todo)".into(), + tooltip: "Open the node graph associated with this layer".into(), + on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(800) }.into()), + ..Default::default() + }))], + }, + LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::TextButton(TextButton { + label: "Generate".into(), + tooltip: "Fill layer frame by generating a new image".into(), + on_update: WidgetCallback::new(|_| DocumentMessage::NodeGraphFrameGenerate.into()), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextButton(TextButton { + label: "Clear".into(), + tooltip: "Remove generated image from the layer frame".into(), + disabled: node_graph_frame.blob_url.is_none(), + on_update: WidgetCallback::new(|_| DocumentMessage::FrameClear.into()), + ..Default::default() + })), + ], + }, + ], + } +} + fn node_section_font(layer: &TextLayer) -> LayoutGroup { let font = layer.font.clone(); let size = layer.size; @@ -1425,7 +1483,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(stroke.weight() as f64), + value: Some(stroke.weight()), is_integer: false, min: Some(0.), unit: " px".into(), @@ -1473,7 +1531,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(stroke.dash_offset() as f64), + value: Some(stroke.dash_offset()), is_integer: true, min: Some(0.), unit: " px".into(), diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 6bc5e77ad1..cd44a9483c 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -109,6 +109,12 @@ pub enum PortfolioMessage { data: String, }, PrevDocument, + ProcessNodeGraphFrame { + document_id: u64, + layer_path: Vec, + image_data: Vec, + size: (u32, u32), + }, SelectDocument { document_id: u64, }, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 3f19892f53..1b6ff73d77 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -2,16 +2,16 @@ 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; +use crate::messages::frontend::utility_types::{FrontendDocumentDetails, FrontendImageData}; use crate::messages::layout::utility_types::layout_widget::PropertyHolder; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; use crate::messages::portfolio::utility_types::ImaginateServerStatus; use crate::messages::prelude::*; -use graphene::layers::layer_info::LayerDataTypeDiscriminant; +use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::text_layer::Font; -use graphene::Operation as DocumentOperation; +use graphene::{LayerId, Operation as DocumentOperation}; #[derive(Debug, Clone, Default)] pub struct PortfolioMessageHandler { @@ -413,6 +413,86 @@ impl MessageHandler { + fn read_image(document: Option<&DocumentMessageHandler>, layer_path: &[LayerId], image_data: Vec, (width, height): (u32, u32)) -> Result, String> { + use graphene_std::raster::Image; + use image::{ImageBuffer, Rgba}; + use std::io::Cursor; + + let data = image_data.chunks_exact(4).map(|v| graphene_core::raster::color::Color::from_rgba8(v[0], v[1], v[2], v[3])).collect(); + let image = graphene_std::raster::Image { width, height, data }; + + let document = document.ok_or_else(|| "Invalid document".to_string())?; + let layer = document.graphene_document.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()), + }?; + + // Execute the node graph + + let mut network = node_graph_frame.network.clone(); + + let stack = borrow_stack::FixedSizeStack::new(256); + network.flatten(0); + + let mut proto_network = network.into_proto_network(); + proto_network.reorder_ids(); + + for (_id, node) in proto_network.nodes { + graph_craft::node_registry::push_node(node, &stack); + } + + use borrow_stack::BorrowStack; + use dyn_any::IntoDynAny; + use graphene_core::Node; + let result = unsafe { stack.get().last().unwrap().eval(image.into_dyn()) }; + let result = *dyn_any::downcast::(result).unwrap(); + + let mut bytes: Vec = Vec::new(); + let [result_width, result_height] = [result.width, result.height]; + let size_estimate = (result_width * result_height * 4) as usize; + + let mut result_bytes = Vec::with_capacity(size_estimate); + result_bytes.extend(result.data.into_iter().flat_map(|colour| colour.to_rgba8())); + let output: ImageBuffer, _> = image::ImageBuffer::from_raw(result_width, result_height, result_bytes).ok_or_else(|| "Invalid image size".to_string())?; + output.write_to(&mut Cursor::new(&mut bytes), image::ImageOutputFormat::Bmp).map_err(|e| e.to_string())?; + + Ok(bytes) + } + + match read_image(self.documents.get(&document_id), &layer_path, image_data, size) { + Ok(image_data) => { + responses.push_back( + DocumentOperation::SetNodeGraphFrameImageData { + layer_path: layer_path.clone(), + image_data: image_data.clone(), + } + .into(), + ); + let mime = "image/bmp".to_string(); + responses.push_back( + FrontendMessage::UpdateImageData { + document_id, + image_data: vec![FrontendImageData { path: layer_path, image_data, mime }], + } + .into(), + ); + } + Err(description) => responses.push_back( + DialogMessage::DisplayDialogError { + title: "Failed to update image".to_string(), + description, + } + .into(), + ), + } + } PortfolioMessage::SelectDocument { document_id } => { if let Some(document) = self.active_document() { if !document.is_auto_saved() { diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 4eb1641fd8..437b4040aa 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -37,6 +37,7 @@ pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessag pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant}; +pub use crate::messages::tool::tool_messages::node_graph_frame_tool::{NodeGraphFrameToolMessage, NodeGraphFrameToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant}; diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index 60c2695b77..986f02a7d6 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -78,6 +78,9 @@ pub enum ToolMessage { #[remain::unsorted] #[child] Imaginate(ImaginateToolMessage), + #[remain::unsorted] + #[child] + NodeGraphFrame(NodeGraphFrameToolMessage), // Messages #[remain::unsorted] @@ -114,6 +117,8 @@ pub enum ToolMessage { #[remain::unsorted] ActivateToolImaginate, + #[remain::unsorted] + ActivateToolNodeGraphFrame, ActivateTool { tool_type: ToolType, diff --git a/editor/src/messages/tool/tool_messages/mod.rs b/editor/src/messages/tool/tool_messages/mod.rs index 5b69343437..8b5633b778 100644 --- a/editor/src/messages/tool/tool_messages/mod.rs +++ b/editor/src/messages/tool/tool_messages/mod.rs @@ -7,6 +7,7 @@ pub mod gradient_tool; pub mod imaginate_tool; pub mod line_tool; pub mod navigate_tool; +pub mod node_graph_frame_tool; pub mod path_tool; pub mod pen_tool; pub mod rectangle_tool; diff --git a/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs b/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs new file mode 100644 index 0000000000..92a1be507d --- /dev/null +++ b/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs @@ -0,0 +1,231 @@ +use crate::consts::DRAG_THRESHOLD; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion}; +use crate::messages::layout::utility_types::layout_widget::PropertyHolder; +use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::resize::Resize; +use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; +use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; + +use graphene::Operation; + +use glam::DAffine2; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct NodeGraphFrameTool { + fsm_state: NodeGraphToolFsmState, + tool_data: NodeGraphToolData, +} + +#[remain::sorted] +#[impl_message(Message, ToolMessage, NodeGraphFrame)] +#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum NodeGraphFrameToolMessage { + // Standard messages + #[remain::unsorted] + Abort, + + // Tool-specific messages + DragStart, + DragStop, + Resize { + center: Key, + lock_ratio: Key, + }, +} + +impl PropertyHolder for NodeGraphFrameTool {} + +impl<'a> MessageHandler> for NodeGraphFrameTool { + fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + if message == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + if message == ToolMessage::UpdateCursor { + self.fsm_state.update_cursor(responses); + return; + } + + let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + self.fsm_state.update_cursor(responses); + } + } + + fn actions(&self) -> ActionList { + use NodeGraphToolFsmState::*; + + match self.fsm_state { + Ready => actions!(NodeGraphFrameToolMessageDiscriminant; + DragStart, + ), + Drawing => actions!(NodeGraphFrameToolMessageDiscriminant; + DragStop, + Abort, + Resize, + ), + } + } +} + +impl ToolMetadata for NodeGraphFrameTool { + fn icon_name(&self) -> String { + "RasterNodesTool".into() + } + fn tooltip(&self) -> String { + "Node Graph Frame Tool".into() + } + fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { + ToolType::NodeGraphFrame + } +} + +impl ToolTransition for NodeGraphFrameTool { + fn event_to_message_map(&self) -> EventToMessageMap { + EventToMessageMap { + document_dirty: None, + tool_abort: Some(NodeGraphFrameToolMessage::Abort.into()), + selection_changed: None, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum NodeGraphToolFsmState { + Ready, + Drawing, +} + +impl Default for NodeGraphToolFsmState { + fn default() -> Self { + NodeGraphToolFsmState::Ready + } +} +#[derive(Clone, Debug, Default)] +struct NodeGraphToolData { + data: Resize, +} + +impl Fsm for NodeGraphToolFsmState { + type ToolData = NodeGraphToolData; + type ToolOptions = (); + + fn transition( + self, + event: ToolMessage, + tool_data: &mut Self::ToolData, + (document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData, + _tool_options: &Self::ToolOptions, + responses: &mut VecDeque, + ) -> Self { + use NodeGraphFrameToolMessage::*; + use NodeGraphToolFsmState::*; + + let mut shape_data = &mut tool_data.data; + + if let ToolMessage::NodeGraphFrame(event) = event { + match (self, event) { + (Ready, DragStart) => { + shape_data.start(responses, document, input.mouse.position, font_cache); + responses.push_back(DocumentMessage::StartTransaction.into()); + shape_data.path = Some(document.get_path_for_new_layer()); + responses.push_back(DocumentMessage::DeselectAllLayers.into()); + + responses.push_back( + Operation::AddNodeGraphFrame { + path: shape_data.path.clone().unwrap(), + insert_index: -1, + transform: DAffine2::ZERO.to_cols_array(), + } + .into(), + ); + + Drawing + } + (state, Resize { center, lock_ratio }) => { + if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) { + responses.push_back(message); + } + + state + } + (Drawing, DragStop) => { + match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD { + true => responses.push_back(DocumentMessage::AbortTransaction.into()), + false => responses.push_back(DocumentMessage::CommitTransaction.into()), + } + + shape_data.cleanup(responses); + + Ready + } + (Drawing, Abort) => { + responses.push_back(DocumentMessage::AbortTransaction.into()); + + shape_data.cleanup(responses); + + Ready + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + NodeGraphToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + key_groups_mac: None, + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Draw Repaint Frame"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::Shift])], + key_groups_mac: None, + mouse: None, + label: String::from("Constrain Square"), + plus: true, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::Alt])], + key_groups_mac: None, + mouse: None, + label: String::from("From Center"), + plus: true, + }, + ])]), + NodeGraphToolFsmState::Drawing => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::Shift])], + key_groups_mac: None, + mouse: None, + label: String::from("Constrain Square"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::Alt])], + key_groups_mac: None, + mouse: None, + label: String::from("From Center"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into()); + } +} diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 896b0ff448..bed1f8701a 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -281,6 +281,7 @@ pub enum ToolType { Detail, Relight, Imaginate, + NodeGraphFrame, } enum ToolAvailability { @@ -293,28 +294,29 @@ fn list_tools_in_groups() -> Vec> { vec![ vec![ // General tool group - ToolAvailability::Available(Box::new(select_tool::SelectTool::default())), - ToolAvailability::Available(Box::new(artboard_tool::ArtboardTool::default())), - ToolAvailability::Available(Box::new(navigate_tool::NavigateTool::default())), - ToolAvailability::Available(Box::new(eyedropper_tool::EyedropperTool::default())), - ToolAvailability::Available(Box::new(fill_tool::FillTool::default())), - ToolAvailability::Available(Box::new(gradient_tool::GradientTool::default())), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), ], vec![ // Vector tool group - ToolAvailability::Available(Box::new(path_tool::PathTool::default())), - ToolAvailability::Available(Box::new(pen_tool::PenTool::default())), - ToolAvailability::Available(Box::new(freehand_tool::FreehandTool::default())), - ToolAvailability::Available(Box::new(spline_tool::SplineTool::default())), - ToolAvailability::Available(Box::new(line_tool::LineTool::default())), - ToolAvailability::Available(Box::new(rectangle_tool::RectangleTool::default())), - ToolAvailability::Available(Box::new(ellipse_tool::EllipseTool::default())), - ToolAvailability::Available(Box::new(shape_tool::ShapeTool::default())), - ToolAvailability::Available(Box::new(text_tool::TextTool::default())), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), ], vec![ // Raster tool group - ToolAvailability::Available(Box::new(imaginate_tool::ImaginateTool::default())), + ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), ToolAvailability::ComingSoon(ToolEntry { tool_type: ToolType::Brush, icon_name: "RasterBrushTool".into(), @@ -384,6 +386,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType { // ToolMessage::Detail(_) => ToolType::Detail, // ToolMessage::Relight(_) => ToolType::Relight, ToolMessage::Imaginate(_) => ToolType::Imaginate, + ToolMessage::NodeGraphFrame(_) => ToolType::NodeGraphFrame, _ => panic!( "Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}", tool_message @@ -420,6 +423,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis // ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail, // ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight, ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate, + ToolType::NodeGraphFrame => ToolMessageDiscriminant::ActivateToolNodeGraphFrame, _ => panic!( "Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}", tool_type diff --git a/frontend/assets/icon-16px-solid/node-nodes.svg b/frontend/assets/icon-16px-solid/node-nodes.svg new file mode 100644 index 0000000000..62c3b769a6 --- /dev/null +++ b/frontend/assets/icon-16px-solid/node-nodes.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/assets/icon-24px-two-tone/raster-nodes-tool.svg b/frontend/assets/icon-24px-two-tone/raster-nodes-tool.svg new file mode 100644 index 0000000000..9004b139ba --- /dev/null +++ b/frontend/assets/icon-24px-two-tone/raster-nodes-tool.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index b06235de61..4bc1f87d62 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -3,7 +3,7 @@ import { reactive, readonly } from "vue"; import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files"; import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, preloadAndSetImaginateBlobURL } from "@/utility-functions/imaginate"; -import { rasterizeSVG } from "@/utility-functions/rasterization"; +import { rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization"; import { type Editor } from "@/wasm-communication/editor"; import { type FrontendDocumentDetails, @@ -14,6 +14,7 @@ import { TriggerImaginateGenerate, TriggerImaginateTerminate, TriggerImaginateCheckServerStatus, + TriggerNodeGraphFrameGenerate, UpdateActiveDocument, UpdateOpenDocumentsList, UpdateImageData, @@ -100,6 +101,14 @@ export function createPortfolioState(editor: Editor) { editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight); }); }); + editor.subscriptions.subscribeJsMessage(TriggerNodeGraphFrameGenerate, async (triggerNodeGraphFrameGenerate) => { + const { documentId, layerPath, svg, size } = triggerNodeGraphFrameGenerate; + + // Rasterize the SVG to an image file + const imageData = (await rasterizeSVGCanvas(svg, size[0], size[1])).getContext("2d")?.getImageData(0, 0, size[0], size[1]); + + if (imageData) editor.instance.processNodeGraphFrame(documentId, layerPath, new Uint8Array(imageData.data), imageData.width, imageData.height); + }); editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => { URL.revokeObjectURL(triggerRevokeBlobUrl.url); }); diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index b5a0aaf295..d7a7b4dc76 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -112,6 +112,7 @@ import NodeImaginate from "@/../assets/icon-16px-solid/node-imaginate.svg"; import NodeMagicWand from "@/../assets/icon-16px-solid/node-magic-wand.svg"; import NodeMask from "@/../assets/icon-16px-solid/node-mask.svg"; import NodeMotionBlur from "@/../assets/icon-16px-solid/node-motion-blur.svg"; +import NodeNodes from "@/../assets/icon-16px-solid/node-nodes.svg"; import NodeOutput from "@/../assets/icon-16px-solid/node-output.svg"; import NodeShape from "@/../assets/icon-16px-solid/node-shape.svg"; import NodeText from "@/../assets/icon-16px-solid/node-text.svg"; @@ -168,6 +169,7 @@ const SOLID_16PX = { NodeMagicWand: { component: NodeMagicWand, size: 16 }, NodeMask: { component: NodeMask, size: 16 }, NodeMotionBlur: { component: NodeMotionBlur, size: 16 }, + NodeNodes: { component: NodeNodes, size: 16 }, NodeOutput: { component: NodeOutput, size: 16 }, NodeShape: { component: NodeShape, size: 16 }, NodeText: { component: NodeText, size: 16 }, @@ -228,6 +230,7 @@ import RasterCloneTool from "@/../assets/icon-24px-two-tone/raster-clone-tool.sv import RasterDetailTool from "@/../assets/icon-24px-two-tone/raster-detail-tool.svg"; import RasterHealTool from "@/../assets/icon-24px-two-tone/raster-heal-tool.svg"; import RasterImaginateTool from "@/../assets/icon-24px-two-tone/raster-imaginate-tool.svg"; +import RasterNodesTool from "@/../assets/icon-24px-two-tone/raster-nodes-tool.svg"; import RasterPatchTool from "@/../assets/icon-24px-two-tone/raster-patch-tool.svg"; import RasterRelightTool from "@/../assets/icon-24px-two-tone/raster-relight-tool.svg"; import VectorEllipseTool from "@/../assets/icon-24px-two-tone/vector-ellipse-tool.svg"; @@ -248,6 +251,7 @@ const TWO_TONE_24PX = { GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 }, GeneralSelectTool: { component: GeneralSelectTool, size: 24 }, RasterImaginateTool: { component: RasterImaginateTool, size: 24 }, + RasterNodesTool: { component: RasterNodesTool, size: 24 }, RasterBrushTool: { component: RasterBrushTool, size: 24 }, RasterCloneTool: { component: RasterCloneTool, size: 24 }, RasterDetailTool: { component: RasterDetailTool, size: 24 }, diff --git a/frontend/src/wasm-communication/editor.ts b/frontend/src/wasm-communication/editor.ts index e1f8fa6000..e61d32c660 100644 --- a/frontend/src/wasm-communication/editor.ts +++ b/frontend/src/wasm-communication/editor.ts @@ -9,6 +9,20 @@ export type Editor = Readonly>; // `wasmImport` starts uninitialized because its initialization needs to occur asynchronously, and thus needs to occur by manually calling and awaiting `initWasm()` let wasmImport: WasmRawInstance | undefined; +let editorInstance: WasmEditorInstance | undefined; + +export async function updateImage(path: BigUint64Array, mime: string, imageData: Uint8Array, documentId: bigint): Promise { + const blob = new Blob([imageData], { type: mime }); + + const blobURL = URL.createObjectURL(blob); + + // Pre-decode the image so it is ready to be drawn instantly once it's placed into the viewport SVG + const image = new Image(); + image.src = blobURL; + await image.decode(); + + editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight); +} // Should be called asynchronously before `createEditor()` export async function initWasm(): Promise { @@ -16,6 +30,7 @@ export async function initWasm(): Promise { if (wasmImport !== undefined) return; // Import the WASM module JS bindings and wrap them in the panic proxy + // eslint-disable-next-line import/no-cycle wasmImport = await import("@/../wasm/pkg").then(panicProxy); // Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers @@ -36,6 +51,7 @@ export function createEditor() { // We pass along the first two arguments then add our own `raw` and `instance` context for the last two arguments subscriptions.handleJsMessage(messageType, messageData, raw, instance); }); + editorInstance = instance; // Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend const subscriptions: SubscriptionRouter = createSubscriptionRouter(); diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 298869e03d..542bb59713 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -514,6 +514,16 @@ export class TriggerImaginateTerminate extends JsMessage { readonly hostname!: string; } +export class TriggerNodeGraphFrameGenerate extends JsMessage { + readonly documentId!: bigint; + + readonly layerPath!: BigUint64Array; + + readonly svg!: string; + + readonly size!: [number, number]; +} + export class TriggerRefreshBoundsOfViewports extends JsMessage {} export class TriggerRevokeBlobUrl extends JsMessage { @@ -641,7 +651,7 @@ export class LayerMetadata { selected!: boolean; } -export type LayerType = "Imaginate" | "Folder" | "Image" | "Shape" | "Text"; +export type LayerType = "Imaginate" | "NodeGraphFrame" | "Folder" | "Image" | "Shape" | "Text"; export type LayerTypeData = { name: string; @@ -651,6 +661,7 @@ export type LayerTypeData = { export function layerTypeData(layerType: LayerType): LayerTypeData | undefined { const entries: Record = { Imaginate: { name: "Imaginate", icon: "NodeImaginate" }, + NodeGraphFrame: { name: "Node Graph Frame", icon: "NodeNodes" }, Folder: { name: "Folder", icon: "NodeFolder" }, Image: { name: "Image", icon: "NodeImage" }, Shape: { name: "Shape", icon: "NodeShape" }, @@ -1218,6 +1229,7 @@ export const messageMakers: Record = { TriggerImaginateCheckServerStatus, TriggerImaginateGenerate, TriggerImaginateTerminate, + TriggerNodeGraphFrameGenerate, TriggerFileDownload, TriggerFontLoad, TriggerImport, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 6755adee5e..5ae44609d5 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -28,6 +28,13 @@ pub fn set_random_seed(seed: u64) { editor::application::set_uuid_seed(seed); } +/// We directly interface with the updateImage JS function for massively increased performance over serializing and deserializing. +/// This avoids creating a json with a list millions of numbers long. +#[wasm_bindgen(module = "@/wasm-communication/editor")] +extern "C" { + fn updateImage(path: Vec, mime: String, imageData: Vec, document_id: u64); +} + /// Provides a handle to access the raw WASM memory #[wasm_bindgen(js_name = wasmMemory)] pub fn wasm_memory() -> JsValue { @@ -90,6 +97,14 @@ impl JsEditorHandle { // Sends a FrontendMessage to JavaScript fn send_frontend_message_to_js(&self, message: FrontendMessage) { + // Special case for update image data to avoid serialization times. + if let FrontendMessage::UpdateImageData { document_id, image_data } = message { + for image in image_data { + updateImage(image.path, image.mime, image.image_data, document_id); + } + return; + } + let message_type = message.to_discriminant().local_name(); let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); @@ -512,6 +527,18 @@ impl JsEditorHandle { self.dispatch(message); } + /// Sends the blob URL generated by JS to the Imaginate layer in the respective document + #[wasm_bindgen(js_name = processNodeGraphFrame)] + pub fn process_node_graph_frame(&self, document_id: u64, layer_path: Vec, image_data: Vec, width: u32, height: u32) { + let message = PortfolioMessage::ProcessNodeGraphFrame { + document_id, + layer_path, + image_data, + size: (width, height), + }; + self.dispatch(message); + } + /// Pastes an image #[wasm_bindgen(js_name = pasteImage)] pub fn paste_image(&self, mime: String, image_data: Vec, mouse_x: Option, mouse_y: Option) { diff --git a/graphene/Cargo.toml b/graphene/Cargo.toml index 621f7bf3a0..9960e568de 100644 --- a/graphene/Cargo.toml +++ b/graphene/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/GraphiteEditor/Graphite" license = "Apache-2.0" [dependencies] +graph-craft = { path = "../node-graph/graph-craft", features = ["serde"] } +image = { version = "0.24", default-features = false } + log = "0.4" bezier-rs = { path = "../libraries/bezier-rs" } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 63ed07d8ab..674298d927 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -4,6 +4,7 @@ use crate::layers::folder_layer::FolderLayer; use crate::layers::image_layer::ImageLayer; use crate::layers::imaginate_layer::{ImaginateImageData, ImaginateLayer, ImaginateStatus}; use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant}; +use crate::layers::nodegraph_layer::NodeGraphFrameLayer; use crate::layers::shape_layer::ShapeLayer; use crate::layers::style::RenderData; use crate::layers::text_layer::{Font, FontCache, TextLayer}; @@ -595,6 +596,22 @@ impl Document { Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } + Operation::AddNodeGraphFrame { path, insert_index, transform } => { + let layer = Layer::new(LayerDataType::NodeGraphFrame(NodeGraphFrameLayer::default()), transform); + + self.set_layer(&path, layer, insert_index)?; + + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) + } + Operation::SetNodeGraphFrameImageData { layer_path, image_data } => { + let layer = self.layer_mut(&layer_path).expect("Setting NodeGraphFrame image data for invalid layer"); + if let LayerDataType::NodeGraphFrame(node_graph_frame) = &mut layer.data { + node_graph_frame.image_data = Some(crate::layers::nodegraph_layer::ImageData { image_data }); + } else { + panic!("Incorrectly trying to set image data for a layer that is not an NodeGraphFrame layer type"); + } + Some(vec![LayerChanged { path: layer_path.clone() }]) + } Operation::SetTextEditability { path, editable } => { self.layer_mut(&path)?.as_text_mut()?.editable = editable; self.mark_as_dirty(&path)?; @@ -797,11 +814,15 @@ impl Document { image.blob_url = Some(blob_url); image.dimensions = resolution.into(); } + LayerDataType::NodeGraphFrame(node_graph_frame) => { + node_graph_frame.blob_url = Some(blob_url); + node_graph_frame.dimensions = resolution.into(); + } LayerDataType::Imaginate(imaginate) => { imaginate.blob_url = Some(blob_url); imaginate.dimensions = resolution.into(); } - _ => panic!("Incorrectly trying to set the image blob URL for a layer that is not an Image or Imaginate layer type"), + _ => panic!("Incorrectly trying to set the image blob URL for a layer that is not an Image, NodeGraphFrame or Imaginate layer type"), } self.mark_as_dirty(&layer_path)?; @@ -833,15 +854,20 @@ impl Document { } Some(vec![LayerChanged { path: path.clone() }]) } - Operation::ImaginateClear { path } => { + Operation::ClearBlobURL { path } => { let layer = self.layer_mut(&path).expect("Clearing Imaginate image for invalid layer"); - if let LayerDataType::Imaginate(imaginate) = &mut layer.data { - imaginate.image_data = None; - imaginate.blob_url = None; - imaginate.status = ImaginateStatus::Idle; - imaginate.percent_complete = 0.; - } else { - panic!("Incorrectly trying to clear the blob URL for a layer that is not an Imaginate layer type"); + match &mut layer.data { + LayerDataType::Imaginate(imaginate) => { + imaginate.image_data = None; + imaginate.blob_url = None; + imaginate.status = ImaginateStatus::Idle; + imaginate.percent_complete = 0.; + } + LayerDataType::NodeGraphFrame(node_graph) => { + node_graph.image_data = None; + node_graph.blob_url = None; + } + e => panic!("Incorrectly trying to clear the blob URL for layer of type {}", LayerDataTypeDiscriminant::from(&*e)), } self.mark_as_dirty(&path)?; Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) diff --git a/graphene/src/layers/layer_info.rs b/graphene/src/layers/layer_info.rs index b93296926a..a0bbee01a7 100644 --- a/graphene/src/layers/layer_info.rs +++ b/graphene/src/layers/layer_info.rs @@ -2,6 +2,7 @@ use super::blend_mode::BlendMode; use super::folder_layer::FolderLayer; use super::image_layer::ImageLayer; use super::imaginate_layer::ImaginateLayer; +use super::nodegraph_layer::NodeGraphFrameLayer; use super::shape_layer::ShapeLayer; use super::style::{PathStyle, RenderData}; use super::text_layer::TextLayer; @@ -27,8 +28,10 @@ pub enum LayerDataType { Text(TextLayer), /// A layer that wraps an [ImageLayer] struct. Image(ImageLayer), - /// A layer that wraps an [ImageLayer] struct. + /// A layer that wraps an [ImaginateLayer] struct. Imaginate(ImaginateLayer), + /// A layer that wraps an [NodeGraphFrameLayer] struct. + NodeGraphFrame(NodeGraphFrameLayer), } impl LayerDataType { @@ -39,6 +42,7 @@ impl LayerDataType { LayerDataType::Text(t) => t, LayerDataType::Image(i) => i, LayerDataType::Imaginate(a) => a, + LayerDataType::NodeGraphFrame(n) => n, } } @@ -49,6 +53,7 @@ impl LayerDataType { LayerDataType::Text(t) => t, LayerDataType::Image(i) => i, LayerDataType::Imaginate(a) => a, + LayerDataType::NodeGraphFrame(n) => n, } } } @@ -60,6 +65,7 @@ pub enum LayerDataTypeDiscriminant { Text, Image, Imaginate, + NodeGraphFrame, } impl fmt::Display for LayerDataTypeDiscriminant { @@ -70,6 +76,7 @@ impl fmt::Display for LayerDataTypeDiscriminant { LayerDataTypeDiscriminant::Text => write!(f, "Text"), LayerDataTypeDiscriminant::Image => write!(f, "Image"), LayerDataTypeDiscriminant::Imaginate => write!(f, "Imaginate"), + LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "NodeGraphFrame"), } } } @@ -84,6 +91,7 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant { Text(_) => LayerDataTypeDiscriminant::Text, Image(_) => LayerDataTypeDiscriminant::Image, Imaginate(_) => LayerDataTypeDiscriminant::Imaginate, + NodeGraphFrame(_) => LayerDataTypeDiscriminant::NodeGraphFrame, } } } diff --git a/graphene/src/layers/mod.rs b/graphene/src/layers/mod.rs index 1f15e904d3..78c8ab8191 100644 --- a/graphene/src/layers/mod.rs +++ b/graphene/src/layers/mod.rs @@ -27,6 +27,8 @@ pub mod image_layer; pub mod imaginate_layer; /// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers. pub mod layer_info; +/// Contains the [NodegraphLayer](nodegraph_layer::NodegraphLayer) type that contains a node graph. +pub mod nodegraph_layer; /// Contains the [ShapeLayer](shape_layer::ShapeLayer) type, a generic SVG element defined using Bezier paths. pub mod shape_layer; pub mod style; diff --git a/graphene/src/layers/nodegraph_layer.rs b/graphene/src/layers/nodegraph_layer.rs new file mode 100644 index 0000000000..694eab2876 --- /dev/null +++ b/graphene/src/layers/nodegraph_layer.rs @@ -0,0 +1,152 @@ +use super::base64_serde; +use super::layer_info::LayerData; +use super::style::{RenderData, ViewMode}; +use crate::intersection::{intersect_quad_bez_path, Quad}; +use crate::layers::text_layer::FontCache; +use crate::LayerId; + +use glam::{DAffine2, DMat2, DVec2}; +use kurbo::{Affine, BezPath, Shape as KurboShape}; +use serde::{Deserialize, Serialize}; +use std::fmt::Write; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct NodeGraphFrameLayer { + // Image stored in layer after generation completes + pub mime: String, + + /// The document node network that this layer contains + pub network: graph_craft::document::NodeNetwork, + + // TODO: Have the browser dispose of this blob URL when this is dropped (like when the layer is deleted) + #[serde(skip)] + pub blob_url: Option, + #[serde(skip)] + pub dimensions: DVec2, + pub image_data: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct ImageData { + #[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")] + pub image_data: Vec, +} + +impl LayerData for NodeGraphFrameLayer { + fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec, render_data: RenderData) { + let transform = self.transform(transforms, render_data.view_mode); + let inverse = transform.inverse(); + + let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); + + if !inverse.is_finite() { + let _ = write!(svg, ""); + return; + } + + let _ = writeln!(svg, r#""#); + + let matrix = (transform * DAffine2::from_scale((width, height).into()).inverse()) + .to_cols_array() + .iter() + .enumerate() + .fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," })); + + if let Some(blob_url) = &self.blob_url { + let _ = write!( + svg, + r#""#, + width.abs(), + height.abs(), + blob_url, + matrix + ); + } + let _ = write!( + svg, + r#""#, + width.abs(), + height.abs(), + matrix, + ); + + let _ = svg.write_str(r#""#); + } + + fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> { + let mut path = self.bounds(); + + if transform.matrix2 == DMat2::ZERO { + return None; + } + path.apply_affine(glam_to_kurbo(transform)); + + let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box(); + Some([(x0, y0).into(), (x1, y1).into()]) + } + + fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>, _font_cache: &FontCache) { + if intersect_quad_bez_path(quad, &self.bounds(), true) { + intersections.push(path.clone()); + } + } +} + +impl NodeGraphFrameLayer { + pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 { + let start = match mode { + ViewMode::Outline => 0, + _ => (transforms.len() as i32 - 1).max(0) as usize, + }; + transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY) + } + + fn bounds(&self) -> BezPath { + kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)).to_path(0.) + } +} + +fn glam_to_kurbo(transform: DAffine2) -> Affine { + Affine::new(transform.to_cols_array()) +} + +impl Default for NodeGraphFrameLayer { + fn default() -> Self { + use graph_craft::document::*; + use graph_craft::proto::NodeIdentifier; + Self { + mime: String::new(), + network: NodeNetwork { + inputs: vec![1], + output: 1, + nodes: [ + ( + 0, + DocumentNode { + name: "grayscale".into(), + inputs: vec![NodeInput::Network], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[])), + }, + ), + ( + 1, + DocumentNode { + name: "map image".into(), + inputs: vec![NodeInput::Network, NodeInput::Node(0)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_std::raster::MapImageNode", &[])), + }, + ), + ] + .into_iter() + .collect(), + }, + blob_url: None, + dimensions: DVec2::ZERO, + image_data: None, + } + } +} diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index 331d086b24..bfca4dfde6 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -57,6 +57,15 @@ pub enum Operation { insert_index: isize, transform: [f64; 6], }, + AddNodeGraphFrame { + path: Vec, + insert_index: isize, + transform: [f64; 6], + }, + SetNodeGraphFrameImageData { + layer_path: Vec, + image_data: Vec, + }, /// Sets a blob URL as the image source for an Image or Imaginate layer type. /// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.** SetLayerBlobUrl { @@ -64,9 +73,9 @@ pub enum Operation { blob_url: String, resolution: (f64, f64), }, - /// Clears the image to leave the Imaginate layer un-rendered. + /// Clears the image to leave the layer un-rendered. /// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.** - ImaginateClear { + ClearBlobURL { path: Vec, }, ImaginateSetGeneratingStatus { diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index 0c3eeb6bd0..e5934334be 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -14,3 +14,8 @@ num-traits = "0.2" borrow_stack = { path = "../borrow_stack" } dyn-clone = "1.0" rand_chacha = "0.3.1" + +[dependencies.serde] +version = "1.0" +optional = true +features = ["derive"] diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 07caa2e09a..d50a7a7850 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -28,9 +28,8 @@ fn merge_ids(a: u64, b: u64) -> u64 { hasher.finish() } -type Fqn = NodeIdentifier<'static>; - -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DocumentNode { pub name: String, pub inputs: Vec, @@ -55,9 +54,9 @@ impl DocumentNode { let first = self.inputs.remove(0); if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation { let (input, mut args) = match first { - NodeInput::Value(value) => { + NodeInput::Value(tagged_value) => { assert_eq!(self.inputs.len(), 0); - (ProtoNodeInput::None, ConstructionArgs::Value(value)) + (ProtoNodeInput::None, ConstructionArgs::Value(tagged_value.to_value())) } NodeInput::Node(id) => (ProtoNodeInput::Node(id), ConstructionArgs::Nodes(vec![])), NodeInput::Network => (ProtoNodeInput::Network, ConstructionArgs::Nodes(vec![])), @@ -82,10 +81,11 @@ impl DocumentNode { } } -#[derive(Debug)] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum NodeInput { Node(NodeId), - Value(value::Value), + Value(value::TaggedValue), Network, } @@ -107,13 +107,15 @@ impl PartialEq for NodeInput { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum DocumentNodeImplementation { Network(NodeNetwork), - Unresolved(Fqn), + Unresolved(NodeIdentifier), } -#[derive(Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeNetwork { pub inputs: Vec, pub output: NodeId, @@ -160,7 +162,7 @@ impl NodeNetwork { network_input.populate_first_network_input(node, *offset); } NodeInput::Value(value) => { - let name = format!("Value: {:?}", value); + let name = format!("Value: {:?}", value.clone().to_value()); let new_id = map_ids(id, gen_id()); let value_node = DocumentNode { name: name.clone(), @@ -284,7 +286,7 @@ mod test { 1, DocumentNode { name: "Inc".into(), - inputs: vec![NodeInput::Network, NodeInput::Value(2_u32.into_any())], + inputs: vec![NodeInput::Network, NodeInput::Value(value::TaggedValue::U32(2))], implementation: DocumentNodeImplementation::Network(add_network()), }, )] @@ -384,7 +386,7 @@ mod test { 14, DocumentNode { name: "Value: 2".into(), - inputs: vec![NodeInput::Value(2_u32.into_any())], + inputs: vec![NodeInput::Value(value::TaggedValue::U32(2))], implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic])), }, ), diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 640313af87..927fe0305b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -4,6 +4,26 @@ use dyn_clone::DynClone; use dyn_any::{DynAny, Upcast}; +/// A type that is known, allowing serialization (serde::Deserialize is not object safe) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum TaggedValue { + String(String), + U32(u32), + //Image(graphene_std::raster::Image), + Color(graphene_core::raster::color::Color), +} + +impl TaggedValue { + pub fn to_value(self) -> Value { + match self { + TaggedValue::String(x) => Box::new(x), + TaggedValue::U32(x) => Box::new(x), + TaggedValue::Color(x) => Box::new(x), + } + } +} + pub type Value = Box; pub trait ValueTrait: DynAny<'static> + Upcast> + std::fmt::Debug + DynClone {} diff --git a/node-graph/graph-craft/src/lib.rs b/node-graph/graph-craft/src/lib.rs index 81e975602c..6ea8a72453 100644 --- a/node-graph/graph-craft/src/lib.rs +++ b/node-graph/graph-craft/src/lib.rs @@ -11,7 +11,6 @@ mod tests { use graphene_core::value::ValueNode; use graphene_core::{structural::*, RefNode}; - use crate::document::value::IntoValue; use borrow_stack::BorrowStack; use borrow_stack::FixedSizeStack; use dyn_any::{downcast, IntoDynAny}; @@ -68,7 +67,10 @@ mod tests { DocumentNode { name: "cons".into(), inputs: vec![NodeInput::Network, NodeInput::Network], - implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[Type::Concrete("u32"), Type::Concrete("u32")])), + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new( + "graphene_core::structural::ConsNode", + &[Type::Concrete(std::borrow::Cow::Borrowed("u32")), Type::Concrete(std::borrow::Cow::Borrowed("u32"))], + )), }, ), ( @@ -76,7 +78,10 @@ mod tests { DocumentNode { name: "add".into(), inputs: vec![NodeInput::Node(0)], - implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete("u32"), Type::Concrete("u32")])), + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new( + "graphene_core::ops::AddNode", + &[Type::Concrete(std::borrow::Cow::Borrowed("u32")), Type::Concrete(std::borrow::Cow::Borrowed("u32"))], + )), }, ), ] @@ -92,7 +97,7 @@ mod tests { 0, DocumentNode { name: "Inc".into(), - inputs: vec![NodeInput::Network, NodeInput::Value(1_u32.into_any())], + inputs: vec![NodeInput::Network, NodeInput::Value(value::TaggedValue::U32(1))], implementation: DocumentNodeImplementation::Network(add_network()), }, )] diff --git a/node-graph/graph-craft/src/node_registry.rs b/node-graph/graph-craft/src/node_registry.rs index afddfae512..7711ccadcd 100644 --- a/node-graph/graph-craft/src/node_registry.rs +++ b/node-graph/graph-craft/src/node_registry.rs @@ -1,11 +1,9 @@ -use std::marker::PhantomData; - use borrow_stack::FixedSizeStack; use graphene_core::generic::FnNode; -use graphene_core::ops::{AddNode, IdNode}; +use graphene_core::ops::AddNode; use graphene_core::raster::color::Color; use graphene_core::structural::{ConsNode, Then}; -use graphene_core::{AsRefNode, Node}; +use graphene_core::Node; use graphene_std::any::DowncastBothNode; use graphene_std::any::{Any, DowncastNode, DynAnyNode, IntoTypeErasedNode, TypeErasedNode}; use graphene_std::raster::Image; @@ -13,17 +11,12 @@ use graphene_std::raster::Image; use crate::proto::Type; use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNode, ProtoNodeInput, Type::Concrete}; -use dyn_any::Upcast; - type NodeConstructor = fn(ProtoNode, &FixedSizeStack>); //TODO: turn into hasmap -static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ +static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[ ( - NodeIdentifier { - name: "graphene_core::ops::IdNode", - types: &[Concrete("Any<'_>")], - }, + NodeIdentifier::new("graphene_core::ops::IdNode", &[Concrete(std::borrow::Cow::Borrowed("Any<'_>"))]), |proto_node, stack| { stack.push_fn(|nodes| { let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); @@ -32,24 +25,18 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }) }, ), + (NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic]), |proto_node, stack| { + stack.push_fn(|nodes| { + let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); + let node = pre_node.then(graphene_core::ops::IdNode); + node.into_type_erased() + }) + }), ( - NodeIdentifier { - name: "graphene_core::ops::IdNode", - types: &[Type::Generic], - }, - |proto_node, stack| { - stack.push_fn(|nodes| { - let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); - let node = pre_node.then(graphene_core::ops::IdNode); - node.into_type_erased() - }) - }, - ), - ( - NodeIdentifier { - name: "graphene_core::ops::AddNode", - types: &[Concrete("u32"), Concrete("u32")], - }, + NodeIdentifier::new( + "graphene_core::ops::AddNode", + &[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))], + ), |proto_node, stack| { stack.push_fn(|nodes| { let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); @@ -61,10 +48,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_core::ops::AddNode", - types: &[Concrete("&u32"), Concrete("&u32")], - }, + NodeIdentifier::new( + "graphene_core::ops::AddNode", + &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("&u32"))], + ), |proto_node, stack| { stack.push_fn(|nodes| { let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); @@ -76,10 +63,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_core::ops::AddNode", - types: &[Concrete("&u32"), Concrete("u32")], - }, + NodeIdentifier::new( + "graphene_core::ops::AddNode", + &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("u32"))], + ), |proto_node, stack| { stack.push_fn(|nodes| { let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); @@ -91,10 +78,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_core::structural::ConsNode", - types: &[Concrete("&u32"), Concrete("u32")], - }, + NodeIdentifier::new( + "graphene_core::structural::ConsNode", + &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("u32"))], + ), |proto_node, stack| { if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { stack.push_fn(move |nodes| { @@ -118,10 +105,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_core::structural::ConsNode", - types: &[Concrete("u32"), Concrete("u32")], - }, + NodeIdentifier::new( + "graphene_core::structural::ConsNode", + &[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))], + ), |proto_node, stack| { if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { stack.push_fn(move |nodes| { @@ -146,10 +133,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ ), // TODO: create macro to impl for all types ( - NodeIdentifier { - name: "graphene_core::structural::ConsNode", - types: &[Concrete("&u32"), Concrete("&u32")], - }, + NodeIdentifier::new( + "graphene_core::structural::ConsNode", + &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("&u32"))], + ), |proto_node, stack| { let node_id = proto_node.input.unwrap_node() as usize; if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { @@ -168,10 +155,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_core::any::DowncastNode", - types: &[Concrete("&u32")], - }, + NodeIdentifier::new("graphene_core::any::DowncastNode", &[Concrete(std::borrow::Cow::Borrowed("&u32"))]), |proto_node, stack| { stack.push_fn(|nodes| { let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); @@ -181,10 +165,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_core::value::ValueNode", - types: &[Concrete("Any<'_>")], - }, + NodeIdentifier::new("graphene_core::value::ValueNode", &[Concrete(std::borrow::Cow::Borrowed("Any<'_>"))]), |proto_node, stack| { stack.push_fn(|_nodes| { if let ConstructionArgs::Value(value) = proto_node.construction_args { @@ -197,69 +178,48 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }) }, ), - ( - NodeIdentifier { - name: "graphene_core::value::ValueNode", - types: &[Type::Generic], - }, - |proto_node, stack| { - stack.push_fn(|_nodes| { - if let ConstructionArgs::Value(value) = proto_node.construction_args { - let node = FnNode::new(move |_| value.clone().up_box() as Any<'static>); - node.into_type_erased() - } else { - unreachable!() - } - }) - }, - ), - ( - NodeIdentifier { - name: "graphene_core::raster::GrayscaleNode", - types: &[], - }, - |proto_node, stack| { - stack.push_fn(|nodes| { - let node = DynAnyNode::new(graphene_core::raster::GrayscaleNode); - - if let ProtoNodeInput::Node(pre_id) = proto_node.input { - let pre_node = nodes.get(pre_id as usize).unwrap(); - (pre_node).then(node).into_type_erased() - } else { - node.into_type_erased() - } - }) - }, - ), - ( - NodeIdentifier { - name: "graphene_std::raster::MapImageNode", - types: &[], - }, - |proto_node, stack| { - let node_id = proto_node.input.unwrap_node() as usize; - if let ConstructionArgs::Nodes(operation_node_id) = proto_node.construction_args { - stack.push_fn(move |nodes| { - let pre_node = nodes.get(node_id).unwrap(); - - let operation_node = nodes.get(operation_node_id[0] as usize).unwrap(); - let operation_node: DowncastBothNode<_, Color, Color> = DowncastBothNode::new(operation_node); - let map_node = DynAnyNode::new(graphene_std::raster::MapImageNode::new(operation_node)); - - let node = (pre_node).then(map_node); - - node.into_type_erased() - }) + (NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic]), |proto_node, stack| { + stack.push_fn(|_nodes| { + if let ConstructionArgs::Value(value) = proto_node.construction_args { + let node = FnNode::new(move |_| value.clone().up_box() as Any<'static>); + node.into_type_erased() } else { - unimplemented!() + unreachable!() } - }, - ), + }) + }), + (NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[]), |proto_node, stack| { + stack.push_fn(|nodes| { + let node = DynAnyNode::new(graphene_core::raster::GrayscaleNode); + + if let ProtoNodeInput::Node(pre_id) = proto_node.input { + let pre_node = nodes.get(pre_id as usize).unwrap(); + (pre_node).then(node).into_type_erased() + } else { + node.into_type_erased() + } + }) + }), + (NodeIdentifier::new("graphene_std::raster::MapImageNode", &[]), |proto_node, stack| { + if let ConstructionArgs::Nodes(operation_node_id) = proto_node.construction_args { + stack.push_fn(move |nodes| { + let operation_node = nodes.get(operation_node_id[0] as usize).unwrap(); + let operation_node: DowncastBothNode<_, Color, Color> = DowncastBothNode::new(operation_node); + let map_node = DynAnyNode::new(graphene_std::raster::MapImageNode::new(operation_node)); + + if let ProtoNodeInput::Node(node_id) = proto_node.input { + let pre_node = nodes.get(node_id as usize).unwrap(); + (pre_node).then(map_node).into_type_erased() + } else { + map_node.into_type_erased() + } + }) + } else { + unimplemented!() + } + }), ( - NodeIdentifier { - name: "graphene_std::raster::ImageNode", - types: &[Concrete("&str")], - }, + NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]), |_proto_node, stack| { stack.push_fn(|_nodes| { let image = FnNode::new(|s: &str| graphene_std::raster::image_node::<&str>().eval(s).unwrap()); @@ -269,10 +229,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_std::raster::ExportImageNode", - types: &[Concrete("&str")], - }, + NodeIdentifier::new("graphene_std::raster::ExportImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]), |proto_node, stack| { stack.push_fn(|nodes| { let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); @@ -285,10 +242,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ }, ), ( - NodeIdentifier { - name: "graphene_core::structural::ConsNode", - types: &[Concrete("Image"), Concrete("&str")], - }, + NodeIdentifier::new( + "graphene_core::structural::ConsNode", + &[Concrete(std::borrow::Cow::Borrowed("Image")), Concrete(std::borrow::Cow::Borrowed("&str"))], + ), |proto_node, stack| { let node_id = proto_node.input.unwrap_node() as usize; if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { @@ -322,11 +279,6 @@ mod protograph_testing { use super::*; - /// Lookup a node by th suffix of the name (for testing only) - fn simple_lookup(suffix: &str) -> &(NodeIdentifier, fn(ProtoNode, &FixedSizeStack>)) { - NODE_REGISTRY.iter().find(|node| node.0.name.ends_with(suffix)).unwrap() - } - #[test] fn add_values() { let stack = FixedSizeStack::new(256); @@ -339,14 +291,20 @@ mod protograph_testing { let cons_protonode = ProtoNode { construction_args: ConstructionArgs::Nodes(vec![1]), input: ProtoNodeInput::Node(0), - identifier: NodeIdentifier::new("graphene_core::structural::ConsNode", &[Concrete("u32"), Concrete("u32")]), + identifier: NodeIdentifier::new( + "graphene_core::structural::ConsNode", + &[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))], + ), }; push_node(cons_protonode, &stack); let add_protonode = ProtoNode { construction_args: ConstructionArgs::Nodes(vec![]), input: ProtoNodeInput::Node(2), - identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Concrete("u32"), Concrete("u32")]), + identifier: NodeIdentifier::new( + "graphene_core::ops::AddNode", + &[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))], + ), }; push_node(add_protonode, &stack); @@ -379,7 +337,7 @@ mod protograph_testing { let image_protonode = ProtoNode { construction_args: ConstructionArgs::Nodes(vec![]), input: ProtoNodeInput::None, - identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete("&str")]), + identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]), }; push_node(image_protonode, &stack); @@ -394,7 +352,7 @@ mod protograph_testing { let image_protonode = ProtoNode { construction_args: ConstructionArgs::Nodes(vec![]), input: ProtoNodeInput::None, - identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete("&str")]), + identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]), }; push_node(image_protonode, &stack); diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 4ec92e2454..052c789c1d 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -1,35 +1,49 @@ use std::collections::HashMap; -use std::collections::HashSet; use crate::document::value; use crate::document::NodeId; -#[derive(Debug, PartialEq)] -pub struct NodeIdentifier<'a> { - pub name: &'a str, - pub types: &'a [Type<'a>], +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct NodeIdentifier { + pub name: std::borrow::Cow<'static, str>, + pub types: std::borrow::Cow<'static, [Type]>, } -#[derive(Debug, PartialEq)] -pub enum Type<'a> { +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Type { Generic, - Concrete(&'a str), + Concrete(std::borrow::Cow<'static, str>), } -impl<'a> From<&'a str> for Type<'a> { - fn from(s: &'a str) -> Self { - Type::Concrete(s) - } -} -impl<'a> From<&'a str> for NodeIdentifier<'a> { - fn from(s: &'a str) -> Self { - NodeIdentifier { name: s, types: &[] } +impl From<&'static str> for Type { + fn from(s: &'static str) -> Self { + Type::Concrete(std::borrow::Cow::Borrowed(s)) } } -impl<'a> NodeIdentifier<'a> { - pub fn new(name: &'a str, types: &'a [Type<'a>]) -> Self { - NodeIdentifier { name, types } +impl Type { + pub const fn from_str(concrete: &'static str) -> Self { + Type::Concrete(std::borrow::Cow::Borrowed(concrete)) + } +} + +impl From<&'static str> for NodeIdentifier { + fn from(s: &'static str) -> Self { + NodeIdentifier { + name: std::borrow::Cow::Borrowed(s), + types: std::borrow::Cow::Borrowed(&[]), + } + } +} + +impl NodeIdentifier { + pub const fn new(name: &'static str, types: &'static [Type]) -> Self { + NodeIdentifier { + name: std::borrow::Cow::Borrowed(name), + types: std::borrow::Cow::Borrowed(types), + } } } @@ -60,7 +74,7 @@ impl PartialEq for ConstructionArgs { pub struct ProtoNode { pub construction_args: ConstructionArgs, pub input: ProtoNodeInput, - pub identifier: NodeIdentifier<'static>, + pub identifier: NodeIdentifier, } #[derive(Debug, Default, PartialEq, Eq)] @@ -83,10 +97,7 @@ impl ProtoNodeInput { impl ProtoNode { pub fn value(value: ConstructionArgs) -> Self { Self { - identifier: NodeIdentifier { - name: "graphene_core::value::ValueNode", - types: &[Type::Generic], - }, + identifier: NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic]), construction_args: value, input: ProtoNodeInput::None, } @@ -110,7 +121,22 @@ impl ProtoNode { } impl ProtoNetwork { - fn reverse_edges(&self) -> HashMap> { + pub fn collect_outwards_edges(&self) -> HashMap> { + let mut edges: HashMap> = HashMap::new(); + for (id, node) in &self.nodes { + if let ProtoNodeInput::Node(ref_id) = &node.input { + edges.entry(*ref_id).or_default().push(*id) + } + if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args { + for ref_id in ref_nodes { + edges.entry(*ref_id).or_default().push(*id) + } + } + } + edges + } + + pub fn collect_inwards_edges(&self) -> HashMap> { let mut edges: HashMap> = HashMap::new(); for (id, node) in &self.nodes { if let ProtoNodeInput::Node(ref_id) = &node.input { @@ -125,38 +151,28 @@ impl ProtoNetwork { edges } + // Based on https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm pub fn topological_sort(&self) -> Vec { - let mut visited = HashSet::new(); - let mut stack = Vec::new(); let mut sorted = Vec::new(); - let graph = self.reverse_edges(); - // TODO: remove - println!("{:#?}", graph); + let outwards_edges = self.collect_outwards_edges(); + let mut inwards_edges = self.collect_inwards_edges(); + let mut no_incoming_edges: Vec<_> = self.nodes.iter().map(|entry| entry.0).filter(|id| !inwards_edges.contains_key(id)).collect(); - for (id, _) in &self.nodes { - if !visited.contains(id) { - stack.push(*id); + assert_ne!(no_incoming_edges.len(), 0, "Acyclic graphs must have at least one node with no incoming edge"); - while let Some(id) = stack.pop() { - //TODO remove - println!("{:?}", stack); - if !visited.contains(&id) { - visited.insert(id); - if let Some(refs) = graph.get(&id) { - for ref_id in refs { - if !visited.contains(ref_id) { - stack.push(id); - stack.push(*ref_id); - break; - } - } - } - sorted.push(id); + while let Some(node_id) = no_incoming_edges.pop() { + sorted.push(node_id); + + if let Some(outwards_edges) = outwards_edges.get(&node_id) { + for &ref_id in outwards_edges { + let dependencies = inwards_edges.get_mut(&ref_id).unwrap(); + dependencies.retain(|&id| id != node_id); + if dependencies.is_empty() { + no_incoming_edges.push(ref_id) } } } } - sorted.reverse(); sorted }