Skip to content
Snippets Groups Projects
node_wrangler.py 187 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
        "name": "Node Wrangler",
    
        "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
    
        "blender": (2, 93, 0),
    
        "location": "Node Editor Toolbar or Shift-W",
    
        "description": "Various tools to enhance and speed up node-based workflow",
        "warning": "",
    
        "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
    
    from bpy.types import Operator, Panel, Menu
    
    Campbell Barton's avatar
    Campbell Barton committed
    from bpy.props import (
        FloatProperty,
        EnumProperty,
        BoolProperty,
        IntProperty,
        StringProperty,
        FloatVectorProperty,
        CollectionProperty,
    )
    
    from bpy_extras.io_utils import ImportHelper, ExportHelper
    
    from gpu_extras.batch import batch_for_shader
    
    Bartek Skorupa's avatar
    Bartek Skorupa committed
    from mathutils import Vector
    
    from nodeitems_utils import node_categories_iter, NodeItemCustom
    
    from math import cos, sin, pi, hypot
    
    from glob import glob
    
    from itertools import chain
    
    from collections import namedtuple
    
    
    #################
    # rl_outputs:
    # list of outputs of Input Render Layer
    
    # with attributes determining if pass is used,
    
    # and MultiLayer EXR outputs names and corresponding render engines
    #
    
    # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
    RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
    
        RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
        RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
        RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
        RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
        RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
    
        RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
        RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
    
        RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
        RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
        RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
    
        RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
        RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
        RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
    
        RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
    
        RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
        RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
        RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
        RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
    
        RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
        RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
        RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
        RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
        RL_entry('use_pass_uv', 'UV', 'UV', True, True),
    
        RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
    
        RL_entry('use_pass_z', 'Z', 'Depth', True, True),
        )
    
    # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
    
    # used list, not tuple for easy merging with other lists.
    
    blend_types = [
        ('MIX', 'Mix', 'Mix Mode'),
        ('ADD', 'Add', 'Add Mode'),
        ('MULTIPLY', 'Multiply', 'Multiply Mode'),
        ('SUBTRACT', 'Subtract', 'Subtract Mode'),
        ('SCREEN', 'Screen', 'Screen Mode'),
        ('DIVIDE', 'Divide', 'Divide Mode'),
        ('DIFFERENCE', 'Difference', 'Difference Mode'),
        ('DARKEN', 'Darken', 'Darken Mode'),
        ('LIGHTEN', 'Lighten', 'Lighten Mode'),
        ('OVERLAY', 'Overlay', 'Overlay Mode'),
        ('DODGE', 'Dodge', 'Dodge Mode'),
        ('BURN', 'Burn', 'Burn Mode'),
        ('HUE', 'Hue', 'Hue Mode'),
        ('SATURATION', 'Saturation', 'Saturation Mode'),
        ('VALUE', 'Value', 'Value Mode'),
        ('COLOR', 'Color', 'Color Mode'),
        ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
        ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
    
    # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
    
    # used list, not tuple for easy merging with other lists.
    
    operations = [
        ('ADD', 'Add', 'Add Mode'),
        ('SUBTRACT', 'Subtract', 'Subtract Mode'),
    
        ('MULTIPLY', 'Multiply', 'Multiply Mode'),
    
        ('DIVIDE', 'Divide', 'Divide Mode'),
    
        ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
    
        ('SINE', 'Sine', 'Sine Mode'),
        ('COSINE', 'Cosine', 'Cosine Mode'),
        ('TANGENT', 'Tangent', 'Tangent Mode'),
        ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
        ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
        ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
    
        ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
        ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
        ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
        ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
    
        ('POWER', 'Power', 'Power Mode'),
    
        ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
    
        ('SQRT', 'Square Root', 'Square Root Mode'),
        ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
        ('EXPONENT', 'Exponent', 'Exponent Mode'),
    
        ('MINIMUM', 'Minimum', 'Minimum Mode'),
        ('MAXIMUM', 'Maximum', 'Maximum Mode'),
    
        ('LESS_THAN', 'Less Than', 'Less Than Mode'),
    
        ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
    
        ('SIGN', 'Sign', 'Sign Mode'),
        ('COMPARE', 'Compare', 'Compare Mode'),
        ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
        ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
        ('FRACT', 'Fraction', 'Fraction Mode'),
    
        ('MODULO', 'Modulo', 'Modulo Mode'),
    
        ('SNAP', 'Snap', 'Snap Mode'),
        ('WRAP', 'Wrap', 'Wrap Mode'),
        ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
    
        ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
    
        ('ROUND', 'Round', 'Round Mode'),
        ('FLOOR', 'Floor', 'Floor Mode'),
        ('CEIL', 'Ceil', 'Ceil Mode'),
        ('TRUNCATE', 'Truncate', 'Truncate Mode'),
        ('RADIANS', 'To Radians', 'To Radians Mode'),
        ('DEGREES', 'To Degrees', 'To Degrees Mode'),
    
    # Operations used by the geometry boolean node and join geometry node
    geo_combine_operations = [
        ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
        ('INTERSECT', 'Intersect', 'Intersect Mode'),
        ('UNION', 'Union', 'Union Mode'),
        ('DIFFERENCE', 'Difference', 'Difference Mode'),
    ]
    
    
    # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
    # used list, not tuple for easy merging with other lists.
    
    navs = [
        ('CURRENT', 'Current', 'Leave at current state'),
        ('NEXT', 'Next', 'Next blend type/operation'),
        ('PREV', 'Prev', 'Previous blend type/operation'),
    
    ]
    
    draw_color_sets = {
        "red_white": (
            (1.0, 1.0, 1.0, 0.7),
            (1.0, 0.0, 0.0, 0.7),
            (0.8, 0.2, 0.2, 1.0)
        ),
        "green": (
            (0.0, 0.0, 0.0, 1.0),
            (0.38, 0.77, 0.38, 1.0),
            (0.38, 0.77, 0.38, 1.0)
        ),
        "yellow": (
            (0.0, 0.0, 0.0, 1.0),
            (0.77, 0.77, 0.16, 1.0),
            (0.77, 0.77, 0.16, 1.0)
        ),
        "purple": (
            (0.0, 0.0, 0.0, 1.0),
            (0.38, 0.38, 0.77, 1.0),
            (0.38, 0.38, 0.77, 1.0)
        ),
        "grey": (
            (0.0, 0.0, 0.0, 1.0),
            (0.63, 0.63, 0.63, 1.0),
            (0.63, 0.63, 0.63, 1.0)
        ),
        "black": (
            (1.0, 1.0, 1.0, 0.7),
            (0.0, 0.0, 0.0, 0.7),
            (0.2, 0.2, 0.2, 1.0)
    
    viewer_socket_name = "tmp_viewer"
    
    def get_nodes_from_category(category_name, context):
        for category in node_categories_iter(context):
            if category.name == category_name:
                return sorted(category.items(context), key=lambda node: node.label)
    
    
    def get_first_enabled_output(node):
        for output in node.outputs:
            if output.enabled:
                return output
        else:
            return node.outputs[0]
    
    
    def is_visible_socket(socket):
    
        return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
    
    def nice_hotkey_name(punc):
        # convert the ugly string name into the actual character
    
        nice_name = {
            'LEFTMOUSE': "LMB",
            'MIDDLEMOUSE': "MMB",
            'RIGHTMOUSE': "RMB",
            'WHEELUPMOUSE': "Wheel Up",
            'WHEELDOWNMOUSE': "Wheel Down",
            'WHEELINMOUSE': "Wheel In",
            'WHEELOUTMOUSE': "Wheel Out",
            'ZERO': "0",
            'ONE': "1",
            'TWO': "2",
            'THREE': "3",
            'FOUR': "4",
            'FIVE': "5",
            'SIX': "6",
            'SEVEN': "7",
            'EIGHT': "8",
            'NINE': "9",
            'OSKEY': "Super",
            'RET': "Enter",
            'LINE_FEED': "Enter",
            'SEMI_COLON': ";",
            'PERIOD': ".",
            'COMMA': ",",
            'QUOTE': '"',
            'MINUS': "-",
            'SLASH': "/",
            'BACK_SLASH': "\\",
            'EQUAL': "=",
            'NUMPAD_1': "Numpad 1",
            'NUMPAD_2': "Numpad 2",
            'NUMPAD_3': "Numpad 3",
            'NUMPAD_4': "Numpad 4",
            'NUMPAD_5': "Numpad 5",
            'NUMPAD_6': "Numpad 6",
            'NUMPAD_7': "Numpad 7",
            'NUMPAD_8': "Numpad 8",
            'NUMPAD_9': "Numpad 9",
            'NUMPAD_0': "Numpad 0",
            'NUMPAD_PERIOD': "Numpad .",
            'NUMPAD_SLASH': "Numpad /",
            'NUMPAD_ASTERIX': "Numpad *",
            'NUMPAD_MINUS': "Numpad -",
            'NUMPAD_ENTER': "Numpad Enter",
            'NUMPAD_PLUS': "Numpad +",
        }
        try:
            return nice_name[punc]
        except KeyError:
            return punc.replace("_", " ").title()
    
    def force_update(context):
        context.space_data.node_tree.update_tag()
    
        prefs = bpy.context.preferences.system
    
        return prefs.dpi * prefs.pixel_size / 72
    
    def node_mid_pt(node, axis):
        if axis == 'x':
            d = node.location.x + (node.dimensions.x / 2)
        elif axis == 'y':
            d = node.location.y - (node.dimensions.y / 2)
        else:
            d = 0
        return d
    
    
    def autolink(node1, node2, links):
        link_made = False
    
        available_inputs = [inp for inp in node2.inputs if inp.enabled]
        available_outputs = [outp for outp in node1.outputs if outp.enabled]
        for outp in available_outputs:
            for inp in available_inputs:
    
                if not inp.is_linked and inp.name == outp.name:
                    link_made = True
                    links.new(outp, inp)
                    return True
    
    
        for outp in available_outputs:
            for inp in available_inputs:
    
                if not inp.is_linked and inp.type == outp.type:
                    link_made = True
                    links.new(outp, inp)
                    return True
    
        # force some connection even if the type doesn't match
    
        if available_outputs:
            for inp in available_inputs:
    
                if not inp.is_linked:
                    link_made = True
    
                    links.new(available_outputs[0], inp)
    
                    return True
    
        # even if no sockets are open, force one of matching type
    
        for outp in available_outputs:
            for inp in available_inputs:
    
                if inp.type == outp.type:
                    link_made = True
                    links.new(outp, inp)
                    return True
    
        # do something!
    
        for outp in available_outputs:
            for inp in available_inputs:
    
                link_made = True
                links.new(outp, inp)
                return True
    
        print("Could not make a link from " + node1.name + " to " + node2.name)
        return link_made
    
    
    def abs_node_location(node):
        abs_location = node.location
        if node.parent is None:
            return abs_location
        return abs_location + abs_node_location(node.parent)
    
    
    def node_at_pos(nodes, context, event):
        nodes_under_mouse = []
        target_node = None
    
        store_mouse_cursor(context, event)
        x, y = context.space_data.cursor_location
    
    
        # Make a list of each corner (and middle of border) for each node.
        # Will be sorted to find nearest point and thus nearest node
        node_points_with_dist = []
        for node in nodes:
    
            skipnode = False
            if node.type != 'FRAME':  # no point trying to link to a frame node
                dimx = node.dimensions.x/dpifac()
                dimy = node.dimensions.y/dpifac()
    
                locx, locy = abs_node_location(node)
    
    
                if not skipnode:
    
                    node_points_with_dist.append([node, hypot(x - locx, y - locy)])  # Top Left
                    node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)])  # Top Right
                    node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))])  # Bottom Left
                    node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))])  # Bottom Right
    
                    node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)])  # Mid Top
                    node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))])  # Mid Bottom
                    node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))])  # Mid Left
                    node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))])  # Mid Right
    
        nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
    
            if node.type != 'FRAME' and skipnode == False:
    
                locx, locy = abs_node_location(node)
    
                dimx = node.dimensions.x/dpifac()
                dimy = node.dimensions.y/dpifac()
                if (locx <= x <= locx + dimx) and \
                   (locy - dimy <= y <= locy):
                    nodes_under_mouse.append(node)
    
    
        if len(nodes_under_mouse) == 1:
    
            if nodes_under_mouse[0] != nearest_node:
    
                target_node = nodes_under_mouse[0]  # use the node under the mouse if there is one and only one
            else:
    
                target_node = nearest_node  # else use the nearest node
    
            target_node = nearest_node
    
        return target_node
    
    
    def store_mouse_cursor(context, event):
        space = context.space_data
        v2d = context.region.view2d
        tree = space.edit_tree
    
        # convert mouse position to the View2D for later node placement
        if context.region.type == 'WINDOW':
            space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
        else:
            space.cursor_location = tree.view_center
    
    
    def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
        shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
    
        vertices = ((x1, y1), (x2, y2))
        vertex_colors = ((colour[0]+(1.0-colour[0])/4,
                          colour[1]+(1.0-colour[1])/4,
                          colour[2]+(1.0-colour[2])/4,
                          colour[3]+(1.0-colour[3])/4),
                          colour)
    
        batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
    
        bgl.glLineWidth(size * dpifac())
    
    
        shader.bind()
        batch.draw(shader)
    
    
    def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
    
        radius = radius * dpifac()
        sides = 12
    
        vertices = [(radius * cos(i * 2 * pi / sides) + mx,
                     radius * sin(i * 2 * pi / sides) + my)
                     for i in range(sides + 1)]
    
        batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
        shader.bind()
        shader.uniform_float("color", colour)
        batch.draw(shader)
    
    def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
    
        radius = radius*dpifac()
    
        nlocx, nlocy = abs_node_location(node)
    
        nlocx = (nlocx+1)*dpifac()
        nlocy = (nlocy+1)*dpifac()
    
        ndimx = node.dimensions.x
        ndimy = node.dimensions.y
    
    Greg Zaal's avatar
    Greg Zaal committed
        if node.hide:
            nlocx += -1
            nlocy += 5
        if node.type == 'REROUTE':
            #nlocx += 1
            nlocy -= 1
            ndimx = 0
            ndimy = 0
            radius += 6
    
        # Top left corner
    
        mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
    
        vertices = [(mx,my)]
    
        for i in range(sides+1):
            if (4<=i<=8):
    
                    cosine = radius * cos(i * 2 * pi / sides) + mx
                    sine = radius * sin(i * 2 * pi / sides) + my
    
                    vertices.append((cosine,sine))
        batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
        shader.bind()
        shader.uniform_float("color", colour)
        batch.draw(shader)
    
    Greg Zaal's avatar
    Greg Zaal committed
        # Top right corner
    
        mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
    
        vertices = [(mx,my)]
    
        for i in range(sides+1):
            if (0<=i<=4):
    
                    cosine = radius * cos(i * 2 * pi / sides) + mx
                    sine = radius * sin(i * 2 * pi / sides) + my
    
                    vertices.append((cosine,sine))
        batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
        shader.bind()
        shader.uniform_float("color", colour)
        batch.draw(shader)
    
    Greg Zaal's avatar
    Greg Zaal committed
    
        # Bottom left corner
    
        mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
    
        vertices = [(mx,my)]
    
        for i in range(sides+1):
            if (8<=i<=12):
    
                    cosine = radius * cos(i * 2 * pi / sides) + mx
                    sine = radius * sin(i * 2 * pi / sides) + my
    
                    vertices.append((cosine,sine))
        batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
        shader.bind()
        shader.uniform_float("color", colour)
        batch.draw(shader)
    
    Greg Zaal's avatar
    Greg Zaal committed
        # Bottom right corner
    
        mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
    
        vertices = [(mx,my)]
    
        for i in range(sides+1):
            if (12<=i<=16):
    
                    cosine = radius * cos(i * 2 * pi / sides) + mx
                    sine = radius * sin(i * 2 * pi / sides) + my
    
                    vertices.append((cosine,sine))
        batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
        shader.bind()
        shader.uniform_float("color", colour)
        batch.draw(shader)
    
        # prepare drawing all edges in one batch
        vertices = []
        indices = []
        id_last = 0
    
    Greg Zaal's avatar
    Greg Zaal committed
        # Left edge
    
        m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
        m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
        if m1x < area_width and m2x < area_width:
    
            vertices.extend([(m2x-radius,m2y), (m2x,m2y),
                             (m1x,m1y), (m1x-radius,m1y)])
            indices.extend([(id_last, id_last+1, id_last+3),
                            (id_last+3, id_last+1, id_last+2)])
            id_last += 4
    
    Greg Zaal's avatar
    Greg Zaal committed
        # Top edge
    
        m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
        m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
        m1x = min(m1x, area_width)
        m2x = min(m2x, area_width)
    
        vertices.extend([(m1x,m1y), (m2x,m1y),
                         (m2x,m1y+radius), (m1x,m1y+radius)])
        indices.extend([(id_last, id_last+1, id_last+3),
                        (id_last+3, id_last+1, id_last+2)])
        id_last += 4
    
    Greg Zaal's avatar
    Greg Zaal committed
        # Right edge
    
        m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
        m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
        if m1x < area_width and m2x < area_width:
    
            vertices.extend([(m1x,m2y), (m1x+radius,m2y),
                             (m1x+radius,m1y), (m1x,m1y)])
            indices.extend([(id_last, id_last+1, id_last+3),
                            (id_last+3, id_last+1, id_last+2)])
            id_last += 4
    
    Greg Zaal's avatar
    Greg Zaal committed
        # Bottom edge
    
        m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
        m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
        m1x = min(m1x, area_width)
        m2x = min(m2x, area_width)
    
        vertices.extend([(m1x,m2y), (m2x,m2y),
                         (m2x,m1y-radius), (m1x,m1y-radius)])
        indices.extend([(id_last, id_last+1, id_last+3),
                        (id_last+3, id_last+1, id_last+2)])
    
    
        # now draw all edges in one batch
        if len(vertices) != 0:
            batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
            shader.bind()
            shader.uniform_float("color", colour)
            batch.draw(shader)
    
    def draw_callback_nodeoutline(self, context, mode):
    
    
            bgl.glLineWidth(1)
            bgl.glEnable(bgl.GL_BLEND)
    
            bgl.glEnable(bgl.GL_LINE_SMOOTH)
    
            bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
    
            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 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]
    
        # 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')
    
        transmission: StringProperty(
            name='Transmission',
            default='transmission transparency',
            description='Naming Components for transmission maps')
        emission: StringProperty(
            name='Emission',
            default='emission emissive emit',
            description='Naming Components for emission maps')
        alpha: StringProperty(
            name='Alpha',
            default='alpha opacity',
            description='Naming Components for alpha maps')
        ambient_occlusion: StringProperty(
            name='Ambient Occlusion',
            default='ao ambient occlusion',
            description='Naming Components for AO 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",
            options={'TEXTEDIT_UPDATE'}
    
    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")
    
                col.prop(tags, "transmission")
                col.prop(tags, "emission")
                col.prop(tags, "alpha")
                col.prop(tags, "ambient_occlusion")
    
            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"]
    
        valid = False
        if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
            valid = True
    
        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'}