Skip to content
Snippets Groups Projects
node_wrangler.py 171 KiB
Newer Older
  • Learn to ignore specific revisions
  •     is_main_tree = True
        if active:
            is_main_tree = context_active == active
        if not is_main_tree:  # if group is currently edited
            tree = active.node_tree
            nodes = tree.nodes
            links = tree.links
    
        return nodes, links
    
    
    
    # Addon prefs
    class NWNodeWrangler(bpy.types.AddonPreferences):
        bl_idname = __name__
    
        merge_hide = EnumProperty(
            name="Hide Mix nodes",
            items=(
                ("ALWAYS", "Always", "Always collapse the new merge nodes"),
                ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
                ("NEVER", "Never", "Never collapse the new merge nodes")
            ),
            default='NON_SHADER',
            description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded")
        merge_position = EnumProperty(
            name="Mix Node Position",
            items=(
                ("CENTER", "Center", "Place the Mix node between the two nodes"),
                ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
            ),
            default='CENTER',
            description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes")
    
        show_hotkey_list = BoolProperty(
            name="Show Hotkey List",
            default=False,
            description="Expand this box into a list of all the hotkeys for functions in this addon"
        )
        hotkey_list_filter = StringProperty(
            name="        Filter by Name",
            default="",
            description="Show only hotkeys that have this text in their name"
        )
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
            col.prop(self, "merge_position")
            col.prop(self, "merge_hide")
    
            box = col.box()
            col = box.column(align=True)
    
            hotkey_button_name = "Show Hotkey List"
            if self.show_hotkey_list:
                hotkey_button_name = "Hide Hotkey List"
            col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
            if self.show_hotkey_list:
                col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
                col.separator()
                for hotkey in kmi_defs:
    
    Bartek Skorupa's avatar
    Bartek Skorupa committed
                    if hotkey[7]:
                        hotkey_name = hotkey[7]
    
    
                        if self.hotkey_list_filter.lower() in hotkey_name.lower():
                            row = col.row(align=True)
                            row.label(hotkey_name)
                            keystr = nice_hotkey_name(hotkey[1])
                            if hotkey[4]:
    
    Bartek Skorupa's avatar
    Bartek Skorupa committed
                                keystr = "Shift " + keystr
                            if hotkey[5]:
    
    Bartek Skorupa's avatar
    Bartek Skorupa committed
                            if hotkey[3]:
    
                                keystr = "Ctrl " + keystr
                            row.label(keystr)
    
    
    def nw_check(context):
        space = context.space_data
        valid = False
        if space.type == 'NODE_EDITOR' and space.node_tree is not None:
    
        @classmethod
        def poll(cls, context):
    
            return nw_check(context)
    
    # OPERATORS
    class NWLazyMix(Operator, NWBase):
        """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
        bl_idname = "node.nw_lazy_mix"
        bl_label = "Mix Nodes"
        bl_options = {'REGISTER', 'UNDO'}
    
        def modal(self, context, event):
            context.area.tag_redraw()
            nodes, links = get_nodes_links(context)
            cont = True
    
            start_pos = [event.mouse_region_x, event.mouse_region_y]
    
            node1 = None
            if not context.scene.NWBusyDrawing:
                node1 = node_at_pos(nodes, context, event)
                if node1:
                    context.scene.NWBusyDrawing = node1.name
            else:
                if context.scene.NWBusyDrawing != 'STOP':
                    node1 = nodes[context.scene.NWBusyDrawing]
    
    
            context.scene.NWLazySource = node1.name
            context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
    
    
            if event.type == 'MOUSEMOVE':
                self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
    
            elif event.type == 'RIGHTMOUSE':
                end_pos = [event.mouse_region_x, event.mouse_region_y]
                bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
    
                node2 = None
                node2 = node_at_pos(nodes, context, event)
                if node2:
                    context.scene.NWBusyDrawing = node2.name
    
                if node1 == node2:
                    cont = False
    
                if cont:
                    if node1 and node2:
                        for node in nodes:
                            node.select = False
                        node1.select = True
                        node2.select = True
    
                        bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
    
                context.scene.NWBusyDrawing = ""
                return {'FINISHED'}
    
            elif event.type == 'ESC':
                print('cancelled')
                bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
                return {'CANCELLED'}
    
            return {'RUNNING_MODAL'}
    
        def invoke(self, context, event):
            if context.area.type == 'NODE_EDITOR':
                # the arguments we pass the the callback
                args = (self, context, 'MIX')
                # 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 NWLazyConnect(Operator, NWBase):
        """Connect two nodes without clicking a specific socket (automatically determined"""
        bl_idname = "node.nw_lazy_connect"
        bl_label = "Lazy Connect"
        bl_options = {'REGISTER', 'UNDO'}
    
        with_menu = BoolProperty()
    
    
        def modal(self, context, event):
            context.area.tag_redraw()
            nodes, links = get_nodes_links(context)
            cont = True
    
            start_pos = [event.mouse_region_x, event.mouse_region_y]
    
            node1 = None
            if not context.scene.NWBusyDrawing:
                node1 = node_at_pos(nodes, context, event)
                if node1:
                    context.scene.NWBusyDrawing = node1.name
            else:
                if context.scene.NWBusyDrawing != 'STOP':
                    node1 = nodes[context.scene.NWBusyDrawing]
    
    
            context.scene.NWLazySource = node1.name
            context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
    
    
            if event.type == 'MOUSEMOVE':
                self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
    
            elif event.type == 'RIGHTMOUSE':
                end_pos = [event.mouse_region_x, event.mouse_region_y]
                bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
    
                node2 = None
                node2 = node_at_pos(nodes, context, event)
                if node2:
                    context.scene.NWBusyDrawing = node2.name
    
                if node1 == node2:
                    cont = False
    
                link_success = False
                if cont:
                    if node1 and node2:
                        original_sel = []
                        original_unsel = []
                        for node in nodes:
                            if node.select == True:
                                node.select = False
                                original_sel.append(node)
                            else:
                                original_unsel.append(node)
                        node1.select = True
                        node2.select = True
    
    
                        #link_success = autolink(node1, node2, links)
                        if self.with_menu:
                            if len(node1.outputs) > 1 and node2.inputs:
                                bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
                            elif len(node1.outputs) == 1:
                                bpy.ops.node.nw_call_inputs_menu(from_socket=0)
                        else:
                            link_success = autolink(node1, node2, links)
    
    
                        for node in original_sel:
                            node.select = True
                        for node in original_unsel:
                            node.select = False
    
                if link_success:
                    hack_force_update(context, nodes)
                context.scene.NWBusyDrawing = ""
                return {'FINISHED'}
    
            elif event.type == 'ESC':
                bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
                return {'CANCELLED'}
    
            return {'RUNNING_MODAL'}
    
        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'}
    
        @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)
            end_types = 'OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
                'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LAMP', \
                'OUTPUT_WORLD', 'GROUP', 'GROUP_INPUT', 'GROUP_OUTPUT'
    
            # Store selection
            selection = []
            for node in nodes:
                if node.select == True:
                    selection.append(node.name)
    
            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:
                    node.select = False
                for node in nodes:
                    if is_end_node(node) and not node.type in end_types and node.type != 'FRAME':
                        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
            # 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:
                    types = []
                    i=0
                    for i1 in n1.inputs:
                        if i1.is_linked:
                            similar_types = 0
                            for i2 in n1.inputs:
                                if i1.type == i2.type and i2.is_linked:
                                    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!")
    
    
            hack_force_update(context, nodes)
            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_x = 0
            context.space_data.backdrop_y = 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'
        attr_name = StringProperty()
        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 NWEmissionViewer(Operator, NWBase):
        bl_idname = "node.nw_emission_viewer"
        bl_label = "Emission Viewer"
        bl_description = "Connect active node to Emission Shader for shadeless previews"
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context):
    
            is_cycles = context.scene.render.engine == 'CYCLES'
    
            if nw_check(context):
                space = context.space_data
    
                if space.tree_type == 'ShaderNodeTree' and is_cycles:
                    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 invoke(self, context, event):
    
            space = context.space_data
            shader_type = space.shader_type
    
            if shader_type == 'OBJECT':
    
                if space.id not in [lamp for lamp in bpy.data.lamps]:  # cannot use bpy.data.lamps directly as iterable
                    shader_output_type = "OUTPUT_MATERIAL"
                    shader_output_ident = "ShaderNodeOutputMaterial"
                    shader_viewer_ident = "ShaderNodeEmission"
                else:
                    shader_output_type = "OUTPUT_LAMP"
                    shader_output_ident = "ShaderNodeOutputLamp"
                    shader_viewer_ident = "ShaderNodeEmission"
    
    
            elif shader_type == 'WORLD':
                shader_output_type = "OUTPUT_WORLD"
                shader_output_ident = "ShaderNodeOutputWorld"
                shader_viewer_ident = "ShaderNodeBackground"
            shader_types = [x[1] for x in shaders_shader_nodes_props]
            mlocx = event.mouse_region_x
            mlocy = event.mouse_region_y
            select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
            if 'FINISHED' in select_node:  # only run if mouse click is on a node
                nodes, links = get_nodes_links(context)
    
                in_group = context.active_node != space.node_tree.nodes.active
    
                active = nodes.active
                output_types = [x[1] for x in shaders_output_nodes_props]
    
                valid = False
    
                if active:
                    if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
    
                        for out in active.outputs:
                            if not out.hide:
                                valid = True
                                break
    
                    # get material_output node, store selection, deselect all
    
                    materialout = None  # placeholder node
    
                    selection = []
    
                    for node in nodes:
                        if node.type == shader_output_type:
                            materialout = node
    
                        if node.select:
                            selection.append(node.name)
                        node.select = False
    
                        # get right-most location
    
                        sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
                        max_xloc_node = sorted_by_xloc[-1]
                        if max_xloc_node.name == 'Emission Viewer':
                            max_xloc_node = sorted_by_xloc[-2]
    
    
                        # get average y location
    
                        sum_yloc = 0
                        for node in nodes:
                            sum_yloc += node.location.y
    
    
                        new_locx = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
                        new_locy = sum_yloc / len(nodes)
    
                        materialout = nodes.new(shader_output_ident)
                        materialout.location.x = new_locx
                        materialout.location.y = new_locy
    
                    # Analyze outputs, add "Emission Viewer" if needed, make links
    
                    out_i = None
                    valid_outputs = []
    
                    for i, out in enumerate(active.outputs):
    
                        if not out.hide:
                            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:
    
                            linked_to_out = False
                            if "Emission Viewer" in out_link.to_node.name or out_link.to_node == materialout:
                                linked_to_out = True
                            if linked_to_out:
    
                                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 output type not 'SHADER' - "Emission Viewer" needed
                        if active.outputs[out_i].type != 'SHADER':
                            # get Emission Viewer node
                            emission_exists = False
                            emission_placeholder = nodes[0]
                            for node in nodes:
                                if "Emission Viewer" in node.name:
                                    emission_exists = True
                                    emission_placeholder = node
                            if not emission_exists:
                                emission = nodes.new(shader_viewer_ident)
                                emission.hide = True
                                emission.location = [materialout.location.x, (materialout.location.y + 40)]
                                emission.label = "Viewer"
                                emission.name = "Emission Viewer"
                                emission.use_custom_color = True
                                emission.color = (0.6, 0.5, 0.4)
                                emission.select = False
                            else:
                                emission = emission_placeholder
                            make_links.append((active.outputs[out_i], emission.inputs[0]))
                            make_links.append((emission.outputs[0], materialout.inputs[0]))
                        else:
    
                            # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
                            make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
    
                            for node in nodes:
                                if node.name == 'Emission Viewer':
                                    node.select = True
                                    bpy.ops.node.delete()
                        for li_from, li_to in make_links:
                            links.new(li_from, li_to)
    
                    # Restore selection
                    nodes.active = active
                    for node in nodes:
                        if node.name in selection:
                            node.select = True
    
                    hack_force_update(context, nodes)
    
                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', default=' ', description='The visual name of the frame node')
        color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6),
                                         min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3)
    
        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 = True
            frm.color = self.color_prop
    
            for node in selected:
                node.parent = frm
    
            return {'FINISHED'}
    
    
    class NWReloadImages(Operator, NWBase):
        bl_idname = "node.nw_reload_images"
        bl_label = "Reload Images"
        bl_description = "Update all the image nodes to match their files on disk"
    
        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")
                hack_force_update(context, nodes)
                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'}
    
        to_type = EnumProperty(
            name="Switch to type",
            items=list(shaders_input_nodes_props) +
            list(shaders_output_nodes_props) +
            list(shaders_shader_nodes_props) +
            list(shaders_texture_nodes_props) +
            list(shaders_color_nodes_props) +
            list(shaders_vector_nodes_props) +
            list(shaders_converter_nodes_props) +
            list(shaders_layout_nodes_props) +
            list(compo_input_nodes_props) +
            list(compo_output_nodes_props) +
            list(compo_color_nodes_props) +
            list(compo_converter_nodes_props) +
            list(compo_filter_nodes_props) +
            list(compo_vector_nodes_props) +
            list(compo_matte_nodes_props) +
            list(compo_distort_nodes_props) +
    
            list(compo_layout_nodes_props) +
            list(blender_mat_input_nodes_props) +
            list(blender_mat_output_nodes_props) +
            list(blender_mat_color_nodes_props) +
            list(blender_mat_vector_nodes_props) +
            list(blender_mat_converter_nodes_props) +
            list(blender_mat_layout_nodes_props) +
            list(texture_input_nodes_props) +
            list(texture_output_nodes_props) +
            list(texture_color_nodes_props) +
            list(texture_pattern_nodes_props) +
            list(texture_textures_nodes_props) +
            list(texture_converter_nodes_props) +
            list(texture_distort_nodes_props) +
            list(texture_layout_nodes_props)
    
        )
    
        def execute(self, context):
            nodes, links = get_nodes_links(context)
            to_type = self.to_type
            # 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)
            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'}
    
        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],
        )
    
        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'),
                ('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'),
    
    
        def execute(self, context):
    
            settings = context.user_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
    
    
            tree_type = context.space_data.node_tree.type
            if tree_type == 'COMPOSITING':
                node_type = 'CompositorNode'
            elif tree_type == 'SHADER':
                node_type = 'ShaderNode'
    
            elif tree_type == 'TEXTURE':
                node_type = 'TextureNode'
    
            nodes, links = get_nodes_links(context)
            mode = self.mode
            merge_type = self.merge_type
    
            # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
            # 'ZCOMBINE' works only if mode == 'MIX'
            # Setting mode to None prevents trying to add 'ZCOMBINE' node.
    
            if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
                merge_type = 'MIX'
                mode = 'MIX'
    
            selected_mix = []  # entry = [index, loc]
            selected_shader = []  # entry = [index, loc]
            selected_math = []  # entry = [index, loc]
    
            selected_z = []  # entry = [index, loc]
    
            selected_alphaover = []  # entry = [index, loc]
    
    
            for i, node in enumerate(nodes):
                if node.select and node.outputs:
                    if merge_type == 'AUTO':
                        for (type, types_list, dst) in (
    
                                ('SHADER', ('MIX', 'ADD'), selected_shader),
    
                                ('RGBA', [t[0] for t in blend_types], selected_mix),
                                ('VALUE', [t[0] for t in operations], selected_math),
    
                            output_type = node.outputs[0].type
                            valid_mode = mode in types_list
                            # 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':
                                output_type = 'RGBA'
                                valid_mode = True
                            if output_type == type and valid_mode:
    
                                dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
    
                    else:
                        for (type, types_list, dst) in (
    
                                ('SHADER', ('MIX', 'ADD'), selected_shader),
    
                                ('MIX', [t[0] for t in blend_types], selected_mix),
                                ('MATH', [t[0] for t in operations], selected_math),
    
                                ('ZCOMBINE', ('MIX', ), selected_z),
    
                                ('ALPHAOVER', ('MIX', ), selected_alphaover),
    
                            if merge_type == type and mode in types_list:
    
                                dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
    
            # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
            # use only 'Mix' nodes for merging.
            # For that we add selected_math list to selected_mix list and clear selected_math.
            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)