From 6400953af53bbdd74d4c37bf1779f3a47b5e5b5d Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Thu, 18 May 2023 05:12:50 +0100 Subject: [PATCH] Thumbnails for the layer node (#1210) * Thumbnails for the layer node * Raster node graph frames * Downscale to a random resolution * Cleanup and bug fixes * Generate paths before duplicating outputs * Fix stable id test * Code review changes * Code review pass with minor changes --------- Co-authored-by: Keavon Chambers --- document-legacy/src/layers/layer_layer.rs | 4 +- editor/src/messages/frontend/utility_types.rs | 2 + .../document/document_message_handler.rs | 3 +- .../node_graph/node_graph_message_handler.rs | 89 ++++--- .../document_node_types.rs | 40 ++- .../node_properties.rs | 4 +- .../messages/portfolio/portfolio_message.rs | 1 + .../portfolio/portfolio_message_handler.rs | 6 + editor/src/node_graph_executor.rs | 232 +++++++++++++----- frontend/src-tauri/src/main.rs | 1 + .../src/components/panels/NodeGraph.svelte | 6 +- frontend/src/state-providers/portfolio.ts | 2 +- frontend/src/wasm-communication/editor.ts | 8 +- frontend/src/wasm-communication/messages.ts | 4 + frontend/wasm/src/editor_api.rs | 14 +- node-graph/gcore/src/graphic_element.rs | 2 + .../gcore/src/graphic_element/renderer.rs | 195 +++++++++++++++ .../src/graphic_element/renderer/quad.rs | 51 ++++ node-graph/gcore/src/raster/image.rs | 13 +- node-graph/graph-craft/src/document.rs | 26 ++ node-graph/graph-craft/src/executor.rs | 1 - node-graph/graph-craft/src/proto.rs | 13 +- .../interpreted-executor/src/executor.rs | 2 +- .../interpreted-executor/src/node_registry.rs | 21 ++ 24 files changed, 609 insertions(+), 131 deletions(-) create mode 100644 node-graph/gcore/src/graphic_element/renderer.rs create mode 100644 node-graph/gcore/src/graphic_element/renderer/quad.rs diff --git a/document-legacy/src/layers/layer_layer.rs b/document-legacy/src/layers/layer_layer.rs index 68e89546ae..e56138b045 100644 --- a/document-legacy/src/layers/layer_layer.rs +++ b/document-legacy/src/layers/layer_layer.rs @@ -54,7 +54,7 @@ impl LayerData for LayerLayer { match &self.cached_output_data { CachedOutputData::VectorPath(vector_data) => { let layer_bounds = vector_data.bounding_box().unwrap_or_default(); - let transfomed_bounds = vector_data.bounding_box_with_transform(transform).unwrap_or_default(); + let transformed_bounds = vector_data.bounding_box_with_transform(transform).unwrap_or_default(); let _ = write!(svg, ""); } CachedOutputData::BlobURL(blob_url) => { diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index 2b2d04c038..2332ea5828 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -18,6 +18,8 @@ pub struct FrontendImageData { #[serde(skip)] pub image_data: std::sync::Arc>, pub transform: Option<[f64; 6]>, + #[serde(rename = "nodeId")] + pub node_id: Option, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, specta::Type)] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index be733c74c5..bbca76b634 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -187,8 +187,7 @@ impl MessageHandler { - let selected_layers = &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice())); - self.node_graph_handler.process_message(message, responses, (&mut self.document_legacy, selected_layers)); + self.node_graph_handler.process_message(message, responses, (&mut self.document_legacy, executor)); } #[remain::unsorted] GraphOperation(message) => GraphOperationMessageHandler.process_message(message, responses, (&mut self.document_legacy, &mut self.node_graph_handler)), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 52c8f59fa5..c3c141f6a9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -3,6 +3,7 @@ use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; use crate::messages::prelude::*; +use crate::node_graph_executor::NodeGraphExecutor; use document_legacy::document::Document; use document_legacy::layers::layer_layer::LayerLayer; @@ -84,6 +85,8 @@ pub struct FrontendNode { pub position: (i32, i32), pub disabled: bool, pub previewed: bool, + #[serde(rename = "thumbnailSvg")] + pub thumbnail_svg: Option, } // (link_start, link_end, link_end_input_index) @@ -254,9 +257,11 @@ impl NodeGraphMessageHandler { } } - fn send_graph(network: &NodeNetwork, responses: &mut VecDeque) { + fn send_graph(network: &NodeNetwork, executor: &NodeGraphExecutor, layer_path: &Option>, responses: &mut VecDeque) { responses.add(PropertiesPanelMessage::ResendActiveProperties); + let layer_id = layer_path.as_ref().and_then(|path| path.last().copied()); + // List of links in format (link_start, link_end, link_end_input_index) let links = network .nodes @@ -288,37 +293,49 @@ impl NodeGraphMessageHandler { warn!("Node '{}' does not exist in library", node.name); continue; }; + + let primary_input = node + .inputs + .first() + .filter(|input| input.is_exposed()) + .and_then(|_| node_type.inputs.get(0)) + .map(|input_type| input_type.data_type); + let exposed_inputs = node + .inputs + .iter() + .zip(node_type.inputs.iter()) + .skip(1) + .filter(|(input, _)| input.is_exposed()) + .map(|(_, input_type)| NodeGraphInput { + data_type: input_type.data_type, + name: input_type.name.to_string(), + }) + .collect(); + + let outputs = node_type + .outputs + .iter() + .map(|output_type| NodeGraphOutput { + data_type: output_type.data_type, + name: output_type.name.to_string(), + }) + .collect(); + + let thumbnail_svg = layer_id + .and_then(|layer_id| executor.thumbnails.get(&layer_id)) + .and_then(|layer| layer.get(id)) + .map(|svg| svg.to_string()); + nodes.push(FrontendNode { id: *id, display_name: node.name.clone(), - primary_input: node - .inputs - .first() - .filter(|input| input.is_exposed()) - .and_then(|_| node_type.inputs.get(0)) - .map(|input_type| input_type.data_type), - exposed_inputs: node - .inputs - .iter() - .zip(node_type.inputs.iter()) - .skip(1) - .filter(|(input, _)| input.is_exposed()) - .map(|(_, input_type)| NodeGraphInput { - data_type: input_type.data_type, - name: input_type.name.to_string(), - }) - .collect(), - outputs: node_type - .outputs - .iter() - .map(|output_type| NodeGraphOutput { - data_type: output_type.data_type, - name: output_type.name.to_string(), - }) - .collect(), + primary_input, + exposed_inputs, + outputs, position: node.metadata.position.into(), previewed: network.outputs_contain(*id), disabled: network.disabled.contains(id), + thumbnail_svg, }) } responses.add(FrontendMessage::UpdateNodeGraph { nodes, links }); @@ -405,9 +422,9 @@ impl NodeGraphMessageHandler { } } -impl MessageHandler)> for NodeGraphMessageHandler { +impl MessageHandler for NodeGraphMessageHandler { #[remain::check] - fn process_message(&mut self, message: NodeGraphMessage, responses: &mut VecDeque, (document, _selected): (&mut Document, &mut dyn Iterator)) { + fn process_message(&mut self, message: NodeGraphMessage, responses: &mut VecDeque, (document, executor): (&mut Document, &NodeGraphExecutor)) { #[remain::sorted] match message { NodeGraphMessage::CloseNodeGraph => { @@ -536,7 +553,7 @@ impl MessageHandler { self.layer_path = Some(layer_path); @@ -630,7 +647,7 @@ impl MessageHandler { if let Some(network) = self.get_active_network(document) { - Self::send_graph(network, responses); + Self::send_graph(network, executor, &self.layer_path, responses); if should_rerender { if let Some(layer_path) = self.layer_path.clone() { responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path }); @@ -816,7 +833,7 @@ impl MessageHandler Vec { DocumentNodeType { name: "Layer", category: "General", - identifier: NodeImplementation::proto("graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>"), + identifier: NodeImplementation::DocumentNode(NodeNetwork { + inputs: vec![0; 8], + outputs: vec![NodeOutput::new(1, 0)], + nodes: [ + ( + 0, + DocumentNode { + inputs: vec![ + NodeInput::Network(concrete!(graphene_core::vector::VectorData)), + NodeInput::Network(concrete!(String)), + NodeInput::Network(concrete!(BlendMode)), + NodeInput::Network(concrete!(f32)), + NodeInput::Network(concrete!(bool)), + NodeInput::Network(concrete!(bool)), + NodeInput::Network(concrete!(bool)), + NodeInput::Network(concrete!(graphene_core::GraphicGroup)), + ], + implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>"), + ..Default::default() + }, + ), + // The monitor node is used to display a thumbnail in the UI. + ( + 1, + DocumentNode { + inputs: vec![NodeInput::node(0, 0)], + implementation: DocumentNodeImplementation::proto("graphene_std::memo::MonitorNode<_>"), + ..Default::default() + }, + ), + ] + .into(), + ..Default::default() + }), inputs: vec![ DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), DocumentInputType::value("Name", TaggedValue::String(String::new()), false), @@ -528,7 +561,7 @@ fn static_nodes() -> Vec { identifier: NodeImplementation::DocumentNode(NodeNetwork { inputs: vec![0], outputs: vec![NodeOutput::new(1, 0)], - nodes: vec![ + nodes: [ ( 0, DocumentNode { @@ -548,8 +581,7 @@ fn static_nodes() -> Vec { }, ), ] - .into_iter() - .collect(), + .into(), ..Default::default() }), inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)], diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index bd886ff644..91d5fcecfd 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -727,12 +727,12 @@ pub fn adjust_selective_color_properties(document_node: &DocumentNode, node_id: } = &document_node.inputs[colors_index] { use SelectiveColorChoice::*; - let entries = [vec![Reds, Yellows, Greens, Cyans, Blues, Magentas], vec![Whites, Neutrals, Blacks]] + let entries = [[Reds, Yellows, Greens, Cyans, Blues, Magentas].as_slice(), [Whites, Neutrals, Blacks].as_slice()] .into_iter() .map(|section| { section .into_iter() - .map(|choice| DropdownEntryData::new(choice.to_string()).on_update(update_value(move |_| TaggedValue::SelectiveColorChoice(choice), node_id, colors_index))) + .map(|choice| DropdownEntryData::new(choice.to_string()).on_update(update_value(move |_| TaggedValue::SelectiveColorChoice(*choice), node_id, colors_index))) .collect() }) .collect(); diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index a6235bf863..753bc23187 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -128,6 +128,7 @@ pub enum PortfolioMessage { SetImageBlobUrl { document_id: u64, layer_path: Vec, + node_id: Option, blob_url: String, resolution: (f64, f64), }, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 6da6286747..f9a4bad808 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -503,9 +503,15 @@ impl MessageHandler { + if let (Some(layer_id), Some(node_id)) = (layer_path.last().copied(), node_id) { + self.executor.insert_thumbnail_blob_url(blob_url, layer_id, node_id, responses); + return; + } + let message = DocumentMessage::SetImageBlobUrl { layer_path, blob_url, diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 0a9669cb10..bf14c8b7f4 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -7,10 +7,13 @@ use crate::messages::prelude::*; use document_legacy::{document::pick_safe_imaginate_resolution, layers::layer_info::LayerDataType}; use document_legacy::{LayerId, Operation}; use dyn_any::DynAny; -use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork, NodeOutput}; +use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, NodeOutput}; use graph_craft::executor::Compiler; +use graph_craft::imaginate_input::*; use graph_craft::{concrete, Type, TypeDescriptor}; use graphene_core::raster::{Image, ImageFrame}; +use graphene_core::renderer::{SvgSegment, SvgSegmentList}; +use graphene_core::vector::style::ViewMode; use graphene_core::vector::VectorData; use graphene_core::{Color, EditorApi}; use interpreted_executor::executor::DynamicExecutor; @@ -24,16 +27,33 @@ pub struct NodeGraphExecutor { pub(crate) executor: DynamicExecutor, // TODO: This is a memory leak since layers are never removed pub(crate) last_output_type: HashMap, Option>, + pub(crate) thumbnails: HashMap>, +} + +fn get_imaginate_index(name: &str) -> usize { + use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; + IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")) } impl NodeGraphExecutor { - /// Execute the network by flattening it and creating a borrow stack. - fn execute_network<'a>(&'a mut self, network: NodeNetwork, editor_api: EditorApi<'a>) -> Result, String> { + /// Wraps a network in a scope and returns the new network and the paths to the monitor nodes. + fn wrap_network(network: NodeNetwork) -> (NodeNetwork, Vec>) { let mut scoped_network = wrap_network_in_scope(network); + scoped_network.generate_node_paths(&[]); + let monitor_nodes = scoped_network + .recursive_nodes() + .filter(|(node, _, _)| node.implementation == DocumentNodeImplementation::proto("graphene_std::memo::MonitorNode<_>")) + .map(|(_, _, path)| path) + .collect(); scoped_network.duplicate_outputs(&mut generate_uuid); scoped_network.remove_dead_nodes(); + (scoped_network, monitor_nodes) + } + + /// Executes the network by flattening it and creating a borrow stack. + fn execute_network<'a>(&'a mut self, scoped_network: NodeNetwork, editor_api: EditorApi<'a>) -> Result, String> { // We assume only one output assert_eq!(scoped_network.outputs.len(), 1, "Graph with multiple outputs not yet handled"); let c = Compiler {}; @@ -100,6 +120,7 @@ impl NodeGraphExecutor { } } + let (network, _) = Self::wrap_network(network); let boxed = self.execute_network(network, editor_api.into_owned())?; dyn_any::downcast::(boxed).map(|v| *v) @@ -125,73 +146,61 @@ impl NodeGraphExecutor { Ok::<_, String>((image_data, size)) } - fn generate_imaginate( - &mut self, - network: NodeNetwork, - imaginate_node_path: Vec, - (document, document_id): (&mut DocumentMessageHandler, u64), - layer_path: Vec, - mut editor_api: EditorApi<'_>, - (preferences, persistent_data): (&PreferencesMessageHandler, &PersistentData), - ) -> Result { - use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; - use graph_craft::imaginate_input::*; - - let image = editor_api.image_frame.take(); - - let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); - - // Get the node graph layer - let layer = document.document_legacy.layer(&layer_path).map_err(|e| format!("No layer: {e:?}"))?; - let transform = layer.transform; - - let resolution: Option = self.compute_input(&network, &imaginate_node_path, get("Resolution"), Cow::Borrowed(&editor_api))?; - let resolution = resolution.unwrap_or_else(|| { - let (x, y) = pick_safe_imaginate_resolution((transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length())); - DVec2::new(x as f64, y as f64) - }); - - let parameters = ImaginateGenerationParameters { - seed: self.compute_input::(&network, &imaginate_node_path, get("Seed"), Cow::Borrowed(&editor_api))? as u64, + fn imaginate_parameters(&mut self, network: &NodeNetwork, node_path: &[LayerId], resolution: DVec2, editor_api: &EditorApi) -> Result { + let get = get_imaginate_index; + Ok(ImaginateGenerationParameters { + seed: self.compute_input::(network, node_path, get("Seed"), Cow::Borrowed(editor_api))? as u64, resolution: resolution.as_uvec2().into(), - samples: self.compute_input::(&network, &imaginate_node_path, get("Samples"), Cow::Borrowed(&editor_api))? as u32, + samples: self.compute_input::(network, node_path, get("Samples"), Cow::Borrowed(editor_api))? as u32, sampling_method: self - .compute_input::(&network, &imaginate_node_path, get("Sampling Method"), Cow::Borrowed(&editor_api))? + .compute_input::(network, node_path, get("Sampling Method"), Cow::Borrowed(editor_api))? .api_value() .to_string(), - text_guidance: self.compute_input(&network, &imaginate_node_path, get("Prompt Guidance"), Cow::Borrowed(&editor_api))?, - text_prompt: self.compute_input(&network, &imaginate_node_path, get("Prompt"), Cow::Borrowed(&editor_api))?, - negative_prompt: self.compute_input(&network, &imaginate_node_path, get("Negative Prompt"), Cow::Borrowed(&editor_api))?, - image_creativity: Some(self.compute_input::(&network, &imaginate_node_path, get("Image Creativity"), Cow::Borrowed(&editor_api))? / 100.), - restore_faces: self.compute_input(&network, &imaginate_node_path, get("Improve Faces"), Cow::Borrowed(&editor_api))?, - tiling: self.compute_input(&network, &imaginate_node_path, get("Tiling"), Cow::Borrowed(&editor_api))?, - }; - let use_base_image = self.compute_input::(&network, &imaginate_node_path, get("Adapt Input Image"), Cow::Borrowed(&editor_api))?; + text_guidance: self.compute_input(network, node_path, get("Prompt Guidance"), Cow::Borrowed(editor_api))?, + text_prompt: self.compute_input(network, node_path, get("Prompt"), Cow::Borrowed(editor_api))?, + negative_prompt: self.compute_input(network, node_path, get("Negative Prompt"), Cow::Borrowed(editor_api))?, + image_creativity: Some(self.compute_input::(network, node_path, get("Image Creativity"), Cow::Borrowed(editor_api))? / 100.), + restore_faces: self.compute_input(network, node_path, get("Improve Faces"), Cow::Borrowed(editor_api))?, + tiling: self.compute_input(network, node_path, get("Tiling"), Cow::Borrowed(editor_api))?, + }) + } - editor_api.image_frame = image; + fn imaginate_base_image(&mut self, network: &NodeNetwork, imaginate_node_path: &[LayerId], resolution: DVec2, editor_api: &EditorApi) -> Result, String> { + let use_base_image = self.compute_input::(&network, &imaginate_node_path, get_imaginate_index("Adapt Input Image"), Cow::Borrowed(editor_api))?; let input_image_frame: Option> = if use_base_image { - Some(self.compute_input::>(&network, &imaginate_node_path, get("Input Image"), Cow::Borrowed(&editor_api))?) + Some(self.compute_input::>(&network, &imaginate_node_path, get_imaginate_index("Input Image"), Cow::Borrowed(editor_api))?) } else { None }; - let image_transform = input_image_frame.as_ref().map(|image_frame| image_frame.transform); - let base_image = if let Some(ImageFrame { image, .. }) = input_image_frame { + let base_image = if let Some(ImageFrame { image, transform }) = input_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 }) + Some((ImaginateBaseImage { image_data, size, mime }, transform)) } else { + info!("Base image is input but has no size."); None } } else { None }; + Ok(base_image) + } - let mask_image = if let Some(transform) = image_transform { - let mask_path: Option> = self.compute_input(&network, &imaginate_node_path, get("Masking Layer"), Cow::Borrowed(&editor_api))?; + fn imaginate_mask_image( + &mut self, + network: &NodeNetwork, + node_path: &[LayerId], + editor_api: &EditorApi<'_>, + image_transform: Option, + document: &mut DocumentMessageHandler, + persistent_data: &PersistentData, + ) -> Result, String> { + if let Some(transform) = image_transform { + let mask_path: Option> = self.compute_input(&network, &node_path, get_imaginate_index("Masking Layer"), Cow::Borrowed(&editor_api))?; // Calculate the size of the frame let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); @@ -214,22 +223,53 @@ impl NodeGraphExecutor { } document.restore_document_transform(old_transforms); - mask_image + Ok(mask_image) } else { - None - }; + Ok(None) + } + } + + fn generate_imaginate( + &mut self, + network: NodeNetwork, + imaginate_node_path: Vec, + (document, document_id): (&mut DocumentMessageHandler, u64), + layer_path: Vec, + mut editor_api: EditorApi<'_>, + (preferences, persistent_data): (&PreferencesMessageHandler, &PersistentData), + ) -> Result { + let image = editor_api.image_frame.take(); + + // Get the node graph layer + let layer = document.document_legacy.layer(&layer_path).map_err(|e| format!("No layer: {e:?}"))?; + let transform = layer.transform; + + let resolution: Option = self.compute_input(&network, &imaginate_node_path, get_imaginate_index("Resolution"), Cow::Borrowed(&editor_api))?; + 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 = self.imaginate_parameters(&network, &imaginate_node_path, resolution, &editor_api)?; + + editor_api.image_frame = image; + let base = self.imaginate_base_image(&network, &imaginate_node_path, resolution, &editor_api)?; + let image_transform = base.as_ref().map(|base| base.1); + let base_image = base.map(|base| base.0); + + let mask_image = self.imaginate_mask_image(&network, &imaginate_node_path, &editor_api, image_transform, document, persistent_data)?; Ok(FrontendMessage::TriggerImaginateGenerate { parameters: Box::new(parameters), base_image: base_image.map(Box::new), mask_image: mask_image.map(Box::new), - mask_paint_mode: if self.compute_input::(&network, &imaginate_node_path, get("Inpaint"), Cow::Borrowed(&editor_api))? { + mask_paint_mode: if self.compute_input::(&network, &imaginate_node_path, get_imaginate_index("Inpaint"), Cow::Borrowed(&editor_api))? { ImaginateMaskPaintMode::Inpaint } else { ImaginateMaskPaintMode::Outpaint }, - mask_blur_px: self.compute_input::(&network, &imaginate_node_path, get("Mask Blur"), Cow::Borrowed(&editor_api))? as u32, - imaginate_mask_starting_fill: self.compute_input(&network, &imaginate_node_path, get("Mask Starting Fill"), Cow::Borrowed(&editor_api))?, + mask_blur_px: self.compute_input::(&network, &imaginate_node_path, get_imaginate_index("Mask Blur"), Cow::Borrowed(&editor_api))? as u32, + imaginate_mask_starting_fill: self.compute_input(&network, &imaginate_node_path, get_imaginate_index("Mask Starting Fill"), Cow::Borrowed(&editor_api))?, hostname: preferences.imaginate_server_hostname.clone(), refresh_frequency: preferences.imaginate_refresh_frequency, document_id, @@ -239,6 +279,22 @@ impl NodeGraphExecutor { .into()) } + /// Generate a new [`FrontendImageData`] from the [`Image`]. + fn to_frontend_image_data(image: Image, transform: Option<[f64; 6]>, layer_path: &[LayerId], node_id: Option, resize: Option) -> Result { + let (image_data, _size) = Self::encode_img(image, resize, image::ImageOutputFormat::Bmp)?; + + let mime = "image/bmp".to_string(); + let image_data = std::sync::Arc::new(image_data); + + Ok(FrontendImageData { + path: layer_path.to_vec(), + node_id, + image_data, + mime, + transform, + }) + } + /// Evaluates a node graph, computing either the Imaginate node or the entire graph pub fn evaluate_node_graph( &mut self, @@ -276,6 +332,7 @@ impl NodeGraphExecutor { return Ok(()); } // Execute the node graph + let (network, monitor_nodes) = Self::wrap_network(network); let boxed_node_graph_output = self.execute_network(network, editor_api)?; // Check if the output is vector data @@ -303,29 +360,74 @@ impl NodeGraphExecutor { responses.add(Operation::SetLayerTransform { path: layer_path.clone(), transform }); } } else { - // Update the image data - let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?; - - 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, - transform, - }]; + let image_data = vec![Self::to_frontend_image_data(image, transform, &layer_path, None, None)?]; responses.add(FrontendMessage::UpdateImageData { document_id, image_data }); } } else if core::any::TypeId::of::() == DynAny::type_id(boxed_node_graph_output.as_ref()) { let artboard: graphene_core::Artboard = dyn_any::downcast(boxed_node_graph_output).map(|artboard| *artboard)?; info!("{artboard:#?}"); + self.update_thumbnails(&layer_path, monitor_nodes, responses); + return Err(format!("Artboard (see console)")); } else if core::any::TypeId::of::() == DynAny::type_id(boxed_node_graph_output.as_ref()) { let graphic_group: graphene_core::GraphicGroup = dyn_any::downcast(boxed_node_graph_output).map(|graphic| *graphic)?; info!("{graphic_group:#?}"); + self.update_thumbnails(&layer_path, monitor_nodes, responses); + return Err(format!("Graphic group (see console)")); } Ok(()) } + + /// Recomputes the thumbnails for the layers in the graph, modifying the state and updating the UI. + pub fn update_thumbnails(&mut self, layer_path: &[LayerId], monitor_nodes: Vec>, responses: &mut VecDeque) { + let mut thumbnails_changed: bool = false; + let mut image_data: Vec<_> = Vec::new(); + for node_path in monitor_nodes { + let Some(value) = self.executor.introspect(&node_path).flatten() else { + warn!("No introspect"); + continue; + }; + let Some(graphic_group) = value.downcast_ref::() else { + warn!("Not graphic"); + continue; + }; + use graphene_core::renderer::*; + let bounds = graphic_group.bounding_box(DAffine2::IDENTITY); + let render_params = RenderParams::new(ViewMode::Normal, bounds, true); + let mut render = SvgRender::new(); + graphic_group.render_svg(&mut render, &render_params); + let [min, max] = bounds.unwrap_or_default(); + render.format_svg(min, max); + info!("SVG {}", render.svg); + + if let (Some(layer_id), Some(node_id)) = (layer_path.last().copied(), node_path.get(node_path.len() - 2).copied()) { + let old_thumbnail = self.thumbnails.entry(layer_id).or_default().entry(node_id).or_default(); + if *old_thumbnail != render.svg { + *old_thumbnail = render.svg; + thumbnails_changed = true; + } + } + let resize = Some(DVec2::splat(100.)); + let create_image_data = |(node_id, image)| Self::to_frontend_image_data(image, None, layer_path, Some(node_id), resize).ok(); + image_data.extend(render.image_data.into_iter().filter_map(create_image_data)) + } + if !image_data.is_empty() { + responses.add(FrontendMessage::UpdateImageData { document_id: 0, image_data }); + } else if thumbnails_changed { + responses.add(NodeGraphMessage::SendGraph { should_rerender: false }); + } + } + + /// When a blob url for a thumbnail is loaded, update the state and the UI. + pub fn insert_thumbnail_blob_url(&mut self, blob_url: String, layer_id: LayerId, node_id: NodeId, responses: &mut VecDeque) { + if let Some(layer) = self.thumbnails.get_mut(&layer_id) { + if let Some(segment) = layer.values_mut().flat_map(|segments| segments.iter_mut()).find(|segment| **segment == SvgSegment::BlobUrl(node_id)) { + *segment = SvgSegment::String(blob_url); + responses.add(NodeGraphMessage::SendGraph { should_rerender: false }); + return; + } + } + } } diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index 9ccbc054f2..a1004096ff 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -102,6 +102,7 @@ fn handle_message(message: String) -> String { images.insert(format!("{:?}_{}", &image.path, document_id), image); stub_data.push(FrontendImageData { path, + node_id: None, mime, image_data: Arc::new(Vec::new()), transform, diff --git a/frontend/src/components/panels/NodeGraph.svelte b/frontend/src/components/panels/NodeGraph.svelte index 0b2dddbcf3..b5f0e370f8 100644 --- a/frontend/src/components/panels/NodeGraph.svelte +++ b/frontend/src/components/panels/NodeGraph.svelte @@ -548,7 +548,11 @@ {/if} - + {#if node.thumbnailSvg} + {@html node.thumbnailSvg} + {:else} + + {/if} {node.displayName} {#if exposedInputsOutputs.length > 0} diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index a396154145..56b88fe4c2 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -128,7 +128,7 @@ export function createPortfolioState(editor: Editor) { image.src = blobURL; await image.decode(); - editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight, element.transform); + editor.instance.setImageBlobURL(updateImageData.documentId, element.path, element.nodeId, blobURL, image.naturalWidth, image.naturalHeight, element.transform); }); }); editor.subscriptions.subscribeJsMessage(TriggerRasterizeRegionBelowLayer, async (triggerRasterizeRegionBelowLayer) => { diff --git a/frontend/src/wasm-communication/editor.ts b/frontend/src/wasm-communication/editor.ts index 0e37823513..edfe1ecde5 100644 --- a/frontend/src/wasm-communication/editor.ts +++ b/frontend/src/wasm-communication/editor.ts @@ -11,7 +11,7 @@ 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: WebAssembly.Memory | undefined; -export async function updateImage(path: BigUint64Array, mime: string, imageData: Uint8Array, transform: Float64Array, documentId: bigint): Promise { +export async function updateImage(path: BigUint64Array, nodeId: bigint, mime: string, imageData: Uint8Array, transform: Float64Array, documentId: bigint): Promise { const blob = new Blob([imageData], { type: mime }); const blobURL = URL.createObjectURL(blob); @@ -21,10 +21,10 @@ export async function updateImage(path: BigUint64Array, mime: string, imageData: image.src = blobURL; await image.decode(); - window["editorInstance"]?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight, transform); + window["editorInstance"]?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, transform); } -export async function fetchImage(path: BigUint64Array, mime: string, documentId: bigint, url: string): Promise { +export async function fetchImage(path: BigUint64Array, nodeId: bigint, mime: string, documentId: bigint, url: string): Promise { const data = await fetch(url); const blob = await data.blob(); @@ -35,7 +35,7 @@ export async function fetchImage(path: BigUint64Array, mime: string, documentId: image.src = blobURL; await image.decode(); - window["editorInstance"]?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight, undefined); + window["editorInstance"]?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, undefined); } const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api"); diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 85447717e2..6dd27c6a4c 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -105,6 +105,8 @@ export class FrontendNode { readonly previewed!: boolean; readonly disabled!: boolean; + + readonly thumbnailSvg!: string | undefined; } export class FrontendNodeLink { @@ -771,6 +773,8 @@ export type LayerTypeData = { export class ImaginateImageData { readonly path!: BigUint64Array; + readonly nodeId!: bigint; + readonly mime!: string; readonly imageData!: Uint8Array; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index beb62e3659..841d564a88 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -32,8 +32,8 @@ pub fn set_random_seed(seed: u64) { /// This avoids creating a json with a list millions of numbers long. #[wasm_bindgen(module = "/../src/wasm-communication/editor.ts")] extern "C" { - fn updateImage(path: Vec, mime: String, imageData: &[u8], transform: js_sys::Float64Array, document_id: u64); - fn fetchImage(path: Vec, mime: String, document_id: u64, identifier: String); + fn updateImage(path: Vec, nodeId: Option, mime: String, imageData: &[u8], transform: js_sys::Float64Array, document_id: u64); + fn fetchImage(path: Vec, nodeId: Option, mime: String, document_id: u64, identifier: String); //fn dispatchTauri(message: String) -> String; fn dispatchTauri(message: String); } @@ -122,10 +122,13 @@ impl JsEditorHandle { } else { js_sys::Float64Array::default() }; - updateImage(image.path, image.mime, &image.image_data, transform, document_id); + updateImage(image.path, image.node_id, image.mime, &image.image_data, transform, document_id); } #[cfg(feature = "tauri")] - fetchImage(image.path.clone(), image.mime, document_id, format!("http://localhost:3001/image/{:?}_{}", &image.path, document_id)); + { + let identifier = format!("http://localhost:3001/image/{:?}_{}", image.path, document_id); + fetchImage(image.path, image.node_id, image.mime, document_id, identifier); + } } return; } @@ -504,11 +507,12 @@ impl JsEditorHandle { /// Sends the blob URL generated by JS to the Image layer #[wasm_bindgen(js_name = setImageBlobURL)] - pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec, blob_url: String, width: f64, height: f64, transform: Option) { + pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec, node_id: Option, blob_url: String, width: f64, height: f64, transform: Option) { let resolution = (width, height); let message = PortfolioMessage::SetImageBlobUrl { document_id, layer_path: layer_path.clone(), + node_id, blob_url, resolution, }; diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index e994b3a82a..0437ca2731 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -8,6 +8,8 @@ use core::ops::{Deref, DerefMut}; use glam::IVec2; use node_macro::node_fn; +pub mod renderer; + /// A list of [`GraphicElement`]s #[derive(Clone, Debug, Hash, PartialEq, DynAny, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs new file mode 100644 index 0000000000..7ed833638e --- /dev/null +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -0,0 +1,195 @@ +use crate::raster::{Image, ImageFrame}; +use crate::{uuid::generate_uuid, vector::VectorData, Artboard, Color, GraphicElementData, GraphicGroup}; +use quad::Quad; + +use glam::{DAffine2, DVec2}; + +mod quad; + +/// Mutable state used whilst rendering to an SVG +pub struct SvgRender { + pub svg: SvgSegmentList, + pub svg_defs: String, + pub transform: DAffine2, + pub image_data: Vec<(u64, Image)>, +} + +impl SvgRender { + pub fn new() -> Self { + Self { + svg: SvgSegmentList::default(), + svg_defs: String::new(), + transform: DAffine2::IDENTITY, + image_data: Vec::new(), + } + } + + /// Add an outer `` tag with a `viewBox` and the `` + pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) { + let (x, y) = bounds_min.into(); + let (size_x, size_y) = (bounds_max - bounds_min).into(); + let defs = &self.svg_defs; + let svg_header = format!(r#"{defs}"#,); + self.svg.insert(0, svg_header.into()); + self.svg.push("".into()); + } +} + +impl Default for SvgRender { + fn default() -> Self { + Self::new() + } +} + +/// Static state used whilst rendering +pub struct RenderParams { + pub view_mode: crate::vector::style::ViewMode, + pub culling_bounds: Option<[DVec2; 2]>, + pub thumbnail: bool, +} + +impl RenderParams { + pub fn new(view_mode: crate::vector::style::ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool) -> Self { + Self { view_mode, culling_bounds, thumbnail } + } +} + +fn format_transform_matrix(transform: DAffine2) -> String { + transform.to_cols_array().iter().map(ToString::to_string).collect::>().join(",") +} + +pub trait GraphicElementRendered { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams); + fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>; +} + +impl GraphicElementRendered for GraphicGroup { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + self.iter().for_each(|element| element.graphic_element_data.render_svg(render, render_params)) + } + fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + self.iter().filter_map(|element| element.graphic_element_data.bounding_box(transform)).reduce(Quad::combine_bounds) + } +} + +impl GraphicElementRendered for VectorData { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + let layer_bounds = self.bounding_box().unwrap_or_default(); + let transformed_bounds = self.bounding_box_with_transform(render.transform).unwrap_or_default(); + + render.svg.push("".into()); + } + fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + self.bounding_box_with_transform(self.transform * transform) + } +} + +impl GraphicElementRendered for Artboard { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + self.graphic_group.render_svg(render, render_params) + } + fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + let artboard_bounds = self.bounds.map(|[a, b]| (transform * Quad::from_box([a.as_dvec2(), b.as_dvec2()])).bounding_box()); + [self.graphic_group.bounding_box(transform), artboard_bounds].into_iter().flatten().reduce(Quad::combine_bounds) + } +} + +impl GraphicElementRendered for ImageFrame { + fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { + let transform: String = format_transform_matrix(self.transform * render.transform); + render + .svg + .push(format!(r#"".into()); + render.image_data.push((uuid, self.image.clone())) + } + fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + let transform = self.transform * transform; + (transform.matrix2 != glam::DMat2::ZERO).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box()) + } +} + +impl GraphicElementRendered for GraphicElementData { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + match self { + GraphicElementData::VectorShape(vector_data) => vector_data.render_svg(render, render_params), + GraphicElementData::ImageFrame(image_frame) => image_frame.render_svg(render, render_params), + GraphicElementData::Text(_) => todo!("Render a text GraphicElementData"), + GraphicElementData::GraphicGroup(graphic_group) => graphic_group.render_svg(render, render_params), + GraphicElementData::Artboard(artboard) => artboard.render_svg(render, render_params), + } + } + + fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + match self { + GraphicElementData::VectorShape(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform), + GraphicElementData::ImageFrame(image_frame) => image_frame.bounding_box(transform), + GraphicElementData::Text(_) => todo!("Bounds of a text GraphicElementData"), + GraphicElementData::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform), + GraphicElementData::Artboard(artboard) => artboard.bounding_box(transform), + } + } +} + +/// A segment of an svg string to allow for embedding blob urls +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SvgSegment { + Slice(&'static str), + String(String), + BlobUrl(u64), +} + +impl From for SvgSegment { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From<&'static str> for SvgSegment { + fn from(value: &'static str) -> Self { + Self::Slice(value) + } +} + +/// A list of [`SvgSegment`]s. +/// +/// Can be modified with `list.push("hello".into())`. Use `list.to_string()` to convert the segments into one string. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SvgSegmentList(Vec); + +impl core::ops::Deref for SvgSegmentList { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl core::ops::DerefMut for SvgSegmentList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl core::fmt::Display for SvgSegmentList { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for segment in self.iter() { + f.write_str(match segment { + SvgSegment::Slice(x) => x, + SvgSegment::String(x) => x, + SvgSegment::BlobUrl(_) => "", + })?; + } + Ok(()) + } +} diff --git a/node-graph/gcore/src/graphic_element/renderer/quad.rs b/node-graph/gcore/src/graphic_element/renderer/quad.rs new file mode 100644 index 0000000000..0ce59b41f8 --- /dev/null +++ b/node-graph/gcore/src/graphic_element/renderer/quad.rs @@ -0,0 +1,51 @@ +use glam::{DAffine2, DVec2}; + +#[derive(Debug, Clone, Default, Copy)] +/// A quad defined by four vertices. +pub struct Quad([DVec2; 4]); + +impl Quad { + /// Convert a box defined by two corner points to a quad. + pub fn from_box(bbox: [DVec2; 2]) -> Self { + let size = bbox[1] - bbox[0]; + Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y]) + } + + /// Get all the edges in the quad. + pub fn lines_glam(&self) -> impl Iterator + '_ { + [[self.0[0], self.0[1]], [self.0[1], self.0[2]], [self.0[2], self.0[3]], [self.0[3], self.0[0]]] + .into_iter() + .map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end)) + } + + /// Generates a [crate::vector::Subpath] of the quad + pub fn subpath(&self) -> crate::vector::Subpath { + crate::vector::Subpath::from_points(self.0.into_iter(), true) + } + + /// Generates the axis aligned bounding box of the quad + pub fn bounding_box(&self) -> [DVec2; 2] { + [ + self.0.into_iter().reduce(|a, b| a.min(b)).unwrap_or_default(), + self.0.into_iter().reduce(|a, b| a.max(b)).unwrap_or_default(), + ] + } + + /// Gets the center of a quad + pub fn center(&self) -> DVec2 { + self.0.iter().sum::() / 4. + } + + /// Take the outside bounds of two axis aligned rectangles, which are defined by two corner points. + pub fn combine_bounds(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] { + [a[0].min(b[0]), a[1].max(b[1])] + } +} + +impl core::ops::Mul for DAffine2 { + type Output = Quad; + + fn mul(self, rhs: Quad) -> Self::Output { + Quad(rhs.0.map(|point| self.transform_point2(point))) + } +} diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs index 80a3ba6aa7..0698d00683 100644 --- a/node-graph/gcore/src/raster/image.rs +++ b/node-graph/gcore/src/raster/image.rs @@ -37,7 +37,7 @@ mod base64_serde { } } -#[derive(Clone, Debug, PartialEq, Default, specta::Type)] +#[derive(Clone, PartialEq, Default, specta::Type)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Image { pub width: u32, @@ -46,6 +46,17 @@ pub struct Image { pub data: Vec

, } +impl Debug for Image

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let length = self.data.len(); + f.debug_struct("Image") + .field("width", &self.width) + .field("height", &self.height) + .field("data", if length < 100 { &self.data } else { &length }) + .finish() + } +} + unsafe impl StaticType for Image

