Skip to content
Snippets Groups Projects
node_wrangler.py 187 KiB
Newer Older
  • Learn to ignore specific revisions
  •     def invoke(self, context, event):
            if context.area.type == 'NODE_EDITOR':
                nodes, links = get_nodes_links(context)
                node = node_at_pos(nodes, context, event)
                if node:
                    context.scene.NWBusyDrawing = node.name
    
                # the arguments we pass the the callback
    
                mode = "LINK"
                if self.with_menu:
                    mode = "LINKMENU"
                args = (self, context, mode)
    
                # Add the region OpenGL drawing callback
                # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
    
                self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
    
    
                self.mouse_path = []
    
                context.window_manager.modal_handler_add(self)
                return {'RUNNING_MODAL'}
            else:
                self.report({'WARNING'}, "View3D not found, cannot run operator")
                return {'CANCELLED'}
    
    
    class NWDeleteUnused(Operator, NWBase):
        """Delete all nodes whose output is not used"""
        bl_idname = 'node.nw_del_unused'
        bl_label = 'Delete Unused Nodes'
        bl_options = {'REGISTER', 'UNDO'}
    
    
    Campbell Barton's avatar
    Campbell Barton committed
        delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
        delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
    
        def is_unused_node(self, node):
    
            end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
    
                    'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
    
                    'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
            if node.type in end_types:
                return False
    
            for output in node.outputs:
                if output.links:
                    return False
            return True
    
    
        @classmethod
        def poll(cls, context):
            valid = False
    
            if nw_check(context):
                if context.space_data.node_tree.nodes:
                    valid = True
    
            return valid
    
        def execute(self, context):
            nodes, links = get_nodes_links(context)
    
            # Store selection
            selection = []
            for node in nodes:
                if node.select == True:
                    selection.append(node.name)
    
    
            for node in nodes:
                node.select = False
    
    
            deleted_nodes = []
            temp_deleted_nodes = []
            del_unused_iterations = len(nodes)
            for it in range(0, del_unused_iterations):
                temp_deleted_nodes = list(deleted_nodes)  # keep record of last iteration
                for node in nodes:
    
                    if self.is_unused_node(node):
    
                        node.select = True
                        deleted_nodes.append(node.name)
                        bpy.ops.node.delete()
    
                if temp_deleted_nodes == deleted_nodes:  # stop iterations when there are no more nodes to be deleted
                    break
    
    
            if self.delete_frames:
                repeat = True
                while repeat:
                    frames_in_use = []
                    frames = []
                    repeat = False
                    for node in nodes:
                        if node.parent:
                            frames_in_use.append(node.parent)
    
                    for node in nodes:
                        if node.type == 'FRAME' and node not in frames_in_use:
    
                            frames.append(node)
                            if node.parent:
                                repeat = True  # repeat for nested frames
                    for node in frames:
                        if node not in frames_in_use:
                            node.select = True
                            deleted_nodes.append(node.name)
                    bpy.ops.node.delete()
    
            if self.delete_muted:
                for node in nodes:
                    if node.mute:
                        node.select = True
                        deleted_nodes.append(node.name)
                bpy.ops.node.delete_reconnect()
    
    
            # get unique list of deleted nodes (iterations would count the same node more than once)
            deleted_nodes = list(set(deleted_nodes))
            for n in deleted_nodes:
                self.report({'INFO'}, "Node " + n + " deleted")
            num_deleted = len(deleted_nodes)
            n = ' node'
            if num_deleted > 1:
                n += 's'
            if num_deleted:
                self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
            else:
                self.report({'INFO'}, "Nothing deleted")
    
            # Restore selection
            nodes, links = get_nodes_links(context)
            for node in nodes:
                if node.name in selection:
                    node.select = True
            return {'FINISHED'}
    
        def invoke(self, context, event):
            return context.window_manager.invoke_confirm(self, event)
    
    
    
    class NWSwapLinks(Operator, NWBase):
        """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
        bl_idname = 'node.nw_swap_links'
        bl_label = 'Swap Links'
    
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context):
    
            valid = False
            if nw_check(context):
                if context.selected_nodes:
                    valid = len(context.selected_nodes) <= 2
            return valid
    
    
        def execute(self, context):
            nodes, links = get_nodes_links(context)
            selected_nodes = context.selected_nodes
            n1 = selected_nodes[0]
    
    
            # Swap outputs
            if len(selected_nodes) == 2:
                n2 = selected_nodes[1]
                if n1.outputs and n2.outputs:
                    n1_outputs = []
                    n2_outputs = []
    
                    out_index = 0
                    for output in n1.outputs:
                        if output.links:
                            for link in output.links:
                                n1_outputs.append([out_index, link.to_socket])
                                links.remove(link)
                        out_index += 1
    
                    out_index = 0
                    for output in n2.outputs:
                        if output.links:
                            for link in output.links:
                                n2_outputs.append([out_index, link.to_socket])
                                links.remove(link)
                        out_index += 1
    
                    for connection in n1_outputs:
                        try:
                            links.new(n2.outputs[connection[0]], connection[1])
                        except:
                            self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
                    for connection in n2_outputs:
                        try:
                            links.new(n1.outputs[connection[0]], connection[1])
                        except:
                            self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
                else:
                    if n1.outputs or n2.outputs:
                        self.report({'WARNING'}, "One of the nodes has no outputs!")
                    else:
                        self.report({'WARNING'}, "Neither of the nodes have outputs!")
    
            # 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 and not i1.is_multi_input:
    
                            similar_types = 0
                            for i2 in n1.inputs:
    
                                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
                    types.sort(key=lambda k: k[1], reverse=True)
    
                    if types:
                        t = types[0]
                        if t[1] == 2:
                            for i2 in n1.inputs:
                                if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
                                    pair = [t[0], i2]
                            i1f = pair[0].links[0].from_socket
                            i1t = pair[0].links[0].to_socket
                            i2f = pair[1].links[0].from_socket
                            i2t = pair[1].links[0].to_socket
                            links.new(i1f, i2t)
                            links.new(i2f, i1t)
                        if t[1] == 1:
                            if len(types) == 1:
                                fs = t[0].links[0].from_socket
                                i = t[2]
                                links.remove(t[0].links[0])
                                if i+1 == len(n1.inputs):
                                    i = -1
                                i += 1
                                while n1.inputs[i].is_linked:
                                    i += 1
                                links.new(fs, n1.inputs[i])
                            elif len(types) == 2:
                                i1f = types[0][0].links[0].from_socket
                                i1t = types[0][0].links[0].to_socket
                                i2f = types[1][0].links[0].from_socket
                                i2t = types[1][0].links[0].to_socket
                                links.new(i1f, i2t)
                                links.new(i2f, i1t)
    
                    else:
                        self.report({'WARNING'}, "This node has no input connections to swap!")
                else:
                    self.report({'WARNING'}, "This node has no inputs to swap!")
    
            force_update(context)
    
            return {'FINISHED'}
    
    
    class NWResetBG(Operator, NWBase):
        """Reset the zoom and position of the background image"""
        bl_idname = 'node.nw_bg_reset'
        bl_label = 'Reset Backdrop'
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context):
    
            valid = False
            if nw_check(context):
                snode = context.space_data
                valid = snode.tree_type == 'CompositorNodeTree'
            return valid
    
    
        def execute(self, context):
            context.space_data.backdrop_zoom = 1
    
            context.space_data.backdrop_offset[0] = 0
            context.space_data.backdrop_offset[1] = 0
    
            return {'FINISHED'}
    
    
    class NWAddAttrNode(Operator, NWBase):
        """Add an Attribute node with this name"""
        bl_idname = 'node.nw_add_attr_node'
        bl_label = 'Add UV map'
        bl_options = {'REGISTER', 'UNDO'}
    
    
        def execute(self, context):
            bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
            nodes, links = get_nodes_links(context)
            nodes.active.attribute_name = self.attr_name
            return {'FINISHED'}
    
    
    class NWPreviewNode(Operator, NWBase):
        bl_idname = "node.nw_preview_node"
        bl_label = "Preview Node"
    
        bl_description = "Connect active node to the Node Group output or the Material Output"
    
        bl_options = {'REGISTER', 'UNDO'}
    
    
        # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
        # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
        # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
        run_in_geometry_nodes: BoolProperty(default=True)
    
    
        def __init__(self):
            self.shader_output_type = ""
            self.shader_output_ident = ""
    
    
        @classmethod
        def poll(cls, context):
    
            if nw_check(context):
                space = context.space_data
    
                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
                    else:
                        return True
            return False
    
        def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
            #check if a viewer output already exists in a node group otherwise create
            if hasattr(node, "node_tree"):
                index = None
                if len(node.node_tree.outputs):
                    free_socket = None
                    for i, socket in enumerate(node.node_tree.outputs):
                        if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
                            #if viewer output is already used but leads to the same socket we can still use it
                            is_used = self.is_socket_used_other_mats(socket)
                            if is_used:
                                if connect_socket == None:
                                    continue
                                groupout = get_group_output_node(node.node_tree)
                                groupout_input = groupout.inputs[i]
                                links = groupout_input.links
                                if connect_socket not in [link.from_socket for link in links]:
                                    continue
                                index=i
                                break
                            if not free_socket:
                                free_socket = i
                    if not index and free_socket:
                        index = free_socket
    
                if not index:
                    #create viewer socket
                    node.node_tree.outputs.new(socket_type, viewer_socket_name)
                    index = len(node.node_tree.outputs) - 1
                    node.node_tree.outputs[index].NWViewerSocket = True
                return index
    
        def init_shader_variables(self, space, shader_type):
    
            if shader_type == 'OBJECT':
    
                if space.id not in [light for light in bpy.data.lights]:  # cannot use bpy.data.lights directly as iterable
    
                    self.shader_output_type = "OUTPUT_MATERIAL"
                    self.shader_output_ident = "ShaderNodeOutputMaterial"
    
                    self.shader_output_type = "OUTPUT_LIGHT"
                    self.shader_output_ident = "ShaderNodeOutputLight"
    
            elif shader_type == 'WORLD':
    
                self.shader_output_type = "OUTPUT_WORLD"
                self.shader_output_ident = "ShaderNodeOutputWorld"
    
        def get_shader_output_node(self, tree):
            for node in tree.nodes:
                if node.type == self.shader_output_type and node.is_active_output == True:
                    return node
    
        @classmethod
        def ensure_group_output(cls, tree):
            #check if a group output node exists otherwise create
            groupout = get_group_output_node(tree)
            if not groupout:
                groupout = tree.nodes.new('NodeGroupOutput')
                loc_x, loc_y = get_output_location(tree)
                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):
    
            # recursively scan nodes for viewer sockets and store in list
    
            for i, input_socket in enumerate(node.inputs):
                if index and i != index:
                    continue
                if len(input_socket.links):
                    link = input_socket.links[0]
                    next_node = link.from_node
                    external_socket = link.from_socket
                    if hasattr(next_node, "node_tree"):
                        for socket_index, s in enumerate(next_node.outputs):
                            if s == external_socket:
                                break
                        socket = next_node.node_tree.outputs[socket_index]
                        if is_viewer_socket(socket) and socket not in sockets:
                            sockets.append(socket)
                            #continue search inside of node group but restrict socket to where we came from
                            groupout = get_group_output_node(next_node.node_tree)
                            cls.search_sockets(groupout, sockets, index=socket_index)
    
        @classmethod
    
        def scan_nodes(cls, tree, sockets):
            # get all viewer sockets in a material tree
    
            for node in tree.nodes:
                if hasattr(node, "node_tree"):
                    for socket in node.node_tree.outputs:
                        if is_viewer_socket(socket) and (socket not in sockets):
                            sockets.append(socket)
    
                    cls.scan_nodes(node.node_tree, sockets)
    
    
        def link_leads_to_used_socket(self, link):
            #return True if link leads to a socket that is already used in this material
            socket = get_internal_socket(link.to_socket)
            return (socket and self.is_socket_used_active_mat(socket))
    
        def is_socket_used_active_mat(self, socket):
            #ensure used sockets in active material is calculated and check given socket
            if not hasattr(self, "used_viewer_sockets_active_mat"):
                self.used_viewer_sockets_active_mat = []
                materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
                if materialout:
    
                    self.search_sockets(materialout, self.used_viewer_sockets_active_mat)
    
            return socket in self.used_viewer_sockets_active_mat
    
        def is_socket_used_other_mats(self, socket):
            #ensure used sockets in other materials are calculated and check given socket
            if not hasattr(self, "used_viewer_sockets_other_mats"):
                self.used_viewer_sockets_other_mats = []
                for mat in bpy.data.materials:
                    if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
                        continue
                    # get viewer node
                    materialout = self.get_shader_output_node(mat.node_tree)
                    if materialout:
    
                        self.search_sockets(materialout, self.used_viewer_sockets_other_mats)
    
            return socket in self.used_viewer_sockets_other_mats
    
        def invoke(self, context, event):
            space = context.space_data
    
            # Ignore operator when running in wrong context.
            if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
                return {'PASS_THROUGH'}
    
    
            shader_type = space.shader_type
            self.init_shader_variables(space, shader_type)
    
            mlocx = event.mouse_region_x
            mlocy = event.mouse_region_y
    
            select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
    
            if 'FINISHED' in select_node:  # only run if mouse click is on a node
    
                active_tree, path_to_tree = get_active_tree(context)
                nodes, links = active_tree.nodes, active_tree.links
                base_node_tree = space.node_tree
    
                # For geometry node trees we just connect to the group output
    
                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
                    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)
    
                    nodes.active = active
                    active.select = True
                    force_update(context)
                    return {'FINISHED'}
    
    
                # What follows is code for the shader editor
    
                output_types = [x.nodetype for x in
                                get_nodes_from_category('Output', context)]
    
                valid = False
    
                    if active.rna_type.identifier not in output_types:
    
                        for out in active.outputs:
    
                            if is_visible_socket(out):
    
                    materialout = None  # placeholder node
    
                    #scan through all nodes in tree including nodes inside of groups to find viewer sockets
                    self.scan_nodes(base_node_tree, delete_sockets)
    
                    materialout = self.get_shader_output_node(base_node_tree)
                    if not materialout:
                        materialout = base_node_tree.nodes.new(self.shader_output_ident)
                        materialout.location = get_output_location(base_node_tree)
    
                    out_i = None
                    valid_outputs = []
    
                    for i, out in enumerate(active.outputs):
    
                        if is_visible_socket(out):
    
                            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, materialout):
                                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
                    if active.outputs:
    
                        socket_type = 'NodeSocketShader'
                        materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
                        make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
                        output_socket = materialout.inputs[materialout_index]
    
                        for li_from, li_to in make_links:
    
                            base_node_tree.links.new(li_from, li_to)
    
    
                        # Create links through node groups until we reach the active node
    
                        tree = base_node_tree
                        link_end = output_socket
                        while tree.nodes.active != active:
                            node = tree.nodes.active
                            index = self.ensure_viewer_socket(node, socket_type, 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:
                        if not self.is_socket_used_other_mats(socket):
                            tree = socket.id_data
                            tree.outputs.remove(socket)
    
    
                    force_update(context)
    
                return {'FINISHED'}
            else:
                return {'CANCELLED'}
    
    
    class NWFrameSelected(Operator, NWBase):
        bl_idname = "node.nw_frame_selected"
        bl_label = "Frame Selected"
        bl_description = "Add a frame node and parent the selected nodes to it"
        bl_options = {'REGISTER', 'UNDO'}
    
    
        label_prop: StringProperty(
            name='Label',
            description='The visual name of the frame node',
            default=' '
        )
    
        use_custom_color_prop: BoolProperty(
            name="Custom Color",
            description="Use custom color for the frame node",
            default=False
        )
    
        color_prop: FloatVectorProperty(
            name="Color",
            description="The color of the frame node",
    
            min=0, max=1, step=1, precision=3,
            subtype='COLOR_GAMMA', size=3
        )
    
        def draw(self, context):
            layout = self.layout
            layout.prop(self, 'label_prop')
            layout.prop(self, 'use_custom_color_prop')
            col = layout.column()
            col.active = self.use_custom_color_prop
            col.prop(self, 'color_prop', text="")
    
    
        def execute(self, context):
            nodes, links = get_nodes_links(context)
            selected = []
            for node in nodes:
                if node.select == True:
                    selected.append(node)
    
            bpy.ops.node.add_node(type='NodeFrame')
            frm = nodes.active
            frm.label = self.label_prop
    
            frm.use_custom_color = self.use_custom_color_prop
    
            frm.color = self.color_prop
    
            for node in selected:
                node.parent = frm
    
            return {'FINISHED'}
    
    
    
    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"]
            num_reloaded = 0
            for node in nodes:
                if node.type in image_types:
                    if node.type == "TEXTURE":
                        if node.texture:  # node has texture assigned
                            if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
                                if node.texture.image:  # texture has image assigned
                                    node.texture.image.reload()
                                    num_reloaded += 1
                    else:
                        if node.image:
                            node.image.reload()
                            num_reloaded += 1
    
            if num_reloaded:
                self.report({'INFO'}, "Reloaded images")
                print("Reloaded " + str(num_reloaded) + " images")
    
                force_update(context)
    
                return {'FINISHED'}
            else:
                self.report({'WARNING'}, "No images found to reload in this node tree")
                return {'CANCELLED'}
    
    
    class NWSwitchNodeType(Operator, NWBase):
        """Switch type of selected nodes """
        bl_idname = "node.nw_swtch_node_type"
        bl_label = "Switch Node Type"
        bl_options = {'REGISTER', 'UNDO'}
    
    
            name="Switch to type",
            default = '',
        )
    
    
        def execute(self, context):
            to_type = self.to_type
    
            if len(to_type) == 0:
                return {'CANCELLED'}
    
            nodes, links = get_nodes_links(context)
    
            # Those types of nodes will not swap.
            src_excludes = ('NodeFrame')
            # Those attributes of nodes will be copied if possible
            attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
                             'show_options', 'show_preview', 'show_texture',
                             'use_alpha', 'use_clamp', 'use_custom_color', 'location'
                             )
            selected = [n for n in nodes if n.select]
            reselect = []
            for node in [n for n in selected if
                         n.rna_type.identifier not in src_excludes and
                         n.rna_type.identifier != to_type]:
                new_node = nodes.new(to_type)
                for attr in attrs_to_pass:
                    if hasattr(node, attr) and hasattr(new_node, attr):
                        setattr(new_node, attr, getattr(node, attr))
                # set image datablock of dst to image of src
                if hasattr(node, 'image') and hasattr(new_node, 'image'):
                    if node.image:
                        new_node.image = node.image
                # Special cases
                if new_node.type == 'SWITCH':
                    new_node.hide = True
                # Dictionaries: src_sockets and dst_sockets:
                # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
                # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
                # in 'INPUTS' and 'OUTPUTS':
                # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
                # socket entry:
                # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
                src_sockets = {
                    'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
                    'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
                }
                dst_sockets = {
                    'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
                    'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
                }
                types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
                types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
                # check src node to set src_sockets values and dst node to set dst_sockets dict values
                for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
                    # Check node's inputs and outputs and fill proper entries in "sockets" dict
                    for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
                        # enumerate in inputs, then in outputs
                        # find name, default value and links of socket
                        for i, socket in enumerate(in_out):
                            the_name = socket.name
                            dval = None
                            # Not every socket, especially in outputs has "default_value"
                            if hasattr(socket, 'default_value'):
                                dval = socket.default_value
                            socket_links = []
                            for lnk in socket.links:
                                socket_links.append(lnk)
                            # check type of socket to fill proper keys.
                            for the_type in types_order_one:
                                if socket.type == the_type:
                                    # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
                                    # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
                                    sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
                        # Check which of the types in inputs/outputs is considered to be "main".
                        # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
                        for type_check in types_order_one:
                            if sockets[in_out_name][type_check]:
                                sockets[in_out_name]['MAIN'] = type_check
                                break
    
                matches = {
                    'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
                    'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
                }
    
                for inout, soctype in (
                        ('INPUTS', 'MAIN',),
                        ('INPUTS', 'SHADER',),
                        ('INPUTS', 'RGBA',),
                        ('INPUTS', 'VECTOR',),
                        ('INPUTS', 'VALUE',),
                        ('OUTPUTS', 'MAIN',),
                        ('OUTPUTS', 'SHADER',),
                        ('OUTPUTS', 'RGBA',),
                        ('OUTPUTS', 'VECTOR',),
                        ('OUTPUTS', 'VALUE',),
                ):
                    if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
                        if soctype == 'MAIN':
                            sc = src_sockets[inout][src_sockets[inout]['MAIN']]
                            dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
                        else:
                            sc = src_sockets[inout][soctype]
                            dt = dst_sockets[inout][soctype]
                        # start with 'dt' to determine number of possibilities.
                        for i, soc in enumerate(dt):
                            # if src main has enough entries - match them with dst main sockets by indexes.
                            if len(sc) > i:
                                matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
                            # add 'VALUE_NAME' criterion to inputs.
                            if inout == 'INPUTS' and soctype == 'VALUE':
                                for s in sc:
                                    if s[2] == soc[2]:  # if names match
                                        # append src (index, dval), dst (index, dval)
                                        matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
    
                # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
                # This creates better links when relinking textures.
                if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
                    matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
    
                # Pass default values and RELINK:
                for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
                    # INPUTS: Base on matches in proper order.
                    for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
                        # pass dvals
                        if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
                            new_node.inputs[dst_i].default_value = src_dval
                        # Special case: switch to math
                        if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
                                new_node.type == 'MATH' and\
                                tp == 'MAIN':
                            new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
                            new_node.inputs[dst_i].default_value = new_dst_dval
                            if node.type == 'MIX_RGB':
                                if node.blend_type in [o[0] for o in operations]:
                                    new_node.operation = node.blend_type
                        # Special case: switch from math to some types
                        if node.type == 'MATH' and\
                                new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
                                tp == 'MAIN':
                            for i in range(3):
                                new_node.inputs[dst_i].default_value[i] = src_dval
                            if new_node.type == 'MIX_RGB':
                                if node.operation in [t[0] for t in blend_types]:
                                    new_node.blend_type = node.operation
                                # Set Fac of MIX_RGB to 1.0
                                new_node.inputs[0].default_value = 1.0
                        # make link only when dst matching input is not linked already.
                        if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
                            in_src_link = node.inputs[src_i].links[0]
                            in_dst_socket = new_node.inputs[dst_i]
                            links.new(in_src_link.from_socket, in_dst_socket)
                            links.remove(in_src_link)
                    # OUTPUTS: Base on matches in proper order.
                    for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
                        for out_src_link in node.outputs[src_i].links:
                            out_dst_socket = new_node.outputs[dst_i]
                            links.new(out_dst_socket, out_src_link.to_socket)
                # relink rest inputs if possible, no criteria
                for src_inp in node.inputs:
                    for dst_inp in new_node.inputs:
                        if src_inp.links and not dst_inp.links:
                            src_link = src_inp.links[0]
                            links.new(src_link.from_socket, dst_inp)
                            links.remove(src_link)
                # relink rest outputs if possible, base on node kind if any left.
                for src_o in node.outputs:
                    for out_src_link in src_o.links:
                        for dst_o in new_node.outputs:
                            if src_o.type == dst_o.type:
                                links.new(dst_o, out_src_link.to_socket)
                # relink rest outputs no criteria if any left. Link all from first output.
                for src_o in node.outputs:
                    for out_src_link in src_o.links:
                        if new_node.outputs:
                            links.new(new_node.outputs[0], out_src_link.to_socket)
                nodes.remove(node)
    
            force_update(context)
    
            return {'FINISHED'}
    
    
    class NWMergeNodes(Operator, NWBase):
        bl_idname = "node.nw_merge_nodes"
    
        bl_label = "Merge Nodes"
        bl_description = "Merge Selected Nodes"
        bl_options = {'REGISTER', 'UNDO'}
    
    
    Campbell Barton's avatar
    Campbell Barton committed
        mode: EnumProperty(
    
            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],
    
    Campbell Barton's avatar
    Campbell Barton committed
        merge_type: EnumProperty(
    
            name="merge type",
            description="Type of Merge to be used",
            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'),
                ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
    
        # 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
            merge_position = settings.merge_position  # 'center' or 'bottom'
    
            do_hide = False
            do_hide_shader = False
            if merge_hide == 'ALWAYS':
                do_hide = True
                do_hide_shader = True
            elif merge_hide == 'NON_SHADER':
                do_hide = True