From ae8e0f042f8d0512b2432784b381c747e2ba8b2d Mon Sep 17 00:00:00 2001 From: Kevin Lu Date: Fri, 26 Jun 2026 20:06:11 +0200 Subject: [PATCH 1/3] Implement cursor-based paste --- .../node_graph/node_graph_message_handler.rs | 51 +++++++++++++++++-- .../utility_types/network_interface.rs | 4 ++ 2 files changed, 52 insertions(+), 3 deletions(-) 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 410b8cfb72..a57119d43e 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 @@ -13,7 +13,7 @@ use crate::messages::portfolio::document::node_graph::utility_types::{ContextMen 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, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, + self, FlowType, InputConnector, LayerPosition, NodeNetworkInterface, NodePosition, NodeTemplate, 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}; @@ -763,7 +763,7 @@ impl<'a> MessageHandler> for NodeG network_interface.set_chain_position(&node_id, selection_network_path); } NodeGraphMessage::PasteNodes { serialized_nodes } => { - let data = match serde_json::from_str::>(&serialized_nodes) { + let mut data = match serde_json::from_str::>(&serialized_nodes) { Ok(d) => d, Err(e) => { warn!("Invalid node data {e:?}"); @@ -774,6 +774,48 @@ 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 PasteNodes"); + 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); + + // Sort the selected nodes by the new id so that we know which node was selected first + data.sort_by(|a, b| a.0.cmp(&b.0)); + + // 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 = data.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 = if let Some(position) = copy_position_opt { position } else { &IVec2::default() }; + let graph_delta = IVec2::new( + ((cursor_to_node_graph.x / 24.).round()) as i32 - copy_position.x, + ((cursor_to_node_graph.y / 24.).round()) as i32 - copy_position.y, + ); + responses.add(DocumentMessage::AddTransaction); let new_ids: HashMap<_, _> = data.iter().map(|(id, _)| (*id, NodeId::new())).collect(); @@ -782,7 +824,10 @@ impl<'a> MessageHandler> for NodeG nodes: data, new_ids: new_ids.clone(), }); - 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 cdd8ef06a7..8b0bbe6d99 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -6712,6 +6712,10 @@ impl NodePersistentMetadata { pub fn new(position: NodePosition) -> Self { Self { position } } + + pub fn position(&self) -> &NodePosition { + &self.position + } } /// A layer can either be position as Absolute or in a Stack From 04008faaccb7af6d67b9a6e8663346a1029f72e4 Mon Sep 17 00:00:00 2001 From: Kevin Lu Date: Fri, 26 Jun 2026 20:29:39 +0200 Subject: [PATCH 2/3] Use GRID_SIZE constant and copy IVec2 position --- .../document/node_graph/node_graph_message_handler.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 a57119d43e..c6a657d67c 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 @@ -810,10 +810,10 @@ impl<'a> MessageHandler> for NodeG } }); - let copy_position = if let Some(position) = copy_position_opt { position } else { &IVec2::default() }; + let copy_position = copy_position_opt.copied().unwrap_or_default(); let graph_delta = IVec2::new( - ((cursor_to_node_graph.x / 24.).round()) as i32 - copy_position.x, - ((cursor_to_node_graph.y / 24.).round()) as i32 - copy_position.y, + ((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); From a85b47a0a05c5fab8cb6e78ef6a9e0313c235a8c Mon Sep 17 00:00:00 2001 From: Kevin Lu Date: Fri, 26 Jun 2026 21:16:19 +0200 Subject: [PATCH 3/3] Ran clippy --- .../portfolio/document/node_graph/node_graph_message_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c6a657d67c..1fcc35f583 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 @@ -789,7 +789,7 @@ impl<'a> MessageHandler> for NodeG .transform_point2(cursor_viewport_location); // Sort the selected nodes by the new id so that we know which node was selected first - data.sort_by(|a, b| a.0.cmp(&b.0)); + data.sort_by_key(|a| a.0); // 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