Skip to content
Snippets Groups Projects
node_wrangler.py 226 KiB
Newer Older
  • Learn to ignore specific revisions
  •         nodes, links = get_nodes_links(context)
    
            shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    
            if mode == "LINK":
    
                col_outer = (1.0, 0.2, 0.2, 0.4)
                col_inner = (0.0, 0.0, 0.0, 0.5)
                col_circle_inner = (0.3, 0.05, 0.05, 1.0)
    
            elif mode == "LINKMENU":
    
                col_outer = (0.4, 0.6, 1.0, 0.4)
                col_inner = (0.0, 0.0, 0.0, 0.5)
                col_circle_inner = (0.08, 0.15, .3, 1.0)
    
            elif mode == "MIX":
    
                col_outer = (0.2, 1.0, 0.2, 0.4)
                col_inner = (0.0, 0.0, 0.0, 0.5)
                col_circle_inner = (0.05, 0.3, 0.05, 1.0)
    
    
            m1x = self.mouse_path[0][0]
            m1y = self.mouse_path[0][1]
            m2x = self.mouse_path[-1][0]
            m2y = self.mouse_path[-1][1]
    
    
            n1 = nodes[context.scene.NWLazySource]
            n2 = nodes[context.scene.NWLazyTarget]
    
            if n1 == n2:
    
                col_outer = (0.4, 0.4, 0.4, 0.4)
                col_inner = (0.0, 0.0, 0.0, 0.5)
                col_circle_inner = (0.2, 0.2, 0.2, 1.0)
    
            draw_rounded_node_border(shader, n1, radius=6, colour=col_outer)  # outline
            draw_rounded_node_border(shader, n1, radius=5, colour=col_inner)  # inner
            draw_rounded_node_border(shader, n2, radius=6, colour=col_outer)  # outline
            draw_rounded_node_border(shader, n2, radius=5, colour=col_inner)  # inner
    
            draw_line(m1x, m1y, m2x, m2y, 5, col_outer)  # line outline
    
            draw_line(m1x, m1y, m2x, m2y, 2, col_inner)  # line inner
    
            # circle outline
    
            draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
            draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
    
            draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
            draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
    
            bgl.glDisable(bgl.GL_LINE_SMOOTH)
    
    def get_active_tree(context):
    
        tree = context.space_data.node_tree
    
        # Get nodes from currently edited tree.
        # If user is editing a group, space_data.node_tree is still the base level (outside group).
        # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
        # the same as context.active_node, the user is in a group.
        # Check recursively until we find the real active node_tree:
        if tree.nodes.active:
            while tree.nodes.active != context.active_node:
                tree = tree.nodes.active.node_tree
    
                path.append(tree)
        return tree, path
    
    def get_nodes_links(context):
        tree, path = get_active_tree(context)
    
        return tree.nodes, tree.links
    
    def is_viewer_socket(socket):
        # checks if a internal socket is a valid viewer socket
        return socket.name == viewer_socket_name and socket.NWViewerSocket
    
    def get_internal_socket(socket):
        #get the internal socket from a socket inside or outside the group
        node = socket.node
        if node.type == 'GROUP_OUTPUT':
            source_iterator = node.inputs
            iterator = node.id_data.outputs
        elif node.type == 'GROUP_INPUT':
            source_iterator = node.outputs
            iterator = node.id_data.inputs
        elif hasattr(node, "node_tree"):
            if socket.is_output:
                source_iterator = node.outputs
                iterator = node.node_tree.outputs
            else:
                source_iterator = node.inputs
                iterator = node.node_tree.inputs
        else:
            return None
    
        for i, s in enumerate(source_iterator):
            if s == socket:
                break
        return iterator[i]
    
    def is_viewer_link(link, output_node):
        if "Emission Viewer" in link.to_node.name or link.to_node == output_node and link.to_socket == output_node.inputs[0]:
            return True
        if link.to_node.type == 'GROUP_OUTPUT':
            socket = get_internal_socket(link.to_socket)
            if is_viewer_socket(socket):
                return True
        return False
    
    def get_group_output_node(tree):
        for node in tree.nodes:
            if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
                return node
    
    def get_output_location(tree):
        # get right-most location
        sorted_by_xloc = (sorted(tree.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 tree.nodes:
            sum_yloc += node.location.y
    
        loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
        loc_y = sum_yloc / len(tree.nodes)
        return loc_x, loc_y
    
    
    # Principled prefs
    class NWPrincipledPreferences(bpy.types.PropertyGroup):
    
    Campbell Barton's avatar
    Campbell Barton committed
        base_color: StringProperty(
    
            name='Base Color',
            default='diffuse diff albedo base col color',
            description='Naming Components for Base Color maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        sss_color: StringProperty(
    
            name='Subsurface Color',
            default='sss subsurface',
            description='Naming Components for Subsurface Color maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        metallic: StringProperty(
    
            name='Metallic',
            default='metallic metalness metal mtl',
            description='Naming Components for metallness maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        specular: StringProperty(
    
            name='Specular',
            default='specularity specular spec spc',
            description='Naming Components for Specular maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        normal: StringProperty(
    
            name='Normal',
            default='normal nor nrm nrml norm',
            description='Naming Components for Normal maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        bump: StringProperty(
    
            name='Bump',
            default='bump bmp',
            description='Naming Components for bump maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        rough: StringProperty(
    
            name='Roughness',
            default='roughness rough rgh',
            description='Naming Components for roughness maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        gloss: StringProperty(
    
            name='Gloss',
    
            default='gloss glossy glossiness',
    
            description='Naming Components for glossy maps')
    
    Campbell Barton's avatar
    Campbell Barton committed
        displacement: StringProperty(
    
            name='Displacement',
    
            default='displacement displace disp dsp height heightmap',
    
            description='Naming Components for displacement maps')
    
    # Addon prefs
    class NWNodeWrangler(bpy.types.AddonPreferences):
        bl_idname = __name__
    
    
    Campbell Barton's avatar
    Campbell Barton committed
        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) specify whether to collapse them or show the full node with options expanded")
    
    Campbell Barton's avatar
    Campbell Barton committed
        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) specify the position of the new nodes")
    
    Campbell Barton's avatar
    Campbell Barton committed
        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"
        )
    
    Campbell Barton's avatar
    Campbell Barton committed
        hotkey_list_filter: StringProperty(
    
            name="        Filter by Name",
            default="",
            description="Show only hotkeys that have this text in their name"
        )
    
    Campbell Barton's avatar
    Campbell Barton committed
        show_principled_lists: BoolProperty(
    
            name="Show Principled naming tags",
            default=False,
            description="Expand this box into a list of all naming tags for principled texture setup"
        )
    
    Campbell Barton's avatar
    Campbell Barton committed
        principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
    
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
            col.prop(self, "merge_position")
            col.prop(self, "merge_hide")
    
    
            col = box.column(align=True)
    
            col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
            if self.show_principled_lists:
                tags = self.principled_tags
    
                col.prop(tags, "base_color")
                col.prop(tags, "sss_color")
                col.prop(tags, "metallic")
                col.prop(tags, "specular")
                col.prop(tags, "rough")
                col.prop(tags, "gloss")
                col.prop(tags, "normal")
                col.prop(tags, "bump")
                col.prop(tags, "displacement")
    
            box = layout.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(text=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]:
    
    def nw_check(context):
        space = context.space_data
    
        valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
    
        if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
    
    
        return valid
    
        @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' and event.value == 'RELEASE':
    
                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'}
    
    Campbell Barton's avatar
    Campbell Barton committed
        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' and event.value == 'RELEASE':
    
                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:
    
                    force_update(context)
    
                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'}
    
    
    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 Emission Shader for shadeless previews, or to the geometry node tree's 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 = ""
            self.shader_viewer_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_viewer_ident = "ShaderNodeEmission"
    
                    self.shader_output_type = "OUTPUT_LIGHT"
                    self.shader_output_ident = "ShaderNodeOutputLight"
                    self.shader_viewer_ident = "ShaderNodeEmission"
    
            elif shader_type == 'WORLD':
    
                self.shader_output_type = "OUTPUT_WORLD"
                self.shader_output_ident = "ShaderNodeOutputWorld"
                self.shader_viewer_ident = "ShaderNodeBackground"
    
        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:
                    emission = self.get_viewer_node(materialout)
                    self.search_sockets((emission if emission else 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:
                        emission = self.get_viewer_node(materialout)
                        self.search_sockets((emission if emission else materialout), self.used_viewer_sockets_other_mats)
            return socket in self.used_viewer_sockets_other_mats
    
        @staticmethod
        def get_viewer_node(materialout):
            input_socket = materialout.inputs[0]
            if len(input_socket.links) > 0:
                node = input_socket.links[0].from_node
                if node.type == 'EMISSION' and node.name == "Emission Viewer":
                    return node
    
        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)
    
            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
    
                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,
                # because there is no "viewer node" yet.
                if space.tree_type == "GeometryNodeTree":
                    valid = False
                    if active:
                        for out in active.outputs:
                            if is_visible_socket(out):
                                valid = True
                                break
                    # Exit early
                    if not valid:
                        return {'FINISHED'}
                    
                    delete_sockets = []
    
                    # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
                    self.scan_nodes(base_node_tree, delete_sockets)
    
                    # Find (or create if needed) the output of this node tree
                    geometryoutput = self.ensure_group_output(base_node_tree)
    
                    # Analyze outputs, make links
                    out_i = None
                    valid_outputs = []
                    for i, out in enumerate(active.outputs):
                        if is_visible_socket(out) and out.type == 'GEOMETRY':
                            valid_outputs.append(i)
                    if valid_outputs:
                        out_i = valid_outputs[0]  # Start index of node's outputs
                    for i, valid_i in enumerate(valid_outputs):
                        for out_link in active.outputs[valid_i].links:
                            if is_viewer_link(out_link, geometryoutput):
                                if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
                                    if i < len(valid_outputs) - 1:
                                        out_i = valid_outputs[i + 1]
                                    else:
                                        out_i = valid_outputs[0]
    
                    make_links = []  # store sockets for new links
                    delete_nodes = [] # store unused nodes to delete in the end
                    if active.outputs:
                        # If there is no 'GEOMETRY' output type - We can't preview the node
                        if out_i is None:
                            return {'FINISHED'}
                        socket_type = 'GEOMETRY'
                        # Find an input socket of the output of type geometry 
                        geometryoutindex = None
                        for i,inp in enumerate(geometryoutput.inputs):
                            if inp.type == socket_type:
                                geometryoutindex = i
                                break
                        if geometryoutindex is None:
                            # Create geometry socket
                            geometryoutput.inputs.new(socket_type, 'Geometry')
                            geometryoutindex = len(geometryoutput.inputs) - 1
    
                        make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
                        output_socket = geometryoutput.inputs[geometryoutindex]
                        for li_from, li_to in make_links:
                            base_node_tree.links.new(li_from, li_to)
                        tree = base_node_tree
                        link_end = output_socket
                        while tree.nodes.active != active:
                            node = tree.nodes.active
                            index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
                            link_start = node.outputs[index]
                            node_socket = node.node_tree.outputs[index]
                            if node_socket in delete_sockets:
                                delete_sockets.remove(node_socket)
                            tree.links.new(link_start, link_end)
                            # Iterate
                            link_end = self.ensure_group_output(node.node_tree).inputs[index]
                            tree = tree.nodes.active.node_tree
                        tree.links.new(active.outputs[out_i], link_end)
    
                    # Delete sockets
                    for socket in delete_sockets:
                        tree = socket.id_data
                        tree.outputs.remove(socket)
    
                    # Delete nodes
                    for tree, node in delete_nodes:
                        tree.nodes.remove(node)
    
                    nodes.active = active
                    active.select = True
                    force_update(context)
                    return {'FINISHED'}
    
    
                # What follows is code for the shader editor
    
                output_types = [x[1] for x in shaders_output_nodes_props]
    
                valid = False
    
                    if (active.name != "Emission Viewer") and (active.type 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)
    
                    # Analyze outputs, add "Emission Viewer" if needed, make links