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 239d87cfb0..4957971f5e 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 @@ -12,7 +12,9 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions: use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, NodeGraphErrorDiagnostic}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; -use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, NodeTypePersistentMetadata, OutputConnector, Previewing}; +use crate::messages::portfolio::document::utility_types::network_interface::{ + self, FlowType, InputConnector, LayerPosition, NodeNetworkInterface, NodePosition, NodeTypePersistentMetadata, OutputConnector, Previewing, +}; use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::prelude::*; @@ -776,6 +778,45 @@ impl<'a> MessageHandler> for NodeG return; } + // Get network path of node overlay + let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { + log::error!("Could not get network metadata in InsertNodes"); + return; + }; + + let cursor_viewport_location = ipp.mouse.position; + let cursor_to_node_graph = network_metadata + .persistent_metadata + .navigation_metadata + .node_graph_to_viewport + .inverse() + .transform_point2(cursor_viewport_location); + + // Get position of the first node selected for copying that has an absolute position. Calculate paste offset from the cursor + // If no nodes with absolute position, then there is no offset from cursor + let copy_position_opt = nodes.iter().find_map(|(_, template)| match &template.persistent_node_metadata.node_type_metadata { + NodeTypePersistentMetadata::Layer(layer_metadata) => { + if let LayerPosition::Absolute(position) = &layer_metadata.position { + Some(position) + } else { + None + } + } + NodeTypePersistentMetadata::Node(node_metadata) => { + if let NodePosition::Absolute(position) = node_metadata.position() { + Some(position) + } else { + None + } + } + }); + + let copy_position = copy_position_opt.copied().unwrap_or_default(); + let graph_delta = IVec2::new( + ((cursor_to_node_graph.x / GRID_SIZE as f64).round()) as i32 - copy_position.x, + ((cursor_to_node_graph.y / GRID_SIZE as f64).round()) as i32 - copy_position.y, + ); + responses.add(DocumentMessage::AddTransaction); let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); @@ -783,7 +824,10 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); let nodes: Vec<_> = new_ids.values().copied().collect(); - responses.add(NodeGraphMessage::SelectedNodesSet { nodes }) + responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); + + // Shift nodes based on offset + responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: true }) } NodeGraphMessage::PointerDown { shift_click, 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 b085432dd4..8c7a3d3feb 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -372,7 +372,7 @@ impl NodeNetworkInterface { } /// Creates a copy for each node by disconnecting nodes which are not connected to other copied nodes. - /// Returns an iterator of all persistent metadata for a node and their ids + /// Returns an iterator, sorted by original node id, of all persistent metadata for a node and their ids pub fn copy_nodes<'a>(&'a mut self, new_ids: &'a HashMap, network_path: &'a [NodeId]) -> impl Iterator + 'a { let mut new_nodes = new_ids .iter() @@ -442,6 +442,7 @@ impl NodeNetworkInterface { } } } + new_nodes.sort_by_key(|a| a.0); new_nodes.into_iter().map(move |(new, node_id, node)| (new, self.map_ids(node, &node_id, new_ids, network_path))) } @@ -6931,6 +6932,7 @@ impl NodePersistentMetadata { pub fn new(position: NodePosition) -> Self { Self { position } } + pub fn position(&self) -> &NodePosition { &self.position } @@ -7074,7 +7076,10 @@ pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet #[cfg(test)] mod network_interface_tests { - use crate::test_utils::test_prelude::*; + use crate::{ + messages::portfolio::document::utility_types::network_interface::{LayerPosition, NodePosition, NodeTypePersistentMetadata}, + test_utils::test_prelude::*, + }; #[tokio::test] async fn copy_isolated_node() { let mut editor = EditorTestUtils::create(); @@ -7104,4 +7109,82 @@ mod network_interface_tests { "duplicated node should exist\nother nodes: {nodes:#?}\norignal {orignal:#?}" ); } + + #[tokio::test] + async fn copy_and_paste_nodes() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + let rectangle = editor + .create_node_by_name_and_coordinates(DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER), (0, 0)) + .await; + let circle = editor + .create_node_by_name_and_coordinates(DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::circle::IDENTIFIER), (5, 5)) + .await; + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![circle, rectangle] }).await; + let frontend_messages = editor.handle_message(NodeGraphMessage::Copy).await; + let clipboard = frontend_messages + .into_iter() + .find_map(|msg| match msg { + FrontendMessage::TriggerClipboardWrite { content } => Some(content), + _ => None, + }) + .expect("copy message should be dispatched"); + println!("Clipboard: {clipboard}"); + // Move mouse then paste at new location + let mouse_pos = (24.0, 24.0); + + editor.move_mouse(mouse_pos.0, mouse_pos.1, ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(mouse_pos.0, mouse_pos.1, ModifierKeys::empty()).await; + editor.left_mouseup(mouse_pos.0, mouse_pos.1, ModifierKeys::empty()).await; + editor + .handle_message(ClipboardMessage::ReadClipboard { + content: ClipboardContentRaw::Text(clipboard), + }) + .await; + + // For each pasted node, check that the change in position is based on the delta of the mouse position and original circle. The mouse position in grid coordinates is (1,1). Since the original + // circle is at (5,5), the new circle should be pasted at (1,1), and the new square should be pasted at (-4, -4). Because we preserve selection order, the first selected pasted node should be + // a circle, the second a rectangle. + let new_positions: Vec<&IVec2> = editor + .active_document() + .network_interface + .selected_nodes() + .selected_nodes() + .map(|node_id| { + let node_metadata = &editor + .active_document() + .network_interface + .document_network_metadata() + .persistent_metadata + .node_metadata + .get(node_id) + .expect("Node should exist") + .persistent_metadata + .node_type_metadata; + let new_position = match node_metadata { + NodeTypePersistentMetadata::Layer(layer_metadata) => { + if let LayerPosition::Absolute(position) = &layer_metadata.position { + Some(position) + } else { + None + } + } + NodeTypePersistentMetadata::Node(node_metadata) => { + if let NodePosition::Absolute(position) = node_metadata.position() { + Some(position) + } else { + None + } + } + } + .expect("Pasted node should have a position"); + new_position + }) + .collect(); + println!("positions of pasted nodes: {:?}", new_positions); + // Check position of pasted circle + assert_eq!(new_positions[0], &IVec2::new(1, 1)); + // Check position of pasted rectangle + assert_eq!(new_positions[1], &IVec2::new(-4, -4)); + } } diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 84f67f0418..34861a9af2 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -330,6 +330,18 @@ impl EditorTestUtils { .await; node_id } + + pub async fn create_node_by_name_and_coordinates(&mut self, node_type: DefinitionIdentifier, xy: (i32, i32)) -> NodeId { + let node_id = NodeId::new(); + self.handle_message(NodeGraphMessage::CreateNodeFromContextMenu { + node_id: Some(node_id), + node_type, + xy: Some(xy), + add_transaction: true, + }) + .await; + node_id + } } pub trait FrontendMessageTestUtils {