From 925406e2db27e49d629952902562fca4cb3959cb Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Mon, 10 Nov 2025 19:50:22 +0000 Subject: [PATCH 1/2] Fix copy nodes sometimes failing when no OutputConnector exists --- .../document/utility_types/network_interface.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index b3ae979761..1bd1a2bfb7 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -369,13 +369,8 @@ impl NodeNetworkInterface { return None; }; // TODO: Get downstream connections from all outputs - let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*node_id, 0)) else { - log::error!("Could not get outward wires in copy_nodes"); - return None; - }; - let has_selected_node_downstream = downstream_connections - .iter() - .any(|input_connector| input_connector.node_id().is_some_and(|upstream_id| new_ids.keys().any(|key| *key == upstream_id))); + let mut downstream_connections = outward_wires.get(&OutputConnector::node(*node_id, 0)).map_or([].iter(), |outputs| outputs.iter()); + let has_selected_node_downstream = downstream_connections.any(|input_connector| input_connector.node_id().is_some_and(|upstream_id| new_ids.keys().any(|key| *key == upstream_id))); // If the copied node does not have a downstream connection to another copied node, then set the position to absolute if !has_selected_node_downstream { let Some(position) = self.position(node_id, network_path) else { From 2251896f7faff2e9ac6d47f761d10db271961c1d Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Mon, 10 Nov 2025 19:50:34 +0000 Subject: [PATCH 2/2] Add test for copying a node --- .../utility_types/network_interface.rs | 31 +++++++++++++++++++ editor/src/test_utils.rs | 21 +++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 1bd1a2bfb7..2920cc360e 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -6911,3 +6911,34 @@ pub enum TransactionStatus { #[default] Finished, } + +#[cfg(test)] +mod network_interface_tests { + use crate::test_utils::test_prelude::*; + #[tokio::test] + async fn copy_isolated_node() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + let rectangle = editor.create_node_by_name("Rectangle").await; + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rectangle] }).await; + let frontend_messages = editor.handle_message(NodeGraphMessage::Copy).await; + let serialized_nodes = frontend_messages + .into_iter() + .find_map(|msg| match msg { + FrontendMessage::TriggerTextCopy { copy_text } => Some(copy_text), + _ => None, + }) + .expect("copy message should be dispatched") + .strip_prefix("graphite/nodes: ") + .expect("should start with magic string") + .to_string(); + println!("Serialized: {serialized_nodes}"); + editor.handle_message(NodeGraphMessage::PasteNodes { serialized_nodes }).await; + let nodes = &mut editor.active_document_mut().network_interface.network_mut(&[]).unwrap().nodes; + let orignal = nodes.remove(&rectangle).expect("original node should exist"); + assert!( + nodes.values().any(|other| *other == orignal), + "duplicated node should exist\nother nodes: {nodes:#?}\norignal {orignal:#?}" + ); + } +} diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 4a9e579a9a..3b8a568d04 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -13,6 +13,7 @@ use glam::{DVec2, UVec2}; use graph_craft::document::DocumentNode; use graphene_std::InputAccessor; use graphene_std::raster::color::Color; +use graphene_std::uuid::NodeId; /// A set of utility functions to make the writing of editor test more declarative pub struct EditorTestUtils { @@ -68,13 +69,15 @@ impl EditorTestUtils { run(&mut self.editor, &mut self.runtime) } - pub async fn handle_message(&mut self, message: impl Into) { - self.editor.handle_message(message); + pub async fn handle_message(&mut self, message: impl Into) -> Vec { + let frontend_messages_from_msg = self.editor.handle_message(message); // Required to process any buffered messages if let Err(e) = self.eval_graph().await { panic!("Failed to evaluate graph: {e}"); } + + frontend_messages_from_msg } pub async fn new_document(&mut self) { @@ -222,7 +225,7 @@ impl EditorTestUtils { ToolType::Rectangle => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeRectangle)).await, ToolType::Ellipse => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeEllipse)).await, _ => self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })).await, - } + }; } pub async fn select_primary_color(&mut self, color: Color) { @@ -303,6 +306,18 @@ impl EditorTestUtils { }) .await; } + + pub async fn create_node_by_name(&mut self, name: impl Into) -> NodeId { + let node_id = NodeId::new(); + self.handle_message(NodeGraphMessage::CreateNodeFromContextMenu { + node_id: Some(node_id), + node_type: name.into(), + xy: None, + add_transaction: true, + }) + .await; + node_id + } } pub trait FrontendMessageTestUtils {