"Fossies" - the Fresh Open Source Software Archive

Member "blender-3.3.1/release/scripts/addons/node_wrangler.py" (25 Aug 2022, 191292 Bytes) of package /linux/misc/blender-3.3.1.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "node_wrangler.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 3.2.2_vs_3.3.0.

    1 # SPDX-License-Identifier: GPL-2.0-or-later
    2 
    3 bl_info = {
    4     "name": "Node Wrangler",
    5     "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
    6     "version": (3, 41),
    7     "blender": (2, 93, 0),
    8     "location": "Node Editor Toolbar or Shift-W",
    9     "description": "Various tools to enhance and speed up node-based workflow",
   10     "warning": "",
   11     "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
   12     "category": "Node",
   13 }
   14 
   15 import bpy, blf, bgl
   16 import gpu
   17 from bpy.types import Operator, Panel, Menu
   18 from bpy.props import (
   19     FloatProperty,
   20     EnumProperty,
   21     BoolProperty,
   22     IntProperty,
   23     StringProperty,
   24     FloatVectorProperty,
   25     CollectionProperty,
   26 )
   27 from bpy_extras.io_utils import ImportHelper, ExportHelper
   28 from gpu_extras.batch import batch_for_shader
   29 from mathutils import Vector
   30 from nodeitems_utils import node_categories_iter, NodeItemCustom
   31 from math import cos, sin, pi, hypot
   32 from os import path
   33 from glob import glob
   34 from copy import copy
   35 from itertools import chain
   36 import re
   37 from collections import namedtuple
   38 
   39 #################
   40 # rl_outputs:
   41 # list of outputs of Input Render Layer
   42 # with attributes determining if pass is used,
   43 # and MultiLayer EXR outputs names and corresponding render engines
   44 #
   45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
   46 RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
   47 rl_outputs = (
   48     RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
   49     RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
   50     RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
   51     RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
   52     RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
   53     RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
   54     RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
   55     RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
   56     RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
   57     RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
   58     RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
   59     RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
   60     RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
   61     RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
   62     RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
   63     RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
   64     RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
   65     RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
   66     RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
   67     RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
   68     RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
   69     RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
   70     RL_entry('use_pass_uv', 'UV', 'UV', True, True),
   71     RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
   72     RL_entry('use_pass_z', 'Z', 'Depth', True, True),
   73     )
   74 
   75 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
   76 # used list, not tuple for easy merging with other lists.
   77 blend_types = [
   78     ('MIX', 'Mix', 'Mix Mode'),
   79     ('ADD', 'Add', 'Add Mode'),
   80     ('MULTIPLY', 'Multiply', 'Multiply Mode'),
   81     ('SUBTRACT', 'Subtract', 'Subtract Mode'),
   82     ('SCREEN', 'Screen', 'Screen Mode'),
   83     ('DIVIDE', 'Divide', 'Divide Mode'),
   84     ('DIFFERENCE', 'Difference', 'Difference Mode'),
   85     ('DARKEN', 'Darken', 'Darken Mode'),
   86     ('LIGHTEN', 'Lighten', 'Lighten Mode'),
   87     ('OVERLAY', 'Overlay', 'Overlay Mode'),
   88     ('DODGE', 'Dodge', 'Dodge Mode'),
   89     ('BURN', 'Burn', 'Burn Mode'),
   90     ('HUE', 'Hue', 'Hue Mode'),
   91     ('SATURATION', 'Saturation', 'Saturation Mode'),
   92     ('VALUE', 'Value', 'Value Mode'),
   93     ('COLOR', 'Color', 'Color Mode'),
   94     ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
   95     ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
   96 ]
   97 
   98 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
   99 # used list, not tuple for easy merging with other lists.
  100 operations = [
  101     ('ADD', 'Add', 'Add Mode'),
  102     ('SUBTRACT', 'Subtract', 'Subtract Mode'),
  103     ('MULTIPLY', 'Multiply', 'Multiply Mode'),
  104     ('DIVIDE', 'Divide', 'Divide Mode'),
  105     ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
  106     ('SINE', 'Sine', 'Sine Mode'),
  107     ('COSINE', 'Cosine', 'Cosine Mode'),
  108     ('TANGENT', 'Tangent', 'Tangent Mode'),
  109     ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
  110     ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
  111     ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
  112     ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
  113     ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
  114     ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
  115     ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
  116     ('POWER', 'Power', 'Power Mode'),
  117     ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
  118     ('SQRT', 'Square Root', 'Square Root Mode'),
  119     ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
  120     ('EXPONENT', 'Exponent', 'Exponent Mode'),
  121     ('MINIMUM', 'Minimum', 'Minimum Mode'),
  122     ('MAXIMUM', 'Maximum', 'Maximum Mode'),
  123     ('LESS_THAN', 'Less Than', 'Less Than Mode'),
  124     ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
  125     ('SIGN', 'Sign', 'Sign Mode'),
  126     ('COMPARE', 'Compare', 'Compare Mode'),
  127     ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
  128     ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
  129     ('FRACT', 'Fraction', 'Fraction Mode'),
  130     ('MODULO', 'Modulo', 'Modulo Mode'),
  131     ('SNAP', 'Snap', 'Snap Mode'),
  132     ('WRAP', 'Wrap', 'Wrap Mode'),
  133     ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
  134     ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
  135     ('ROUND', 'Round', 'Round Mode'),
  136     ('FLOOR', 'Floor', 'Floor Mode'),
  137     ('CEIL', 'Ceil', 'Ceil Mode'),
  138     ('TRUNCATE', 'Truncate', 'Truncate Mode'),
  139     ('RADIANS', 'To Radians', 'To Radians Mode'),
  140     ('DEGREES', 'To Degrees', 'To Degrees Mode'),
  141 ]
  142 
  143 # Operations used by the geometry boolean node and join geometry node
  144 geo_combine_operations = [
  145     ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
  146     ('INTERSECT', 'Intersect', 'Intersect Mode'),
  147     ('UNION', 'Union', 'Union Mode'),
  148     ('DIFFERENCE', 'Difference', 'Difference Mode'),
  149 ]
  150 
  151 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
  152 # used list, not tuple for easy merging with other lists.
  153 navs = [
  154     ('CURRENT', 'Current', 'Leave at current state'),
  155     ('NEXT', 'Next', 'Next blend type/operation'),
  156     ('PREV', 'Prev', 'Previous blend type/operation'),
  157 ]
  158 
  159 draw_color_sets = {
  160     "red_white": (
  161         (1.0, 1.0, 1.0, 0.7),
  162         (1.0, 0.0, 0.0, 0.7),
  163         (0.8, 0.2, 0.2, 1.0)
  164     ),
  165     "green": (
  166         (0.0, 0.0, 0.0, 1.0),
  167         (0.38, 0.77, 0.38, 1.0),
  168         (0.38, 0.77, 0.38, 1.0)
  169     ),
  170     "yellow": (
  171         (0.0, 0.0, 0.0, 1.0),
  172         (0.77, 0.77, 0.16, 1.0),
  173         (0.77, 0.77, 0.16, 1.0)
  174     ),
  175     "purple": (
  176         (0.0, 0.0, 0.0, 1.0),
  177         (0.38, 0.38, 0.77, 1.0),
  178         (0.38, 0.38, 0.77, 1.0)
  179     ),
  180     "grey": (
  181         (0.0, 0.0, 0.0, 1.0),
  182         (0.63, 0.63, 0.63, 1.0),
  183         (0.63, 0.63, 0.63, 1.0)
  184     ),
  185     "black": (
  186         (1.0, 1.0, 1.0, 0.7),
  187         (0.0, 0.0, 0.0, 0.7),
  188         (0.2, 0.2, 0.2, 1.0)
  189     )
  190 }
  191 
  192 viewer_socket_name = "tmp_viewer"
  193 
  194 def get_nodes_from_category(category_name, context):
  195     for category in node_categories_iter(context):
  196         if category.name == category_name:
  197             return sorted(category.items(context), key=lambda node: node.label)
  198 
  199 def get_first_enabled_output(node):
  200     for output in node.outputs:
  201         if output.enabled:
  202             return output
  203     else:
  204         return node.outputs[0]
  205 
  206 def is_visible_socket(socket):
  207     return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
  208 
  209 def nice_hotkey_name(punc):
  210     # convert the ugly string name into the actual character
  211     nice_name = {
  212         'LEFTMOUSE': "LMB",
  213         'MIDDLEMOUSE': "MMB",
  214         'RIGHTMOUSE': "RMB",
  215         'WHEELUPMOUSE': "Wheel Up",
  216         'WHEELDOWNMOUSE': "Wheel Down",
  217         'WHEELINMOUSE': "Wheel In",
  218         'WHEELOUTMOUSE': "Wheel Out",
  219         'ZERO': "0",
  220         'ONE': "1",
  221         'TWO': "2",
  222         'THREE': "3",
  223         'FOUR': "4",
  224         'FIVE': "5",
  225         'SIX': "6",
  226         'SEVEN': "7",
  227         'EIGHT': "8",
  228         'NINE': "9",
  229         'OSKEY': "Super",
  230         'RET': "Enter",
  231         'LINE_FEED': "Enter",
  232         'SEMI_COLON': ";",
  233         'PERIOD': ".",
  234         'COMMA': ",",
  235         'QUOTE': '"',
  236         'MINUS': "-",
  237         'SLASH': "/",
  238         'BACK_SLASH': "\\",
  239         'EQUAL': "=",
  240         'NUMPAD_1': "Numpad 1",
  241         'NUMPAD_2': "Numpad 2",
  242         'NUMPAD_3': "Numpad 3",
  243         'NUMPAD_4': "Numpad 4",
  244         'NUMPAD_5': "Numpad 5",
  245         'NUMPAD_6': "Numpad 6",
  246         'NUMPAD_7': "Numpad 7",
  247         'NUMPAD_8': "Numpad 8",
  248         'NUMPAD_9': "Numpad 9",
  249         'NUMPAD_0': "Numpad 0",
  250         'NUMPAD_PERIOD': "Numpad .",
  251         'NUMPAD_SLASH': "Numpad /",
  252         'NUMPAD_ASTERIX': "Numpad *",
  253         'NUMPAD_MINUS': "Numpad -",
  254         'NUMPAD_ENTER': "Numpad Enter",
  255         'NUMPAD_PLUS': "Numpad +",
  256     }
  257     try:
  258         return nice_name[punc]
  259     except KeyError:
  260         return punc.replace("_", " ").title()
  261 
  262 
  263 def force_update(context):
  264     context.space_data.node_tree.update_tag()
  265 
  266 
  267 def dpifac():
  268     prefs = bpy.context.preferences.system
  269     return prefs.dpi * prefs.pixel_size / 72
  270 
  271 
  272 def node_mid_pt(node, axis):
  273     if axis == 'x':
  274         d = node.location.x + (node.dimensions.x / 2)
  275     elif axis == 'y':
  276         d = node.location.y - (node.dimensions.y / 2)
  277     else:
  278         d = 0
  279     return d
  280 
  281 
  282 def autolink(node1, node2, links):
  283     link_made = False
  284     available_inputs = [inp for inp in node2.inputs if inp.enabled]
  285     available_outputs = [outp for outp in node1.outputs if outp.enabled]
  286     for outp in available_outputs:
  287         for inp in available_inputs:
  288             if not inp.is_linked and inp.name == outp.name:
  289                 link_made = True
  290                 links.new(outp, inp)
  291                 return True
  292 
  293     for outp in available_outputs:
  294         for inp in available_inputs:
  295             if not inp.is_linked and inp.type == outp.type:
  296                 link_made = True
  297                 links.new(outp, inp)
  298                 return True
  299 
  300     # force some connection even if the type doesn't match
  301     if available_outputs:
  302         for inp in available_inputs:
  303             if not inp.is_linked:
  304                 link_made = True
  305                 links.new(available_outputs[0], inp)
  306                 return True
  307 
  308     # even if no sockets are open, force one of matching type
  309     for outp in available_outputs:
  310         for inp in available_inputs:
  311             if inp.type == outp.type:
  312                 link_made = True
  313                 links.new(outp, inp)
  314                 return True
  315 
  316     # do something!
  317     for outp in available_outputs:
  318         for inp in available_inputs:
  319             link_made = True
  320             links.new(outp, inp)
  321             return True
  322 
  323     print("Could not make a link from " + node1.name + " to " + node2.name)
  324     return link_made
  325 
  326 def abs_node_location(node):
  327     abs_location = node.location
  328     if node.parent is None:
  329         return abs_location
  330     return abs_location + abs_node_location(node.parent)
  331 
  332 def node_at_pos(nodes, context, event):
  333     nodes_under_mouse = []
  334     target_node = None
  335 
  336     store_mouse_cursor(context, event)
  337     x, y = context.space_data.cursor_location
  338 
  339     # Make a list of each corner (and middle of border) for each node.
  340     # Will be sorted to find nearest point and thus nearest node
  341     node_points_with_dist = []
  342     for node in nodes:
  343         skipnode = False
  344         if node.type != 'FRAME':  # no point trying to link to a frame node
  345             dimx = node.dimensions.x/dpifac()
  346             dimy = node.dimensions.y/dpifac()
  347             locx, locy = abs_node_location(node)
  348 
  349             if not skipnode:
  350                 node_points_with_dist.append([node, hypot(x - locx, y - locy)])  # Top Left
  351                 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)])  # Top Right
  352                 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))])  # Bottom Left
  353                 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))])  # Bottom Right
  354 
  355                 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)])  # Mid Top
  356                 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))])  # Mid Bottom
  357                 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))])  # Mid Left
  358                 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))])  # Mid Right
  359 
  360     nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
  361 
  362     for node in nodes:
  363         if node.type != 'FRAME' and skipnode == False:
  364             locx, locy = abs_node_location(node)
  365             dimx = node.dimensions.x/dpifac()
  366             dimy = node.dimensions.y/dpifac()
  367             if (locx <= x <= locx + dimx) and \
  368                (locy - dimy <= y <= locy):
  369                 nodes_under_mouse.append(node)
  370 
  371     if len(nodes_under_mouse) == 1:
  372         if nodes_under_mouse[0] != nearest_node:
  373             target_node = nodes_under_mouse[0]  # use the node under the mouse if there is one and only one
  374         else:
  375             target_node = nearest_node  # else use the nearest node
  376     else:
  377         target_node = nearest_node
  378     return target_node
  379 
  380 
  381 def store_mouse_cursor(context, event):
  382     space = context.space_data
  383     v2d = context.region.view2d
  384     tree = space.edit_tree
  385 
  386     # convert mouse position to the View2D for later node placement
  387     if context.region.type == 'WINDOW':
  388         space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
  389     else:
  390         space.cursor_location = tree.view_center
  391 
  392 def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
  393     shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
  394 
  395     vertices = ((x1, y1), (x2, y2))
  396     vertex_colors = ((colour[0]+(1.0-colour[0])/4,
  397                       colour[1]+(1.0-colour[1])/4,
  398                       colour[2]+(1.0-colour[2])/4,
  399                       colour[3]+(1.0-colour[3])/4),
  400                       colour)
  401 
  402     batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
  403     bgl.glLineWidth(size * dpifac())
  404 
  405     shader.bind()
  406     batch.draw(shader)
  407 
  408 
  409 def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
  410     radius = radius * dpifac()
  411     sides = 12
  412     vertices = [(radius * cos(i * 2 * pi / sides) + mx,
  413                  radius * sin(i * 2 * pi / sides) + my)
  414                  for i in range(sides + 1)]
  415 
  416     batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
  417     shader.bind()
  418     shader.uniform_float("color", colour)
  419     batch.draw(shader)
  420 
  421 
  422 def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
  423     area_width = bpy.context.area.width
  424     sides = 16
  425     radius = radius*dpifac()
  426 
  427     nlocx, nlocy = abs_node_location(node)
  428 
  429     nlocx = (nlocx+1)*dpifac()
  430     nlocy = (nlocy+1)*dpifac()
  431     ndimx = node.dimensions.x
  432     ndimy = node.dimensions.y
  433 
  434     if node.hide:
  435         nlocx += -1
  436         nlocy += 5
  437     if node.type == 'REROUTE':
  438         #nlocx += 1
  439         nlocy -= 1
  440         ndimx = 0
  441         ndimy = 0
  442         radius += 6
  443 
  444     # Top left corner
  445     mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
  446     vertices = [(mx,my)]
  447     for i in range(sides+1):
  448         if (4<=i<=8):
  449             if mx < area_width:
  450                 cosine = radius * cos(i * 2 * pi / sides) + mx
  451                 sine = radius * sin(i * 2 * pi / sides) + my
  452                 vertices.append((cosine,sine))
  453     batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
  454     shader.bind()
  455     shader.uniform_float("color", colour)
  456     batch.draw(shader)
  457 
  458     # Top right corner
  459     mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
  460     vertices = [(mx,my)]
  461     for i in range(sides+1):
  462         if (0<=i<=4):
  463             if mx < area_width:
  464                 cosine = radius * cos(i * 2 * pi / sides) + mx
  465                 sine = radius * sin(i * 2 * pi / sides) + my
  466                 vertices.append((cosine,sine))
  467     batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
  468     shader.bind()
  469     shader.uniform_float("color", colour)
  470     batch.draw(shader)
  471 
  472     # Bottom left corner
  473     mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
  474     vertices = [(mx,my)]
  475     for i in range(sides+1):
  476         if (8<=i<=12):
  477             if mx < area_width:
  478                 cosine = radius * cos(i * 2 * pi / sides) + mx
  479                 sine = radius * sin(i * 2 * pi / sides) + my
  480                 vertices.append((cosine,sine))
  481     batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
  482     shader.bind()
  483     shader.uniform_float("color", colour)
  484     batch.draw(shader)
  485 
  486     # Bottom right corner
  487     mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
  488     vertices = [(mx,my)]
  489     for i in range(sides+1):
  490         if (12<=i<=16):
  491             if mx < area_width:
  492                 cosine = radius * cos(i * 2 * pi / sides) + mx
  493                 sine = radius * sin(i * 2 * pi / sides) + my
  494                 vertices.append((cosine,sine))
  495     batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
  496     shader.bind()
  497     shader.uniform_float("color", colour)
  498     batch.draw(shader)
  499 
  500     # prepare drawing all edges in one batch
  501     vertices = []
  502     indices = []
  503     id_last = 0
  504 
  505     # Left edge
  506     m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
  507     m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
  508     if m1x < area_width and m2x < area_width:
  509         vertices.extend([(m2x-radius,m2y), (m2x,m2y),
  510                          (m1x,m1y), (m1x-radius,m1y)])
  511         indices.extend([(id_last, id_last+1, id_last+3),
  512                         (id_last+3, id_last+1, id_last+2)])
  513         id_last += 4
  514 
  515     # Top edge
  516     m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
  517     m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
  518     m1x = min(m1x, area_width)
  519     m2x = min(m2x, area_width)
  520     vertices.extend([(m1x,m1y), (m2x,m1y),
  521                      (m2x,m1y+radius), (m1x,m1y+radius)])
  522     indices.extend([(id_last, id_last+1, id_last+3),
  523                     (id_last+3, id_last+1, id_last+2)])
  524     id_last += 4
  525 
  526     # Right edge
  527     m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
  528     m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
  529     if m1x < area_width and m2x < area_width:
  530         vertices.extend([(m1x,m2y), (m1x+radius,m2y),
  531                          (m1x+radius,m1y), (m1x,m1y)])
  532         indices.extend([(id_last, id_last+1, id_last+3),
  533                         (id_last+3, id_last+1, id_last+2)])
  534         id_last += 4
  535 
  536     # Bottom edge
  537     m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
  538     m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
  539     m1x = min(m1x, area_width)
  540     m2x = min(m2x, area_width)
  541     vertices.extend([(m1x,m2y), (m2x,m2y),
  542                      (m2x,m1y-radius), (m1x,m1y-radius)])
  543     indices.extend([(id_last, id_last+1, id_last+3),
  544                     (id_last+3, id_last+1, id_last+2)])
  545 
  546     # now draw all edges in one batch
  547     if len(vertices) != 0:
  548         batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
  549         shader.bind()
  550         shader.uniform_float("color", colour)
  551         batch.draw(shader)
  552 
  553 def draw_callback_nodeoutline(self, context, mode):
  554     if self.mouse_path:
  555 
  556         bgl.glLineWidth(1)
  557         bgl.glEnable(bgl.GL_BLEND)
  558         bgl.glEnable(bgl.GL_LINE_SMOOTH)
  559         bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
  560 
  561         nodes, links = get_nodes_links(context)
  562 
  563         shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
  564 
  565         if mode == "LINK":
  566             col_outer = (1.0, 0.2, 0.2, 0.4)
  567             col_inner = (0.0, 0.0, 0.0, 0.5)
  568             col_circle_inner = (0.3, 0.05, 0.05, 1.0)
  569         elif mode == "LINKMENU":
  570             col_outer = (0.4, 0.6, 1.0, 0.4)
  571             col_inner = (0.0, 0.0, 0.0, 0.5)
  572             col_circle_inner = (0.08, 0.15, .3, 1.0)
  573         elif mode == "MIX":
  574             col_outer = (0.2, 1.0, 0.2, 0.4)
  575             col_inner = (0.0, 0.0, 0.0, 0.5)
  576             col_circle_inner = (0.05, 0.3, 0.05, 1.0)
  577 
  578         m1x = self.mouse_path[0][0]
  579         m1y = self.mouse_path[0][1]
  580         m2x = self.mouse_path[-1][0]
  581         m2y = self.mouse_path[-1][1]
  582 
  583         n1 = nodes[context.scene.NWLazySource]
  584         n2 = nodes[context.scene.NWLazyTarget]
  585 
  586         if n1 == n2:
  587             col_outer = (0.4, 0.4, 0.4, 0.4)
  588             col_inner = (0.0, 0.0, 0.0, 0.5)
  589             col_circle_inner = (0.2, 0.2, 0.2, 1.0)
  590 
  591         draw_rounded_node_border(shader, n1, radius=6, colour=col_outer)  # outline
  592         draw_rounded_node_border(shader, n1, radius=5, colour=col_inner)  # inner
  593         draw_rounded_node_border(shader, n2, radius=6, colour=col_outer)  # outline
  594         draw_rounded_node_border(shader, n2, radius=5, colour=col_inner)  # inner
  595 
  596         draw_line(m1x, m1y, m2x, m2y, 5, col_outer)  # line outline
  597         draw_line(m1x, m1y, m2x, m2y, 2, col_inner)  # line inner
  598 
  599         # circle outline
  600         draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
  601         draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
  602 
  603         # circle inner
  604         draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
  605         draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
  606 
  607         bgl.glDisable(bgl.GL_BLEND)
  608         bgl.glDisable(bgl.GL_LINE_SMOOTH)
  609 def get_active_tree(context):
  610     tree = context.space_data.node_tree
  611     path = []
  612     # Get nodes from currently edited tree.
  613     # If user is editing a group, space_data.node_tree is still the base level (outside group).
  614     # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
  615     # the same as context.active_node, the user is in a group.
  616     # Check recursively until we find the real active node_tree:
  617     if tree.nodes.active:
  618         while tree.nodes.active != context.active_node:
  619             tree = tree.nodes.active.node_tree
  620             path.append(tree)
  621     return tree, path
  622 
  623 def get_nodes_links(context):
  624     tree, path = get_active_tree(context)
  625     return tree.nodes, tree.links
  626 
  627 def is_viewer_socket(socket):
  628     # checks if a internal socket is a valid viewer socket
  629     return socket.name == viewer_socket_name and socket.NWViewerSocket
  630 
  631 def get_internal_socket(socket):
  632     #get the internal socket from a socket inside or outside the group
  633     node = socket.node
  634     if node.type == 'GROUP_OUTPUT':
  635         source_iterator = node.inputs
  636         iterator = node.id_data.outputs
  637     elif node.type == 'GROUP_INPUT':
  638         source_iterator = node.outputs
  639         iterator = node.id_data.inputs
  640     elif hasattr(node, "node_tree"):
  641         if socket.is_output:
  642             source_iterator = node.outputs
  643             iterator = node.node_tree.outputs
  644         else:
  645             source_iterator = node.inputs
  646             iterator = node.node_tree.inputs
  647     else:
  648         return None
  649 
  650     for i, s in enumerate(source_iterator):
  651         if s == socket:
  652             break
  653     return iterator[i]
  654 
  655 def is_viewer_link(link, output_node):
  656     if link.to_node == output_node and link.to_socket == output_node.inputs[0]:
  657         return True
  658     if link.to_node.type == 'GROUP_OUTPUT':
  659         socket = get_internal_socket(link.to_socket)
  660         if is_viewer_socket(socket):
  661             return True
  662     return False
  663 
  664 def get_group_output_node(tree):
  665     for node in tree.nodes:
  666         if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
  667             return node
  668 
  669 def get_output_location(tree):
  670     # get right-most location
  671     sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
  672     max_xloc_node = sorted_by_xloc[-1]
  673 
  674     # get average y location
  675     sum_yloc = 0
  676     for node in tree.nodes:
  677         sum_yloc += node.location.y
  678 
  679     loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
  680     loc_y = sum_yloc / len(tree.nodes)
  681     return loc_x, loc_y
  682 
  683 # Principled prefs
  684 class NWPrincipledPreferences(bpy.types.PropertyGroup):
  685     base_color: StringProperty(
  686         name='Base Color',
  687         default='diffuse diff albedo base col color',
  688         description='Naming Components for Base Color maps')
  689     sss_color: StringProperty(
  690         name='Subsurface Color',
  691         default='sss subsurface',
  692         description='Naming Components for Subsurface Color maps')
  693     metallic: StringProperty(
  694         name='Metallic',
  695         default='metallic metalness metal mtl',
  696         description='Naming Components for metallness maps')
  697     specular: StringProperty(
  698         name='Specular',
  699         default='specularity specular spec spc',
  700         description='Naming Components for Specular maps')
  701     normal: StringProperty(
  702         name='Normal',
  703         default='normal nor nrm nrml norm',
  704         description='Naming Components for Normal maps')
  705     bump: StringProperty(
  706         name='Bump',
  707         default='bump bmp',
  708         description='Naming Components for bump maps')
  709     rough: StringProperty(
  710         name='Roughness',
  711         default='roughness rough rgh',
  712         description='Naming Components for roughness maps')
  713     gloss: StringProperty(
  714         name='Gloss',
  715         default='gloss glossy glossiness',
  716         description='Naming Components for glossy maps')
  717     displacement: StringProperty(
  718         name='Displacement',
  719         default='displacement displace disp dsp height heightmap',
  720         description='Naming Components for displacement maps')
  721     transmission: StringProperty(
  722         name='Transmission',
  723         default='transmission transparency',
  724         description='Naming Components for transmission maps')
  725     emission: StringProperty(
  726         name='Emission',
  727         default='emission emissive emit',
  728         description='Naming Components for emission maps')
  729     alpha: StringProperty(
  730         name='Alpha',
  731         default='alpha opacity',
  732         description='Naming Components for alpha maps')
  733     ambient_occlusion: StringProperty(
  734         name='Ambient Occlusion',
  735         default='ao ambient occlusion',
  736         description='Naming Components for AO maps')
  737 
  738 # Addon prefs
  739 class NWNodeWrangler(bpy.types.AddonPreferences):
  740     bl_idname = __name__
  741 
  742     merge_hide: EnumProperty(
  743         name="Hide Mix nodes",
  744         items=(
  745             ("ALWAYS", "Always", "Always collapse the new merge nodes"),
  746             ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
  747             ("NEVER", "Never", "Never collapse the new merge nodes")
  748         ),
  749         default='NON_SHADER',
  750         description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
  751     merge_position: EnumProperty(
  752         name="Mix Node Position",
  753         items=(
  754             ("CENTER", "Center", "Place the Mix node between the two nodes"),
  755             ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
  756         ),
  757         default='CENTER',
  758         description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
  759 
  760     show_hotkey_list: BoolProperty(
  761         name="Show Hotkey List",
  762         default=False,
  763         description="Expand this box into a list of all the hotkeys for functions in this addon"
  764     )
  765     hotkey_list_filter: StringProperty(
  766         name="        Filter by Name",
  767         default="",
  768         description="Show only hotkeys that have this text in their name",
  769         options={'TEXTEDIT_UPDATE'}
  770     )
  771     show_principled_lists: BoolProperty(
  772         name="Show Principled naming tags",
  773         default=False,
  774         description="Expand this box into a list of all naming tags for principled texture setup"
  775     )
  776     principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
  777 
  778     def draw(self, context):
  779         layout = self.layout
  780         col = layout.column()
  781         col.prop(self, "merge_position")
  782         col.prop(self, "merge_hide")
  783 
  784         box = layout.box()
  785         col = box.column(align=True)
  786         col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
  787         if self.show_principled_lists:
  788             tags = self.principled_tags
  789 
  790             col.prop(tags, "base_color")
  791             col.prop(tags, "sss_color")
  792             col.prop(tags, "metallic")
  793             col.prop(tags, "specular")
  794             col.prop(tags, "rough")
  795             col.prop(tags, "gloss")
  796             col.prop(tags, "normal")
  797             col.prop(tags, "bump")
  798             col.prop(tags, "displacement")
  799             col.prop(tags, "transmission")
  800             col.prop(tags, "emission")
  801             col.prop(tags, "alpha")
  802             col.prop(tags, "ambient_occlusion")
  803 
  804         box = layout.box()
  805         col = box.column(align=True)
  806         hotkey_button_name = "Show Hotkey List"
  807         if self.show_hotkey_list:
  808             hotkey_button_name = "Hide Hotkey List"
  809         col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
  810         if self.show_hotkey_list:
  811             col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
  812             col.separator()
  813             for hotkey in kmi_defs:
  814                 if hotkey[7]:
  815                     hotkey_name = hotkey[7]
  816 
  817                     if self.hotkey_list_filter.lower() in hotkey_name.lower():
  818                         row = col.row(align=True)
  819                         row.label(text=hotkey_name)
  820                         keystr = nice_hotkey_name(hotkey[1])
  821                         if hotkey[4]:
  822                             keystr = "Shift " + keystr
  823                         if hotkey[5]:
  824                             keystr = "Alt " + keystr
  825                         if hotkey[3]:
  826                             keystr = "Ctrl " + keystr
  827                         row.label(text=keystr)
  828 
  829 
  830 
  831 def nw_check(context):
  832     space = context.space_data
  833     valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
  834 
  835     valid = False
  836     if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
  837         valid = True
  838 
  839     return valid
  840 
  841 class NWBase:
  842     @classmethod
  843     def poll(cls, context):
  844         return nw_check(context)
  845 
  846 
  847 # OPERATORS
  848 class NWLazyMix(Operator, NWBase):
  849     """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
  850     bl_idname = "node.nw_lazy_mix"
  851     bl_label = "Mix Nodes"
  852     bl_options = {'REGISTER', 'UNDO'}
  853 
  854     def modal(self, context, event):
  855         context.area.tag_redraw()
  856         nodes, links = get_nodes_links(context)
  857         cont = True
  858 
  859         start_pos = [event.mouse_region_x, event.mouse_region_y]
  860 
  861         node1 = None
  862         if not context.scene.NWBusyDrawing:
  863             node1 = node_at_pos(nodes, context, event)
  864             if node1:
  865                 context.scene.NWBusyDrawing = node1.name
  866         else:
  867             if context.scene.NWBusyDrawing != 'STOP':
  868                 node1 = nodes[context.scene.NWBusyDrawing]
  869 
  870         context.scene.NWLazySource = node1.name
  871         context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
  872 
  873         if event.type == 'MOUSEMOVE':
  874             self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
  875 
  876         elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
  877             end_pos = [event.mouse_region_x, event.mouse_region_y]
  878             bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
  879 
  880             node2 = None
  881             node2 = node_at_pos(nodes, context, event)
  882             if node2:
  883                 context.scene.NWBusyDrawing = node2.name
  884 
  885             if node1 == node2:
  886                 cont = False
  887 
  888             if cont:
  889                 if node1 and node2:
  890                     for node in nodes:
  891                         node.select = False
  892                     node1.select = True
  893                     node2.select = True
  894 
  895                     bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
  896 
  897             context.scene.NWBusyDrawing = ""
  898             return {'FINISHED'}
  899 
  900         elif event.type == 'ESC':
  901             print('cancelled')
  902             bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
  903             return {'CANCELLED'}
  904 
  905         return {'RUNNING_MODAL'}
  906 
  907     def invoke(self, context, event):
  908         if context.area.type == 'NODE_EDITOR':
  909             # the arguments we pass the the callback
  910             args = (self, context, 'MIX')
  911             # Add the region OpenGL drawing callback
  912             # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
  913             self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
  914 
  915             self.mouse_path = []
  916 
  917             context.window_manager.modal_handler_add(self)
  918             return {'RUNNING_MODAL'}
  919         else:
  920             self.report({'WARNING'}, "View3D not found, cannot run operator")
  921             return {'CANCELLED'}
  922 
  923 
  924 class NWLazyConnect(Operator, NWBase):
  925     """Connect two nodes without clicking a specific socket (automatically determined"""
  926     bl_idname = "node.nw_lazy_connect"
  927     bl_label = "Lazy Connect"
  928     bl_options = {'REGISTER', 'UNDO'}
  929     with_menu: BoolProperty()
  930 
  931     def modal(self, context, event):
  932         context.area.tag_redraw()
  933         nodes, links = get_nodes_links(context)
  934         cont = True
  935 
  936         start_pos = [event.mouse_region_x, event.mouse_region_y]
  937 
  938         node1 = None
  939         if not context.scene.NWBusyDrawing:
  940             node1 = node_at_pos(nodes, context, event)
  941             if node1:
  942                 context.scene.NWBusyDrawing = node1.name
  943         else:
  944             if context.scene.NWBusyDrawing != 'STOP':
  945                 node1 = nodes[context.scene.NWBusyDrawing]
  946 
  947         context.scene.NWLazySource = node1.name
  948         context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
  949 
  950         if event.type == 'MOUSEMOVE':
  951             self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
  952 
  953         elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
  954             end_pos = [event.mouse_region_x, event.mouse_region_y]
  955             bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
  956 
  957             node2 = None
  958             node2 = node_at_pos(nodes, context, event)
  959             if node2:
  960                 context.scene.NWBusyDrawing = node2.name
  961 
  962             if node1 == node2:
  963                 cont = False
  964 
  965             link_success = False
  966             if cont:
  967                 if node1 and node2:
  968                     original_sel = []
  969                     original_unsel = []
  970                     for node in nodes:
  971                         if node.select == True:
  972                             node.select = False
  973                             original_sel.append(node)
  974                         else:
  975                             original_unsel.append(node)
  976                     node1.select = True
  977                     node2.select = True
  978 
  979                     #link_success = autolink(node1, node2, links)
  980                     if self.with_menu:
  981                         if len(node1.outputs) > 1 and node2.inputs:
  982                             bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
  983                         elif len(node1.outputs) == 1:
  984                             bpy.ops.node.nw_call_inputs_menu(from_socket=0)
  985                     else:
  986                         link_success = autolink(node1, node2, links)
  987 
  988                     for node in original_sel:
  989                         node.select = True
  990                     for node in original_unsel:
  991                         node.select = False
  992 
  993             if link_success:
  994                 force_update(context)
  995             context.scene.NWBusyDrawing = ""
  996             return {'FINISHED'}
  997 
  998         elif event.type == 'ESC':
  999             bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
 1000             return {'CANCELLED'}
 1001 
 1002         return {'RUNNING_MODAL'}
 1003 
 1004     def invoke(self, context, event):
 1005         if context.area.type == 'NODE_EDITOR':
 1006             nodes, links = get_nodes_links(context)
 1007             node = node_at_pos(nodes, context, event)
 1008             if node:
 1009                 context.scene.NWBusyDrawing = node.name
 1010 
 1011             # the arguments we pass the the callback
 1012             mode = "LINK"
 1013             if self.with_menu:
 1014                 mode = "LINKMENU"
 1015             args = (self, context, mode)
 1016             # Add the region OpenGL drawing callback
 1017             # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
 1018             self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
 1019 
 1020             self.mouse_path = []
 1021 
 1022             context.window_manager.modal_handler_add(self)
 1023             return {'RUNNING_MODAL'}
 1024         else:
 1025             self.report({'WARNING'}, "View3D not found, cannot run operator")
 1026             return {'CANCELLED'}
 1027 
 1028 
 1029 class NWDeleteUnused(Operator, NWBase):
 1030     """Delete all nodes whose output is not used"""
 1031     bl_idname = 'node.nw_del_unused'
 1032     bl_label = 'Delete Unused Nodes'
 1033     bl_options = {'REGISTER', 'UNDO'}
 1034 
 1035     delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
 1036     delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
 1037 
 1038     def is_unused_node(self, node):
 1039         end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
 1040                 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
 1041                 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
 1042         if node.type in end_types:
 1043             return False
 1044 
 1045         for output in node.outputs:
 1046             if output.links:
 1047                 return False
 1048         return True
 1049 
 1050     @classmethod
 1051     def poll(cls, context):
 1052         valid = False
 1053         if nw_check(context):
 1054             if context.space_data.node_tree.nodes:
 1055                 valid = True
 1056         return valid
 1057 
 1058     def execute(self, context):
 1059         nodes, links = get_nodes_links(context)
 1060 
 1061         # Store selection
 1062         selection = []
 1063         for node in nodes:
 1064             if node.select == True:
 1065                 selection.append(node.name)
 1066 
 1067         for node in nodes:
 1068             node.select = False
 1069 
 1070         deleted_nodes = []
 1071         temp_deleted_nodes = []
 1072         del_unused_iterations = len(nodes)
 1073         for it in range(0, del_unused_iterations):
 1074             temp_deleted_nodes = list(deleted_nodes)  # keep record of last iteration
 1075             for node in nodes:
 1076                 if self.is_unused_node(node):
 1077                     node.select = True
 1078                     deleted_nodes.append(node.name)
 1079                     bpy.ops.node.delete()
 1080 
 1081             if temp_deleted_nodes == deleted_nodes:  # stop iterations when there are no more nodes to be deleted
 1082                 break
 1083 
 1084         if self.delete_frames:
 1085             repeat = True
 1086             while repeat:
 1087                 frames_in_use = []
 1088                 frames = []
 1089                 repeat = False
 1090                 for node in nodes:
 1091                     if node.parent:
 1092                         frames_in_use.append(node.parent)
 1093                 for node in nodes:
 1094                     if node.type == 'FRAME' and node not in frames_in_use:
 1095                         frames.append(node)
 1096                         if node.parent:
 1097                             repeat = True  # repeat for nested frames
 1098                 for node in frames:
 1099                     if node not in frames_in_use:
 1100                         node.select = True
 1101                         deleted_nodes.append(node.name)
 1102                 bpy.ops.node.delete()
 1103 
 1104         if self.delete_muted:
 1105             for node in nodes:
 1106                 if node.mute:
 1107                     node.select = True
 1108                     deleted_nodes.append(node.name)
 1109             bpy.ops.node.delete_reconnect()
 1110 
 1111         # get unique list of deleted nodes (iterations would count the same node more than once)
 1112         deleted_nodes = list(set(deleted_nodes))
 1113         for n in deleted_nodes:
 1114             self.report({'INFO'}, "Node " + n + " deleted")
 1115         num_deleted = len(deleted_nodes)
 1116         n = ' node'
 1117         if num_deleted > 1:
 1118             n += 's'
 1119         if num_deleted:
 1120             self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
 1121         else:
 1122             self.report({'INFO'}, "Nothing deleted")
 1123 
 1124         # Restore selection
 1125         nodes, links = get_nodes_links(context)
 1126         for node in nodes:
 1127             if node.name in selection:
 1128                 node.select = True
 1129         return {'FINISHED'}
 1130 
 1131     def invoke(self, context, event):
 1132         return context.window_manager.invoke_confirm(self, event)
 1133 
 1134 
 1135 class NWSwapLinks(Operator, NWBase):
 1136     """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
 1137     bl_idname = 'node.nw_swap_links'
 1138     bl_label = 'Swap Links'
 1139     bl_options = {'REGISTER', 'UNDO'}
 1140 
 1141     @classmethod
 1142     def poll(cls, context):
 1143         valid = False
 1144         if nw_check(context):
 1145             if context.selected_nodes:
 1146                 valid = len(context.selected_nodes) <= 2
 1147         return valid
 1148 
 1149     def execute(self, context):
 1150         nodes, links = get_nodes_links(context)
 1151         selected_nodes = context.selected_nodes
 1152         n1 = selected_nodes[0]
 1153 
 1154         # Swap outputs
 1155         if len(selected_nodes) == 2:
 1156             n2 = selected_nodes[1]
 1157             if n1.outputs and n2.outputs:
 1158                 n1_outputs = []
 1159                 n2_outputs = []
 1160 
 1161                 out_index = 0
 1162                 for output in n1.outputs:
 1163                     if output.links:
 1164                         for link in output.links:
 1165                             n1_outputs.append([out_index, link.to_socket])
 1166                             links.remove(link)
 1167                     out_index += 1
 1168 
 1169                 out_index = 0
 1170                 for output in n2.outputs:
 1171                     if output.links:
 1172                         for link in output.links:
 1173                             n2_outputs.append([out_index, link.to_socket])
 1174                             links.remove(link)
 1175                     out_index += 1
 1176 
 1177                 for connection in n1_outputs:
 1178                     try:
 1179                         links.new(n2.outputs[connection[0]], connection[1])
 1180                     except:
 1181                         self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
 1182                 for connection in n2_outputs:
 1183                     try:
 1184                         links.new(n1.outputs[connection[0]], connection[1])
 1185                     except:
 1186                         self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
 1187             else:
 1188                 if n1.outputs or n2.outputs:
 1189                     self.report({'WARNING'}, "One of the nodes has no outputs!")
 1190                 else:
 1191                     self.report({'WARNING'}, "Neither of the nodes have outputs!")
 1192 
 1193         # Swap Inputs
 1194         elif len(selected_nodes) == 1:
 1195             if n1.inputs and n1.inputs[0].is_multi_input:
 1196                 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
 1197                 return {'FINISHED'}
 1198             if n1.inputs:
 1199                 types = []
 1200                 i=0
 1201                 for i1 in n1.inputs:
 1202                     if i1.is_linked and not i1.is_multi_input:
 1203                         similar_types = 0
 1204                         for i2 in n1.inputs:
 1205                             if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
 1206                                 similar_types += 1
 1207                         types.append ([i1, similar_types, i])
 1208                     i += 1
 1209                 types.sort(key=lambda k: k[1], reverse=True)
 1210 
 1211                 if types:
 1212                     t = types[0]
 1213                     if t[1] == 2:
 1214                         for i2 in n1.inputs:
 1215                             if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
 1216                                 pair = [t[0], i2]
 1217                         i1f = pair[0].links[0].from_socket
 1218                         i1t = pair[0].links[0].to_socket
 1219                         i2f = pair[1].links[0].from_socket
 1220                         i2t = pair[1].links[0].to_socket
 1221                         links.new(i1f, i2t)
 1222                         links.new(i2f, i1t)
 1223                     if t[1] == 1:
 1224                         if len(types) == 1:
 1225                             fs = t[0].links[0].from_socket
 1226                             i = t[2]
 1227                             links.remove(t[0].links[0])
 1228                             if i+1 == len(n1.inputs):
 1229                                 i = -1
 1230                             i += 1
 1231                             while n1.inputs[i].is_linked:
 1232                                 i += 1
 1233                             links.new(fs, n1.inputs[i])
 1234                         elif len(types) == 2:
 1235                             i1f = types[0][0].links[0].from_socket
 1236                             i1t = types[0][0].links[0].to_socket
 1237                             i2f = types[1][0].links[0].from_socket
 1238                             i2t = types[1][0].links[0].to_socket
 1239                             links.new(i1f, i2t)
 1240                             links.new(i2f, i1t)
 1241 
 1242                 else:
 1243                     self.report({'WARNING'}, "This node has no input connections to swap!")
 1244             else:
 1245                 self.report({'WARNING'}, "This node has no inputs to swap!")
 1246 
 1247         force_update(context)
 1248         return {'FINISHED'}
 1249 
 1250 
 1251 class NWResetBG(Operator, NWBase):
 1252     """Reset the zoom and position of the background image"""
 1253     bl_idname = 'node.nw_bg_reset'
 1254     bl_label = 'Reset Backdrop'
 1255     bl_options = {'REGISTER', 'UNDO'}
 1256 
 1257     @classmethod
 1258     def poll(cls, context):
 1259         valid = False
 1260         if nw_check(context):
 1261             snode = context.space_data
 1262             valid = snode.tree_type == 'CompositorNodeTree'
 1263         return valid
 1264 
 1265     def execute(self, context):
 1266         context.space_data.backdrop_zoom = 1
 1267         context.space_data.backdrop_offset[0] = 0
 1268         context.space_data.backdrop_offset[1] = 0
 1269         return {'FINISHED'}
 1270 
 1271 
 1272 class NWAddAttrNode(Operator, NWBase):
 1273     """Add an Attribute node with this name"""
 1274     bl_idname = 'node.nw_add_attr_node'
 1275     bl_label = 'Add UV map'
 1276     bl_options = {'REGISTER', 'UNDO'}
 1277 
 1278     attr_name: StringProperty()
 1279 
 1280     def execute(self, context):
 1281         bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
 1282         nodes, links = get_nodes_links(context)
 1283         nodes.active.attribute_name = self.attr_name
 1284         return {'FINISHED'}
 1285 
 1286 class NWPreviewNode(Operator, NWBase):
 1287     bl_idname = "node.nw_preview_node"
 1288     bl_label = "Preview Node"
 1289     bl_description = "Connect active node to the Node Group output or the Material Output"
 1290     bl_options = {'REGISTER', 'UNDO'}
 1291 
 1292     # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
 1293     # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
 1294     # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
 1295     run_in_geometry_nodes: BoolProperty(default=True)
 1296 
 1297     def __init__(self):
 1298         self.shader_output_type = ""
 1299         self.shader_output_ident = ""
 1300 
 1301     @classmethod
 1302     def poll(cls, context):
 1303         if nw_check(context):
 1304             space = context.space_data
 1305             if space.tree_type == 'ShaderNodeTree' or space.tree_type == 'GeometryNodeTree':
 1306                 if context.active_node:
 1307                     if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
 1308                         return True
 1309                 else:
 1310                     return True
 1311         return False
 1312 
 1313     def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
 1314         #check if a viewer output already exists in a node group otherwise create
 1315         if hasattr(node, "node_tree"):
 1316             index = None
 1317             if len(node.node_tree.outputs):
 1318                 free_socket = None
 1319                 for i, socket in enumerate(node.node_tree.outputs):
 1320                     if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
 1321                         #if viewer output is already used but leads to the same socket we can still use it
 1322                         is_used = self.is_socket_used_other_mats(socket)
 1323                         if is_used:
 1324                             if connect_socket == None:
 1325                                 continue
 1326                             groupout = get_group_output_node(node.node_tree)
 1327                             groupout_input = groupout.inputs[i]
 1328                             links = groupout_input.links
 1329                             if connect_socket not in [link.from_socket for link in links]:
 1330                                 continue
 1331                             index=i
 1332                             break
 1333                         if not free_socket:
 1334                             free_socket = i
 1335                 if not index and free_socket:
 1336                     index = free_socket
 1337 
 1338             if not index:
 1339                 #create viewer socket
 1340                 node.node_tree.outputs.new(socket_type, viewer_socket_name)
 1341                 index = len(node.node_tree.outputs) - 1
 1342                 node.node_tree.outputs[index].NWViewerSocket = True
 1343             return index
 1344 
 1345     def init_shader_variables(self, space, shader_type):
 1346         if shader_type == 'OBJECT':
 1347             if space.id not in [light for light in bpy.data.lights]:  # cannot use bpy.data.lights directly as iterable
 1348                 self.shader_output_type = "OUTPUT_MATERIAL"
 1349                 self.shader_output_ident = "ShaderNodeOutputMaterial"
 1350             else:
 1351                 self.shader_output_type = "OUTPUT_LIGHT"
 1352                 self.shader_output_ident = "ShaderNodeOutputLight"
 1353 
 1354         elif shader_type == 'WORLD':
 1355             self.shader_output_type = "OUTPUT_WORLD"
 1356             self.shader_output_ident = "ShaderNodeOutputWorld"
 1357 
 1358     def get_shader_output_node(self, tree):
 1359         for node in tree.nodes:
 1360             if node.type == self.shader_output_type and node.is_active_output == True:
 1361                 return node
 1362 
 1363     @classmethod
 1364     def ensure_group_output(cls, tree):
 1365         #check if a group output node exists otherwise create
 1366         groupout = get_group_output_node(tree)
 1367         if not groupout:
 1368             groupout = tree.nodes.new('NodeGroupOutput')
 1369             loc_x, loc_y = get_output_location(tree)
 1370             groupout.location.x = loc_x
 1371             groupout.location.y = loc_y
 1372             groupout.select = False
 1373             # So that we don't keep on adding new group outputs
 1374             groupout.is_active_output = True
 1375         return groupout
 1376 
 1377     @classmethod
 1378     def search_sockets(cls, node, sockets, index=None):
 1379         # recursively scan nodes for viewer sockets and store in list
 1380         for i, input_socket in enumerate(node.inputs):
 1381             if index and i != index:
 1382                 continue
 1383             if len(input_socket.links):
 1384                 link = input_socket.links[0]
 1385                 next_node = link.from_node
 1386                 external_socket = link.from_socket
 1387                 if hasattr(next_node, "node_tree"):
 1388                     for socket_index, s in enumerate(next_node.outputs):
 1389                         if s == external_socket:
 1390                             break
 1391                     socket = next_node.node_tree.outputs[socket_index]
 1392                     if is_viewer_socket(socket) and socket not in sockets:
 1393                         sockets.append(socket)
 1394                         #continue search inside of node group but restrict socket to where we came from
 1395                         groupout = get_group_output_node(next_node.node_tree)
 1396                         cls.search_sockets(groupout, sockets, index=socket_index)
 1397 
 1398     @classmethod
 1399     def scan_nodes(cls, tree, sockets):
 1400         # get all viewer sockets in a material tree
 1401         for node in tree.nodes:
 1402             if hasattr(node, "node_tree"):
 1403                 for socket in node.node_tree.outputs:
 1404                     if is_viewer_socket(socket) and (socket not in sockets):
 1405                         sockets.append(socket)
 1406                 cls.scan_nodes(node.node_tree, sockets)
 1407 
 1408     def link_leads_to_used_socket(self, link):
 1409         #return True if link leads to a socket that is already used in this material
 1410         socket = get_internal_socket(link.to_socket)
 1411         return (socket and self.is_socket_used_active_mat(socket))
 1412 
 1413     def is_socket_used_active_mat(self, socket):
 1414         #ensure used sockets in active material is calculated and check given socket
 1415         if not hasattr(self, "used_viewer_sockets_active_mat"):
 1416             self.used_viewer_sockets_active_mat = []
 1417             materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
 1418             if materialout:
 1419                 self.search_sockets(materialout, self.used_viewer_sockets_active_mat)
 1420         return socket in self.used_viewer_sockets_active_mat
 1421 
 1422     def is_socket_used_other_mats(self, socket):
 1423         #ensure used sockets in other materials are calculated and check given socket
 1424         if not hasattr(self, "used_viewer_sockets_other_mats"):
 1425             self.used_viewer_sockets_other_mats = []
 1426             for mat in bpy.data.materials:
 1427                 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
 1428                     continue
 1429                 # get viewer node
 1430                 materialout = self.get_shader_output_node(mat.node_tree)
 1431                 if materialout:
 1432                     self.search_sockets(materialout, self.used_viewer_sockets_other_mats)
 1433         return socket in self.used_viewer_sockets_other_mats
 1434 
 1435     def invoke(self, context, event):
 1436         space = context.space_data
 1437         # Ignore operator when running in wrong context.
 1438         if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
 1439             return {'PASS_THROUGH'}
 1440 
 1441         shader_type = space.shader_type
 1442         self.init_shader_variables(space, shader_type)
 1443         mlocx = event.mouse_region_x
 1444         mlocy = event.mouse_region_y
 1445         select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
 1446         if 'FINISHED' in select_node:  # only run if mouse click is on a node
 1447             active_tree, path_to_tree = get_active_tree(context)
 1448             nodes, links = active_tree.nodes, active_tree.links
 1449             base_node_tree = space.node_tree
 1450             active = nodes.active
 1451 
 1452             # For geometry node trees we just connect to the group output
 1453             if space.tree_type == "GeometryNodeTree":
 1454                 valid = False
 1455                 if active:
 1456                     for out in active.outputs:
 1457                         if is_visible_socket(out):
 1458                             valid = True
 1459                             break
 1460                 # Exit early
 1461                 if not valid:
 1462                     return {'FINISHED'}
 1463 
 1464                 delete_sockets = []
 1465 
 1466                 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
 1467                 self.scan_nodes(base_node_tree, delete_sockets)
 1468 
 1469                 # Find (or create if needed) the output of this node tree
 1470                 geometryoutput = self.ensure_group_output(base_node_tree)
 1471 
 1472                 # Analyze outputs, make links
 1473                 out_i = None
 1474                 valid_outputs = []
 1475                 for i, out in enumerate(active.outputs):
 1476                     if is_visible_socket(out) and out.type == 'GEOMETRY':
 1477                         valid_outputs.append(i)
 1478                 if valid_outputs:
 1479                     out_i = valid_outputs[0]  # Start index of node's outputs
 1480                 for i, valid_i in enumerate(valid_outputs):
 1481                     for out_link in active.outputs[valid_i].links:
 1482                         if is_viewer_link(out_link, geometryoutput):
 1483                             if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
 1484                                 if i < len(valid_outputs) - 1:
 1485                                     out_i = valid_outputs[i + 1]
 1486                                 else:
 1487                                     out_i = valid_outputs[0]
 1488 
 1489                 make_links = []  # store sockets for new links
 1490                 if active.outputs:
 1491                     # If there is no 'GEOMETRY' output type - We can't preview the node
 1492                     if out_i is None:
 1493                         return {'FINISHED'}
 1494                     socket_type = 'GEOMETRY'
 1495                     # Find an input socket of the output of type geometry
 1496                     geometryoutindex = None
 1497                     for i,inp in enumerate(geometryoutput.inputs):
 1498                         if inp.type == socket_type:
 1499                             geometryoutindex = i
 1500                             break
 1501                     if geometryoutindex is None:
 1502                         # Create geometry socket
 1503                         geometryoutput.inputs.new(socket_type, 'Geometry')
 1504                         geometryoutindex = len(geometryoutput.inputs) - 1
 1505 
 1506                     make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
 1507                     output_socket = geometryoutput.inputs[geometryoutindex]
 1508                     for li_from, li_to in make_links:
 1509                         base_node_tree.links.new(li_from, li_to)
 1510                     tree = base_node_tree
 1511                     link_end = output_socket
 1512                     while tree.nodes.active != active:
 1513                         node = tree.nodes.active
 1514                         index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
 1515                         link_start = node.outputs[index]
 1516                         node_socket = node.node_tree.outputs[index]
 1517                         if node_socket in delete_sockets:
 1518                             delete_sockets.remove(node_socket)
 1519                         tree.links.new(link_start, link_end)
 1520                         # Iterate
 1521                         link_end = self.ensure_group_output(node.node_tree).inputs[index]
 1522                         tree = tree.nodes.active.node_tree
 1523                     tree.links.new(active.outputs[out_i], link_end)
 1524 
 1525                 # Delete sockets
 1526                 for socket in delete_sockets:
 1527                     tree = socket.id_data
 1528                     tree.outputs.remove(socket)
 1529 
 1530                 nodes.active = active
 1531                 active.select = True
 1532                 force_update(context)
 1533                 return {'FINISHED'}
 1534 
 1535 
 1536             # What follows is code for the shader editor
 1537             output_types = [x.nodetype for x in
 1538                             get_nodes_from_category('Output', context)]
 1539             valid = False
 1540             if active:
 1541                 if active.rna_type.identifier not in output_types:
 1542                     for out in active.outputs:
 1543                         if is_visible_socket(out):
 1544                             valid = True
 1545                             break
 1546             if valid:
 1547                 # get material_output node
 1548                 materialout = None  # placeholder node
 1549                 delete_sockets = []
 1550 
 1551                 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
 1552                 self.scan_nodes(base_node_tree, delete_sockets)
 1553 
 1554                 materialout = self.get_shader_output_node(base_node_tree)
 1555                 if not materialout:
 1556                     materialout = base_node_tree.nodes.new(self.shader_output_ident)
 1557                     materialout.location = get_output_location(base_node_tree)
 1558                     materialout.select = False
 1559                 # Analyze outputs
 1560                 out_i = None
 1561                 valid_outputs = []
 1562                 for i, out in enumerate(active.outputs):
 1563                     if is_visible_socket(out):
 1564                         valid_outputs.append(i)
 1565                 if valid_outputs:
 1566                     out_i = valid_outputs[0]  # Start index of node's outputs
 1567                 for i, valid_i in enumerate(valid_outputs):
 1568                     for out_link in active.outputs[valid_i].links:
 1569                         if is_viewer_link(out_link, materialout):
 1570                             if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
 1571                                 if i < len(valid_outputs) - 1:
 1572                                     out_i = valid_outputs[i + 1]
 1573                                 else:
 1574                                     out_i = valid_outputs[0]
 1575 
 1576                 make_links = []  # store sockets for new links
 1577                 if active.outputs:
 1578                     socket_type = 'NodeSocketShader'
 1579                     materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
 1580                     make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
 1581                     output_socket = materialout.inputs[materialout_index]
 1582                     for li_from, li_to in make_links:
 1583                         base_node_tree.links.new(li_from, li_to)
 1584 
 1585                     # Create links through node groups until we reach the active node
 1586                     tree = base_node_tree
 1587                     link_end = output_socket
 1588                     while tree.nodes.active != active:
 1589                         node = tree.nodes.active
 1590                         index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
 1591                         link_start = node.outputs[index]
 1592                         node_socket = node.node_tree.outputs[index]
 1593                         if node_socket in delete_sockets:
 1594                             delete_sockets.remove(node_socket)
 1595                         tree.links.new(link_start, link_end)
 1596                         # Iterate
 1597                         link_end = self.ensure_group_output(node.node_tree).inputs[index]
 1598                         tree = tree.nodes.active.node_tree
 1599                     tree.links.new(active.outputs[out_i], link_end)
 1600 
 1601                 # Delete sockets
 1602                 for socket in delete_sockets:
 1603                     if not self.is_socket_used_other_mats(socket):
 1604                         tree = socket.id_data
 1605                         tree.outputs.remove(socket)
 1606 
 1607                 nodes.active = active
 1608                 active.select = True
 1609 
 1610                 force_update(context)
 1611 
 1612             return {'FINISHED'}
 1613         else:
 1614             return {'CANCELLED'}
 1615 
 1616 
 1617 class NWFrameSelected(Operator, NWBase):
 1618     bl_idname = "node.nw_frame_selected"
 1619     bl_label = "Frame Selected"
 1620     bl_description = "Add a frame node and parent the selected nodes to it"
 1621     bl_options = {'REGISTER', 'UNDO'}
 1622 
 1623     label_prop: StringProperty(
 1624         name='Label',
 1625         description='The visual name of the frame node',
 1626         default=' '
 1627     )
 1628     use_custom_color_prop: BoolProperty(
 1629         name="Custom Color",
 1630         description="Use custom color for the frame node",
 1631         default=False
 1632     )
 1633     color_prop: FloatVectorProperty(
 1634         name="Color",
 1635         description="The color of the frame node",
 1636         default=(0.604, 0.604, 0.604),
 1637         min=0, max=1, step=1, precision=3,
 1638         subtype='COLOR_GAMMA', size=3
 1639     )
 1640 
 1641     def draw(self, context):
 1642         layout = self.layout
 1643         layout.prop(self, 'label_prop')
 1644         layout.prop(self, 'use_custom_color_prop')
 1645         col = layout.column()
 1646         col.active = self.use_custom_color_prop
 1647         col.prop(self, 'color_prop', text="")
 1648 
 1649     def execute(self, context):
 1650         nodes, links = get_nodes_links(context)
 1651         selected = []
 1652         for node in nodes:
 1653             if node.select == True:
 1654                 selected.append(node)
 1655 
 1656         bpy.ops.node.add_node(type='NodeFrame')
 1657         frm = nodes.active
 1658         frm.label = self.label_prop
 1659         frm.use_custom_color = self.use_custom_color_prop
 1660         frm.color = self.color_prop
 1661 
 1662         for node in selected:
 1663             node.parent = frm
 1664 
 1665         return {'FINISHED'}
 1666 
 1667 
 1668 class NWReloadImages(Operator):
 1669     bl_idname = "node.nw_reload_images"
 1670     bl_label = "Reload Images"
 1671     bl_description = "Update all the image nodes to match their files on disk"
 1672 
 1673     @classmethod
 1674     def poll(cls, context):
 1675         valid = False
 1676         if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
 1677             if context.active_node is not None:
 1678                 for out in context.active_node.outputs:
 1679                     if is_visible_socket(out):
 1680                         valid = True
 1681                         break
 1682         return valid
 1683 
 1684     def execute(self, context):
 1685         nodes, links = get_nodes_links(context)
 1686         image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
 1687         num_reloaded = 0
 1688         for node in nodes:
 1689             if node.type in image_types:
 1690                 if node.type == "TEXTURE":
 1691                     if node.texture:  # node has texture assigned
 1692                         if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
 1693                             if node.texture.image:  # texture has image assigned
 1694                                 node.texture.image.reload()
 1695                                 num_reloaded += 1
 1696                 else:
 1697                     if node.image:
 1698                         node.image.reload()
 1699                         num_reloaded += 1
 1700 
 1701         if num_reloaded:
 1702             self.report({'INFO'}, "Reloaded images")
 1703             print("Reloaded " + str(num_reloaded) + " images")
 1704             force_update(context)
 1705             return {'FINISHED'}
 1706         else:
 1707             self.report({'WARNING'}, "No images found to reload in this node tree")
 1708             return {'CANCELLED'}
 1709 
 1710 
 1711 class NWSwitchNodeType(Operator, NWBase):
 1712     """Switch type of selected nodes """
 1713     bl_idname = "node.nw_swtch_node_type"
 1714     bl_label = "Switch Node Type"
 1715     bl_options = {'REGISTER', 'UNDO'}
 1716 
 1717     to_type: StringProperty(
 1718         name="Switch to type",
 1719         default = '',
 1720     )
 1721 
 1722     def execute(self, context):
 1723         to_type = self.to_type
 1724         if len(to_type) == 0:
 1725             return {'CANCELLED'}
 1726 
 1727         nodes, links = get_nodes_links(context)
 1728         # Those types of nodes will not swap.
 1729         src_excludes = ('NodeFrame')
 1730         # Those attributes of nodes will be copied if possible
 1731         attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
 1732                          'show_options', 'show_preview', 'show_texture',
 1733                          'use_alpha', 'use_clamp', 'use_custom_color', 'location'
 1734                          )
 1735         selected = [n for n in nodes if n.select]
 1736         reselect = []
 1737         for node in [n for n in selected if
 1738                      n.rna_type.identifier not in src_excludes and
 1739                      n.rna_type.identifier != to_type]:
 1740             new_node = nodes.new(to_type)
 1741             for attr in attrs_to_pass:
 1742                 if hasattr(node, attr) and hasattr(new_node, attr):
 1743                     setattr(new_node, attr, getattr(node, attr))
 1744             # set image datablock of dst to image of src
 1745             if hasattr(node, 'image') and hasattr(new_node, 'image'):
 1746                 if node.image:
 1747                     new_node.image = node.image
 1748             # Special cases
 1749             if new_node.type == 'SWITCH':
 1750                 new_node.hide = True
 1751             # Dictionaries: src_sockets and dst_sockets:
 1752             # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
 1753             # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
 1754             # in 'INPUTS' and 'OUTPUTS':
 1755             # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
 1756             # socket entry:
 1757             # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
 1758             src_sockets = {
 1759                 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
 1760                 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
 1761             }
 1762             dst_sockets = {
 1763                 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
 1764                 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
 1765             }
 1766             types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
 1767             types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
 1768             # check src node to set src_sockets values and dst node to set dst_sockets dict values
 1769             for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
 1770                 # Check node's inputs and outputs and fill proper entries in "sockets" dict
 1771                 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
 1772                     # enumerate in inputs, then in outputs
 1773                     # find name, default value and links of socket
 1774                     for i, socket in enumerate(in_out):
 1775                         the_name = socket.name
 1776                         dval = None
 1777                         # Not every socket, especially in outputs has "default_value"
 1778                         if hasattr(socket, 'default_value'):
 1779                             dval = socket.default_value
 1780                         socket_links = []
 1781                         for lnk in socket.links:
 1782                             socket_links.append(lnk)
 1783                         # check type of socket to fill proper keys.
 1784                         for the_type in types_order_one:
 1785                             if socket.type == the_type:
 1786                                 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
 1787                                 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
 1788                                 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
 1789                     # Check which of the types in inputs/outputs is considered to be "main".
 1790                     # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
 1791                     for type_check in types_order_one:
 1792                         if sockets[in_out_name][type_check]:
 1793                             sockets[in_out_name]['MAIN'] = type_check
 1794                             break
 1795 
 1796             matches = {
 1797                 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
 1798                 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
 1799             }
 1800 
 1801             for inout, soctype in (
 1802                     ('INPUTS', 'MAIN',),
 1803                     ('INPUTS', 'SHADER',),
 1804                     ('INPUTS', 'RGBA',),
 1805                     ('INPUTS', 'VECTOR',),
 1806                     ('INPUTS', 'VALUE',),
 1807                     ('OUTPUTS', 'MAIN',),
 1808                     ('OUTPUTS', 'SHADER',),
 1809                     ('OUTPUTS', 'RGBA',),
 1810                     ('OUTPUTS', 'VECTOR',),
 1811                     ('OUTPUTS', 'VALUE',),
 1812             ):
 1813                 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
 1814                     if soctype == 'MAIN':
 1815                         sc = src_sockets[inout][src_sockets[inout]['MAIN']]
 1816                         dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
 1817                     else:
 1818                         sc = src_sockets[inout][soctype]
 1819                         dt = dst_sockets[inout][soctype]
 1820                     # start with 'dt' to determine number of possibilities.
 1821                     for i, soc in enumerate(dt):
 1822                         # if src main has enough entries - match them with dst main sockets by indexes.
 1823                         if len(sc) > i:
 1824                             matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
 1825                         # add 'VALUE_NAME' criterion to inputs.
 1826                         if inout == 'INPUTS' and soctype == 'VALUE':
 1827                             for s in sc:
 1828                                 if s[2] == soc[2]:  # if names match
 1829                                     # append src (index, dval), dst (index, dval)
 1830                                     matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
 1831 
 1832             # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
 1833             # This creates better links when relinking textures.
 1834             if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
 1835                 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
 1836 
 1837             # Pass default values and RELINK:
 1838             for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
 1839                 # INPUTS: Base on matches in proper order.
 1840                 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
 1841                     # pass dvals
 1842                     if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
 1843                         new_node.inputs[dst_i].default_value = src_dval
 1844                     # Special case: switch to math
 1845                     if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
 1846                             new_node.type == 'MATH' and\
 1847                             tp == 'MAIN':
 1848                         new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
 1849                         new_node.inputs[dst_i].default_value = new_dst_dval
 1850                         if node.type == 'MIX_RGB':
 1851                             if node.blend_type in [o[0] for o in operations]:
 1852                                 new_node.operation = node.blend_type
 1853                     # Special case: switch from math to some types
 1854                     if node.type == 'MATH' and\
 1855                             new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
 1856                             tp == 'MAIN':
 1857                         for i in range(3):
 1858                             new_node.inputs[dst_i].default_value[i] = src_dval
 1859                         if new_node.type == 'MIX_RGB':
 1860                             if node.operation in [t[0] for t in blend_types]:
 1861                                 new_node.blend_type = node.operation
 1862                             # Set Fac of MIX_RGB to 1.0
 1863                             new_node.inputs[0].default_value = 1.0
 1864                     # make link only when dst matching input is not linked already.
 1865                     if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
 1866                         in_src_link = node.inputs[src_i].links[0]
 1867                         in_dst_socket = new_node.inputs[dst_i]
 1868                         links.new(in_src_link.from_socket, in_dst_socket)
 1869                         links.remove(in_src_link)
 1870                 # OUTPUTS: Base on matches in proper order.
 1871                 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
 1872                     for out_src_link in node.outputs[src_i].links:
 1873                         out_dst_socket = new_node.outputs[dst_i]
 1874                         links.new(out_dst_socket, out_src_link.to_socket)
 1875             # relink rest inputs if possible, no criteria
 1876             for src_inp in node.inputs:
 1877                 for dst_inp in new_node.inputs:
 1878                     if src_inp.links and not dst_inp.links:
 1879                         src_link = src_inp.links[0]
 1880                         links.new(src_link.from_socket, dst_inp)
 1881                         links.remove(src_link)
 1882             # relink rest outputs if possible, base on node kind if any left.
 1883             for src_o in node.outputs:
 1884                 for out_src_link in src_o.links:
 1885                     for dst_o in new_node.outputs:
 1886                         if src_o.type == dst_o.type:
 1887                             links.new(dst_o, out_src_link.to_socket)
 1888             # relink rest outputs no criteria if any left. Link all from first output.
 1889             for src_o in node.outputs:
 1890                 for out_src_link in src_o.links:
 1891                     if new_node.outputs:
 1892                         links.new(new_node.outputs[0], out_src_link.to_socket)
 1893             nodes.remove(node)
 1894         force_update(context)
 1895         return {'FINISHED'}
 1896 
 1897 
 1898 class NWMergeNodes(Operator, NWBase):
 1899     bl_idname = "node.nw_merge_nodes"
 1900     bl_label = "Merge Nodes"
 1901     bl_description = "Merge Selected Nodes"
 1902     bl_options = {'REGISTER', 'UNDO'}
 1903 
 1904     mode: EnumProperty(
 1905         name="mode",
 1906         description="All possible blend types, boolean operations and math operations",
 1907         items= blend_types + [op for op in geo_combine_operations if op not in blend_types] + [op for op in operations if op not in blend_types],
 1908     )
 1909     merge_type: EnumProperty(
 1910         name="merge type",
 1911         description="Type of Merge to be used",
 1912         items=(
 1913             ('AUTO', 'Auto', 'Automatic Output Type Detection'),
 1914             ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
 1915             ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
 1916             ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
 1917             ('MATH', 'Math Node', 'Merge using Math Nodes'),
 1918             ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
 1919             ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
 1920         ),
 1921     )
 1922 
 1923     # Check if the link connects to a node that is in selected_nodes
 1924     # If not, then check recursively for each link in the nodes outputs.
 1925     # If yes, return True. If the recursion stops without finding a node
 1926     # in selected_nodes, it returns False. The depth is used to prevent
 1927     # getting stuck in a loop because of an already present cycle.
 1928     @staticmethod
 1929     def link_creates_cycle(link, selected_nodes, depth=0)->bool:
 1930         if depth > 255:
 1931             # We're stuck in a cycle, but that cycle was already present,
 1932             # so we return False.
 1933             # NOTE: The number 255 is arbitrary, but seems to work well.
 1934             return False
 1935         node = link.to_node
 1936         if node in selected_nodes:
 1937             return True
 1938         if not node.outputs:
 1939             return False
 1940         for output in node.outputs:
 1941             if output.is_linked:
 1942                 for olink in output.links:
 1943                     if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1):
 1944                         return True
 1945         # None of the outputs found a node in selected_nodes, so there is no cycle.
 1946         return False
 1947 
 1948     # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
 1949     # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
 1950     # be connected. The last one is assumed to be a multi input socket.
 1951     # For convenience the node is returned.
 1952     @staticmethod
 1953     def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices):
 1954         # The y-location of the last node
 1955         loc_y = nodes_list[-1][2]
 1956         if merge_position == 'CENTER':
 1957             # Average the y-location
 1958             for i in range(len(nodes_list)-1):
 1959                 loc_y += nodes_list[i][2]
 1960             loc_y = loc_y/len(nodes_list)
 1961         new_node = nodes.new(node_name)
 1962         new_node.hide = do_hide
 1963         new_node.location.x = loc_x
 1964         new_node.location.y = loc_y
 1965         selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
 1966         prev_links = []
 1967         outputs_for_multi_input = []
 1968         for i,node in enumerate(selected_nodes):
 1969             node.select = False
 1970             # Search for the first node which had output links that do not create
 1971             # a cycle, which we can then reconnect afterwards.
 1972             if prev_links == [] and node.outputs[0].is_linked:
 1973                 prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)]
 1974             # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
 1975             # To get the placement to look right we need to reverse the order in which we connect the
 1976             # outputs to the multi input socket.
 1977             if i < len(socket_indices) - 1:
 1978                 ind = socket_indices[i]
 1979                 links.new(node.outputs[0], new_node.inputs[ind])
 1980             else:
 1981                 outputs_for_multi_input.insert(0, node.outputs[0])
 1982         if outputs_for_multi_input != []:
 1983             ind = socket_indices[-1]
 1984             for output in outputs_for_multi_input:
 1985                 links.new(output, new_node.inputs[ind])
 1986         if prev_links != []:
 1987             for link in prev_links:
 1988                 links.new(new_node.outputs[0], link.to_node.inputs[0])
 1989         return new_node
 1990 
 1991     def execute(self, context):
 1992         settings = context.preferences.addons[__name__].preferences
 1993         merge_hide = settings.merge_hide
 1994         merge_position = settings.merge_position  # 'center' or 'bottom'
 1995 
 1996         do_hide = False
 1997         do_hide_shader = False
 1998         if merge_hide == 'ALWAYS':
 1999             do_hide = True
 2000             do_hide_shader = True
 2001         elif merge_hide == 'NON_SHADER':
 2002             do_hide = True
 2003 
 2004         tree_type = context.space_data.node_tree.type
 2005         if tree_type == 'GEOMETRY':
 2006             node_type = 'GeometryNode'
 2007         if tree_type == 'COMPOSITING':
 2008             node_type = 'CompositorNode'
 2009         elif tree_type == 'SHADER':
 2010             node_type = 'ShaderNode'
 2011         elif tree_type == 'TEXTURE':
 2012             node_type = 'TextureNode'
 2013         nodes, links = get_nodes_links(context)
 2014         mode = self.mode
 2015         merge_type = self.merge_type
 2016         # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
 2017         # 'ZCOMBINE' works only if mode == 'MIX'
 2018         # Setting mode to None prevents trying to add 'ZCOMBINE' node.
 2019         if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
 2020             merge_type = 'MIX'
 2021             mode = 'MIX'
 2022         if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
 2023             merge_type = 'AUTO'
 2024         # The MixRGB node and math nodes used for geometry nodes are of type 'ShaderNode'
 2025         if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
 2026             node_type = 'ShaderNode'
 2027         selected_mix = []  # entry = [index, loc]
 2028         selected_shader = []  # entry = [index, loc]
 2029         selected_geometry = [] # entry = [index, loc]
 2030         selected_math = []  # entry = [index, loc]
 2031         selected_vector = [] # entry = [index, loc]
 2032         selected_z = []  # entry = [index, loc]
 2033         selected_alphaover = []  # entry = [index, loc]
 2034 
 2035         for i, node in enumerate(nodes):
 2036             if node.select and node.outputs:
 2037                 if merge_type == 'AUTO':
 2038                     for (type, types_list, dst) in (
 2039                             ('SHADER', ('MIX', 'ADD'), selected_shader),
 2040                             ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
 2041                             ('RGBA', [t[0] for t in blend_types], selected_mix),
 2042                             ('VALUE', [t[0] for t in operations], selected_math),
 2043                             ('VECTOR', [], selected_vector),
 2044                     ):
 2045                         output = get_first_enabled_output(node)
 2046                         output_type = output.type
 2047                         valid_mode = mode in types_list
 2048                         # When mode is 'MIX' we have to cheat since the mix node is not used in
 2049                         # geometry nodes.
 2050                         if tree_type == 'GEOMETRY':
 2051                             if mode == 'MIX':
 2052                                 if output_type == 'VALUE' and type == 'VALUE':
 2053                                     valid_mode = True
 2054                                 elif output_type == 'VECTOR' and type == 'VECTOR':
 2055                                     valid_mode = True
 2056                                 elif type == 'GEOMETRY':
 2057                                     valid_mode = True
 2058                         # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
 2059                         # Cheat that output type is 'RGBA',
 2060                         # and that 'MIX' exists in math operations list.
 2061                         # This way when selected_mix list is analyzed:
 2062                         # Node data will be appended even though it doesn't meet requirements.
 2063                         elif output_type != 'SHADER' and mode == 'MIX':
 2064                             output_type = 'RGBA'
 2065                             valid_mode = True
 2066                         if output_type == type and valid_mode:
 2067                             dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
 2068                 else:
 2069                     for (type, types_list, dst) in (
 2070                             ('SHADER', ('MIX', 'ADD'), selected_shader),
 2071                             ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
 2072                             ('MIX', [t[0] for t in blend_types], selected_mix),
 2073                             ('MATH', [t[0] for t in operations], selected_math),
 2074                             ('ZCOMBINE', ('MIX', ), selected_z),
 2075                             ('ALPHAOVER', ('MIX', ), selected_alphaover),
 2076                     ):
 2077                         if merge_type == type and mode in types_list:
 2078                             dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
 2079         # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
 2080         # use only 'Mix' nodes for merging.
 2081         # For that we add selected_math list to selected_mix list and clear selected_math.
 2082         if selected_mix and selected_math and merge_type == 'AUTO':
 2083             selected_mix += selected_math
 2084             selected_math = []
 2085         for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]:
 2086             if not nodes_list:
 2087                 continue
 2088             count_before = len(nodes)
 2089             # sort list by loc_x - reversed
 2090             nodes_list.sort(key=lambda k: k[1], reverse=True)
 2091             # get maximum loc_x
 2092             loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
 2093             nodes_list.sort(key=lambda k: k[2], reverse=True)
 2094 
 2095             # Change the node type for math nodes in a geometry node tree.
 2096             if tree_type == 'GEOMETRY':
 2097                 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
 2098                     node_type = 'ShaderNode'
 2099                     if mode == 'MIX':
 2100                         mode = 'ADD'
 2101                 else:
 2102                     node_type = 'GeometryNode'
 2103             if merge_position == 'CENTER':
 2104                 loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2  # average yloc of last two nodes (lowest two)
 2105                 if nodes_list[len(nodes_list) - 1][-1] == True:  # if last node is hidden, mix should be shifted up a bit
 2106                     if do_hide:
 2107                         loc_y += 40
 2108                     else:
 2109                         loc_y += 80
 2110             else:
 2111                 loc_y = nodes_list[len(nodes_list) - 1][2]
 2112             offset_y = 100
 2113             if not do_hide:
 2114                 offset_y = 200
 2115             if nodes_list == selected_shader and not do_hide_shader:
 2116                 offset_y = 150.0
 2117             the_range = len(nodes_list) - 1
 2118             if len(nodes_list) == 1:
 2119                 the_range = 1
 2120             was_multi = False
 2121             for i in range(the_range):
 2122                 if nodes_list == selected_mix:
 2123                     add_type = node_type + 'MixRGB'
 2124                     add = nodes.new(add_type)
 2125                     add.blend_type = mode
 2126                     if mode != 'MIX':
 2127                         add.inputs[0].default_value = 1.0
 2128                     add.show_preview = False
 2129                     add.hide = do_hide
 2130                     if do_hide:
 2131                         loc_y = loc_y - 50
 2132                     first = 1
 2133                     second = 2
 2134                     add.width_hidden = 100.0
 2135                 elif nodes_list == selected_math:
 2136                     add_type = node_type + 'Math'
 2137                     add = nodes.new(add_type)
 2138                     add.operation = mode
 2139                     add.hide = do_hide
 2140                     if do_hide:
 2141                         loc_y = loc_y - 50
 2142                     first = 0
 2143                     second = 1
 2144                     add.width_hidden = 100.0
 2145                 elif nodes_list == selected_shader:
 2146                     if mode == 'MIX':
 2147                         add_type = node_type + 'MixShader'
 2148                         add = nodes.new(add_type)
 2149                         add.hide = do_hide_shader
 2150                         if do_hide_shader:
 2151                             loc_y = loc_y - 50
 2152                         first = 1
 2153                         second = 2
 2154                         add.width_hidden = 100.0
 2155                     elif mode == 'ADD':
 2156                         add_type = node_type + 'AddShader'
 2157                         add = nodes.new(add_type)
 2158                         add.hide = do_hide_shader
 2159                         if do_hide_shader:
 2160                             loc_y = loc_y - 50
 2161                         first = 0
 2162                         second = 1
 2163                         add.width_hidden = 100.0
 2164                 elif nodes_list == selected_geometry:
 2165                     if mode in ('JOIN', 'MIX'):
 2166                         add_type = node_type + 'JoinGeometry'
 2167                         add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0])
 2168                     else:
 2169                         add_type = node_type + 'Boolean'
 2170                         indices = [0,1] if mode == 'DIFFERENCE' else [1]
 2171                         add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices)
 2172                         add.operation = mode
 2173                     was_multi = True
 2174                     break
 2175                 elif nodes_list == selected_vector:
 2176                     add_type = node_type + 'VectorMath'
 2177                     add = nodes.new(add_type)
 2178                     add.operation = mode
 2179                     add.hide = do_hide
 2180                     if do_hide:
 2181                         loc_y = loc_y - 50
 2182                     first = 0
 2183                     second = 1
 2184                     add.width_hidden = 100.0
 2185                 elif nodes_list == selected_z:
 2186                     add = nodes.new('CompositorNodeZcombine')
 2187                     add.show_preview = False
 2188                     add.hide = do_hide
 2189                     if do_hide:
 2190                         loc_y = loc_y - 50
 2191                     first = 0
 2192                     second = 2
 2193                     add.width_hidden = 100.0
 2194                 elif nodes_list == selected_alphaover:
 2195                     add = nodes.new('CompositorNodeAlphaOver')
 2196                     add.show_preview = False
 2197                     add.hide = do_hide
 2198                     if do_hide:
 2199                         loc_y = loc_y - 50
 2200                     first = 1
 2201                     second = 2
 2202                     add.width_hidden = 100.0
 2203                 add.location = loc_x, loc_y
 2204                 loc_y += offset_y
 2205                 add.select = True
 2206 
 2207             # This has already been handled separately
 2208             if was_multi:
 2209                 continue
 2210             count_adds = i + 1
 2211             count_after = len(nodes)
 2212             index = count_after - 1
 2213             first_selected = nodes[nodes_list[0][0]]
 2214             # "last" node has been added as first, so its index is count_before.
 2215             last_add = nodes[count_before]
 2216             # Create list of invalid indexes.
 2217             invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
 2218 
 2219             # Special case:
 2220             # Two nodes were selected and first selected has no output links, second selected has output links.
 2221             # Then add links from last add to all links 'to_socket' of out links of second selected.
 2222             first_selected_output = get_first_enabled_output(first_selected)
 2223             if len(nodes_list) == 2:
 2224                 if not first_selected_output.links:
 2225                     second_selected = nodes[nodes_list[1][0]]
 2226                     for ss_link in second_selected.outputs[0].links:
 2227                         # Prevent cyclic dependencies when nodes to be merged are linked to one another.
 2228                         # Link only if "to_node" index not in invalid indexes list.
 2229                         if not self.link_creates_cycle(ss_link, invalid_nodes):
 2230                             links.new(last_add.outputs[0], ss_link.to_socket)
 2231             # add links from last_add to all links 'to_socket' of out links of first selected.
 2232             for fs_link in first_selected_output.links:
 2233                 # Link only if "to_node" index not in invalid indexes list.
 2234                 if not self.link_creates_cycle(fs_link, invalid_nodes):
 2235                     links.new(last_add.outputs[0], fs_link.to_socket)
 2236             # add link from "first" selected and "first" add node
 2237             node_to = nodes[count_after - 1]
 2238             links.new(first_selected_output, node_to.inputs[first])
 2239             if node_to.type == 'ZCOMBINE':
 2240                 for fs_out in first_selected.outputs:
 2241                     if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
 2242                         links.new(fs_out, node_to.inputs[1])
 2243                         break
 2244             # add links between added ADD nodes and between selected and ADD nodes
 2245             for i in range(count_adds):
 2246                 if i < count_adds - 1:
 2247                     node_from = nodes[index]
 2248                     node_to = nodes[index - 1]
 2249                     node_to_input_i = first
 2250                     node_to_z_i = 1  # if z combine - link z to first z input
 2251                     links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
 2252                     if node_to.type == 'ZCOMBINE':
 2253                         for from_out in node_from.outputs:
 2254                             if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
 2255                                 links.new(from_out, node_to.inputs[node_to_z_i])
 2256                 if len(nodes_list) > 1:
 2257                     node_from = nodes[nodes_list[i + 1][0]]
 2258                     node_to = nodes[index]
 2259                     node_to_input_i = second
 2260                     node_to_z_i = 3  # if z combine - link z to second z input
 2261                     links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
 2262                     if node_to.type == 'ZCOMBINE':
 2263                         for from_out in node_from.outputs:
 2264                             if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
 2265                                 links.new(from_out, node_to.inputs[node_to_z_i])
 2266                 index -= 1
 2267             # set "last" of added nodes as active
 2268             nodes.active = last_add
 2269             for i, x, y, dx, h in nodes_list:
 2270                 nodes[i].select = False
 2271 
 2272         return {'FINISHED'}
 2273 
 2274 
 2275 class NWBatchChangeNodes(Operator, NWBase):
 2276     bl_idname = "node.nw_batch_change"
 2277     bl_label = "Batch Change"
 2278     bl_description = "Batch Change Blend Type and Math Operation"
 2279     bl_options = {'REGISTER', 'UNDO'}
 2280 
 2281     blend_type: EnumProperty(
 2282         name="Blend Type",
 2283         items=blend_types + navs,
 2284     )
 2285     operation: EnumProperty(
 2286         name="Operation",
 2287         items=operations + navs,
 2288     )
 2289 
 2290     def execute(self, context):
 2291         blend_type = self.blend_type
 2292         operation = self.operation
 2293         for node in context.selected_nodes:
 2294             if node.type == 'MIX_RGB' or node.bl_idname == 'GeometryNodeAttributeMix':
 2295                 if not blend_type in [nav[0] for nav in navs]:
 2296                     node.blend_type = blend_type
 2297                 else:
 2298                     if blend_type == 'NEXT':
 2299                         index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
 2300                         #index = blend_types.index(node.blend_type)
 2301                         if index == len(blend_types) - 1:
 2302                             node.blend_type = blend_types[0][0]
 2303                         else:
 2304                             node.blend_type = blend_types[index + 1][0]
 2305 
 2306                     if blend_type == 'PREV':
 2307                         index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
 2308                         if index == 0:
 2309                             node.blend_type = blend_types[len(blend_types) - 1][0]
 2310                         else:
 2311                             node.blend_type = blend_types[index - 1][0]
 2312 
 2313             if node.type == 'MATH' or node.bl_idname == 'GeometryNodeAttributeMath':
 2314                 if not operation in [nav[0] for nav in navs]:
 2315                     node.operation = operation
 2316                 else:
 2317                     if operation == 'NEXT':
 2318                         index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
 2319                         #index = operations.index(node.operation)
 2320                         if index == len(operations) - 1:
 2321                             node.operation = operations[0][0]
 2322                         else:
 2323                             node.operation = operations[index + 1][0]
 2324 
 2325                     if operation == 'PREV':
 2326                         index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
 2327                         #index = operations.index(node.operation)
 2328                         if index == 0:
 2329                             node.operation = operations[len(operations) - 1][0]
 2330                         else:
 2331                             node.operation = operations[index - 1][0]
 2332 
 2333         return {'FINISHED'}
 2334 
 2335 
 2336 class NWChangeMixFactor(Operator, NWBase):
 2337     bl_idname = "node.nw_factor"
 2338     bl_label = "Change Factor"
 2339     bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
 2340     bl_options = {'REGISTER', 'UNDO'}
 2341 
 2342     # option: Change factor.
 2343     # If option is 1.0 or 0.0 - set to 1.0 or 0.0
 2344     # Else - change factor by option value.
 2345     option: FloatProperty()
 2346 
 2347     def execute(self, context):
 2348         nodes, links = get_nodes_links(context)
 2349         option = self.option
 2350         selected = []  # entry = index
 2351         for si, node in enumerate(nodes):
 2352             if node.select:
 2353                 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
 2354                     selected.append(si)
 2355 
 2356         for si in selected:
 2357             fac = nodes[si].inputs[0]
 2358             nodes[si].hide = False
 2359             if option in {0.0, 1.0}:
 2360                 fac.default_value = option
 2361             else:
 2362                 fac.default_value += option
 2363 
 2364         return {'FINISHED'}
 2365 
 2366 
 2367 class NWCopySettings(Operator, NWBase):
 2368     bl_idname = "node.nw_copy_settings"
 2369     bl_label = "Copy Settings"
 2370     bl_description = "Copy Settings of Active Node to Selected Nodes"
 2371     bl_options = {'REGISTER', 'UNDO'}
 2372 
 2373     @classmethod
 2374     def poll(cls, context):
 2375         valid = False
 2376         if nw_check(context):
 2377             if (
 2378                     context.active_node is not None and
 2379                     context.active_node.type != 'FRAME'
 2380             ):
 2381                 valid = True
 2382         return valid
 2383 
 2384     def execute(self, context):
 2385         node_active = context.active_node
 2386         node_selected = context.selected_nodes
 2387 
 2388         # Error handling
 2389         if not (len(node_selected) > 1):
 2390             self.report({'ERROR'}, "2 nodes must be selected at least")
 2391             return {'CANCELLED'}
 2392 
 2393         # Check if active node is in the selection
 2394         selected_node_names = [n.name for n in node_selected]
 2395         if node_active.name not in selected_node_names:
 2396             self.report({'ERROR'}, "No active node")
 2397             return {'CANCELLED'}
 2398 
 2399         # Get nodes in selection by type
 2400         valid_nodes = [n for n in node_selected if n.type == node_active.type]
 2401 
 2402         if not (len(valid_nodes) > 1) and node_active:
 2403             self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
 2404             return {'CANCELLED'}
 2405 
 2406         if len(valid_nodes) != len(node_selected):
 2407             # Report nodes that are not valid
 2408             valid_node_names = [n.name for n in valid_nodes]
 2409             not_valid_names = list(set(selected_node_names) - set(valid_node_names))
 2410             self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
 2411 
 2412         # Reference original
 2413         orig = node_active
 2414         #node_selected_names = [n.name for n in node_selected]
 2415 
 2416         # Output list
 2417         success_names = []
 2418 
 2419         # Deselect all nodes
 2420         for i in node_selected:
 2421             i.select = False
 2422 
 2423         # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
 2424         # Run through all other nodes
 2425         for node in valid_nodes[1:]:
 2426 
 2427             # Check for frame node
 2428             parent = node.parent if node.parent else None
 2429             node_loc = [node.location.x, node.location.y]
 2430 
 2431             # Select original to duplicate
 2432             orig.select = True
 2433 
 2434             # Duplicate selected node
 2435             bpy.ops.node.duplicate()
 2436             new_node = context.selected_nodes[0]
 2437 
 2438             # Deselect copy
 2439             new_node.select = False
 2440 
 2441             # Properties to copy
 2442             node_tree = node.id_data
 2443             props_to_copy = 'bl_idname name location height width'.split(' ')
 2444 
 2445             # Input and outputs
 2446             reconnections = []
 2447             mappings = chain.from_iterable([node.inputs, node.outputs])
 2448             for i in (i for i in mappings if i.is_linked):
 2449                 for L in i.links:
 2450                     reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
 2451 
 2452             # Properties
 2453             props = {j: getattr(node, j) for j in props_to_copy}
 2454             props_to_copy.pop(0)
 2455 
 2456             for prop in props_to_copy:
 2457                 setattr(new_node, prop, props[prop])
 2458 
 2459             # Get the node tree to remove the old node
 2460             nodes = node_tree.nodes
 2461             nodes.remove(node)
 2462             new_node.name = props['name']
 2463 
 2464             if parent:
 2465                 new_node.parent = parent
 2466                 new_node.location = node_loc
 2467 
 2468             for str_from, str_to in reconnections:
 2469                 node_tree.links.new(eval(str_from), eval(str_to))
 2470 
 2471             success_names.append(new_node.name)
 2472 
 2473         orig.select = True
 2474         node_tree.nodes.active = orig
 2475         self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
 2476         return {'FINISHED'}
 2477 
 2478 
 2479 class NWCopyLabel(Operator, NWBase):
 2480     bl_idname = "node.nw_copy_label"
 2481     bl_label = "Copy Label"
 2482     bl_options = {'REGISTER', 'UNDO'}
 2483 
 2484     option: EnumProperty(
 2485         name="option",
 2486         description="Source of name of label",
 2487         items=(
 2488             ('FROM_ACTIVE', 'from active', 'from active node',),
 2489             ('FROM_NODE', 'from node', 'from node linked to selected node'),
 2490             ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
 2491         )
 2492     )
 2493 
 2494     def execute(self, context):
 2495         nodes, links = get_nodes_links(context)
 2496         option = self.option
 2497         active = nodes.active
 2498         if option == 'FROM_ACTIVE':
 2499             if active:
 2500                 src_label = active.label
 2501                 for node in [n for n in nodes if n.select and nodes.active != n]:
 2502                     node.label = src_label
 2503         elif option == 'FROM_NODE':
 2504             selected = [n for n in nodes if n.select]
 2505             for node in selected:
 2506                 for input in node.inputs:
 2507                     if input.links:
 2508                         src = input.links[0].from_node
 2509                         node.label = src.label
 2510                         break
 2511         elif option == 'FROM_SOCKET':
 2512             selected = [n for n in nodes if n.select]
 2513             for node in selected:
 2514                 for input in node.inputs:
 2515                     if input.links:
 2516                         src = input.links[0].from_socket
 2517                         node.label = src.name
 2518                         break
 2519 
 2520         return {'FINISHED'}
 2521 
 2522 
 2523 class NWClearLabel(Operator, NWBase):
 2524     bl_idname = "node.nw_clear_label"
 2525     bl_label = "Clear Label"
 2526     bl_options = {'REGISTER', 'UNDO'}
 2527 
 2528     option: BoolProperty()
 2529 
 2530     def execute(self, context):
 2531         nodes, links = get_nodes_links(context)
 2532         for node in [n for n in nodes if n.select]:
 2533             node.label = ''
 2534 
 2535         return {'FINISHED'}
 2536 
 2537     def invoke(self, context, event):
 2538         if self.option:
 2539             return self.execute(context)
 2540         else:
 2541             return context.window_manager.invoke_confirm(self, event)
 2542 
 2543 
 2544 class NWModifyLabels(Operator, NWBase):
 2545     """Modify Labels of all selected nodes"""
 2546     bl_idname = "node.nw_modify_labels"
 2547     bl_label = "Modify Labels"
 2548     bl_options = {'REGISTER', 'UNDO'}
 2549 
 2550     prepend: StringProperty(
 2551         name="Add to Beginning"
 2552     )
 2553     append: StringProperty(
 2554         name="Add to End"
 2555     )
 2556     replace_from: StringProperty(
 2557         name="Text to Replace"
 2558     )
 2559     replace_to: StringProperty(
 2560         name="Replace with"
 2561     )
 2562 
 2563     def execute(self, context):
 2564         nodes, links = get_nodes_links(context)
 2565         for node in [n for n in nodes if n.select]:
 2566             node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
 2567 
 2568         return {'FINISHED'}
 2569 
 2570     def invoke(self, context, event):
 2571         self.prepend = ""
 2572         self.append = ""
 2573         self.remove = ""
 2574         return context.window_manager.invoke_props_dialog(self)
 2575 
 2576 
 2577 class NWAddTextureSetup(Operator, NWBase):
 2578     bl_idname = "node.nw_add_texture"
 2579     bl_label = "Texture Setup"
 2580     bl_description = "Add Texture Node Setup to Selected Shaders"
 2581     bl_options = {'REGISTER', 'UNDO'}
 2582 
 2583     add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
 2584 
 2585     @classmethod
 2586     def poll(cls, context):
 2587         if nw_check(context):
 2588             space = context.space_data
 2589             if space.tree_type == 'ShaderNodeTree':
 2590                 return True
 2591         return False
 2592 
 2593     def execute(self, context):
 2594         nodes, links = get_nodes_links(context)
 2595 
 2596         texture_types = [x.nodetype for x in
 2597                          get_nodes_from_category('Texture', context)]
 2598         selected_nodes = [n for n in nodes if n.select]
 2599 
 2600         for node in selected_nodes:
 2601             if not node.inputs:
 2602                 continue
 2603 
 2604             input_index = 0
 2605             target_input = node.inputs[0]
 2606             for input in node.inputs:
 2607                 if input.enabled:
 2608                     input_index += 1
 2609                     if not input.is_linked:
 2610                         target_input = input
 2611                         break
 2612             else:
 2613                 self.report({'WARNING'}, "No free inputs for node: " + node.name)
 2614                 continue
 2615 
 2616             x_offset = 0
 2617             padding = 40.0
 2618             locx = node.location.x
 2619             locy = node.location.y - (input_index * padding)
 2620 
 2621             is_texture_node = node.rna_type.identifier in texture_types
 2622             use_environment_texture = node.type == 'BACKGROUND'
 2623 
 2624             # Add an image texture before normal shader nodes.
 2625             if not is_texture_node:
 2626                 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
 2627                 image_texture_node = nodes.new(image_texture_type)
 2628                 x_offset = x_offset + image_texture_node.width + padding
 2629                 image_texture_node.location = [locx - x_offset, locy]
 2630                 nodes.active = image_texture_node
 2631                 links.new(image_texture_node.outputs[0], target_input)
 2632 
 2633                 # The mapping setup following this will connect to the firrst input of this image texture.
 2634                 target_input = image_texture_node.inputs[0]
 2635 
 2636             node.select = False
 2637 
 2638             if is_texture_node or self.add_mapping:
 2639                 # Add Mapping node.
 2640                 mapping_node = nodes.new('ShaderNodeMapping')
 2641                 x_offset = x_offset + mapping_node.width + padding
 2642                 mapping_node.location = [locx - x_offset, locy]
 2643                 links.new(mapping_node.outputs[0], target_input)
 2644 
 2645                 # Add Texture Coordinates node.
 2646                 tex_coord_node = nodes.new('ShaderNodeTexCoord')
 2647                 x_offset = x_offset + tex_coord_node.width + padding
 2648                 tex_coord_node.location = [locx - x_offset, locy]
 2649 
 2650                 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
 2651                 use_generated_coordinates = is_procedural_texture or use_environment_texture
 2652                 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
 2653                 links.new(tex_coord_output, mapping_node.inputs[0])
 2654 
 2655         return {'FINISHED'}
 2656 
 2657 
 2658 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
 2659     bl_idname = "node.nw_add_textures_for_principled"
 2660     bl_label = "Principled Texture Setup"
 2661     bl_description = "Add Texture Node Setup for Principled BSDF"
 2662     bl_options = {'REGISTER', 'UNDO'}
 2663 
 2664     directory: StringProperty(
 2665         name='Directory',
 2666         subtype='DIR_PATH',
 2667         default='',
 2668         description='Folder to search in for image files'
 2669     )
 2670     files: CollectionProperty(
 2671         type=bpy.types.OperatorFileListElement,
 2672         options={'HIDDEN', 'SKIP_SAVE'}
 2673     )
 2674 
 2675     relative_path: BoolProperty(
 2676         name='Relative Path',
 2677         description='Set the file path relative to the blend file, when possible',
 2678         default=True
 2679     )
 2680 
 2681     order = [
 2682         "filepath",
 2683         "files",
 2684     ]
 2685 
 2686     def draw(self, context):
 2687         layout = self.layout
 2688         layout.alignment = 'LEFT'
 2689 
 2690         layout.prop(self, 'relative_path')
 2691 
 2692     @classmethod
 2693     def poll(cls, context):
 2694         valid = False
 2695         if nw_check(context):
 2696             space = context.space_data
 2697             if space.tree_type == 'ShaderNodeTree':
 2698                 valid = True
 2699         return valid
 2700 
 2701     def execute(self, context):
 2702         # Check if everything is ok
 2703         if not self.directory:
 2704             self.report({'INFO'}, 'No Folder Selected')
 2705             return {'CANCELLED'}
 2706         if not self.files[:]:
 2707             self.report({'INFO'}, 'No Files Selected')
 2708             return {'CANCELLED'}
 2709 
 2710         nodes, links = get_nodes_links(context)
 2711         active_node = nodes.active
 2712         if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
 2713             self.report({'INFO'}, 'Select Principled BSDF')
 2714             return {'CANCELLED'}
 2715 
 2716         # Helper_functions
 2717         def split_into__components(fname):
 2718             # Split filename into components
 2719             # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
 2720             # Remove extension
 2721             fname = path.splitext(fname)[0]
 2722             # Remove digits
 2723             fname = ''.join(i for i in fname if not i.isdigit())
 2724             # Separate CamelCase by space
 2725             fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",fname)
 2726             # Replace common separators with SPACE
 2727             separators = ['_', '.', '-', '__', '--', '#']
 2728             for sep in separators:
 2729                 fname = fname.replace(sep, ' ')
 2730 
 2731             components = fname.split(' ')
 2732             components = [c.lower() for c in components]
 2733             return components
 2734 
 2735         # Filter textures names for texturetypes in filenames
 2736         # [Socket Name, [abbreviations and keyword list], Filename placeholder]
 2737         tags = context.preferences.addons[__name__].preferences.principled_tags
 2738         normal_abbr = tags.normal.split(' ')
 2739         bump_abbr = tags.bump.split(' ')
 2740         gloss_abbr = tags.gloss.split(' ')
 2741         rough_abbr = tags.rough.split(' ')
 2742         socketnames = [
 2743         ['Displacement', tags.displacement.split(' '), None],
 2744         ['Base Color', tags.base_color.split(' '), None],
 2745         ['Subsurface Color', tags.sss_color.split(' '), None],
 2746         ['Metallic', tags.metallic.split(' '), None],
 2747         ['Specular', tags.specular.split(' '), None],
 2748         ['Roughness', rough_abbr + gloss_abbr, None],
 2749         ['Normal', normal_abbr + bump_abbr, None],
 2750         ['Transmission', tags.transmission.split(' '), None],
 2751         ['Emission', tags.emission.split(' '), None],
 2752         ['Alpha', tags.alpha.split(' '), None],
 2753         ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
 2754         ]
 2755 
 2756         # Look through texture_types and set value as filename of first matched file
 2757         def match_files_to_socket_names():
 2758             for sname in socketnames:
 2759                 for file in self.files:
 2760                     fname = file.name
 2761                     filenamecomponents = split_into__components(fname)
 2762                     matches = set(sname[1]).intersection(set(filenamecomponents))
 2763                     # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
 2764                     if matches:
 2765                         sname[2] = fname
 2766                         break
 2767 
 2768         match_files_to_socket_names()
 2769         # Remove socketnames without found files
 2770         socketnames = [s for s in socketnames if s[2]
 2771                        and path.exists(self.directory+s[2])]
 2772         if not socketnames:
 2773             self.report({'INFO'}, 'No matching images found')
 2774             print('No matching images found')
 2775             return {'CANCELLED'}
 2776 
 2777         # Don't override path earlier as os.path is used to check the absolute path
 2778         import_path = self.directory
 2779         if self.relative_path:
 2780             if bpy.data.filepath:
 2781                 try:
 2782                     import_path = bpy.path.relpath(self.directory)
 2783                 except ValueError:
 2784                     pass
 2785 
 2786         # Add found images
 2787         print('\nMatched Textures:')
 2788         texture_nodes = []
 2789         disp_texture = None
 2790         ao_texture = None
 2791         normal_node = None
 2792         roughness_node = None
 2793         for i, sname in enumerate(socketnames):
 2794             print(i, sname[0], sname[2])
 2795 
 2796             # DISPLACEMENT NODES
 2797             if sname[0] == 'Displacement':
 2798                 disp_texture = nodes.new(type='ShaderNodeTexImage')
 2799                 img = bpy.data.images.load(path.join(import_path, sname[2]))
 2800                 disp_texture.image = img
 2801                 disp_texture.label = 'Displacement'
 2802                 if disp_texture.image:
 2803                     disp_texture.image.colorspace_settings.is_data = True
 2804 
 2805                 # Add displacement offset nodes
 2806                 disp_node = nodes.new(type='ShaderNodeDisplacement')
 2807                 # Align the Displacement node under the active Principled BSDF node
 2808                 disp_node.location = active_node.location + Vector((100, -700))
 2809                 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
 2810 
 2811                 # TODO Turn on true displacement in the material
 2812                 # Too complicated for now
 2813 
 2814                 # Find output node
 2815                 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
 2816                 if output_node:
 2817                     if not output_node[0].inputs[2].is_linked:
 2818                         link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
 2819 
 2820                 continue
 2821 
 2822             # AMBIENT OCCLUSION TEXTURE
 2823             if sname[0] == 'Ambient Occlusion':
 2824                 ao_texture = nodes.new(type='ShaderNodeTexImage')
 2825                 img = bpy.data.images.load(path.join(import_path, sname[2]))
 2826                 ao_texture.image = img
 2827                 ao_texture.label = sname[0]
 2828                 if ao_texture.image:
 2829                     ao_texture.image.colorspace_settings.is_data = True
 2830 
 2831                 continue
 2832 
 2833             if not active_node.inputs[sname[0]].is_linked:
 2834                 # No texture node connected -> add texture node with new image
 2835                 texture_node = nodes.new(type='ShaderNodeTexImage')
 2836                 img = bpy.data.images.load(path.join(import_path, sname[2]))
 2837                 texture_node.image = img
 2838 
 2839                 # NORMAL NODES
 2840                 if sname[0] == 'Normal':
 2841                     # Test if new texture node is normal or bump map
 2842                     fname_components = split_into__components(sname[2])
 2843                     match_normal = set(normal_abbr).intersection(set(fname_components))
 2844                     match_bump = set(bump_abbr).intersection(set(fname_components))
 2845                     if match_normal:
 2846                         # If Normal add normal node in between
 2847                         normal_node = nodes.new(type='ShaderNodeNormalMap')
 2848                         link = links.new(normal_node.inputs[1], texture_node.outputs[0])
 2849                     elif match_bump:
 2850                         # If Bump add bump node in between
 2851                         normal_node = nodes.new(type='ShaderNodeBump')
 2852                         link = links.new(normal_node.inputs[2], texture_node.outputs[0])
 2853 
 2854                     link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
 2855                     normal_node_texture = texture_node
 2856 
 2857                 elif sname[0] == 'Roughness':
 2858                     # Test if glossy or roughness map
 2859                     fname_components = split_into__components(sname[2])
 2860                     match_rough = set(rough_abbr).intersection(set(fname_components))
 2861                     match_gloss = set(gloss_abbr).intersection(set(fname_components))
 2862 
 2863                     if match_rough:
 2864                         # If Roughness nothing to to
 2865                         link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
 2866 
 2867                     elif match_gloss:
 2868                         # If Gloss Map add invert node
 2869                         invert_node = nodes.new(type='ShaderNodeInvert')
 2870                         link = links.new(invert_node.inputs[1], texture_node.outputs[0])
 2871 
 2872                         link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
 2873                         roughness_node = texture_node
 2874 
 2875                 else:
 2876                     # This is a simple connection Texture --> Input slot
 2877                     link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
 2878 
 2879                 # Use non-color for all but 'Base Color' Textures
 2880                 if not sname[0] in ['Base Color', 'Emission'] and texture_node.image:
 2881                     texture_node.image.colorspace_settings.is_data = True
 2882 
 2883             else:
 2884                 # If already texture connected. add to node list for alignment
 2885                 texture_node = active_node.inputs[sname[0]].links[0].from_node
 2886 
 2887             # This are all connected texture nodes
 2888             texture_nodes.append(texture_node)
 2889             texture_node.label = sname[0]
 2890 
 2891         if disp_texture:
 2892             texture_nodes.append(disp_texture)
 2893 
 2894         if ao_texture:
 2895             # We want the ambient occlusion texture to be the top most texture node
 2896             texture_nodes.insert(0, ao_texture)
 2897 
 2898         # Alignment
 2899         for i, texture_node in enumerate(texture_nodes):
 2900             offset = Vector((-550, (i * -280) + 200))
 2901             texture_node.location = active_node.location + offset
 2902 
 2903         if normal_node:
 2904             # Extra alignment if normal node was added
 2905             normal_node.location = normal_node_texture.location + Vector((300, 0))
 2906 
 2907         if roughness_node:
 2908             # Alignment of invert node if glossy map
 2909             invert_node.location = roughness_node.location + Vector((300, 0))
 2910 
 2911         # Add texture input + mapping
 2912         mapping = nodes.new(type='ShaderNodeMapping')
 2913         mapping.location = active_node.location + Vector((-1050, 0))
 2914         if len(texture_nodes) > 1:
 2915             # If more than one texture add reroute node in between
 2916             reroute = nodes.new(type='NodeReroute')
 2917             texture_nodes.append(reroute)
 2918             tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
 2919             reroute.location = tex_coords + Vector((-50, -120))
 2920             for texture_node in texture_nodes:
 2921                 link = links.new(texture_node.inputs[0], reroute.outputs[0])
 2922             link = links.new(reroute.inputs[0], mapping.outputs[0])
 2923         else:
 2924             link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
 2925 
 2926         # Connect texture_coordiantes to mapping node
 2927         texture_input = nodes.new(type='ShaderNodeTexCoord')
 2928         texture_input.location = mapping.location + Vector((-200, 0))
 2929         link = links.new(mapping.inputs[0], texture_input.outputs[2])
 2930 
 2931         # Create frame around tex coords and mapping
 2932         frame = nodes.new(type='NodeFrame')
 2933         frame.label = 'Mapping'
 2934         mapping.parent = frame
 2935         texture_input.parent = frame
 2936         frame.update()
 2937 
 2938         # Create frame around texture nodes
 2939         frame = nodes.new(type='NodeFrame')
 2940         frame.label = 'Textures'
 2941         for tnode in texture_nodes:
 2942             tnode.parent = frame
 2943         frame.update()
 2944 
 2945         # Just to be sure
 2946         active_node.select = False
 2947         nodes.update()
 2948         links.update()
 2949         force_update(context)
 2950         return {'FINISHED'}
 2951 
 2952 
 2953 class NWAddReroutes(Operator, NWBase):
 2954     """Add Reroute Nodes and link them to outputs of selected nodes"""
 2955     bl_idname = "node.nw_add_reroutes"
 2956     bl_label = "Add Reroutes"
 2957     bl_description = "Add Reroutes to Outputs"
 2958     bl_options = {'REGISTER', 'UNDO'}
 2959 
 2960     option: EnumProperty(
 2961         name="option",
 2962         items=[
 2963             ('ALL', 'to all', 'Add to all outputs'),
 2964             ('LOOSE', 'to loose', 'Add only to loose outputs'),
 2965             ('LINKED', 'to linked', 'Add only to linked outputs'),
 2966         ]
 2967     )
 2968 
 2969     def execute(self, context):
 2970         tree_type = context.space_data.node_tree.type
 2971         option = self.option
 2972         nodes, links = get_nodes_links(context)
 2973         # output valid when option is 'all' or when 'loose' output has no links
 2974         valid = False
 2975         post_select = []  # nodes to be selected after execution
 2976         # create reroutes and recreate links
 2977         for node in [n for n in nodes if n.select]:
 2978             if node.outputs:
 2979                 x = node.location.x
 2980                 y = node.location.y
 2981                 width = node.width
 2982                 # unhide 'REROUTE' nodes to avoid issues with location.y
 2983                 if node.type == 'REROUTE':
 2984                     node.hide = False
 2985                 # When node is hidden - width_hidden not usable.
 2986                 # Hack needed to calculate real width
 2987                 if node.hide:
 2988                     bpy.ops.node.select_all(action='DESELECT')
 2989                     helper = nodes.new('NodeReroute')
 2990                     helper.select = True
 2991                     node.select = True
 2992                     # resize node and helper to zero. Then check locations to calculate width
 2993                     bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
 2994                     width = 2.0 * (helper.location.x - node.location.x)
 2995                     # restore node location
 2996                     node.location = x, y
 2997                     # delete helper
 2998                     node.select = False
 2999                     # only helper is selected now
 3000                     bpy.ops.node.delete()
 3001                 x = node.location.x + width + 20.0
 3002                 if node.type != 'REROUTE':
 3003                     y -= 35.0
 3004                 y_offset = -22.0
 3005                 loc = x, y
 3006             reroutes_count = 0  # will be used when aligning reroutes added to hidden nodes
 3007             for out_i, output in enumerate(node.outputs):
 3008                 pass_used = False  # initial value to be analyzed if 'R_LAYERS'
 3009                 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
 3010                 if node.type != 'R_LAYERS':
 3011                     pass_used = True
 3012                 else:  # if 'R_LAYERS' check if output represent used render pass
 3013                     node_scene = node.scene
 3014                     node_layer = node.layer
 3015                     # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
 3016                     if output.name == 'Alpha':
 3017                         pass_used = True
 3018                     else:
 3019                         # check entries in global 'rl_outputs' variable
 3020                         for rlo in rl_outputs:
 3021                             if output.name in {rlo.output_name, rlo.exr_output_name}:
 3022                                 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
 3023                                 break
 3024                 if pass_used:
 3025                     valid = ((option == 'ALL') or
 3026                              (option == 'LOOSE' and not output.links) or
 3027                              (option == 'LINKED' and output.links))
 3028                     # Add reroutes only if valid, but offset location in all cases.
 3029                     if valid:
 3030                         n = nodes.new('NodeReroute')
 3031                         nodes.active = n
 3032                         for link in output.links:
 3033                             links.new(n.outputs[0], link.to_socket)
 3034                         links.new(output, n.inputs[0])
 3035                         n.location = loc
 3036                         post_select.append(n)
 3037                     reroutes_count += 1
 3038                     y += y_offset
 3039                     loc = x, y
 3040             # disselect the node so that after execution of script only newly created nodes are selected
 3041             node.select = False
 3042             # nicer reroutes distribution along y when node.hide
 3043             if node.hide:
 3044                 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
 3045                 for reroute in [r for r in nodes if r.select]:
 3046                     reroute.location.y -= y_translate
 3047             for node in post_select:
 3048                 node.select = True
 3049 
 3050         return {'FINISHED'}
 3051 
 3052 
 3053 class NWLinkActiveToSelected(Operator, NWBase):
 3054     """Link active node to selected nodes basing on various criteria"""
 3055     bl_idname = "node.nw_link_active_to_selected"
 3056     bl_label = "Link Active Node to Selected"
 3057     bl_options = {'REGISTER', 'UNDO'}
 3058 
 3059     replace: BoolProperty()
 3060     use_node_name: BoolProperty()
 3061     use_outputs_names: BoolProperty()
 3062 
 3063     @classmethod
 3064     def poll(cls, context):
 3065         valid = False
 3066         if nw_check(context):
 3067             if context.active_node is not None:
 3068                 if context.active_node.select:
 3069                     valid = True
 3070         return valid
 3071 
 3072     def execute(self, context):
 3073         nodes, links = get_nodes_links(context)
 3074         replace = self.replace
 3075         use_node_name = self.use_node_name
 3076         use_outputs_names = self.use_outputs_names
 3077         active = nodes.active
 3078         selected = [node for node in nodes if node.select and node != active]
 3079         outputs = []  # Only usable outputs of active nodes will be stored here.
 3080         for out in active.outputs:
 3081             if active.type != 'R_LAYERS':
 3082                 outputs.append(out)
 3083             else:
 3084                 # 'R_LAYERS' node type needs special handling.
 3085                 # outputs of 'R_LAYERS' are callable even if not seen in UI.
 3086                 # Only outputs that represent used passes should be taken into account
 3087                 # Check if pass represented by output is used.
 3088                 # global 'rl_outputs' list will be used for that
 3089                 for rlo in rl_outputs:
 3090                     pass_used = False  # initial value. Will be set to True if pass is used
 3091                     if out.name == 'Alpha':
 3092                         # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
 3093                         pass_used = True
 3094                     elif out.name in {rlo.output_name, rlo.exr_output_name}:
 3095                         # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
 3096                         pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
 3097                         break
 3098                 if pass_used:
 3099                     outputs.append(out)
 3100         doit = True  # Will be changed to False when links successfully added to previous output.
 3101         for out in outputs:
 3102             if doit:
 3103                 for node in selected:
 3104                     dst_name = node.name  # Will be compared with src_name if needed.
 3105                     # When node has label - use it as dst_name
 3106                     if node.label:
 3107                         dst_name = node.label
 3108                     valid = True  # Initial value. Will be changed to False if names don't match.
 3109                     src_name = dst_name  # If names not used - this assignment will keep valid = True.
 3110                     if use_node_name:
 3111                         # Set src_name to source node name or label
 3112                         src_name = active.name
 3113                         if active.label:
 3114                             src_name = active.label
 3115                     elif use_outputs_names:
 3116                         src_name = (out.name, )
 3117                         for rlo in rl_outputs:
 3118                             if out.name in {rlo.output_name, rlo.exr_output_name}:
 3119                                 src_name = (rlo.output_name, rlo.exr_output_name)
 3120                     if dst_name not in src_name:
 3121                         valid = False
 3122                     if valid:
 3123                         for input in node.inputs:
 3124                             if input.type == out.type or node.type == 'REROUTE':
 3125                                 if replace or not input.is_linked:
 3126                                     links.new(out, input)
 3127                                     if not use_node_name and not use_outputs_names:
 3128                                         doit = False
 3129                                     break
 3130 
 3131         return {'FINISHED'}
 3132 
 3133 
 3134 class NWAlignNodes(Operator, NWBase):
 3135     '''Align the selected nodes neatly in a row/column'''
 3136     bl_idname = "node.nw_align_nodes"
 3137     bl_label = "Align Nodes"
 3138     bl_options = {'REGISTER', 'UNDO'}
 3139     margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
 3140 
 3141     def execute(self, context):
 3142         nodes, links = get_nodes_links(context)
 3143         margin = self.margin
 3144 
 3145         selection = []
 3146         for node in nodes:
 3147             if node.select and node.type != 'FRAME':
 3148                 selection.append(node)
 3149 
 3150         # If no nodes are selected, align all nodes
 3151         active_loc = None
 3152         if not selection:
 3153             selection = nodes
 3154         elif nodes.active in selection:
 3155             active_loc = copy(nodes.active.location)  # make a copy, not a reference
 3156 
 3157         # Check if nodes should be laid out horizontally or vertically
 3158         x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]  # use dimension to get center of node, not corner
 3159         y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
 3160         x_range = max(x_locs) - min(x_locs)
 3161         y_range = max(y_locs) - min(y_locs)
 3162         mid_x = (max(x_locs) + min(x_locs)) / 2
 3163         mid_y = (max(y_locs) + min(y_locs)) / 2
 3164         horizontal = x_range > y_range
 3165 
 3166         # Sort selection by location of node mid-point
 3167         if horizontal:
 3168             selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
 3169         else:
 3170             selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
 3171 
 3172         # Alignment
 3173         current_pos = 0
 3174         for node in selection:
 3175             current_margin = margin
 3176             current_margin = current_margin * 0.5 if node.hide else current_margin  # use a smaller margin for hidden nodes
 3177 
 3178             if horizontal:
 3179                 node.location.x = current_pos
 3180                 current_pos += current_margin + node.dimensions.x
 3181                 node.location.y = mid_y + (node.dimensions.y / 2)
 3182             else:
 3183                 node.location.y = current_pos
 3184                 current_pos -= (current_margin * 0.3) + node.dimensions.y  # use half-margin for vertical alignment
 3185                 node.location.x = mid_x - (node.dimensions.x / 2)
 3186 
 3187         # If active node is selected, center nodes around it
 3188         if active_loc is not None:
 3189             active_loc_diff = active_loc - nodes.active.location
 3190             for node in selection:
 3191                 node.location += active_loc_diff
 3192         else:  # Position nodes centered around where they used to be
 3193             locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
 3194             new_mid = (max(locs) + min(locs)) / 2
 3195             for node in selection:
 3196                 if horizontal:
 3197                     node.location.x += (mid_x - new_mid)
 3198                 else:
 3199                     node.location.y += (mid_y - new_mid)
 3200 
 3201         return {'FINISHED'}
 3202 
 3203 
 3204 class NWSelectParentChildren(Operator, NWBase):
 3205     bl_idname = "node.nw_select_parent_child"
 3206     bl_label = "Select Parent or Children"
 3207     bl_options = {'REGISTER', 'UNDO'}
 3208 
 3209     option: EnumProperty(
 3210         name="option",
 3211         items=(
 3212             ('PARENT', 'Select Parent', 'Select Parent Frame'),
 3213             ('CHILD', 'Select Children', 'Select members of selected frame'),
 3214         )
 3215     )
 3216 
 3217     def execute(self, context):
 3218         nodes, links = get_nodes_links(context)
 3219         option = self.option
 3220         selected = [node for node in nodes if node.select]
 3221         if option == 'PARENT':
 3222             for sel in selected:
 3223                 parent = sel.parent
 3224                 if parent:
 3225                     parent.select = True
 3226         else:  # option == 'CHILD'
 3227             for sel in selected:
 3228                 children = [node for node in nodes if node.parent == sel]
 3229                 for kid in children:
 3230                     kid.select = True
 3231 
 3232         return {'FINISHED'}
 3233 
 3234 
 3235 class NWDetachOutputs(Operator, NWBase):
 3236     """Detach outputs of selected node leaving inputs linked"""
 3237     bl_idname = "node.nw_detach_outputs"
 3238     bl_label = "Detach Outputs"
 3239     bl_options = {'REGISTER', 'UNDO'}
 3240 
 3241     def execute(self, context):
 3242         nodes, links = get_nodes_links(context)
 3243         selected = context.selected_nodes
 3244         bpy.ops.node.duplicate_move_keep_inputs()
 3245         new_nodes = context.selected_nodes
 3246         bpy.ops.node.select_all(action="DESELECT")
 3247         for node in selected:
 3248             node.select = True
 3249         bpy.ops.node.delete_reconnect()
 3250         for new_node in new_nodes:
 3251             new_node.select = True
 3252         bpy.ops.transform.translate('INVOKE_DEFAULT')
 3253 
 3254         return {'FINISHED'}
 3255 
 3256 
 3257 class NWLinkToOutputNode(Operator):
 3258     """Link to Composite node or Material Output node"""
 3259     bl_idname = "node.nw_link_out"
 3260     bl_label = "Connect to Output"
 3261     bl_options = {'REGISTER', 'UNDO'}
 3262 
 3263     @classmethod
 3264     def poll(cls, context):
 3265         valid = False
 3266         if nw_check(context):
 3267             if context.active_node is not None:
 3268                 for out in context.active_node.outputs:
 3269                     if is_visible_socket(out):
 3270                         valid = True
 3271                         break
 3272         return valid
 3273 
 3274     def execute(self, context):
 3275         nodes, links = get_nodes_links(context)
 3276         active = nodes.active
 3277         output_index = None
 3278         tree_type = context.space_data.tree_type
 3279         shader_outputs = {'OBJECT':    'ShaderNodeOutputMaterial',
 3280                           'WORLD':     'ShaderNodeOutputWorld',
 3281                           'LINESTYLE': 'ShaderNodeOutputLineStyle'}
 3282         output_type = {
 3283             'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
 3284             'CompositorNodeTree': 'CompositorNodeComposite',
 3285             'TextureNodeTree': 'TextureNodeOutput',
 3286             'GeometryNodeTree': 'NodeGroupOutput',
 3287         }[tree_type]
 3288         for node in nodes:
 3289             # check whether the node is an output node and,
 3290             # if supported, whether it's the active one
 3291             if node.rna_type.identifier == output_type \
 3292                and (node.is_active_output if hasattr(node, 'is_active_output')
 3293                     else True):
 3294                 output_node = node
 3295                 break
 3296         else:  # No output node exists
 3297             bpy.ops.node.select_all(action="DESELECT")
 3298             output_node = nodes.new(output_type)
 3299             output_node.location.x = active.location.x + active.dimensions.x + 80
 3300             output_node.location.y = active.location.y
 3301 
 3302         if active.outputs:
 3303             for i, output in enumerate(active.outputs):
 3304                 if is_visible_socket(output):
 3305                     output_index = i
 3306                     break
 3307             for i, output in enumerate(active.outputs):
 3308                 if output.type == output_node.inputs[0].type and is_visible_socket(output):
 3309                     output_index = i
 3310                     break
 3311 
 3312             out_input_index = 0
 3313             if tree_type == 'ShaderNodeTree':
 3314                 if active.outputs[output_index].name == 'Volume':
 3315                     out_input_index = 1
 3316                 elif active.outputs[output_index].name == 'Displacement':
 3317                     out_input_index = 2
 3318             elif tree_type == 'GeometryNodeTree':
 3319                 if active.outputs[output_index].type != 'GEOMETRY':
 3320                     return {'CANCELLED'}
 3321             links.new(active.outputs[output_index], output_node.inputs[out_input_index])
 3322 
 3323         force_update(context)  # viewport render does not update
 3324 
 3325         return {'FINISHED'}
 3326 
 3327 
 3328 class NWMakeLink(Operator, NWBase):
 3329     """Make a link from one socket to another"""
 3330     bl_idname = 'node.nw_make_link'
 3331     bl_label = 'Make Link'
 3332     bl_options = {'REGISTER', 'UNDO'}
 3333     from_socket: IntProperty()
 3334     to_socket: IntProperty()
 3335 
 3336     def execute(self, context):
 3337         nodes, links = get_nodes_links(context)
 3338 
 3339         n1 = nodes[context.scene.NWLazySource]
 3340         n2 = nodes[context.scene.NWLazyTarget]
 3341 
 3342         links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
 3343 
 3344         force_update(context)
 3345 
 3346         return {'FINISHED'}
 3347 
 3348 
 3349 class NWCallInputsMenu(Operator, NWBase):
 3350     """Link from this output"""
 3351     bl_idname = 'node.nw_call_inputs_menu'
 3352     bl_label = 'Make Link'
 3353     bl_options = {'REGISTER', 'UNDO'}
 3354     from_socket: IntProperty()
 3355 
 3356     def execute(self, context):
 3357         nodes, links = get_nodes_links(context)
 3358 
 3359         context.scene.NWSourceSocket = self.from_socket
 3360 
 3361         n1 = nodes[context.scene.NWLazySource]
 3362         n2 = nodes[context.scene.NWLazyTarget]
 3363         if len(n2.inputs) > 1:
 3364             bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
 3365         elif len(n2.inputs) == 1:
 3366             links.new(n1.outputs[self.from_socket], n2.inputs[0])
 3367         return {'FINISHED'}
 3368 
 3369 
 3370 class NWAddSequence(Operator, NWBase, ImportHelper):
 3371     """Add an Image Sequence"""
 3372     bl_idname = 'node.nw_add_sequence'
 3373     bl_label = 'Import Image Sequence'
 3374     bl_options = {'REGISTER', 'UNDO'}
 3375 
 3376     directory: StringProperty(
 3377         subtype="DIR_PATH"
 3378     )
 3379     filename: StringProperty(
 3380         subtype="FILE_NAME"
 3381     )
 3382     files: CollectionProperty(
 3383         type=bpy.types.OperatorFileListElement,
 3384         options={'HIDDEN', 'SKIP_SAVE'}
 3385     )
 3386     relative_path: BoolProperty(
 3387         name='Relative Path',
 3388         description='Set the file path relative to the blend file, when possible',
 3389         default=True
 3390     )
 3391 
 3392     def draw(self, context):
 3393         layout = self.layout
 3394         layout.alignment = 'LEFT'
 3395 
 3396         layout.prop(self, 'relative_path')
 3397 
 3398     def execute(self, context):
 3399         nodes, links = get_nodes_links(context)
 3400         directory = self.directory
 3401         filename = self.filename
 3402         files = self.files
 3403         tree = context.space_data.node_tree
 3404 
 3405         # DEBUG
 3406         # print ("\nDIR:", directory)
 3407         # print ("FN:", filename)
 3408         # print ("Fs:", list(f.name for f in files), '\n')
 3409 
 3410         if tree.type == 'SHADER':
 3411             node_type = "ShaderNodeTexImage"
 3412         elif tree.type == 'COMPOSITING':
 3413             node_type = "CompositorNodeImage"
 3414         else:
 3415             self.report({'ERROR'}, "Unsupported Node Tree type!")
 3416             return {'CANCELLED'}
 3417 
 3418         if not files[0].name and not filename:
 3419             self.report({'ERROR'}, "No file chosen")
 3420             return {'CANCELLED'}
 3421         elif files[0].name and (not filename or not path.exists(directory+filename)):
 3422             # User has selected multiple files without an active one, or the active one is non-existant
 3423             filename = files[0].name
 3424 
 3425         if not path.exists(directory+filename):
 3426             self.report({'ERROR'}, filename+" does not exist!")
 3427             return {'CANCELLED'}
 3428 
 3429         without_ext = '.'.join(filename.split('.')[:-1])
 3430 
 3431         # if last digit isn't a number, it's not a sequence
 3432         if not without_ext[-1].isdigit():
 3433             self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
 3434             return {'CANCELLED'}
 3435 
 3436 
 3437         extension = filename.split('.')[-1]
 3438         reverse = without_ext[::-1] # reverse string
 3439 
 3440         count_numbers = 0
 3441         for char in reverse:
 3442             if char.isdigit():
 3443                 count_numbers += 1
 3444             else:
 3445                 break
 3446 
 3447         without_num = without_ext[:count_numbers*-1]
 3448 
 3449         files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
 3450 
 3451         num_frames = len(files)
 3452 
 3453         nodes_list = [node for node in nodes]
 3454         if nodes_list:
 3455             nodes_list.sort(key=lambda k: k.location.x)
 3456             xloc = nodes_list[0].location.x - 220  # place new nodes at far left
 3457             yloc = 0
 3458             for node in nodes:
 3459                 node.select = False
 3460                 yloc += node_mid_pt(node, 'y')
 3461             yloc = yloc/len(nodes)
 3462         else:
 3463             xloc = 0
 3464             yloc = 0
 3465 
 3466         name_with_hashes = without_num + "#"*count_numbers + '.' + extension
 3467 
 3468         bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
 3469         node = nodes.active
 3470         node.label = name_with_hashes
 3471 
 3472         filepath = directory+(without_ext+'.'+extension)
 3473         if self.relative_path:
 3474             if bpy.data.filepath:
 3475                 try:
 3476                     filepath = bpy.path.relpath(filepath)
 3477                 except ValueError:
 3478                     pass
 3479 
 3480         img = bpy.data.images.load(filepath)
 3481         img.source = 'SEQUENCE'
 3482         img.name = name_with_hashes
 3483         node.image = img
 3484         image_user = node.image_user if tree.type == 'SHADER' else node
 3485         image_user.frame_offset = int(files[0][len(without_num)+len(directory):-1*(len(extension)+1)]) - 1  # separate the number from the file name of the first  file
 3486         image_user.frame_duration = num_frames
 3487 
 3488         return {'FINISHED'}
 3489 
 3490 
 3491 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
 3492     """Add multiple images at once"""
 3493     bl_idname = 'node.nw_add_multiple_images'
 3494     bl_label = 'Open Selected Images'
 3495     bl_options = {'REGISTER', 'UNDO'}
 3496     directory: StringProperty(
 3497         subtype="DIR_PATH"
 3498     )
 3499     files: CollectionProperty(
 3500         type=bpy.types.OperatorFileListElement,
 3501         options={'HIDDEN', 'SKIP_SAVE'}
 3502     )
 3503 
 3504     def execute(self, context):
 3505         nodes, links = get_nodes_links(context)
 3506 
 3507         xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
 3508 
 3509         if context.space_data.node_tree.type == 'SHADER':
 3510             node_type = "ShaderNodeTexImage"
 3511         elif context.space_data.node_tree.type == 'COMPOSITING':
 3512             node_type = "CompositorNodeImage"
 3513         else:
 3514             self.report({'ERROR'}, "Unsupported Node Tree type!")
 3515             return {'CANCELLED'}
 3516 
 3517         new_nodes = []
 3518         for f in self.files:
 3519             fname = f.name
 3520 
 3521             node = nodes.new(node_type)
 3522             new_nodes.append(node)
 3523             node.label = fname
 3524             node.hide = True
 3525             node.width_hidden = 100
 3526             node.location.x = xloc
 3527             node.location.y = yloc
 3528             yloc -= 40
 3529 
 3530             img = bpy.data.images.load(self.directory+fname)
 3531             node.image = img
 3532 
 3533         # shift new nodes up to center of tree
 3534         list_size = new_nodes[0].location.y - new_nodes[-1].location.y
 3535         for node in nodes:
 3536             if node in new_nodes:
 3537                 node.select = True
 3538                 node.location.y += (list_size/2)
 3539             else:
 3540                 node.select = False
 3541         return {'FINISHED'}
 3542 
 3543 
 3544 class NWViewerFocus(bpy.types.Operator):
 3545     """Set the viewer tile center to the mouse position"""
 3546     bl_idname = "node.nw_viewer_focus"
 3547     bl_label = "Viewer Focus"
 3548 
 3549     x: bpy.props.IntProperty()
 3550     y: bpy.props.IntProperty()
 3551 
 3552     @classmethod
 3553     def poll(cls, context):
 3554         return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
 3555 
 3556     def execute(self, context):
 3557         return {'FINISHED'}
 3558 
 3559     def invoke(self, context, event):
 3560         render = context.scene.render
 3561         space = context.space_data
 3562         percent = render.resolution_percentage*0.01
 3563 
 3564         nodes, links = get_nodes_links(context)
 3565         viewers = [n for n in nodes if n.type == 'VIEWER']
 3566 
 3567         if viewers:
 3568             mlocx = event.mouse_region_x
 3569             mlocy = event.mouse_region_y
 3570             select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
 3571 
 3572             if not 'FINISHED' in select_node:  # only run if we're not clicking on a node
 3573                 region_x = context.region.width
 3574                 region_y = context.region.height
 3575 
 3576                 region_center_x = context.region.width  / 2
 3577                 region_center_y = context.region.height / 2
 3578 
 3579                 bd_x = render.resolution_x * percent * space.backdrop_zoom
 3580                 bd_y = render.resolution_y * percent * space.backdrop_zoom
 3581 
 3582                 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
 3583                 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
 3584 
 3585                 margin_x = region_center_x - backdrop_center_x
 3586                 margin_y = region_center_y - backdrop_center_y
 3587 
 3588                 abs_mouse_x = (mlocx - margin_x) / bd_x
 3589                 abs_mouse_y = (mlocy - margin_y) / bd_y
 3590 
 3591                 for node in viewers:
 3592                     node.center_x = abs_mouse_x
 3593                     node.center_y = abs_mouse_y
 3594             else:
 3595                 return {'PASS_THROUGH'}
 3596 
 3597         return self.execute(context)
 3598 
 3599 
 3600 class NWSaveViewer(bpy.types.Operator, ExportHelper):
 3601     """Save the current viewer node to an image file"""
 3602     bl_idname = "node.nw_save_viewer"
 3603     bl_label = "Save This Image"
 3604     filepath: StringProperty(subtype="FILE_PATH")
 3605     filename_ext: EnumProperty(
 3606             name="Format",
 3607             description="Choose the file format to save to",
 3608             items=(('.bmp', "BMP", ""),
 3609                    ('.rgb', 'IRIS', ""),
 3610                    ('.png', 'PNG', ""),
 3611                    ('.jpg', 'JPEG', ""),
 3612                    ('.jp2', 'JPEG2000', ""),
 3613                    ('.tga', 'TARGA', ""),
 3614                    ('.cin', 'CINEON', ""),
 3615                    ('.dpx', 'DPX', ""),
 3616                    ('.exr', 'OPEN_EXR', ""),
 3617                    ('.hdr', 'HDR', ""),
 3618                    ('.tif', 'TIFF', "")),
 3619             default='.png',
 3620             )
 3621 
 3622     @classmethod
 3623     def poll(cls, context):
 3624         valid = False
 3625         if nw_check(context):
 3626             if context.space_data.tree_type == 'CompositorNodeTree':
 3627                 if "Viewer Node" in [i.name for i in bpy.data.images]:
 3628                     if sum(bpy.data.images["Viewer Node"].size) > 0:  # False if not connected or connected but no image
 3629                         valid = True
 3630         return valid
 3631 
 3632     def execute(self, context):
 3633         fp = self.filepath
 3634         if fp:
 3635             formats = {
 3636                        '.bmp': 'BMP',
 3637                        '.rgb': 'IRIS',
 3638                        '.png': 'PNG',
 3639                        '.jpg': 'JPEG',
 3640                        '.jpeg': 'JPEG',
 3641                        '.jp2': 'JPEG2000',
 3642                        '.tga': 'TARGA',
 3643                        '.cin': 'CINEON',
 3644                        '.dpx': 'DPX',
 3645                        '.exr': 'OPEN_EXR',
 3646                        '.hdr': 'HDR',
 3647                        '.tiff': 'TIFF',
 3648                        '.tif': 'TIFF'}
 3649             basename, ext = path.splitext(fp)
 3650             old_render_format = context.scene.render.image_settings.file_format
 3651             context.scene.render.image_settings.file_format = formats[self.filename_ext]
 3652             context.area.type = "IMAGE_EDITOR"
 3653             context.area.spaces[0].image = bpy.data.images['Viewer Node']
 3654             context.area.spaces[0].image.save_render(fp)
 3655             context.area.type = "NODE_EDITOR"
 3656             context.scene.render.image_settings.file_format = old_render_format
 3657             return {'FINISHED'}
 3658 
 3659 
 3660 class NWResetNodes(bpy.types.Operator):
 3661     """Reset Nodes in Selection"""
 3662     bl_idname = "node.nw_reset_nodes"
 3663     bl_label = "Reset Nodes"
 3664     bl_options = {'REGISTER', 'UNDO'}
 3665 
 3666     @classmethod
 3667     def poll(cls, context):
 3668         space = context.space_data
 3669         return space.type == 'NODE_EDITOR'
 3670 
 3671     def execute(self, context):
 3672         node_active = context.active_node
 3673         node_selected = context.selected_nodes
 3674         node_ignore = ["FRAME","REROUTE", "GROUP"]
 3675 
 3676         # Check if one node is selected at least
 3677         if not (len(node_selected) > 0):
 3678             self.report({'ERROR'}, "1 node must be selected at least")
 3679             return {'CANCELLED'}
 3680 
 3681         active_node_name = node_active.name if node_active.select else None
 3682         valid_nodes = [n for n in node_selected if n.type not in node_ignore]
 3683 
 3684         # Create output lists
 3685         selected_node_names = [n.name for n in node_selected]
 3686         success_names = []
 3687 
 3688         # Reset all valid children in a frame
 3689         node_active_is_frame = False
 3690         if len(node_selected) == 1 and node_active.type == "FRAME":
 3691             node_tree = node_active.id_data
 3692             children = [n for n in node_tree.nodes if n.parent == node_active]
 3693             if children:
 3694                 valid_nodes = [n for n in children if n.type not in node_ignore]
 3695                 selected_node_names = [n.name for n in children if n.type not in node_ignore]
 3696                 node_active_is_frame = True
 3697 
 3698         # Check if valid nodes in selection
 3699         if not (len(valid_nodes) > 0):
 3700             # Check for frames only
 3701             frames_selected = [n for n in node_selected if n.type == "FRAME"]
 3702             if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
 3703                 self.report({'ERROR'}, "Please select only 1 frame to reset")
 3704             else:
 3705                 self.report({'ERROR'}, "No valid node(s) in selection")
 3706             return {'CANCELLED'}
 3707 
 3708         # Report nodes that are not valid
 3709         if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
 3710             valid_node_names = [n.name for n in valid_nodes]
 3711             not_valid_names = list(set(selected_node_names) - set(valid_node_names))
 3712             self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
 3713 
 3714         # Deselect all nodes
 3715         for i in node_selected:
 3716             i.select = False
 3717 
 3718         # Run through all valid nodes
 3719         for node in valid_nodes:
 3720 
 3721             parent = node.parent if node.parent else None
 3722             node_loc = [node.location.x, node.location.y]
 3723 
 3724             node_tree = node.id_data
 3725             props_to_copy = 'bl_idname name location height width'.split(' ')
 3726 
 3727             reconnections = []
 3728             mappings = chain.from_iterable([node.inputs, node.outputs])
 3729             for i in (i for i in mappings if i.is_linked):
 3730                 for L in i.links:
 3731                     reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
 3732 
 3733             props = {j: getattr(node, j) for j in props_to_copy}
 3734 
 3735             new_node = node_tree.nodes.new(props['bl_idname'])
 3736             props_to_copy.pop(0)
 3737 
 3738             for prop in props_to_copy:
 3739                 setattr(new_node, prop, props[prop])
 3740 
 3741             nodes = node_tree.nodes
 3742             nodes.remove(node)
 3743             new_node.name = props['name']
 3744 
 3745             if parent:
 3746                 new_node.parent = parent
 3747                 new_node.location = node_loc
 3748 
 3749             for str_from, str_to in reconnections:
 3750                 node_tree.links.new(eval(str_from), eval(str_to))
 3751 
 3752             new_node.select = False
 3753             success_names.append(new_node.name)
 3754 
 3755         # Reselect all nodes
 3756         if selected_node_names and node_active_is_frame is False:
 3757             for i in selected_node_names:
 3758                 node_tree.nodes[i].select = True
 3759 
 3760         if active_node_name is not None:
 3761             node_tree.nodes[active_node_name].select = True
 3762             node_tree.nodes.active = node_tree.nodes[active_node_name]
 3763 
 3764         self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
 3765         return {'FINISHED'}
 3766 
 3767 
 3768 #
 3769 #  P A N E L
 3770 #
 3771 
 3772 def drawlayout(context, layout, mode='non-panel'):
 3773     tree_type = context.space_data.tree_type
 3774 
 3775     col = layout.column(align=True)
 3776     col.menu(NWMergeNodesMenu.bl_idname)
 3777     col.separator()
 3778 
 3779     col = layout.column(align=True)
 3780     col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
 3781     col.separator()
 3782 
 3783     if tree_type == 'ShaderNodeTree':
 3784         col = layout.column(align=True)
 3785         col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
 3786         col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
 3787         col.separator()
 3788 
 3789     col = layout.column(align=True)
 3790     col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
 3791     col.operator(NWSwapLinks.bl_idname)
 3792     col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
 3793     col.separator()
 3794 
 3795     col = layout.column(align=True)
 3796     col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
 3797     if tree_type != 'GeometryNodeTree':
 3798         col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
 3799     col.separator()
 3800 
 3801     col = layout.column(align=True)
 3802     if mode == 'panel':
 3803         row = col.row(align=True)
 3804         row.operator(NWClearLabel.bl_idname).option = True
 3805         row.operator(NWModifyLabels.bl_idname)
 3806     else:
 3807         col.operator(NWClearLabel.bl_idname).option = True
 3808         col.operator(NWModifyLabels.bl_idname)
 3809     col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
 3810     col.separator()
 3811     col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
 3812     col.separator()
 3813 
 3814     col = layout.column(align=True)
 3815     if tree_type == 'CompositorNodeTree':
 3816         col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
 3817     if tree_type != 'GeometryNodeTree':
 3818         col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
 3819     col.separator()
 3820 
 3821     col = layout.column(align=True)
 3822     col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
 3823     col.separator()
 3824 
 3825     col = layout.column(align=True)
 3826     col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
 3827     col.separator()
 3828 
 3829     col = layout.column(align=True)
 3830     col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
 3831     col.separator()
 3832 
 3833 
 3834 class NodeWranglerPanel(Panel, NWBase):
 3835     bl_idname = "NODE_PT_nw_node_wrangler"
 3836     bl_space_type = 'NODE_EDITOR'
 3837     bl_label = "Node Wrangler"
 3838     bl_region_type = "UI"
 3839     bl_category = "Node Wrangler"
 3840 
 3841     prepend: StringProperty(
 3842         name='prepend',
 3843     )
 3844     append: StringProperty()
 3845     remove: StringProperty()
 3846 
 3847     def draw(self, context):
 3848         self.layout.label(text="(Quick access: Shift+W)")
 3849         drawlayout(context, self.layout, mode='panel')
 3850 
 3851 
 3852 #
 3853 #  M E N U S
 3854 #
 3855 class NodeWranglerMenu(Menu, NWBase):
 3856     bl_idname = "NODE_MT_nw_node_wrangler_menu"
 3857     bl_label = "Node Wrangler"
 3858 
 3859     def draw(self, context):
 3860         self.layout.operator_context = 'INVOKE_DEFAULT'
 3861         drawlayout(context, self.layout)
 3862 
 3863 
 3864 class NWMergeNodesMenu(Menu, NWBase):
 3865     bl_idname = "NODE_MT_nw_merge_nodes_menu"
 3866     bl_label = "Merge Selected Nodes"
 3867 
 3868     def draw(self, context):
 3869         type = context.space_data.tree_type
 3870         layout = self.layout
 3871         if type == 'ShaderNodeTree':
 3872             layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
 3873         if type == 'GeometryNodeTree':
 3874             layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes")
 3875             layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
 3876         else:
 3877             layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
 3878             layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
 3879             props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
 3880             props.mode = 'MIX'
 3881             props.merge_type = 'ZCOMBINE'
 3882             props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
 3883             props.mode = 'MIX'
 3884             props.merge_type = 'ALPHAOVER'
 3885 
 3886 class NWMergeGeometryMenu(Menu, NWBase):
 3887     bl_idname = "NODE_MT_nw_merge_geometry_menu"
 3888     bl_label = "Merge Selected Nodes using Geometry Nodes"
 3889     def draw(self, context):
 3890         layout = self.layout
 3891         # The boolean node + Join Geometry node
 3892         for type, name, description in geo_combine_operations:
 3893             props = layout.operator(NWMergeNodes.bl_idname, text=name)
 3894             props.mode = type
 3895             props.merge_type = 'GEOMETRY'
 3896 
 3897 class NWMergeShadersMenu(Menu, NWBase):
 3898     bl_idname = "NODE_MT_nw_merge_shaders_menu"
 3899     bl_label = "Merge Selected Nodes using Shaders"
 3900 
 3901     def draw(self, context):
 3902         layout = self.layout
 3903         for type in ('MIX', 'ADD'):
 3904             props = layout.operator(NWMergeNodes.bl_idname, text=type)
 3905             props.mode = type
 3906             props.merge_type = 'SHADER'
 3907 
 3908 
 3909 class NWMergeMixMenu(Menu, NWBase):
 3910     bl_idname = "NODE_MT_nw_merge_mix_menu"
 3911     bl_label = "Merge Selected Nodes using Mix"
 3912 
 3913     def draw(self, context):
 3914         layout = self.layout
 3915         for type, name, description in blend_types:
 3916             props = layout.operator(NWMergeNodes.bl_idname, text=name)
 3917             props.mode = type
 3918             props.merge_type = 'MIX'
 3919 
 3920 
 3921 class NWConnectionListOutputs(Menu, NWBase):
 3922     bl_idname = "NODE_MT_nw_connection_list_out"
 3923     bl_label = "From:"
 3924 
 3925     def draw(self, context):
 3926         layout = self.layout
 3927         nodes, links = get_nodes_links(context)
 3928 
 3929         n1 = nodes[context.scene.NWLazySource]
 3930         for index, output in enumerate(n1.outputs):
 3931             # Only show sockets that are exposed.
 3932             if output.enabled:
 3933                 layout.operator(NWCallInputsMenu.bl_idname, text=output.name, icon="RADIOBUT_OFF").from_socket=index
 3934 
 3935 
 3936 class NWConnectionListInputs(Menu, NWBase):
 3937     bl_idname = "NODE_MT_nw_connection_list_in"
 3938     bl_label = "To:"
 3939 
 3940     def draw(self, context):
 3941         layout = self.layout
 3942         nodes, links = get_nodes_links(context)
 3943 
 3944         n2 = nodes[context.scene.NWLazyTarget]
 3945 
 3946         for index, input in enumerate(n2.inputs):
 3947             # Only show sockets that are exposed.
 3948             # This prevents, for example, the scale value socket
 3949             # of the vector math node being added to the list when
 3950             # the mode is not 'SCALE'.
 3951             if input.enabled:
 3952                 op = layout.operator(NWMakeLink.bl_idname, text=input.name, icon="FORWARD")
 3953                 op.from_socket = context.scene.NWSourceSocket
 3954                 op.to_socket = index
 3955 
 3956 
 3957 class NWMergeMathMenu(Menu, NWBase):
 3958     bl_idname = "NODE_MT_nw_merge_math_menu"
 3959     bl_label = "Merge Selected Nodes using Math"
 3960 
 3961     def draw(self, context):
 3962         layout = self.layout
 3963         for type, name, description in operations:
 3964             props = layout.operator(NWMergeNodes.bl_idname, text=name)
 3965             props.mode = type
 3966             props.merge_type = 'MATH'
 3967 
 3968 
 3969 class NWBatchChangeNodesMenu(Menu, NWBase):
 3970     bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
 3971     bl_label = "Batch Change Selected Nodes"
 3972 
 3973     def draw(self, context):
 3974         layout = self.layout
 3975         layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
 3976         layout.menu(NWBatchChangeOperationMenu.bl_idname)
 3977 
 3978 
 3979 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
 3980     bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
 3981     bl_label = "Batch Change Blend Type"
 3982 
 3983     def draw(self, context):
 3984         layout = self.layout
 3985         for type, name, description in blend_types:
 3986             props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
 3987             props.blend_type = type
 3988             props.operation = 'CURRENT'
 3989 
 3990 
 3991 class NWBatchChangeOperationMenu(Menu, NWBase):
 3992     bl_idname = "NODE_MT_nw_batch_change_operation_menu"
 3993     bl_label = "Batch Change Math Operation"
 3994 
 3995     def draw(self, context):
 3996         layout = self.layout
 3997         for type, name, description in operations:
 3998             props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
 3999             props.blend_type = 'CURRENT'
 4000             props.operation = type
 4001 
 4002 
 4003 class NWCopyToSelectedMenu(Menu, NWBase):
 4004     bl_idname = "NODE_MT_nw_copy_node_properties_menu"
 4005     bl_label = "Copy to Selected"
 4006 
 4007     def draw(self, context):
 4008         layout = self.layout
 4009         layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
 4010         layout.menu(NWCopyLabelMenu.bl_idname)
 4011 
 4012 
 4013 class NWCopyLabelMenu(Menu, NWBase):
 4014     bl_idname = "NODE_MT_nw_copy_label_menu"
 4015     bl_label = "Copy Label"
 4016 
 4017     def draw(self, context):
 4018         layout = self.layout
 4019         layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
 4020         layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
 4021         layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
 4022 
 4023 
 4024 class NWAddReroutesMenu(Menu, NWBase):
 4025     bl_idname = "NODE_MT_nw_add_reroutes_menu"
 4026     bl_label = "Add Reroutes"
 4027     bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
 4028 
 4029     def draw(self, context):
 4030         layout = self.layout
 4031         layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
 4032         layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
 4033         layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
 4034 
 4035 
 4036 class NWLinkActiveToSelectedMenu(Menu, NWBase):
 4037     bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
 4038     bl_label = "Link Active to Selected"
 4039 
 4040     def draw(self, context):
 4041         layout = self.layout
 4042         layout.menu(NWLinkStandardMenu.bl_idname)
 4043         layout.menu(NWLinkUseNodeNameMenu.bl_idname)
 4044         layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
 4045 
 4046 
 4047 class NWLinkStandardMenu(Menu, NWBase):
 4048     bl_idname = "NODE_MT_nw_link_standard_menu"
 4049     bl_label = "To All Selected"
 4050 
 4051     def draw(self, context):
 4052         layout = self.layout
 4053         props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
 4054         props.replace = False
 4055         props.use_node_name = False
 4056         props.use_outputs_names = False
 4057         props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
 4058         props.replace = True
 4059         props.use_node_name = False
 4060         props.use_outputs_names = False
 4061 
 4062 
 4063 class NWLinkUseNodeNameMenu(Menu, NWBase):
 4064     bl_idname = "NODE_MT_nw_link_use_node_name_menu"
 4065     bl_label = "Use Node Name/Label"
 4066 
 4067     def draw(self, context):
 4068         layout = self.layout
 4069         props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
 4070         props.replace = False
 4071         props.use_node_name = True
 4072         props.use_outputs_names = False
 4073         props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
 4074         props.replace = True
 4075         props.use_node_name = True
 4076         props.use_outputs_names = False
 4077 
 4078 
 4079 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
 4080     bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
 4081     bl_label = "Use Outputs Names"
 4082 
 4083     def draw(self, context):
 4084         layout = self.layout
 4085         props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
 4086         props.replace = False
 4087         props.use_node_name = False
 4088         props.use_outputs_names = True
 4089         props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
 4090         props.replace = True
 4091         props.use_node_name = False
 4092         props.use_outputs_names = True
 4093 
 4094 
 4095 class NWAttributeMenu(bpy.types.Menu):
 4096     bl_idname = "NODE_MT_nw_node_attribute_menu"
 4097     bl_label = "Attributes"
 4098 
 4099     @classmethod
 4100     def poll(cls, context):
 4101         valid = False
 4102         if nw_check(context):
 4103             snode = context.space_data
 4104             valid = snode.tree_type == 'ShaderNodeTree'
 4105         return valid
 4106 
 4107     def draw(self, context):
 4108         l = self.layout
 4109         nodes, links = get_nodes_links(context)
 4110         mat = context.object.active_material
 4111 
 4112         objs = []
 4113         for obj in bpy.data.objects:
 4114             for slot in obj.material_slots:
 4115                 if slot.material == mat:
 4116                     objs.append(obj)
 4117         attrs = []
 4118         for obj in objs:
 4119             if obj.data.attributes:
 4120                 for attr in obj.data.attributes:
 4121                     attrs.append(attr.name)
 4122         attrs = list(set(attrs))  # get a unique list
 4123 
 4124         if attrs:
 4125             for attr in attrs:
 4126                 l.operator(NWAddAttrNode.bl_idname, text=attr).attr_name = attr
 4127         else:
 4128             l.label(text="No attributes on objects with this material")
 4129 
 4130 
 4131 class NWSwitchNodeTypeMenu(Menu, NWBase):
 4132     bl_idname = "NODE_MT_nw_switch_node_type_menu"
 4133     bl_label = "Switch Type to..."
 4134 
 4135     def draw(self, context):
 4136         layout = self.layout
 4137         categories = [c for c in node_categories_iter(context)
 4138                       if c.name not in ['Group', 'Script']]
 4139         for cat in categories:
 4140             idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
 4141             if hasattr(bpy.types, idname):
 4142                 layout.menu(idname)
 4143             else:
 4144                 layout.label(text="Unable to load altered node lists.")
 4145                 layout.label(text="Please re-enable Node Wrangler.")
 4146                 break
 4147 
 4148 
 4149 def draw_switch_category_submenu(self, context):
 4150     layout = self.layout
 4151     if self.category.name == 'Layout':
 4152         for node in self.category.items(context):
 4153             if node.nodetype != 'NodeFrame':
 4154                 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
 4155                 props.to_type = node.nodetype
 4156     else:
 4157         for node in self.category.items(context):
 4158             if isinstance(node, NodeItemCustom):
 4159                 node.draw(self, layout, context)
 4160                 continue
 4161             props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
 4162             props.to_type = node.nodetype
 4163 
 4164 #
 4165 #  APPENDAGES TO EXISTING UI
 4166 #
 4167 
 4168 
 4169 def select_parent_children_buttons(self, context):
 4170     layout = self.layout
 4171     layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
 4172     layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
 4173 
 4174 
 4175 def attr_nodes_menu_func(self, context):
 4176     col = self.layout.column(align=True)
 4177     col.menu("NODE_MT_nw_node_attribute_menu")
 4178     col.separator()
 4179 
 4180 
 4181 def multipleimages_menu_func(self, context):
 4182     col = self.layout.column(align=True)
 4183     col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
 4184     col.operator(NWAddSequence.bl_idname, text="Image Sequence")
 4185     col.separator()
 4186 
 4187 
 4188 def bgreset_menu_func(self, context):
 4189     self.layout.operator(NWResetBG.bl_idname)
 4190 
 4191 
 4192 def save_viewer_menu_func(self, context):
 4193     if nw_check(context):
 4194         if context.space_data.tree_type == 'CompositorNodeTree':
 4195             if context.scene.node_tree.nodes.active:
 4196                 if context.scene.node_tree.nodes.active.type == "VIEWER":
 4197                     self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
 4198 
 4199 
 4200 def reset_nodes_button(self, context):
 4201     node_active = context.active_node
 4202     node_selected = context.selected_nodes
 4203     node_ignore = ["FRAME","REROUTE", "GROUP"]
 4204 
 4205     # Check if active node is in the selection and respective type
 4206     if (len(node_selected) == 1) and node_active and node_active.select and node_active.type not in node_ignore:
 4207         row = self.layout.row()
 4208         row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
 4209         self.layout.separator()
 4210 
 4211     elif (len(node_selected) == 1) and node_active and node_active.select and node_active.type == "FRAME":
 4212         row = self.layout.row()
 4213         row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
 4214         self.layout.separator()
 4215 
 4216 
 4217 #
 4218 #  REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
 4219 #
 4220 switch_category_menus = []
 4221 addon_keymaps = []
 4222 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
 4223 # props entry: (property name, property value)
 4224 kmi_defs = (
 4225     # MERGE NODES
 4226     # NWMergeNodes with Ctrl (AUTO).
 4227     (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
 4228         (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
 4229     (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
 4230         (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
 4231     (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
 4232         (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
 4233     (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
 4234         (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
 4235     (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
 4236         (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
 4237     (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
 4238         (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
 4239     (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
 4240         (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
 4241     (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
 4242         (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
 4243     (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
 4244         (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
 4245     (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
 4246         (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
 4247     (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
 4248         (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
 4249     (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
 4250         (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
 4251     (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
 4252         (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
 4253     # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
 4254     (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
 4255         (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
 4256     (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
 4257         (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
 4258     (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
 4259         (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
 4260     (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
 4261         (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
 4262     (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
 4263         (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
 4264     (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
 4265         (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
 4266     (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
 4267         (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
 4268     (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
 4269         (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
 4270     (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
 4271         (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
 4272     (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
 4273         (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
 4274     # NWMergeNodes with Ctrl Shift (MATH)
 4275     (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
 4276         (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
 4277     (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
 4278         (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
 4279     (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
 4280         (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
 4281     (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
 4282         (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
 4283     (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
 4284         (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
 4285     (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
 4286         (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
 4287     (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
 4288         (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
 4289     (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
 4290         (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
 4291     (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
 4292         (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
 4293     (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
 4294         (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
 4295     # BATCH CHANGE NODES
 4296     # NWBatchChangeNodes with Alt
 4297     (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
 4298         (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
 4299     (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
 4300         (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
 4301     (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
 4302         (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
 4303     (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
 4304         (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
 4305     (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
 4306         (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
 4307     (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
 4308         (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
 4309     (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
 4310         (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
 4311     (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
 4312         (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
 4313     (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
 4314         (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
 4315     (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
 4316         (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
 4317     (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
 4318         (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
 4319     (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
 4320         (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
 4321     (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
 4322         (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
 4323     (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
 4324         (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
 4325     # LINK ACTIVE TO SELECTED
 4326     # Don't use names, don't replace links (K)
 4327     (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
 4328         (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
 4329     # Don't use names, replace links (Shift K)
 4330     (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
 4331         (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
 4332     # Use node name, don't replace links (')
 4333     (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
 4334         (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
 4335     # Use node name, replace links (Shift ')
 4336     (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
 4337         (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
 4338     # Don't use names, don't replace links (;)
 4339     (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
 4340         (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
 4341     # Don't use names, replace links (')
 4342     (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
 4343         (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
 4344     # CHANGE MIX FACTOR
 4345     (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
 4346     (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
 4347     (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
 4348     (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
 4349     (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
 4350     (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
 4351     (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
 4352     (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
 4353     (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
 4354     (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
 4355     # CLEAR LABEL (Alt L)
 4356     (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
 4357     # MODIFY LABEL (Alt Shift L)
 4358     (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
 4359     # Copy Label from active to selected
 4360     (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
 4361     # DETACH OUTPUTS (Alt Shift D)
 4362     (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
 4363     # LINK TO OUTPUT NODE (O)
 4364     (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
 4365     # SELECT PARENT/CHILDREN
 4366     # Select Children
 4367     (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
 4368     # Select Parent
 4369     (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
 4370     # Add Texture Setup
 4371     (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
 4372     # Add Principled BSDF Texture Setup
 4373     (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
 4374     # Reset backdrop
 4375     (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
 4376     # Delete unused
 4377     (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
 4378     # Frame Selected
 4379     (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
 4380     # Swap Links
 4381     (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Links"),
 4382     # Preview Node
 4383     (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
 4384     (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
 4385     # Reload Images
 4386     (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
 4387     # Lazy Mix
 4388     (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
 4389     # Lazy Connect
 4390     (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
 4391     # Lazy Connect with Menu
 4392     (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
 4393     # Viewer Tile Center
 4394     (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
 4395     # Align Nodes
 4396     (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
 4397     # Reset Nodes (Back Space)
 4398     (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
 4399     # MENUS
 4400     ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
 4401     ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
 4402     ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
 4403     ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
 4404     ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
 4405     ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
 4406 )
 4407 
 4408 
 4409 classes = (
 4410     NWPrincipledPreferences,
 4411     NWNodeWrangler,
 4412     NWLazyMix,
 4413     NWLazyConnect,
 4414     NWDeleteUnused,
 4415     NWSwapLinks,
 4416     NWResetBG,
 4417     NWAddAttrNode,
 4418     NWPreviewNode,
 4419     NWFrameSelected,
 4420     NWReloadImages,
 4421     NWSwitchNodeType,
 4422     NWMergeNodes,
 4423     NWBatchChangeNodes,
 4424     NWChangeMixFactor,
 4425     NWCopySettings,
 4426     NWCopyLabel,
 4427     NWClearLabel,
 4428     NWModifyLabels,
 4429     NWAddTextureSetup,
 4430     NWAddPrincipledSetup,
 4431     NWAddReroutes,
 4432     NWLinkActiveToSelected,
 4433     NWAlignNodes,
 4434     NWSelectParentChildren,
 4435     NWDetachOutputs,
 4436     NWLinkToOutputNode,
 4437     NWMakeLink,
 4438     NWCallInputsMenu,
 4439     NWAddSequence,
 4440     NWAddMultipleImages,
 4441     NWViewerFocus,
 4442     NWSaveViewer,
 4443     NWResetNodes,
 4444     NodeWranglerPanel,
 4445     NodeWranglerMenu,
 4446     NWMergeNodesMenu,
 4447     NWMergeShadersMenu,
 4448     NWMergeGeometryMenu,
 4449     NWMergeMixMenu,
 4450     NWConnectionListOutputs,
 4451     NWConnectionListInputs,
 4452     NWMergeMathMenu,
 4453     NWBatchChangeNodesMenu,
 4454     NWBatchChangeBlendTypeMenu,
 4455     NWBatchChangeOperationMenu,
 4456     NWCopyToSelectedMenu,
 4457     NWCopyLabelMenu,
 4458     NWAddReroutesMenu,
 4459     NWLinkActiveToSelectedMenu,
 4460     NWLinkStandardMenu,
 4461     NWLinkUseNodeNameMenu,
 4462     NWLinkUseOutputsNamesMenu,
 4463     NWAttributeMenu,
 4464     NWSwitchNodeTypeMenu,
 4465 )
 4466 
 4467 def register():
 4468     from bpy.utils import register_class
 4469 
 4470     # props
 4471     bpy.types.Scene.NWBusyDrawing = StringProperty(
 4472         name="Busy Drawing!",
 4473         default="",
 4474         description="An internal property used to store only the first mouse position")
 4475     bpy.types.Scene.NWLazySource = StringProperty(
 4476         name="Lazy Source!",
 4477         default="x",
 4478         description="An internal property used to store the first node in a Lazy Connect operation")
 4479     bpy.types.Scene.NWLazyTarget = StringProperty(
 4480         name="Lazy Target!",
 4481         default="x",
 4482         description="An internal property used to store the last node in a Lazy Connect operation")
 4483     bpy.types.Scene.NWSourceSocket = IntProperty(
 4484         name="Source Socket!",
 4485         default=0,
 4486         description="An internal property used to store the source socket in a Lazy Connect operation")
 4487     bpy.types.NodeSocketInterface.NWViewerSocket = BoolProperty(
 4488         name="NW Socket",
 4489         default=False,
 4490         description="An internal property used to determine if a socket is generated by the addon"
 4491     )
 4492 
 4493     for cls in classes:
 4494         register_class(cls)
 4495 
 4496     # keymaps
 4497     addon_keymaps.clear()
 4498     kc = bpy.context.window_manager.keyconfigs.addon
 4499     if kc:
 4500         km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
 4501         for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
 4502             kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
 4503             if props:
 4504                 for prop, value in props:
 4505                     setattr(kmi.properties, prop, value)
 4506             addon_keymaps.append((km, kmi))
 4507 
 4508     # menu items
 4509     bpy.types.NODE_MT_select.append(select_parent_children_buttons)
 4510     bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
 4511     bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
 4512     bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
 4513     bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
 4514     bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
 4515     bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
 4516     bpy.types.NODE_MT_node.prepend(reset_nodes_button)
 4517 
 4518     # switch submenus
 4519     switch_category_menus.clear()
 4520     for cat in node_categories_iter(None):
 4521         if cat.name not in ['Group', 'Script']:
 4522             idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
 4523             switch_category_type = type(idname, (bpy.types.Menu,), {
 4524                 "bl_space_type": 'NODE_EDITOR',
 4525                 "bl_label": cat.name,
 4526                 "category": cat,
 4527                 "poll": cat.poll,
 4528                 "draw": draw_switch_category_submenu,
 4529             })
 4530 
 4531             switch_category_menus.append(switch_category_type)
 4532 
 4533             bpy.utils.register_class(switch_category_type)
 4534 
 4535 
 4536 def unregister():
 4537     from bpy.utils import unregister_class
 4538 
 4539     # props
 4540     del bpy.types.Scene.NWBusyDrawing
 4541     del bpy.types.Scene.NWLazySource
 4542     del bpy.types.Scene.NWLazyTarget
 4543     del bpy.types.Scene.NWSourceSocket
 4544     del bpy.types.NodeSocketInterface.NWViewerSocket