From b389e68d27d157818a97067fde4cfdd28edcf8c6 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 21:52:53 +0000 Subject: [PATCH 01/11] 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 13632c96a92..f85a32ae4da 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 @@ -57,6 +57,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, @@ -799,7 +801,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(), }); @@ -1178,6 +1186,7 @@ impl<'a> MessageHandler> for NodeG rubber_band: false, }); self.preview_on_mouse_up = None; + self.duplicated_in_drag = true; } } @@ -1483,6 +1492,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; @@ -2929,6 +2939,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, @@ -2960,6 +2971,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 e4cbed12dd6a93c5248a2b93a93d590b6873b61f Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 22:23:43 +0000 Subject: [PATCH 02/11] 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 5908d70c640..3b74e75ec94 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -557,7 +557,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 f85a32ae4da..679d2d6bc0c 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,7 +58,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 3ad7999e9c7f17cb0aae444c0ec3a22515b96337 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:13:22 +0000 Subject: [PATCH 03/11] 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 8023d0f926e..450aef1aaf4 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -82,7 +82,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 d7df7271fc7..0580462eb26 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 679d2d6bc0c..59f2d181f08 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 @@ -411,7 +411,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::>(); @@ -420,7 +420,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(), @@ -803,7 +807,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); @@ -1179,7 +1182,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), @@ -1372,6 +1375,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 8e2424d67ec351cdde9b3568fb0ecb8d5e8d4583 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:19:30 +0000 Subject: [PATCH 04/11] 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 450aef1aaf4..375b21cb0a5 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -82,7 +82,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 0580462eb26..4bb2c1cf2c2 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 59f2d181f08..2f6773df8e1 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 @@ -411,24 +411,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 @@ -1182,7 +1169,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 a3f55191d2afbe564c2be11fd85552d76a6a0f56 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:31:55 +0000 Subject: [PATCH 05/11] 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 2f6773df8e1..21cf5560d06 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 @@ -2247,6 +2247,7 @@ impl NodeGraphMessageHandler { Cut, DeleteSelectedNodes, DuplicateSelectedNodes, + DuplicateSelectedNodesForDrag, MergeSelectedNodes, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked, @@ -2264,6 +2265,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 feee61fcf044e6df82d7a10680ca63232498c6bc Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 17 Feb 2026 23:36:33 +0000 Subject: [PATCH 06/11] 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 375b21cb0a5..8023d0f926e 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -82,6 +82,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 e95f69d1945f1e32f93a8b5293ba4589e9b4ca14 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 07:40:53 +0000 Subject: [PATCH 07/11] 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 3b74e75ec94..acc7cd3e844 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -557,13 +557,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 21cf5560d06..09afdb3be27 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 @@ -792,12 +792,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(), }); @@ -1362,10 +1358,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"); @@ -2275,8 +2268,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 4eda9fe708a3793ebd31ab5bd1899a8823d60119 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Wed, 18 Feb 2026 07:57:15 +0000 Subject: [PATCH 08/11] 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 09afdb3be27..1fec5c22873 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 @@ -1358,8 +1358,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; @@ -2240,7 +2238,6 @@ impl NodeGraphMessageHandler { Cut, DeleteSelectedNodes, DuplicateSelectedNodes, - DuplicateSelectedNodesForDrag, MergeSelectedNodes, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked, From 5f6e60b2052301884c279d69f1872cb79a9741ec Mon Sep 17 00:00:00 2001 From: Kulratan Date: Fri, 13 Mar 2026 16:26:02 +0000 Subject: [PATCH 09/11] 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 acc7cd3e844..94298ff31ae 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -557,7 +557,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 1fec5c22873..73f9cd9bcd2 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 @@ -415,7 +415,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 @@ -792,7 +795,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 0f8c3b8cb51e3ca237b0407c4f2853e296fb3ac4 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Wed, 15 Jul 2026 14:25:33 +0000 Subject: [PATCH 10/11] 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 94298ff31ae..db0289e815a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -555,14 +555,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 4bb2c1cf2c2..d7df7271fc7 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 73f9cd9bcd2..cb252e51cd9 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 @@ -57,8 +57,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, @@ -412,13 +410,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 @@ -795,9 +799,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(), }); @@ -1169,14 +1171,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; } } @@ -1482,7 +1493,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; @@ -2259,24 +2269,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 { @@ -2947,7 +2939,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, @@ -2979,7 +2970,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 From 4c8b0c143154dde71734b0f4913f3919ea216a5d Mon Sep 17 00:00:00 2001 From: Kulratan Date: Wed, 12 Aug 2026 08:59:55 +0000 Subject: [PATCH 11/11] fmt --- .../portfolio/document/document_message_handler.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index db0289e815a..5908d70c640 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -555,12 +555,12 @@ impl MessageHandler> for DocumentMes responses.add(NodeGraphMessage::UpdateNodeGraphWidth); } DocumentMessage::Escape => { - // 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 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;