From e7cae0b220714e158fb65fee361468d2fe2eda9e Mon Sep 17 00:00:00 2001 From: quackarooni Date: Sun, 28 Sep 2025 19:40:55 +0200 Subject: [PATCH] Fix: Nodes: Occasional crashing when using swap operators Calling the swap operators can result in Blender crashing. From my testing, the behavior is inconsistent, as sometimes a crash is triggered and other times it isn't. Though notably, they seem involve nodes already linked to other nodes, or zones. The crashing stems from the operators trying to access deleted data. There were two places where this happens that were identified, one involving removed links and the other involving removed nodes. Pull Request: https://projects.blender.org/blender/blender/pulls/146909 --- scripts/startup/bl_operators/node.py | 29 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/scripts/startup/bl_operators/node.py b/scripts/startup/bl_operators/node.py index 8464083aa9d..79e65c6590e 100644 --- a/scripts/startup/bl_operators/node.py +++ b/scripts/startup/bl_operators/node.py @@ -362,13 +362,12 @@ class NodeSwapOperator(NodeOperator): if new_socket.hide or not new_socket.enabled: continue + is_multi_input = link.to_socket.is_multi_input + new_link = tree.links.new(new_socket, link.to_socket) - try: - if link.to_socket.is_multi_input: - new_link.swap_multi_input_sort_id(link) - except AttributeError: - pass + if is_multi_input: + new_link.swap_multi_input_sort_id(link) except KeyError: pass @@ -480,9 +479,10 @@ class NODE_OT_swap_node(NodeSwapOperator, Operator): def execute(self, context): tree = context.space_data.edit_tree + nodes_to_delete = set() for old_node in context.selected_nodes[:]: - if tree.nodes.get(old_node.name) is None: + if old_node in nodes_to_delete: continue if old_node.bl_idname == self.type: @@ -518,7 +518,7 @@ class NODE_OT_swap_node(NodeSwapOperator, Operator): self.transfer_links(tree, output_node, new_node, is_input=False) for node in zone_pair: - tree.nodes.remove(node) + nodes_to_delete.add(node) else: if (old_node.bl_idname in switch_nodes) and (new_node.bl_idname in switch_nodes): self.transfer_switch_data(old_node, new_node) @@ -528,7 +528,10 @@ class NODE_OT_swap_node(NodeSwapOperator, Operator): self.transfer_links(tree, old_node, new_node, is_input=True) self.transfer_links(tree, old_node, new_node, is_input=False) - tree.nodes.remove(old_node) + nodes_to_delete.add(old_node) + + for node in nodes_to_delete: + tree.nodes.remove(node) return {'FINISHED'} @@ -733,9 +736,10 @@ class NODE_OT_swap_zone(ZoneOperator, NodeSwapOperator, Operator): def execute(self, context): tree = context.space_data.edit_tree + nodes_to_delete = set() for old_node in context.selected_nodes[:]: - if tree.nodes.get(old_node.name) is None: + if old_node in nodes_to_delete: continue zone_pair = self.get_zone_pair(tree, old_node) @@ -782,7 +786,7 @@ class NODE_OT_swap_zone(ZoneOperator, NodeSwapOperator, Operator): self.transfer_links(tree, old_output_node, output_node, is_input=False) for node in zone_pair: - tree.nodes.remove(node) + nodes_to_delete.add(node) else: with temporary_unframe((old_node,)): input_node.location = old_node.location @@ -799,7 +803,7 @@ class NODE_OT_swap_zone(ZoneOperator, NodeSwapOperator, Operator): self.transfer_links(tree, old_node, input_node, is_input=True) self.transfer_links(tree, old_node, output_node, is_input=False) - tree.nodes.remove(old_node) + nodes_to_delete.add(old_node) if tree.type == "GEOMETRY" and self.add_default_geometry_link: # Connect geometry sockets by default if available. @@ -810,6 +814,9 @@ class NODE_OT_swap_zone(ZoneOperator, NodeSwapOperator, Operator): if not (from_socket.is_linked or to_socket.is_linked): tree.links.new(to_socket, from_socket) + for node in nodes_to_delete: + tree.nodes.remove(node) + return {'FINISHED'}