From a91f5724d2866a718857697ab3ec45b7a9eaf6c0 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 21:52:53 +0000 Subject: [PATCH 01/10] 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 3ac4622911..c9ed0b66ef 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, @@ -808,7 +810,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(), }); @@ -1179,6 +1187,7 @@ impl<'a> MessageHandler> for NodeG rubber_band: false, }); self.preview_on_mouse_up = None; + self.duplicated_in_drag = true; } } @@ -1482,6 +1491,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; @@ -2886,6 +2896,7 @@ impl Default for NodeGraphMessageHandler { widgets: [LayoutGroup::row(Vec::new()), LayoutGroup::row(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, @@ -2917,6 +2928,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 c21ab6d240b64d9b939c37f8ff5a1f89e13cf341 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 22:23:43 +0000 Subject: [PATCH 02/10] 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 7ffceb9f5a..fcda49c068 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -542,7 +542,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 c9ed0b66ef..daa7f60244 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 189a3896fdf84530d72af6a012c0c7373da12533 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:13:22 +0000 Subject: [PATCH 03/10] 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 2d9669a355..302d9dc2ba 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 8e864f66a2..671e8fef23 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 daa7f60244..770b0feb97 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 @@ -396,7 +396,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::>(); @@ -405,7 +405,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(), @@ -812,7 +816,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); @@ -1180,7 +1183,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), @@ -1373,6 +1376,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 3afd4171bc723f2be558ae251e813b052b0b66d8 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:19:30 +0000 Subject: [PATCH 04/10] 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 302d9dc2ba..167e79d278 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 671e8fef23..78e90160c0 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 770b0feb97..8b1aac0f36 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 @@ -396,24 +396,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 @@ -1183,7 +1170,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 cb1ebb17db2237f8b7cf0b19133b2a212580fe06 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:31:55 +0000 Subject: [PATCH 05/10] 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 8b1aac0f36..c2ccbcadc9 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 @@ -2205,6 +2205,7 @@ impl NodeGraphMessageHandler { Cut, DeleteSelectedNodes, DuplicateSelectedNodes, + DuplicateSelectedNodesForDrag, MergeSelectedNodes, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked, @@ -2222,6 +2223,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 6e5e3d273527c751b880d4e1e25117ff84b9d8d4 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:36:33 +0000 Subject: [PATCH 06/10] 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 167e79d278..2d9669a355 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 87ff27f04adf939056a3d18119bdf19ffc9ae78a Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 07:40:53 +0000 Subject: [PATCH 07/10] 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 fcda49c068..12edb92b62 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -542,13 +542,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 c2ccbcadc9..40bd715e98 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 @@ -801,12 +801,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(), }); @@ -1363,10 +1359,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"); @@ -2233,8 +2226,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 9eae26b8ce6e53ef66db26a251dd067ed8b8143b Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 07:57:15 +0000 Subject: [PATCH 08/10] 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 40bd715e98..9a9c2e1786 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 @@ -1359,8 +1359,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; @@ -2198,7 +2196,6 @@ impl NodeGraphMessageHandler { Cut, DeleteSelectedNodes, DuplicateSelectedNodes, - DuplicateSelectedNodesForDrag, MergeSelectedNodes, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked, From f51a06652f11c9024893ee765deafd96e856482b Mon Sep 17 00:00:00 2001 From: Kulratan Date: Fri, 13 Mar 2026 16:26:02 +0000 Subject: [PATCH 09/10] Logic improvment --- .../messages/portfolio/document/document_message_handler.rs | 3 ++- .../document/node_graph/node_graph_message_handler.rs | 6 +++++- 2 files changed, 7 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 12edb92b62..cd9b1b0c57 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -542,7 +542,8 @@ impl MessageHandler> for DocumentMes DocumentMessage::Escape => { // Abort dragging nodes if self.node_graph_handler.drag_start.is_some() { - responses.add(DocumentMessage::AbortTransaction); + let undo_count = if self.node_graph_handler.duplicated_in_drag { 2 } else { 1 }; + responses.add(DocumentMessage::RepeatedAbortTransaction { undo_count }); 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 9a9c2e1786..09e3497ecd 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 @@ -400,7 +400,10 @@ impl<'a> MessageHandler> for NodeG self.duplicate_selected_nodes_impl(network_interface, selection_network_path, responses, true); } NodeGraphMessage::DuplicateSelectedNodesForDrag => { + responses.add(DocumentMessage::StartTransaction); self.duplicate_selected_nodes_impl(network_interface, selection_network_path, responses, false); + responses.add(DocumentMessage::EndTransaction); + responses.add(DocumentMessage::StartTransaction); } NodeGraphMessage::EnterNestedNetwork => { // Do not enter the nested network if the node was dragged @@ -801,7 +804,8 @@ 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); + let undo_count = if self.duplicated_in_drag { 2 } else { 1 }; + responses.add(DocumentMessage::RepeatedAbortTransaction { undo_count }); self.duplicated_in_drag = false; responses.add(NodeGraphMessage::SelectedNodesSet { nodes: self.selection_before_pointer_down.clone(), From 8da14637c8fb3ad3e4f0189bfa4d88a8dc86b35c Mon Sep 17 00:00:00 2001 From: Kulratan Date: Wed, 15 Jul 2026 14:25:33 +0000 Subject: [PATCH 10/10] Claude Review --- .../document/document_message_handler.rs | 14 ++--- .../document/node_graph/node_graph_message.rs | 1 - .../node_graph/node_graph_message_handler.rs | 60 ++++++++----------- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index cd9b1b0c57..3926205e75 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -540,14 +540,12 @@ impl MessageHandler> for DocumentMes responses.add(NodeGraphMessage::UpdateNodeGraphWidth); } DocumentMessage::Escape => { - // Abort dragging nodes - if self.node_graph_handler.drag_start.is_some() { - let undo_count = if self.node_graph_handler.duplicated_in_drag { 2 } else { 1 }; - responses.add(DocumentMessage::RepeatedAbortTransaction { undo_count }); - self.node_graph_handler.duplicated_in_drag = false; - self.node_graph_handler.drag_start = None; - self.node_graph_handler.select_if_not_dragged = None; - } + // Abort dragging nodes + if self.node_graph_handler.drag_start.is_some() { + responses.add(DocumentMessage::AbortTransaction); + self.node_graph_handler.drag_start = None; + self.node_graph_handler.select_if_not_dragged = None; + } // Abort box selection else if self.node_graph_handler.box_selection_start.is_some() { self.node_graph_handler.box_selection_start = None; 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 78e90160c0..8e864f66a2 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 @@ -63,7 +63,6 @@ pub enum NodeGraphMessage { DisconnectRootNode, EnterNestedNetwork, 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 09e3497ecd..6e91eca119 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,8 +58,6 @@ 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. - 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, @@ -397,13 +395,19 @@ impl<'a> MessageHandler> for NodeG network_interface.start_previewing_without_restore(selection_network_path); } NodeGraphMessage::DuplicateSelectedNodes => { - self.duplicate_selected_nodes_impl(network_interface, selection_network_path, responses, true); - } - NodeGraphMessage::DuplicateSelectedNodesForDrag => { - responses.add(DocumentMessage::StartTransaction); - self.duplicate_selected_nodes_impl(network_interface, selection_network_path, responses, false); - responses.add(DocumentMessage::EndTransaction); - responses.add(DocumentMessage::StartTransaction); + 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::>(); + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); + responses.add(NodeGraphMessage::SelectedNodesSet { + nodes: new_ids.values().cloned().collect(), + }); } NodeGraphMessage::EnterNestedNetwork => { // Do not enter the nested network if the node was dragged @@ -804,9 +808,7 @@ impl<'a> MessageHandler> for NodeG if self.drag_start.is_some() { self.drag_start = None; self.select_if_not_dragged = None; - let undo_count = if self.duplicated_in_drag { 2 } else { 1 }; - responses.add(DocumentMessage::RepeatedAbortTransaction { undo_count }); - self.duplicated_in_drag = false; + responses.add(DocumentMessage::AbortTransaction); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: self.selection_before_pointer_down.clone(), }); @@ -1170,14 +1172,23 @@ impl<'a> MessageHandler> for NodeG if self.begin_dragging { self.begin_dragging = false; if ipp.keyboard.get(Key::Alt as usize) { - responses.add(NodeGraphMessage::DuplicateSelectedNodesForDrag); + // Duplicate nodes inline within the existing PointerDown transaction. + // This means a completed Alt-drag is one undo step, and aborting (right-click / Escape) + // rolls back both the duplication and the move with a single plain AbortTransaction. + 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::>(); + responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); + responses.add(NodeGraphMessage::SelectedNodesSet { + nodes: new_ids.values().cloned().collect(), + }); // Duplicating sets a 2x2 offset, so shift the nodes back to the original position responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta: IVec2::new(-2, -2), rubber_band: false, }); self.preview_on_mouse_up = None; - self.duplicated_in_drag = true; } } @@ -1481,7 +1492,6 @@ 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; @@ -2217,24 +2227,6 @@ 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); - } - 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 { @@ -2904,7 +2896,6 @@ impl Default for NodeGraphMessageHandler { widgets: [LayoutGroup::row(Vec::new()), LayoutGroup::row(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, @@ -2936,7 +2927,6 @@ 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