where P::Static: Pixel, diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index edd4aa947b..00bb0d0e72 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -243,6 +243,10 @@ impl DocumentNodeImplementation { _ => None, } } + + pub const fn proto(name: &'static str) -> Self { + Self::Unresolved(NodeIdentifier::new(name)) + } } #[derive(Clone, Copy, Debug, Default, PartialEq, DynAny, specta::Type, Hash)] @@ -812,6 +816,28 @@ impl NodeNetwork { nodes: nodes.clone(), }) } + + /// Create a [`RecursiveNodeIter`] that iterates over all [`DocumentNode`]s, including ones that are deeply nested. + pub fn recursive_nodes(&self) -> RecursiveNodeIter { + let nodes = self.nodes.iter().map(|(id, node)| (node, self, vec![*id])).collect(); + RecursiveNodeIter { nodes } + } +} + +/// An iterator over all [`DocumentNode`]s, including ones that are deeply nested. +pub struct RecursiveNodeIter<'a> { + nodes: Vec<(&'a DocumentNode, &'a NodeNetwork, Vec)>, +} + +impl<'a> Iterator for RecursiveNodeIter<'a> { + type Item = (&'a DocumentNode, &'a NodeNetwork, Vec); + fn next(&mut self) -> Option { + let (node, network, path) = self.nodes.pop()?; + if let DocumentNodeImplementation::Network(network) = &node.implementation { + self.nodes.extend(network.nodes.iter().map(|(id, node)| (node, network, [path.as_slice(), &[*id]].concat()))); + } + Some((node, network, path)) + } } #[cfg(test)] diff --git a/node-graph/graph-craft/src/executor.rs b/node-graph/graph-craft/src/executor.rs index 314cc64a9e..dbc1dd2e0e 100644 --- a/node-graph/graph-craft/src/executor.rs +++ b/node-graph/graph-craft/src/executor.rs @@ -10,7 +10,6 @@ pub struct Compiler {} impl Compiler { pub fn compile(&self, mut network: NodeNetwork, resolve_inputs: bool) -> impl Iterator { let node_ids = network.nodes.keys().copied().collect::>(); - network.generate_node_paths(&[]); network.resolve_extract_nodes(); println!("flattening"); for id in node_ids { diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 7d63f09484..dbc29fd983 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -156,6 +156,7 @@ impl ProtoNode { self.identifier.name.hash(&mut hasher); self.construction_args.hash(&mut hasher); + self.document_node_path.hash(&mut hasher); match self.input { ProtoNodeInput::None => "none".hash(&mut hasher), ProtoNodeInput::ShortCircut(ref ty) => { @@ -629,12 +630,12 @@ mod test { assert_eq!( ids, vec![ - 10739226043134366700, - 17332796976541881019, - 7897288931440576543, - 7388412494950743023, - 359700384277940942, - 12822947441562012352 + 4471348669260178714, + 12892313567093808068, + 6883586777044498729, + 13841339389284532934, + 4412916056300566478, + 15358108940336208665 ] ); } diff --git a/node-graph/interpreted-executor/src/executor.rs b/node-graph/interpreted-executor/src/executor.rs index fa1cdf13a2..96b4c2d0cb 100644 --- a/node-graph/interpreted-executor/src/executor.rs +++ b/node-graph/interpreted-executor/src/executor.rs @@ -139,8 +139,8 @@ impl BorrowTree { node.reset(); } old_nodes.remove(&id); - self.source_map.retain(|_, nid| !old_nodes.contains(nid)); } + self.source_map.retain(|_, nid| !old_nodes.contains(nid)); Ok(old_nodes.into_iter().collect()) } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 5a9e0164ff..d63150ec31 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -155,6 +155,7 @@ fn node_registry() -> HashMap, input: ImageFrame, params: [ImageFrame]), register_node!(graphene_std::raster::EmptyImageNode<_, _>, input: DAffine2, params: [Color]), register_node!(graphene_std::memo::MonitorNode<_>, input: ImageFrame, params: []), + register_node!(graphene_std::memo::MonitorNode<_>, input: graphene_core::GraphicGroup, params: []), #[cfg(feature = "gpu")] register_node!(graphene_std::executor::MapGpuSingleImageNode<_>, input: Image, params: [String]), vec![( @@ -366,6 +367,26 @@ fn node_registry() -> HashMap"), + |args| { + let input: DowncastBothNode<(), graphene_core::GraphicGroup> = DowncastBothNode::new(args[0]); + let node = graphene_std::memo::EndLetNode::new(input); + let any: DynAnyInRefNode = graphene_std::any::DynAnyInRefNode::new(node); + any.into_type_erased() + }, + NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(graphene_core::GraphicGroup)]), + ), + ( + NodeIdentifier::new("graphene_std::memo::EndLetNode<_>"), + |args| { + let input: DowncastBothNode<(), graphene_core::Artboard> = DowncastBothNode::new(args[0]); + let node = graphene_std::memo::EndLetNode::new(input); + let any: DynAnyInRefNode = graphene_std::any::DynAnyInRefNode::new(node); + any.into_type_erased() + }, + NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(graphene_core::Artboard)]), + ), ( NodeIdentifier::new("graphene_std::memo::RefNode<_, _>"), |args| {