From 8697bffe22db5a69cffcd66b3cd0841a503c2e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20T=C3=B6nne?= Date: Tue, 30 Sep 2025 10:49:20 +0200 Subject: [PATCH] Fix #146946: Breaking a node zone crashes on valid pointer assumption The Node Wrangler addon has a _Reset Nodes_ operator that can remove the input node of a node zone. This crashes in reference set updates because the code expects valid input/output node pairs in each zone. The fix is two-fold: 1. Finding zones for the runtime now returns an empty result to ensure no invalid node pointers are being accessed. This should not happen in practice, all operators should make sure zone relationships are not broken. 2. The node wrangler addon is updated to ignore all zone types, including the newer repeat, closure, and for-each-element zones. The type filtering was outdated and now uses the `bl_idname` consistently. Pull Request: https://projects.blender.org/blender/blender/pulls/147028 --- .../addons_core/node_wrangler/operators.py | 30 ++++++++++++++----- .../blenkernel/intern/node_tree_zones.cc | 9 +++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/scripts/addons_core/node_wrangler/operators.py b/scripts/addons_core/node_wrangler/operators.py index 463a77f8d4f..723db9b7e9c 100644 --- a/scripts/addons_core/node_wrangler/operators.py +++ b/scripts/addons_core/node_wrangler/operators.py @@ -2261,13 +2261,28 @@ class NWResetNodes(bpy.types.Operator): and nw_check_selected(cls, context) and nw_check_active(cls, context)) + @staticmethod + def is_frame_node(node): + return node.bl_idname == "NodeFrame" + + group_node_types = {"CompositorNodeGroup", "GeometryNodeGroup", "ShaderNodeGroup"} + # TODO All zone nodes are ignored here for now, because replacing one of the input/output pair breaks the zone. + # It's possible to handle zones by using the `paired_output` function of an input node + # and reconstruct the zone using the `pair_with_output` function. + zone_node_types = {"GeometryNodeRepeatInput", "GeometryNodeRepeatOutput", "NodeClosureInput", + "NodeClosureOutput", "GeometryNodeSimulationInput", "GeometryNodeSimulationOutput", + "GeometryNodeForeachGeometryElementInput", "GeometryNodeForeachGeometryElementOutput"} + node_ignore = group_node_types | zone_node_types | {"NodeFrame", "NodeReroute"} + + @classmethod + def ignore_node(cls, node): + return node.bl_idname in cls.node_ignore + def execute(self, context): node_active = context.active_node node_selected = context.selected_nodes - node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"] - active_node_name = node_active.name if node_active.select else None - valid_nodes = [n for n in node_selected if n.type not in node_ignore] + valid_nodes = [n for n in node_selected if not self.ignore_node(n)] # Create output lists selected_node_names = [n.name for n in node_selected] @@ -2275,18 +2290,18 @@ class NWResetNodes(bpy.types.Operator): # Reset all valid children in a frame node_active_is_frame = False - if len(node_selected) == 1 and node_active.type == "FRAME": + if len(node_selected) == 1 and self.is_frame_node(node_active): node_tree = node_active.id_data children = [n for n in node_tree.nodes if n.parent == node_active] if children: - valid_nodes = [n for n in children if n.type not in node_ignore] - selected_node_names = [n.name for n in children if n.type not in node_ignore] + valid_nodes = [n for n in children if not self.ignore_node(n)] + selected_node_names = [n.name for n in children if not self.ignore_node(n)] node_active_is_frame = True # Check if valid nodes in selection if not (len(valid_nodes) > 0): # Check for frames only - frames_selected = [n for n in node_selected if n.type == "FRAME"] + frames_selected = [n for n in node_selected if self.is_frame_node(n)] if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)): self.report({'ERROR'}, "Please select only 1 frame to reset") else: @@ -2306,7 +2321,6 @@ class NWResetNodes(bpy.types.Operator): # Run through all valid nodes for node in valid_nodes: - parent = node.parent if node.parent else None node_loc = [node.location.x, node.location.y] diff --git a/source/blender/blenkernel/intern/node_tree_zones.cc b/source/blender/blenkernel/intern/node_tree_zones.cc index a34953979ed..68cfcf752d5 100644 --- a/source/blender/blenkernel/intern/node_tree_zones.cc +++ b/source/blender/blenkernel/intern/node_tree_zones.cc @@ -46,7 +46,7 @@ static Vector> find_zone_nodes( zone->index = zones.size(); zone->output_node_id = node->identifier; r_zone_by_inout_node.add(node, zone.get()); - zones.append_and_get_index(std::move(zone)); + zones.append(std::move(zone)); } for (const bNodeZoneType *zone_type : zone_types) { for (const bNode *input_node : tree.nodes_by_type(zone_type->input_idname)) { @@ -58,6 +58,13 @@ static Vector> find_zone_nodes( } } } + /* Avoid incomplete zones, all zones must have a valid input and output node. */ + for (const std::unique_ptr &zone : zones) { + if (!zone->input_node_id || !zone->output_node_id) { + r_zone_by_inout_node.clear(); + return {}; + } + } return zones; }