diff options
-rw-r--r-- | node_wrangler.py | 757 |
1 files changed, 534 insertions, 223 deletions
diff --git a/node_wrangler.py b/node_wrangler.py index e9ba5e74..118408c4 100644 --- a/node_wrangler.py +++ b/node_wrangler.py @@ -19,8 +19,8 @@ bl_info = { "name": "Node Wrangler", "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer", - "version": (3, 37), - "blender": (2, 83, 0), + "version": (3, 38), + "blender": (2, 93, 0), "location": "Node Editor Toolbar or Shift-W", "description": "Various tools to enhance and speed up node-based workflow", "warning": "", @@ -43,6 +43,7 @@ from bpy.props import ( from bpy_extras.io_utils import ImportHelper, ExportHelper from gpu_extras.batch import batch_for_shader from mathutils import Vector +from nodeitems_utils import node_categories_iter from math import cos, sin, pi, hypot from os import path from glob import glob @@ -54,7 +55,7 @@ from collections import namedtuple ################# # rl_outputs: # list of outputs of Input Render Layer -# with attributes determinig if pass is used, +# with attributes determining if pass is used, # and MultiLayer EXR outputs names and corresponding render engines # # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles) @@ -90,7 +91,7 @@ rl_outputs = ( # shader nodes # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. shaders_input_nodes_props = ( ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'), ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'), @@ -115,7 +116,7 @@ shaders_input_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. shaders_output_nodes_props = ( ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'), ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'), @@ -124,7 +125,7 @@ shaders_output_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. shaders_shader_nodes_props = ( ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'), ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'), @@ -149,7 +150,7 @@ shaders_shader_nodes_props = ( ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'), ) # (rna_type.identifier, type, rna_type.name) -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. shaders_texture_nodes_props = ( ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'), @@ -169,7 +170,7 @@ shaders_texture_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. shaders_color_nodes_props = ( ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'), ('ShaderNodeGamma', 'GAMMA', 'Gamma'), @@ -181,7 +182,7 @@ shaders_color_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. shaders_vector_nodes_props = ( ('ShaderNodeBump', 'BUMP', 'Bump'), ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'), @@ -194,7 +195,7 @@ shaders_vector_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. shaders_converter_nodes_props = ( ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'), ('ShaderNodeClamp', 'CLAMP', 'Clamp'), @@ -213,7 +214,7 @@ shaders_converter_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. shaders_layout_nodes_props = ( ('NodeFrame', 'FRAME', 'Frame'), ('NodeReroute', 'REROUTE', 'Reroute'), @@ -222,7 +223,7 @@ shaders_layout_nodes_props = ( # compositing nodes # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_input_nodes_props = ( ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'), ('CompositorNodeImage', 'IMAGE', 'Image'), @@ -237,7 +238,7 @@ compo_input_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_output_nodes_props = ( ('CompositorNodeComposite', 'COMPOSITE', 'Composite'), ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'), @@ -247,7 +248,7 @@ compo_output_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_color_nodes_props = ( ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'), ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'), @@ -264,7 +265,7 @@ compo_color_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_converter_nodes_props = ( ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'), ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'), @@ -284,7 +285,7 @@ compo_converter_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_filter_nodes_props = ( ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'), ('CompositorNodeBlur', 'BLUR', 'Blur'), @@ -303,7 +304,7 @@ compo_filter_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_vector_nodes_props = ( ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'), ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'), @@ -313,7 +314,7 @@ compo_vector_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_matte_nodes_props = ( ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'), ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'), @@ -331,7 +332,7 @@ compo_matte_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_distort_nodes_props = ( ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'), ('CompositorNodeCrop', 'CROP', 'Crop'), @@ -349,7 +350,7 @@ compo_distort_nodes_props = ( ) # (rna_type.identifier, type, rna_type.name) # Keeping mixed case to avoid having to translate entries when adding new nodes in operators. -# Keeping things in alphabetical orde so we don't need to sort later. +# Keeping things in alphabetical order so we don't need to sort later. compo_layout_nodes_props = ( ('NodeFrame', 'FRAME', 'Frame'), ('NodeReroute', 'REROUTE', 'Reroute'), @@ -529,7 +530,7 @@ operations = [ ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'), ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'), ('POWER', 'Power', 'Power Mode'), - ('LOGARITHM', 'Logatithm', 'Logarithm Mode'), + ('LOGARITHM', 'Logarithm', 'Logarithm Mode'), ('SQRT', 'Square Root', 'Square Root Mode'), ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'), ('EXPONENT', 'Exponent', 'Exponent Mode'), @@ -555,6 +556,14 @@ operations = [ ('DEGREES', 'To Degrees', 'To Degrees Mode'), ] +# Operations used by the geometry boolean node and join geometry node +geo_combine_operations = [ + ('JOIN', 'Join Geometry', 'Join Geometry Mode'), + ('INTERSECT', 'Intersect', 'Intersect Mode'), + ('UNION', 'Union', 'Union Mode'), + ('DIFFERENCE', 'Difference', 'Difference Mode'), +] + # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty. # used list, not tuple for easy merging with other lists. navs = [ @@ -598,6 +607,11 @@ draw_color_sets = { viewer_socket_name = "tmp_viewer" +def get_nodes_from_category(category_name, context): + for category in node_categories_iter(context): + if category.name == category_name: + return sorted(category.items(context), key=lambda node: node.label) + def is_visible_socket(socket): return not socket.hide and socket.enabled and socket.type != 'CUSTOM' @@ -680,40 +694,41 @@ def node_mid_pt(node, axis): def autolink(node1, node2, links): link_made = False - - for outp in node1.outputs: - for inp in node2.inputs: + available_inputs = [inp for inp in node2.inputs if inp.enabled] + available_outputs = [outp for outp in node1.outputs if outp.enabled] + for outp in available_outputs: + for inp in available_inputs: if not inp.is_linked and inp.name == outp.name: link_made = True links.new(outp, inp) return True - for outp in node1.outputs: - for inp in node2.inputs: + for outp in available_outputs: + for inp in available_inputs: if not inp.is_linked and inp.type == outp.type: link_made = True links.new(outp, inp) return True # force some connection even if the type doesn't match - for outp in node1.outputs: - for inp in node2.inputs: + if available_outputs: + for inp in available_inputs: if not inp.is_linked: link_made = True - links.new(outp, inp) + links.new(available_outputs[0], inp) return True # even if no sockets are open, force one of matching type - for outp in node1.outputs: - for inp in node2.inputs: + for outp in available_outputs: + for inp in available_inputs: if inp.type == outp.type: link_made = True links.new(outp, inp) return True # do something! - for outp in node1.outputs: - for inp in node2.inputs: + for outp in available_outputs: + for inp in available_inputs: link_made = True links.new(outp, inp) return True @@ -1236,7 +1251,7 @@ class NWNodeWrangler(bpy.types.AddonPreferences): def nw_check(context): space = context.space_data - valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree"] + valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"] valid = False if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees: @@ -1598,14 +1613,17 @@ class NWSwapLinks(Operator, NWBase): # Swap Inputs elif len(selected_nodes) == 1: + if n1.inputs and n1.inputs[0].is_multi_input: + self.report({'WARNING'}, "Can't swap inputs of a multi input socket!") + return {'FINISHED'} if n1.inputs: types = [] i=0 for i1 in n1.inputs: - if i1.is_linked: + if i1.is_linked and not i1.is_multi_input: similar_types = 0 for i2 in n1.inputs: - if i1.type == i2.type and i2.is_linked: + if i1.type == i2.type and i2.is_linked and not i2.is_multi_input: similar_types += 1 types.append ([i1, similar_types, i]) i += 1 @@ -1686,10 +1704,10 @@ class NWAddAttrNode(Operator, NWBase): nodes.active.attribute_name = self.attr_name return {'FINISHED'} -class NWEmissionViewer(Operator, NWBase): - bl_idname = "node.nw_emission_viewer" - bl_label = "Emission Viewer" - bl_description = "Connect active node to Emission Shader for shadeless previews" +class NWPreviewNode(Operator, NWBase): + bl_idname = "node.nw_preview_node" + bl_label = "Preview Node" + bl_description = "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output" bl_options = {'REGISTER', 'UNDO'} def __init__(self): @@ -1701,7 +1719,7 @@ class NWEmissionViewer(Operator, NWBase): def poll(cls, context): if nw_check(context): space = context.space_data - if space.tree_type == 'ShaderNodeTree': + if space.tree_type == 'ShaderNodeTree' or space.tree_type == 'GeometryNodeTree': if context.active_node: if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD": return True @@ -1772,11 +1790,13 @@ class NWEmissionViewer(Operator, NWBase): groupout.location.x = loc_x groupout.location.y = loc_y groupout.select = False + # So that we don't keep on adding new group outputs + groupout.is_active_output = True return groupout @classmethod def search_sockets(cls, node, sockets, index=None): - #recursevley scan nodes for viewer sockets and store in list + # recursively scan nodes for viewer sockets and store in list for i, input_socket in enumerate(node.inputs): if index and i != index: continue @@ -1855,6 +1875,98 @@ class NWEmissionViewer(Operator, NWBase): nodes, links = active_tree.nodes, active_tree.links base_node_tree = space.node_tree active = nodes.active + + # For geometry node trees we just connect to the group output, + # because there is no "viewer node" yet. + if space.tree_type == "GeometryNodeTree": + valid = False + if active: + for out in active.outputs: + if is_visible_socket(out): + valid = True + break + # Exit early + if not valid: + return {'FINISHED'} + + delete_sockets = [] + + # Scan through all nodes in tree including nodes inside of groups to find viewer sockets + self.scan_nodes(base_node_tree, delete_sockets) + + # Find (or create if needed) the output of this node tree + geometryoutput = self.ensure_group_output(base_node_tree) + + # Analyze outputs, make links + out_i = None + valid_outputs = [] + for i, out in enumerate(active.outputs): + if is_visible_socket(out) and out.type == 'GEOMETRY': + valid_outputs.append(i) + if valid_outputs: + out_i = valid_outputs[0] # Start index of node's outputs + for i, valid_i in enumerate(valid_outputs): + for out_link in active.outputs[valid_i].links: + if is_viewer_link(out_link, geometryoutput): + if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link): + if i < len(valid_outputs) - 1: + out_i = valid_outputs[i + 1] + else: + out_i = valid_outputs[0] + + make_links = [] # store sockets for new links + delete_nodes = [] # store unused nodes to delete in the end + if active.outputs: + # If there is no 'GEOMETRY' output type - We can't preview the node + if out_i is None: + return {'FINISHED'} + socket_type = 'GEOMETRY' + # Find an input socket of the output of type geometry + geometryoutindex = None + for i,inp in enumerate(geometryoutput.inputs): + if inp.type == socket_type: + geometryoutindex = i + break + if geometryoutindex is None: + # Create geometry socket + geometryoutput.inputs.new(socket_type, 'Geometry') + geometryoutindex = len(geometryoutput.inputs) - 1 + + make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex])) + output_socket = geometryoutput.inputs[geometryoutindex] + for li_from, li_to in make_links: + base_node_tree.links.new(li_from, li_to) + tree = base_node_tree + link_end = output_socket + while tree.nodes.active != active: + node = tree.nodes.active + index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None) + link_start = node.outputs[index] + node_socket = node.node_tree.outputs[index] + if node_socket in delete_sockets: + delete_sockets.remove(node_socket) + tree.links.new(link_start, link_end) + # Iterate + link_end = self.ensure_group_output(node.node_tree).inputs[index] + tree = tree.nodes.active.node_tree + tree.links.new(active.outputs[out_i], link_end) + + # Delete sockets + for socket in delete_sockets: + tree = socket.id_data + tree.outputs.remove(socket) + + # Delete nodes + for tree, node in delete_nodes: + tree.nodes.remove(node) + + nodes.active = active + active.select = True + force_update(context) + return {'FINISHED'} + + + # What follows is code for the shader editor output_types = [x[1] for x in shaders_output_nodes_props] valid = False if active: @@ -2021,11 +2133,22 @@ class NWFrameSelected(Operator, NWBase): return {'FINISHED'} -class NWReloadImages(Operator, NWBase): +class NWReloadImages(Operator): bl_idname = "node.nw_reload_images" bl_label = "Reload Images" bl_description = "Update all the image nodes to match their files on disk" + @classmethod + def poll(cls, context): + valid = False + if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree': + if context.active_node is not None: + for out in context.active_node.outputs: + if is_visible_socket(out): + valid = True + break + return valid + def execute(self, context): nodes, links = get_nodes_links(context) image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"] @@ -2094,9 +2217,16 @@ class NWSwitchNodeType(Operator, NWBase): list(texture_layout_nodes_props) ) + geo_to_type: StringProperty( + name="Switch to type", + default = '', + ) + def execute(self, context): nodes, links = get_nodes_links(context) to_type = self.to_type + if self.geo_to_type != '': + to_type = self.geo_to_type # Those types of nodes will not swap. src_excludes = ('NodeFrame') # Those attributes of nodes will be copied if possible @@ -2275,8 +2405,8 @@ class NWMergeNodes(Operator, NWBase): mode: EnumProperty( name="mode", - description="All possible blend types and math operations", - items=blend_types + [op for op in operations if op not in blend_types], + description="All possible blend types, boolean operations and math operations", + items= blend_types + [op for op in geo_combine_operations if op not in blend_types] + [op for op in operations if op not in blend_types], ) merge_type: EnumProperty( name="merge type", @@ -2284,6 +2414,7 @@ class NWMergeNodes(Operator, NWBase): items=( ('AUTO', 'Auto', 'Automatic Output Type Detection'), ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'), + ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'), ('MIX', 'Mix Node', 'Merge using Mix Nodes'), ('MATH', 'Math Node', 'Merge using Math Nodes'), ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'), @@ -2291,6 +2422,74 @@ class NWMergeNodes(Operator, NWBase): ), ) + # Check if the link connects to a node that is in selected_nodes + # If not, then check recursively for each link in the nodes outputs. + # If yes, return True. If the recursion stops without finding a node + # in selected_nodes, it returns False. The depth is used to prevent + # getting stuck in a loop because of an already present cycle. + @staticmethod + def link_creates_cycle(link, selected_nodes, depth=0)->bool: + if depth > 255: + # We're stuck in a cycle, but that cycle was already present, + # so we return False. + # NOTE: The number 255 is arbitrary, but seems to work well. + return False + node = link.to_node + if node in selected_nodes: + return True + if not node.outputs: + return False + for output in node.outputs: + if output.is_linked: + for olink in output.links: + if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1): + return True + # None of the outputs found a node in selected_nodes, so there is no cycle. + return False + + # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket. + # The parameters `socket_indices` gives the indices of the node sockets in the order that they should + # be connected. The last one is assumed to be a multi input socket. + # For convenience the node is returned. + @staticmethod + def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices): + # The y-location of the last node + loc_y = nodes_list[-1][2] + if merge_position == 'CENTER': + # Average the y-location + for i in range(len(nodes_list)-1): + loc_y += nodes_list[i][2] + loc_y = loc_y/len(nodes_list) + new_node = nodes.new(node_name) + new_node.hide = do_hide + new_node.location.x = loc_x + new_node.location.y = loc_y + selected_nodes = [nodes[node_info[0]] for node_info in nodes_list] + prev_links = [] + outputs_for_multi_input = [] + for i,node in enumerate(selected_nodes): + node.select = False + # Search for the first node which had output links that do not create + # a cycle, which we can then reconnect afterwards. + if prev_links == [] and node.outputs[0].is_linked: + prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)] + # Get the index of the socket, the last one is a multi input, and is thus used repeatedly + # To get the placement to look right we need to reverse the order in which we connect the + # outputs to the multi input socket. + if i < len(socket_indices) - 1: + ind = socket_indices[i] + links.new(node.outputs[0], new_node.inputs[ind]) + else: + outputs_for_multi_input.insert(0, node.outputs[0]) + if outputs_for_multi_input != []: + ind = socket_indices[-1] + for output in outputs_for_multi_input: + links.new(output, new_node.inputs[ind]) + if prev_links != []: + for link in prev_links: + links.new(new_node.outputs[0], link.to_node.inputs[0]) + return new_node + def execute(self, context): settings = context.preferences.addons[__name__].preferences merge_hide = settings.merge_hide @@ -2305,6 +2504,8 @@ class NWMergeNodes(Operator, NWBase): do_hide = True tree_type = context.space_data.node_tree.type + if tree_type == 'GEOMETRY': + node_type = 'GeometryNode' if tree_type == 'COMPOSITING': node_type = 'CompositorNode' elif tree_type == 'SHADER': @@ -2320,9 +2521,16 @@ class NWMergeNodes(Operator, NWBase): if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING': merge_type = 'MIX' mode = 'MIX' + if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY': + merge_type = 'AUTO' + # The math nodes used for geometry nodes are of type 'ShaderNode' + if merge_type == 'MATH' and tree_type == 'GEOMETRY': + node_type = 'ShaderNode' selected_mix = [] # entry = [index, loc] selected_shader = [] # entry = [index, loc] + selected_geometry = [] # entry = [index, loc] selected_math = [] # entry = [index, loc] + selected_vector = [] # entry = [index, loc] selected_z = [] # entry = [index, loc] selected_alphaover = [] # entry = [index, loc] @@ -2331,17 +2539,29 @@ class NWMergeNodes(Operator, NWBase): if merge_type == 'AUTO': for (type, types_list, dst) in ( ('SHADER', ('MIX', 'ADD'), selected_shader), + ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry), ('RGBA', [t[0] for t in blend_types], selected_mix), ('VALUE', [t[0] for t in operations], selected_math), + ('VECTOR', [], selected_vector), ): output_type = node.outputs[0].type valid_mode = mode in types_list + # When mode is 'MIX' we have to cheat since the mix node is not used in + # geometry nodes. + if tree_type == 'GEOMETRY': + if mode == 'MIX': + if output_type == 'VALUE' and type == 'VALUE': + valid_mode = True + elif output_type == 'VECTOR' and type == 'VECTOR': + valid_mode = True + elif type == 'GEOMETRY': + valid_mode = True # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types. # Cheat that output type is 'RGBA', # and that 'MIX' exists in math operations list. # This way when selected_mix list is analyzed: # Node data will be appended even though it doesn't meet requirements. - if output_type != 'SHADER' and mode == 'MIX': + elif output_type != 'SHADER' and mode == 'MIX': output_type = 'RGBA' valid_mode = True if output_type == type and valid_mode: @@ -2349,6 +2569,7 @@ class NWMergeNodes(Operator, NWBase): else: for (type, types_list, dst) in ( ('SHADER', ('MIX', 'ADD'), selected_shader), + ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry), ('MIX', [t[0] for t in blend_types], selected_mix), ('MATH', [t[0] for t in operations], selected_math), ('ZCOMBINE', ('MIX', ), selected_z), @@ -2362,158 +2583,191 @@ class NWMergeNodes(Operator, NWBase): if selected_mix and selected_math and merge_type == 'AUTO': selected_mix += selected_math selected_math = [] - - for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]: - if nodes_list: - count_before = len(nodes) - # sort list by loc_x - reversed - nodes_list.sort(key=lambda k: k[1], reverse=True) - # get maximum loc_x - loc_x = nodes_list[0][1] + nodes_list[0][3] + 70 - nodes_list.sort(key=lambda k: k[2], reverse=True) - if merge_position == 'CENTER': - loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2 # average yloc of last two nodes (lowest two) - if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit - if do_hide: - loc_y += 40 - else: - loc_y += 80 + for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]: + if not nodes_list: + continue + count_before = len(nodes) + # sort list by loc_x - reversed + nodes_list.sort(key=lambda k: k[1], reverse=True) + # get maximum loc_x + loc_x = nodes_list[0][1] + nodes_list[0][3] + 70 + nodes_list.sort(key=lambda k: k[2], reverse=True) + + # Change the node type for math nodes in a geometry node tree. + if tree_type == 'GEOMETRY': + if nodes_list is selected_math or nodes_list is selected_vector: + node_type = 'ShaderNode' + if mode == 'MIX': + mode = 'ADD' else: - loc_y = nodes_list[len(nodes_list) - 1][2] - offset_y = 100 - if not do_hide: - offset_y = 200 - if nodes_list == selected_shader and not do_hide_shader: - offset_y = 150.0 - the_range = len(nodes_list) - 1 - if len(nodes_list) == 1: - the_range = 1 - for i in range(the_range): - if nodes_list == selected_mix: - add_type = node_type + 'MixRGB' + node_type = 'GeometryNode' + if merge_position == 'CENTER': + loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2 # average yloc of last two nodes (lowest two) + if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit + if do_hide: + loc_y += 40 + else: + loc_y += 80 + else: + loc_y = nodes_list[len(nodes_list) - 1][2] + offset_y = 100 + if not do_hide: + offset_y = 200 + if nodes_list == selected_shader and not do_hide_shader: + offset_y = 150.0 + the_range = len(nodes_list) - 1 + if len(nodes_list) == 1: + the_range = 1 + was_multi = False + for i in range(the_range): + if nodes_list == selected_mix: + add_type = node_type + 'MixRGB' + add = nodes.new(add_type) + add.blend_type = mode + if mode != 'MIX': + add.inputs[0].default_value = 1.0 + add.show_preview = False + add.hide = do_hide + if do_hide: + loc_y = loc_y - 50 + first = 1 + second = 2 + add.width_hidden = 100.0 + elif nodes_list == selected_math: + add_type = node_type + 'Math' + add = nodes.new(add_type) + add.operation = mode + add.hide = do_hide + if do_hide: + loc_y = loc_y - 50 + first = 0 + second = 1 + add.width_hidden = 100.0 + elif nodes_list == selected_shader: + if mode == 'MIX': + add_type = node_type + 'MixShader' add = nodes.new(add_type) - add.blend_type = mode - if mode != 'MIX': - add.inputs[0].default_value = 1.0 - add.show_preview = False - add.hide = do_hide - if do_hide: + add.hide = do_hide_shader + if do_hide_shader: loc_y = loc_y - 50 first = 1 second = 2 add.width_hidden = 100.0 - elif nodes_list == selected_math: - add_type = node_type + 'Math' + elif mode == 'ADD': + add_type = node_type + 'AddShader' add = nodes.new(add_type) - add.operation = mode - add.hide = do_hide - if do_hide: + add.hide = do_hide_shader + if do_hide_shader: loc_y = loc_y - 50 first = 0 second = 1 add.width_hidden = 100.0 - elif nodes_list == selected_shader: - if mode == 'MIX': - add_type = node_type + 'MixShader' - add = nodes.new(add_type) - add.hide = do_hide_shader - if do_hide_shader: - loc_y = loc_y - 50 - first = 1 - second = 2 - add.width_hidden = 100.0 - elif mode == 'ADD': - add_type = node_type + 'AddShader' - add = nodes.new(add_type) - add.hide = do_hide_shader - if do_hide_shader: - loc_y = loc_y - 50 - first = 0 - second = 1 - add.width_hidden = 100.0 - elif nodes_list == selected_z: - add = nodes.new('CompositorNodeZcombine') - add.show_preview = False - add.hide = do_hide - if do_hide: - loc_y = loc_y - 50 - first = 0 - second = 2 - add.width_hidden = 100.0 - elif nodes_list == selected_alphaover: - add = nodes.new('CompositorNodeAlphaOver') - add.show_preview = False - add.hide = do_hide - if do_hide: - loc_y = loc_y - 50 - first = 1 - second = 2 - add.width_hidden = 100.0 - add.location = loc_x, loc_y - loc_y += offset_y - add.select = True - count_adds = i + 1 - count_after = len(nodes) - index = count_after - 1 - first_selected = nodes[nodes_list[0][0]] - # "last" node has been added as first, so its index is count_before. - last_add = nodes[count_before] - # Special case: - # Two nodes were selected and first selected has no output links, second selected has output links. - # Then add links from last add to all links 'to_socket' of out links of second selected. - if len(nodes_list) == 2: - if not first_selected.outputs[0].links: - second_selected = nodes[nodes_list[1][0]] - for ss_link in second_selected.outputs[0].links: - # Prevent cyclic dependencies when nodes to be marged are linked to one another. - # Create list of invalid indexes. - invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)] - # Link only if "to_node" index not in invalid indexes list. - if ss_link.to_node not in [nodes[i] for i in invalid_i]: - links.new(last_add.outputs[0], ss_link.to_socket) - # add links from last_add to all links 'to_socket' of out links of first selected. - for fs_link in first_selected.outputs[0].links: - # Prevent cyclic dependencies when nodes to be marged are linked to one another. - # Create list of invalid indexes. - invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)] - # Link only if "to_node" index not in invalid indexes list. - if fs_link.to_node not in [nodes[i] for i in invalid_i]: - links.new(last_add.outputs[0], fs_link.to_socket) - # add link from "first" selected and "first" add node - node_to = nodes[count_after - 1] - links.new(first_selected.outputs[0], node_to.inputs[first]) - if node_to.type == 'ZCOMBINE': - for fs_out in first_selected.outputs: - if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'): - links.new(fs_out, node_to.inputs[1]) - break - # add links between added ADD nodes and between selected and ADD nodes - for i in range(count_adds): - if i < count_adds - 1: - node_from = nodes[index] - node_to = nodes[index - 1] - node_to_input_i = first - node_to_z_i = 1 # if z combine - link z to first z input - links.new(node_from.outputs[0], node_to.inputs[node_to_input_i]) - if node_to.type == 'ZCOMBINE': - for from_out in node_from.outputs: - if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'): - links.new(from_out, node_to.inputs[node_to_z_i]) - if len(nodes_list) > 1: - node_from = nodes[nodes_list[i + 1][0]] - node_to = nodes[index] - node_to_input_i = second - node_to_z_i = 3 # if z combine - link z to second z input - links.new(node_from.outputs[0], node_to.inputs[node_to_input_i]) - if node_to.type == 'ZCOMBINE': - for from_out in node_from.outputs: - if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'): - links.new(from_out, node_to.inputs[node_to_z_i]) - index -= 1 - # set "last" of added nodes as active - nodes.active = last_add - for i, x, y, dx, h in nodes_list: - nodes[i].select = False + elif nodes_list == selected_geometry: + if mode in ('JOIN', 'MIX'): + add_type = node_type + 'JoinGeometry' + add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0]) + else: + add_type = node_type + 'Boolean' + indices = [0,1] if mode == 'DIFFERENCE' else [1] + add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices) + add.operation = mode + was_multi = True + break + elif nodes_list == selected_vector: + add_type = node_type + 'VectorMath' + add = nodes.new(add_type) + add.operation = mode + add.hide = do_hide + if do_hide: + loc_y = loc_y - 50 + first = 0 + second = 1 + add.width_hidden = 100.0 + elif nodes_list == selected_z: + add = nodes.new('CompositorNodeZcombine') + add.show_preview = False + add.hide = do_hide + if do_hide: + loc_y = loc_y - 50 + first = 0 + second = 2 + add.width_hidden = 100.0 + elif nodes_list == selected_alphaover: + add = nodes.new('CompositorNodeAlphaOver') + add.show_preview = False + add.hide = do_hide + if do_hide: + loc_y = loc_y - 50 + first = 1 + second = 2 + add.width_hidden = 100.0 + add.location = loc_x, loc_y + loc_y += offset_y + add.select = True + + # This has already been handled separately + if was_multi: + continue + count_adds = i + 1 + count_after = len(nodes) + index = count_after - 1 + first_selected = nodes[nodes_list[0][0]] + # "last" node has been added as first, so its index is count_before. + last_add = nodes[count_before] + # Create list of invalid indexes. + invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)] + + # Special case: + # Two nodes were selected and first selected has no output links, second selected has output links. + # Then add links from last add to all links 'to_socket' of out links of second selected. + if len(nodes_list) == 2: + if not first_selected.outputs[0].links: + second_selected = nodes[nodes_list[1][0]] + for ss_link in second_selected.outputs[0].links: + # Prevent cyclic dependencies when nodes to be merged are linked to one another. + # Link only if "to_node" index not in invalid indexes list. + if not self.link_creates_cycle(ss_link, invalid_nodes): + links.new(last_add.outputs[0], ss_link.to_socket) + # add links from last_add to all links 'to_socket' of out links of first selected. + for fs_link in first_selected.outputs[0].links: + # Link only if "to_node" index not in invalid indexes list. + if not self.link_creates_cycle(fs_link, invalid_nodes): + links.new(last_add.outputs[0], fs_link.to_socket) + # add link from "first" selected and "first" add node + node_to = nodes[count_after - 1] + links.new(first_selected.outputs[0], node_to.inputs[first]) + if node_to.type == 'ZCOMBINE': + for fs_out in first_selected.outputs: + if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'): + links.new(fs_out, node_to.inputs[1]) + break + # add links between added ADD nodes and between selected and ADD nodes + for i in range(count_adds): + if i < count_adds - 1: + node_from = nodes[index] + node_to = nodes[index - 1] + node_to_input_i = first + node_to_z_i = 1 # if z combine - link z to first z input + links.new(node_from.outputs[0], node_to.inputs[node_to_input_i]) + if node_to.type == 'ZCOMBINE': + for from_out in node_from.outputs: + if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'): + links.new(from_out, node_to.inputs[node_to_z_i]) + if len(nodes_list) > 1: + node_from = nodes[nodes_list[i + 1][0]] + node_to = nodes[index] + node_to_input_i = second + node_to_z_i = 3 # if z combine - link z to second z input + links.new(node_from.outputs[0], node_to.inputs[node_to_input_i]) + if node_to.type == 'ZCOMBINE': + for from_out in node_from.outputs: + if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'): + links.new(from_out, node_to.inputs[node_to_z_i]) + index -= 1 + # set "last" of added nodes as active + nodes.active = last_add + for i, x, y, dx, h in nodes_list: + nodes[i].select = False return {'FINISHED'} @@ -2534,12 +2788,10 @@ class NWBatchChangeNodes(Operator, NWBase): ) def execute(self, context): - - nodes, links = get_nodes_links(context) blend_type = self.blend_type operation = self.operation for node in context.selected_nodes: - if node.type == 'MIX_RGB': + if node.type == 'MIX_RGB' or node.bl_idname == 'GeometryNodeAttributeMix': if not blend_type in [nav[0] for nav in navs]: node.blend_type = blend_type else: @@ -2558,7 +2810,7 @@ class NWBatchChangeNodes(Operator, NWBase): else: node.blend_type = blend_types[index - 1][0] - if node.type == 'MATH': + if node.type == 'MATH' or node.bl_idname == 'GeometryNodeAttributeMath': if not operation in [nav[0] for nav in navs]: node.operation = operation else: @@ -3478,7 +3730,7 @@ class NWDetachOutputs(Operator, NWBase): return {'FINISHED'} -class NWLinkToOutputNode(Operator, NWBase): +class NWLinkToOutputNode(Operator): """Link to Composite node or Material Output node""" bl_idname = "node.nw_link_out" bl_label = "Connect to Output" @@ -3487,7 +3739,7 @@ class NWLinkToOutputNode(Operator, NWBase): @classmethod def poll(cls, context): valid = False - if nw_check(context): + if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree': if context.active_node is not None: for out in context.active_node.outputs: if is_visible_socket(out): @@ -3993,7 +4245,8 @@ def drawlayout(context, layout, mode='non-panel'): col = layout.column(align=True) col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED') - col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER') + if tree_type != 'GeometryNodeTree': + col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER') col.separator() col = layout.column(align=True) @@ -4012,7 +4265,8 @@ def drawlayout(context, layout, mode='non-panel'): col = layout.column(align=True) if tree_type == 'CompositorNodeTree': col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS') - col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH') + if tree_type != 'GeometryNodeTree': + col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH') col.separator() col = layout.column(align=True) @@ -4067,15 +4321,29 @@ class NWMergeNodesMenu(Menu, NWBase): layout = self.layout if type == 'ShaderNodeTree': layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders") - layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes") - layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes") - props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes") - props.mode = 'MIX' - props.merge_type = 'ZCOMBINE' - props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes") - props.mode = 'MIX' - props.merge_type = 'ALPHAOVER' - + if type == 'GeometryNodeTree': + layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes") + layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes") + else: + layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes") + layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes") + props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes") + props.mode = 'MIX' + props.merge_type = 'ZCOMBINE' + props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes") + props.mode = 'MIX' + props.merge_type = 'ALPHAOVER' + +class NWMergeGeometryMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_merge_geometry_menu" + bl_label = "Merge Selected Nodes using Geometry Nodes" + def draw(self, context): + layout = self.layout + # The boolean node + Join Geometry node + for type, name, description in geo_combine_operations: + props = layout.operator(NWMergeNodes.bl_idname, text=name) + props.mode = type + props.merge_type = 'GEOMETRY' class NWMergeShadersMenu(Menu, NWBase): bl_idname = "NODE_MT_nw_merge_shaders_menu" @@ -4110,18 +4378,12 @@ class NWConnectionListOutputs(Menu, NWBase): nodes, links = get_nodes_links(context) n1 = nodes[context.scene.NWLazySource] - - if n1.type == "R_LAYERS": - index=0 - for o in n1.outputs: - if o.enabled: # Check which passes the render layer has enabled - layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index - index+=1 - else: - index=0 - for o in n1.outputs: + index=0 + for o in n1.outputs: + # Only show sockets that are exposed. + if o.enabled: layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index - index+=1 + index+=1 class NWConnectionListInputs(Menu, NWBase): @@ -4136,10 +4398,15 @@ class NWConnectionListInputs(Menu, NWBase): index = 0 for i in n2.inputs: - op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD") - op.from_socket = context.scene.NWSourceSocket - op.to_socket = index - index+=1 + # Only show sockets that are exposed. + # This prevents, for example, the scale value socket + # of the vector math node being added to the list when + # the mode is not 'SCALE'. + if i.enabled: + op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD") + op.from_socket = context.scene.NWSourceSocket + op.to_socket = index + index+=1 class NWMergeMathMenu(Menu, NWBase): @@ -4351,6 +4618,17 @@ class NWSwitchNodeTypeMenu(Menu, NWBase): layout.menu(NWSwitchTexConverterSubmenu.bl_idname) layout.menu(NWSwitchTexDistortSubmenu.bl_idname) layout.menu(NWSwitchTexLayoutSubmenu.bl_idname) + if tree.type == 'GEOMETRY': + categories = [c for c in node_categories_iter(context) + if c.name not in ['Group', 'Script']] + for cat in categories: + idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu" + if hasattr(bpy.types, idname): + layout.menu(idname) + else: + layout.label(text="Unable to load altered node lists.") + layout.label(text="Please re-enable Node Wrangler.") + break class NWSwitchShadersInputSubmenu(Menu, NWBase): @@ -4697,6 +4975,17 @@ class NWSwitchTexLayoutSubmenu(Menu, NWBase): props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) props.to_type = ident +def draw_switch_category_submenu(self, context): + layout = self.layout + if self.category.name == 'Layout': + for node in self.category.items(context): + if node.nodetype != 'NodeFrame': + props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label) + props.to_type = node.nodetype + else: + for node in self.category.items(context): + props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label) + props.geo_to_type = node.nodetype # # APPENDAGES TO EXISTING UI @@ -4754,7 +5043,7 @@ def reset_nodes_button(self, context): # # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS # - +switch_category_menus = [] addon_keymaps = [] # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name) # props entry: (property name, property value) @@ -4916,8 +5205,8 @@ kmi_defs = ( (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"), # Swap Outputs (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"), - # Emission Viewer - (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"), + # Preview Node + (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Preview node output"), # Reload Images (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"), # Lazy Mix @@ -4951,7 +5240,7 @@ classes = ( NWSwapLinks, NWResetBG, NWAddAttrNode, - NWEmissionViewer, + NWPreviewNode, NWFrameSelected, NWReloadImages, NWSwitchNodeType, @@ -4981,6 +5270,7 @@ classes = ( NodeWranglerMenu, NWMergeNodesMenu, NWMergeShadersMenu, + NWMergeGeometryMenu, NWMergeMixMenu, NWConnectionListOutputs, NWConnectionListInputs, @@ -5081,6 +5371,23 @@ def register(): bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button) bpy.types.NODE_MT_node.prepend(reset_nodes_button) + # switch submenus + switch_category_menus.clear() + for cat in node_categories_iter(None): + if cat.name not in ['Group', 'Script'] and cat.identifier.startswith('GEO'): + idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu" + switch_category_type = type(idname, (bpy.types.Menu,), { + "bl_space_type": 'NODE_EDITOR', + "bl_label": cat.name, + "category": cat, + "poll": cat.poll, + "draw": draw_switch_category_submenu, + }) + + switch_category_menus.append(switch_category_type) + + bpy.utils.register_class(switch_category_type) + def unregister(): from bpy.utils import unregister_class @@ -5092,6 +5399,10 @@ def unregister(): del bpy.types.Scene.NWSourceSocket del bpy.types.NodeSocketInterface.NWViewerSocket + for cat_types in switch_category_menus: + bpy.utils.unregister_class(cat_types) + switch_category_menus.clear() + # keymaps for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) |