From 1f505c6228bc303c10daaa373583db92bbd61156 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 21:52:53 +0000 Subject: [PATCH 1/8] Final Fix --- .../node_graph/node_graph_message_handler.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 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 433a282bbe..851097396e 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 @@ -58,6 +58,8 @@ pub struct NodeGraphMessageHandler { widgets: [LayoutGroup; 2], /// Used to add a transaction for the first node move when dragging. begin_dragging: bool, + /// Tracks whether nodes were duplicated via Alt-drag, so aborting undoes both the move and duplication. + duplicated_in_drag: bool, /// Used to prevent entering a nested network if the node is dragged after double clicking node_has_moved_in_drag: bool, /// If dragging the selected nodes, this stores the starting position both in viewport and node graph coordinates, @@ -780,7 +782,13 @@ impl<'a> MessageHandler> for NodeG if self.drag_start.is_some() { self.drag_start = None; self.select_if_not_dragged = None; - responses.add(DocumentMessage::AbortTransaction); + if self.duplicated_in_drag { + responses.add(DocumentMessage::AbortTransaction); + responses.add(DocumentMessage::Undo); + self.duplicated_in_drag = false; + } else { + responses.add(DocumentMessage::AbortTransaction); + } responses.add(NodeGraphMessage::SelectedNodesSet { nodes: self.selection_before_pointer_down.clone(), }); @@ -1121,6 +1129,7 @@ impl<'a> MessageHandler> for NodeG rubber_band: false, }); self.preview_on_mouse_up = None; + self.duplicated_in_drag = true; } } @@ -1423,6 +1432,7 @@ impl<'a> MessageHandler> for NodeG self.drag_start = None; self.begin_dragging = false; + self.duplicated_in_drag = false; self.box_selection_start = None; self.wire_in_progress_from_connector = None; @@ -2810,6 +2820,7 @@ impl Default for NodeGraphMessageHandler { widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: Vec::new() }], drag_start: None, begin_dragging: false, + duplicated_in_drag: false, node_has_moved_in_drag: false, shift_without_push: false, box_selection_start: None, @@ -2841,6 +2852,7 @@ impl PartialEq for NodeGraphMessageHandler { && self.widgets == other.widgets && self.drag_start == other.drag_start && self.begin_dragging == other.begin_dragging + && self.duplicated_in_drag == other.duplicated_in_drag && self.node_has_moved_in_drag == other.node_has_moved_in_drag && self.box_selection_start == other.box_selection_start && self.initial_disconnecting == other.initial_disconnecting From f0b82bd4ae0a7688db6f326d9fd1d141d8f423a4 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 22:23:43 +0000 Subject: [PATCH 2/8] Fix-3 --- .../portfolio/document/document_message_handler.rs | 8 +++++++- .../document/node_graph/node_graph_message_handler.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 461e443a35..0ad31ca077 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -477,7 +477,13 @@ impl MessageHandler> for DocumentMes DocumentMessage::Escape => { // Abort dragging nodes if self.node_graph_handler.drag_start.is_some() { - responses.add(DocumentMessage::AbortTransaction); + if self.node_graph_handler.duplicated_in_drag { + responses.add(DocumentMessage::AbortTransaction); + responses.add(DocumentMessage::Undo); + self.node_graph_handler.duplicated_in_drag = false; + } else { + responses.add(DocumentMessage::AbortTransaction); + } self.node_graph_handler.drag_start = None; self.node_graph_handler.select_if_not_dragged = None; } 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 851097396e..77eddb3298 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 @@ -59,7 +59,7 @@ pub struct NodeGraphMessageHandler { /// Used to add a transaction for the first node move when dragging. begin_dragging: bool, /// Tracks whether nodes were duplicated via Alt-drag, so aborting undoes both the move and duplication. - duplicated_in_drag: bool, + pub duplicated_in_drag: bool, /// Used to prevent entering a nested network if the node is dragged after double clicking node_has_moved_in_drag: bool, /// If dragging the selected nodes, this stores the starting position both in viewport and node graph coordinates, From 8c7322f5b77340178770d2230327da9d4d9944f3 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:13:22 +0000 Subject: [PATCH 3/8] Fix-4 --- .../src/messages/input_mapper/input_mappings.rs | 2 +- .../document/node_graph/node_graph_message.rs | 4 +++- .../node_graph/node_graph_message_handler.rs | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 644b8d5a7b..b4b42694ba 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -83,7 +83,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: true }), entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy), - entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes), + entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes { add_transaction: true }), entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility), entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked), entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedAsLayersOrNodes), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 4ec91d9981..484fc42b10 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -62,7 +62,9 @@ pub enum NodeGraphMessage { }, DisconnectRootNode, EnterNestedNetwork, - DuplicateSelectedNodes, + DuplicateSelectedNodes { + add_transaction: bool, + }, ExposeInput { input_connector: InputConnector, set_to_exposed: bool, 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 77eddb3298..8edf3aed28 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 @@ -377,7 +377,7 @@ impl<'a> MessageHandler> for NodeG NodeGraphMessage::DisconnectRootNode => { network_interface.start_previewing_without_restore(selection_network_path); } - NodeGraphMessage::DuplicateSelectedNodes => { + NodeGraphMessage::DuplicateSelectedNodes { add_transaction } => { let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path); let copy_ids = all_selected_nodes.iter().enumerate().map(|(new, id)| (*id, NodeId(new as u64))).collect::>(); @@ -386,7 +386,11 @@ impl<'a> MessageHandler> for NodeG let nodes = network_interface.copy_nodes(©_ids, selection_network_path).collect::>(); let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::>(); - responses.add(DocumentMessage::AddTransaction); + if add_transaction { + responses.add(DocumentMessage::AddTransaction); + } else { + responses.add(DocumentMessage::StartTransaction); + } responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_ids.values().cloned().collect(), @@ -784,7 +788,6 @@ impl<'a> MessageHandler> for NodeG self.select_if_not_dragged = None; if self.duplicated_in_drag { responses.add(DocumentMessage::AbortTransaction); - responses.add(DocumentMessage::Undo); self.duplicated_in_drag = false; } else { responses.add(DocumentMessage::AbortTransaction); @@ -1122,7 +1125,7 @@ impl<'a> MessageHandler> for NodeG if self.begin_dragging { self.begin_dragging = false; if ipp.keyboard.get(Key::Alt as usize) { - responses.add(NodeGraphMessage::DuplicateSelectedNodes); + responses.add(NodeGraphMessage::DuplicateSelectedNodes { add_transaction: false }); // Duplicating sets a 2x2 offset, so shift the nodes back to the original position responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta: IVec2::new(-2, -2), @@ -1314,6 +1317,11 @@ impl<'a> MessageHandler> for NodeG } responses.add(NodeGraphMessage::SendGraph); + if self.duplicated_in_drag { + responses.add(DocumentMessage::CommitTransaction); + self.duplicated_in_drag = false; + } + let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in PointerUp"); return; From 6cda43279b718b665f0683ba89e42898c7903166 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:19:30 +0000 Subject: [PATCH 4/8] separate version --- .../messages/input_mapper/input_mappings.rs | 1 - .../document/node_graph/node_graph_message.rs | 5 ++-- .../node_graph/node_graph_message_handler.rs | 25 +++++-------------- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index b4b42694ba..41d8e554e8 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -83,7 +83,6 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: true }), entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy), - entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes { add_transaction: true }), entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility), entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked), entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedAsLayersOrNodes), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 484fc42b10..1711d63c0c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -62,9 +62,8 @@ pub enum NodeGraphMessage { }, DisconnectRootNode, EnterNestedNetwork, - DuplicateSelectedNodes { - add_transaction: bool, - }, + DuplicateSelectedNodes, + DuplicateSelectedNodesForDrag, ExposeInput { input_connector: InputConnector, set_to_exposed: bool, 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 8edf3aed28..5cd2ec2007 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 @@ -377,24 +377,11 @@ impl<'a> MessageHandler> for NodeG NodeGraphMessage::DisconnectRootNode => { network_interface.start_previewing_without_restore(selection_network_path); } - NodeGraphMessage::DuplicateSelectedNodes { add_transaction } => { - let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path); - - let copy_ids = all_selected_nodes.iter().enumerate().map(|(new, id)| (*id, NodeId(new as u64))).collect::>(); - - // Copy the selected nodes - let nodes = network_interface.copy_nodes(©_ids, selection_network_path).collect::>(); - - let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::>(); - if add_transaction { - responses.add(DocumentMessage::AddTransaction); - } else { - responses.add(DocumentMessage::StartTransaction); - } - responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); - responses.add(NodeGraphMessage::SelectedNodesSet { - nodes: new_ids.values().cloned().collect(), - }); + NodeGraphMessage::DuplicateSelectedNodes => { + self.duplicate_selected_nodes_impl(network_interface, selection_network_path, responses, true); + } + NodeGraphMessage::DuplicateSelectedNodesForDrag => { + self.duplicate_selected_nodes_impl(network_interface, selection_network_path, responses, false); } NodeGraphMessage::EnterNestedNetwork => { // Do not enter the nested network if the node was dragged @@ -1125,7 +1112,7 @@ impl<'a> MessageHandler> for NodeG if self.begin_dragging { self.begin_dragging = false; if ipp.keyboard.get(Key::Alt as usize) { - responses.add(NodeGraphMessage::DuplicateSelectedNodes { add_transaction: false }); + responses.add(NodeGraphMessage::DuplicateSelectedNodesForDrag); // Duplicating sets a 2x2 offset, so shift the nodes back to the original position responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta: IVec2::new(-2, -2), From 041e33c805e23838f6e5f222f0da7d9a2962b8a6 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:31:55 +0000 Subject: [PATCH 5/8] Fix -5 --- .../node_graph/node_graph_message_handler.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 5cd2ec2007..d43582777b 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 @@ -2100,6 +2100,7 @@ impl NodeGraphMessageHandler { Cut, DeleteSelectedNodes, DuplicateSelectedNodes, + DuplicateSelectedNodesForDrag, MergeSelectedNodes, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked, @@ -2117,6 +2118,26 @@ impl NodeGraphMessageHandler { common } + fn duplicate_selected_nodes_impl(&mut self, network_interface: &mut NodeNetworkInterface, selection_network_path: &[NodeId], responses: &mut VecDeque, add_transaction: bool) { + let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path); + + let copy_ids = all_selected_nodes.iter().enumerate().map(|(new, id)| (*id, NodeId(new as u64))).collect::>(); + + let nodes = network_interface.copy_nodes(©_ids, selection_network_path).collect::>(); + + let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::>(); + if add_transaction { + responses.add(DocumentMessage::AddTransaction); + } else { + responses.add(DocumentMessage::StartTransaction); + } + responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); + responses.add(NodeGraphMessage::SelectedNodesSet { + nodes: new_ids.values().cloned().collect(), + }); + self.duplicated_in_drag = !add_transaction; + } + /// Send the cached layout to the frontend for the control bar at the top of the node panel fn send_node_bar_layout(&self, responses: &mut VecDeque) { responses.add(LayoutMessage::SendLayout { From 1e70008324285ffa3d6f3e7dcf788abb773e877f Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:36:33 +0000 Subject: [PATCH 6/8] Revert --- editor/src/messages/input_mapper/input_mappings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 41d8e554e8..644b8d5a7b 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -83,6 +83,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: true }), entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy), + entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes), entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility), entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked), entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedAsLayersOrNodes), From c26e0bf4dcade7b14b4472ce96d7b9794cca5ac7 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 07:40:53 +0000 Subject: [PATCH 7/8] refactor --- .../document/document_message_handler.rs | 9 ++------- .../node_graph/node_graph_message_handler.rs | 15 +++------------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 0ad31ca077..55be5c355f 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -477,13 +477,8 @@ impl MessageHandler> for DocumentMes DocumentMessage::Escape => { // Abort dragging nodes if self.node_graph_handler.drag_start.is_some() { - if self.node_graph_handler.duplicated_in_drag { - responses.add(DocumentMessage::AbortTransaction); - responses.add(DocumentMessage::Undo); - self.node_graph_handler.duplicated_in_drag = false; - } else { - responses.add(DocumentMessage::AbortTransaction); - } + responses.add(DocumentMessage::AbortTransaction); + self.node_graph_handler.duplicated_in_drag = false; self.node_graph_handler.drag_start = None; self.node_graph_handler.select_if_not_dragged = None; } 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 d43582777b..f643fec834 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 @@ -773,12 +773,8 @@ impl<'a> MessageHandler> for NodeG if self.drag_start.is_some() { self.drag_start = None; self.select_if_not_dragged = None; - if self.duplicated_in_drag { - responses.add(DocumentMessage::AbortTransaction); - self.duplicated_in_drag = false; - } else { - responses.add(DocumentMessage::AbortTransaction); - } + responses.add(DocumentMessage::AbortTransaction); + self.duplicated_in_drag = false; responses.add(NodeGraphMessage::SelectedNodesSet { nodes: self.selection_before_pointer_down.clone(), }); @@ -1304,10 +1300,7 @@ impl<'a> MessageHandler> for NodeG } responses.add(NodeGraphMessage::SendGraph); - if self.duplicated_in_drag { - responses.add(DocumentMessage::CommitTransaction); - self.duplicated_in_drag = false; - } + let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in PointerUp"); @@ -2128,8 +2121,6 @@ impl NodeGraphMessageHandler { let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::>(); if add_transaction { responses.add(DocumentMessage::AddTransaction); - } else { - responses.add(DocumentMessage::StartTransaction); } responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); responses.add(NodeGraphMessage::SelectedNodesSet { From f520fce6d9a43b1dc0e3c0ca1aa83a0c39452561 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 07:57:15 +0000 Subject: [PATCH 8/8] Fix-5 --- .../document/node_graph/node_graph_message_handler.rs | 3 --- 1 file changed, 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 f643fec834..0c4aa6c2df 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 @@ -1300,8 +1300,6 @@ impl<'a> MessageHandler> for NodeG } responses.add(NodeGraphMessage::SendGraph); - - let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in PointerUp"); return; @@ -2093,7 +2091,6 @@ impl NodeGraphMessageHandler { Cut, DeleteSelectedNodes, DuplicateSelectedNodes, - DuplicateSelectedNodesForDrag, MergeSelectedNodes, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked,