diff --git a/node_efficiency_tools.py b/node_efficiency_tools.py index 64461aa266c937d249f766cd8c9f59c368c65d7c..952608779dea007022b174c70aab0fd80cf3637c 100644 --- a/node_efficiency_tools.py +++ b/node_efficiency_tools.py @@ -17,22 +17,23 @@ # ##### END GPL LICENSE BLOCK ##### bl_info = { - 'name': "Nodes Efficiency Tools", - 'author': "Bartek Skorupa", - 'version': (2, 33), + 'name': "Node Wrangler (aka Nodes Efficiency Tools)", + 'author': "Bartek Skorupa, Greg Zaal", + 'version': (3, 00), 'blender': (2, 69, 0), - 'location': "Node Editor Properties Panel (Ctrl+Space)", - 'description': "Nodes Efficiency Tools", + 'location': "Node Editor Properties Panel or Ctrl-SPACE", + 'description': "Various tools to enhance and speed up node-based workflow", 'warning': "", 'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Nodes/Nodes_Efficiency_Tools", 'tracker_url': "http://projects.blender.org/tracker/index.php?func=detail&aid=33543&group_id=153&atid=469", 'category': "Node", - } +} -import bpy +import bpy, blf, bgl from bpy.types import Operator, Panel, Menu -from bpy.props import FloatProperty, EnumProperty, BoolProperty +from bpy.props import FloatProperty, EnumProperty, BoolProperty, StringProperty, FloatVectorProperty from mathutils import Vector +from math import cos, sin, pi, sqrt ################# # rl_outputs: @@ -72,8 +73,236 @@ rl_outputs = ( ('use_pass_uv', 'UV', 'UV', True, True), ('use_pass_vector', 'Speed', 'Vector', True, True), ('use_pass_z', 'Z', 'Depth', True, True), - ) +) + +# shader nodes +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_input_nodes_props = ( + ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'), + ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'), + ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'), + ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'), + ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'), + ('ShaderNodeRGB', 'RGB', 'RGB'), + ('ShaderNodeValue', 'VALUE', 'Value'), + ('ShaderNodeTangent', 'TANGENT', 'Tangent'), + ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'), + ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'), + ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'), + ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'), + ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'), + ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_output_nodes_props = ( + ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'), + ('ShaderNodeOutputLamp', 'OUTPUT_LAMP', 'Lamp Output'), + ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_shader_nodes_props = ( + ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'), + ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'), + ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'), + ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'), + ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'), + ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'), + ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'), + ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'), + ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'), + ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'), + ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'), + ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'), + ('ShaderNodeEmission', 'EMISSION', 'Emission'), + ('ShaderNodeBackground', 'BACKGROUND', 'Background'), + ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'), + ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_texture_nodes_props = ( + ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image'), + ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment'), + ('ShaderNodeTexSky', 'TEX_SKY', 'Sky'), + ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise'), + ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave'), + ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'), + ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'), + ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient'), + ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic'), + ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker'), + ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick') +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_color_nodes_props = ( + ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'), + ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'), + ('ShaderNodeInvert', 'INVERT', 'Invert'), + ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'), + ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'), + ('ShaderNodeGamma', 'GAMMA', 'Gamma'), + ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_vector_nodes_props = ( + ('ShaderNodeMapping', 'MAPPING', 'Mapping'), + ('ShaderNodeBump', 'BUMP', 'Bump'), + ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'), + ('ShaderNodeNormal', 'NORMAL', 'Normal'), + ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'), + ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_converter_nodes_props = ( + ('ShaderNodeMath', 'MATH', 'Math'), + ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'), + ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'), + ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'), + ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'), + ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'), + ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'), + ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'), + ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'), + ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +shaders_layout_nodes_props = ( + ('NodeFrame', 'FRAME', 'Frame'), + ('NodeReroute', 'REROUTE', 'Reroute'), +) + +# compositing nodes +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_input_nodes_props = ( + ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'), + ('CompositorNodeImage', 'IMAGE', 'Image'), + ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'), + ('CompositorNodeMask', 'MASK', 'Mask'), + ('CompositorNodeRGB', 'RGB', 'RGB'), + ('CompositorNodeValue', 'VALUE', 'Value'), + ('CompositorNodeTexture', 'TEXTURE', 'Texture'), + ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'), + ('CompositorNodeTime', 'TIME', 'Time'), + ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_output_nodes_props = ( + ('CompositorNodeComposite', 'COMPOSITE', 'Composite'), + ('CompositorNodeViewer', 'VIEWER', 'Viewer'), + ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'), + ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'), + ('CompositorNodeLevels', 'LEVELS', 'Levels'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_color_nodes_props = ( + ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'), + ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'), + ('CompositorNodeInvert', 'INVERT', 'Invert'), + ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'), + ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'), + ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'), + ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'), + ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'), + ('CompositorNodeGamma', 'GAMMA', 'Gamma'), + ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'), + ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'), + ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_converter_nodes_props = ( + ('CompositorNodeMath', 'MATH', 'Math'), + ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'), + ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'), + ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'), + ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'), + ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'), + ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'), + ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'), + ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'), + ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'), + ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'), + ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'), + ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'), + ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_filter_nodes_props = ( + ('CompositorNodeBlur', 'BLUR', 'Blur'), + ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'), + ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'), + ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'), + ('CompositorNodeFilter', 'FILTER', 'Filter'), + ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'), + ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'), + ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'), + ('CompositorNodeGlare', 'GLARE', 'Glare'), + ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'), + ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'), + ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_vector_nodes_props = ( + ('CompositorNodeNormal', 'NORMAL', 'Normal'), + ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'), + ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'), + ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'), + ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_matte_nodes_props = ( + ('CompositorNodeKeying', 'KEYING', 'Keying'), + ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'), + ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'), + ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'), + ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'), + ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'), + ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'), + ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'), + ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'), + ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'), + ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'), + ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_distort_nodes_props = ( + ('CompositorNodeScale', 'SCALE', 'Scale'), + ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'), + ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'), + ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'), + ('CompositorNodeRotate', 'ROTATE', 'Rotate'), + ('CompositorNodeFlip', 'FLIP', 'Flip'), + ('CompositorNodeCrop', 'CROP', 'Crop'), + ('CompositorNodeDisplace', 'DISPLACE', 'Displace'), + ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'), + ('CompositorNodeTransform', 'TRANSFORM', 'Transform'), + ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'), + ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'), +) +# (rna_type.identifier, type, rna_type.name) +# Keeping mixed case to avoid having to translate entries when adding new nodes in operators. +compo_layout_nodes_props = ( + ('NodeFrame', 'FRAME', 'Frame'), + ('NodeReroute', 'REROUTE', 'Reroute'), + ('CompositorNodeSwitch', 'SWITCH', 'Switch'), +) + # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty. +# used list, not tuple for easy merging with other lists. blend_types = [ ('MIX', 'Mix', 'Mix Mode'), ('ADD', 'Add', 'Add Mode'), @@ -93,8 +322,10 @@ blend_types = [ ('COLOR', 'Color', 'Color Mode'), ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'), ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'), - ] +] + # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty. +# used list, not tuple for easy merging with other lists. operations = [ ('ADD', 'Add', 'Add Mode'), ('MULTIPLY', 'Multiply', 'Multiply Mode'), @@ -111,39 +342,282 @@ operations = [ ('MINIMUM', 'Minimum', 'Minimum Mode'), ('MAXIMUM', 'Maximum', 'Maximum Mode'), ('ROUND', 'Round', 'Round Mode'), - ('LESS_THAN', 'Less Than', 'Less Thann Mode'), + ('LESS_THAN', 'Less Than', 'Less Than Mode'), ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'), - ] -# in BatchChangeNodes additional types/operations in a form that can be used as 'items' for EnumProperty. +] + +# in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty. +# used list, not tuple for easy merging with other lists. navs = [ ('CURRENT', 'Current', 'Leave at current state'), ('NEXT', 'Next', 'Next blend type/operation'), ('PREV', 'Prev', 'Previous blend type/operation'), - ] -# list of mixing shaders -merge_shaders_types = ('MIX', 'ADD') -# list of regular shaders. Entry: (identified, type, name for humans). Will be used in SwapShaders and menus. -# Keeping mixed case to avoid having to translate entries when adding new nodes in SwapNodes. -regular_shaders = ( - ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'), - ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'), - ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'), - ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'), - ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'), - ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'), - ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'), - ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'), - ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'), - ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'), - ('ShaderNodeEmission', 'EMISSION', 'Emission'), - ('ShaderNodeBackground', 'BACKGROUND', 'Background'), - ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'), - ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'), +] + +draw_color_sets = { + "red_white": ( + (1.0, 1.0, 1.0, 0.7), + (1.0, 0.0, 0.0, 0.7), + (0.8, 0.2, 0.2, 1.0) + ), + "green": ( + (0.0, 0.0, 0.0, 1.0), + (0.38, 0.77, 0.38, 1.0), + (0.38, 0.77, 0.38, 1.0) + ), + "yellow": ( + (0.0, 0.0, 0.0, 1.0), + (0.77, 0.77, 0.16, 1.0), + (0.77, 0.77, 0.16, 1.0) + ), + "purple": ( + (0.0, 0.0, 0.0, 1.0), + (0.38, 0.38, 0.77, 1.0), + (0.38, 0.38, 0.77, 1.0) + ), + "grey": ( + (0.0, 0.0, 0.0, 1.0), + (0.63, 0.63, 0.63, 1.0), + (0.63, 0.63, 0.63, 1.0) + ), + "black": ( + (1.0, 1.0, 1.0, 0.7), + (0.0, 0.0, 0.0, 0.7), + (0.2, 0.2, 0.2, 1.0) ) -merge_shaders = ( - ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'), - ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'), +} + + +def nice_hotkey_name(punc): + # convert the ugly string name into the actual character + pairs = ( + ('LEFTMOUSE', "LMB"), + ('MIDDLEMOUSE', "MMB"), + ('RIGHTMOUSE', "RMB"), + ('SELECTMOUSE', "Select"), + ('WHEELUPMOUSE', "Wheel Up"), + ('WHEELDOWNMOUSE', "Wheel Down"), + ('WHEELINMOUSE', "Wheel In"), + ('WHEELOUTMOUSE', "Wheel Out"), + ('ZERO', "0"), + ('ONE', "1"), + ('TWO', "2"), + ('THREE', "3"), + ('FOUR', "4"), + ('FIVE', "5"), + ('SIX', "6"), + ('SEVEN', "7"), + ('EIGHT', "8"), + ('NINE', "9"), + ('OSKEY', "Super"), + ('RET', "Enter"), + ('LINE_FEED', "Enter"), + ('SEMI_COLON', ";"), + ('PERIOD', "."), + ('COMMA', ","), + ('QUOTE', '"'), + ('MINUS', "-"), + ('SLASH', "/"), + ('BACK_SLASH', "\\"), + ('EQUAL', "="), + ('NUMPAD_1', "Numpad 1"), + ('NUMPAD_2', "Numpad 2"), + ('NUMPAD_3', "Numpad 3"), + ('NUMPAD_4', "Numpad 4"), + ('NUMPAD_5', "Numpad 5"), + ('NUMPAD_6', "Numpad 6"), + ('NUMPAD_7', "Numpad 7"), + ('NUMPAD_8', "Numpad 8"), + ('NUMPAD_9', "Numpad 9"), + ('NUMPAD_0', "Numpad 0"), + ('NUMPAD_PERIOD', "Numpad ."), + ('NUMPAD_SLASH', "Numpad /"), + ('NUMPAD_ASTERIX', "Numpad *"), + ('NUMPAD_MINUS', "Numpad -"), + ('NUMPAD_ENTER', "Numpad Enter"), + ('NUMPAD_PLUS', "Numpad +"), ) + nice_punc = False + for (ugly, nice) in pairs: + if punc == ugly: + nice_punc = nice + break + if not nice_punc: + nice_punc = punc.replace("_", " ").title() + return nice_punc + + +def hack_force_update(context, nodes): + if context.space_data.tree_type == "ShaderNodeTree": + node = nodes.new('ShaderNodeMath') + node.inputs[0].default_value = 0.0 + nodes.remove(node) + return False + + +def is_end_node(node): + bool = True + for output in node.outputs: + if output.links: + bool = False + break + return bool + + +def node_mid_pt(node, axis): + if axis == 'x': + d = node.location.x + (node.dimensions.x / 2) + elif axis == 'y': + d = node.location.y - (node.dimensions.y / 2) + else: + d = 0 + return d + + +def autolink(node1, node2, links): + link_made = False + for outp in node1.outputs: + for inp in node2.inputs: + if not inp.is_linked and inp.type == outp.type: + link_made = True + links.new(outp, inp) + return True + + # force some connection even if the type doesn't match + for outp in node1.outputs: + for inp in node2.inputs: + if not inp.is_linked: + link_made = True + links.new(outp, inp) + return True + + # even if no sockets are open, force one of matching type + for outp in node1.outputs: + for inp in node2.inputs: + if inp.type == outp.type: + link_made = True + links.new(outp, inp) + return True + + # do something! + for outp in node1.outputs: + for inp in node2.inputs: + link_made = True + links.new(outp, inp) + return True + + print("Could not make a link from " + node1.name + " to " + node2.name) + return link_made + + +def node_at_pos(nodes, context, event): + nodes_near_mouse = [] + nodes_under_mouse = [] + target_node = None + + store_mouse_cursor(context, event) + x, y = context.space_data.cursor_location + + # nearest node + nodes_near_mouse = sorted(nodes, key=lambda k: sqrt((x - node_mid_pt(k, 'x')) ** 2 + (y - node_mid_pt(k, 'y')) ** 2)) + + for node in nodes: + if (node.location.x <= x <= node.location.x + node.dimensions.x) and \ + (node.location.y - node.dimensions.y <= y <= node.location.y): + nodes_under_mouse.append(node) + + if len(nodes_under_mouse) == 1: + if nodes_under_mouse[0] != nodes_near_mouse[0]: + target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one + else: + target_node = nodes_near_mouse[0] # else use the nearest node + else: + target_node = nodes_near_mouse[0] + return target_node + + +def store_mouse_cursor(context, event): + space = context.space_data + v2d = context.region.view2d + tree = space.edit_tree + + # convert mouse position to the View2D for later node placement + if context.region.type == 'WINDOW': + space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y) + else: + space.cursor_location = tree.view_center + + +def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]): + bgl.glEnable(bgl.GL_BLEND) + bgl.glColor4f(colour[0], colour[1], colour[2], colour[3]) + bgl.glLineWidth(size) + + bgl.glBegin(bgl.GL_LINE_STRIP) + try: + bgl.glVertex2f(x1, y1) + bgl.glVertex2f(x2, y2) + except: + pass + bgl.glEnd() + + +def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]): + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(colour[0], colour[1], colour[2], colour[3]) + radius = radius + sides = 32 + for i in range(sides + 1): + cosine = radius * cos(i * 2 * pi / sides) + mx + sine = radius * sin(i * 2 * pi / sides) + my + bgl.glVertex2f(cosine, sine) + bgl.glEnd() + + +def draw_callback_mixnodes(self, context, mode="MIX"): + if self.mouse_path: + settings = context.user_preferences.addons[__name__].preferences + if settings.bgl_antialiasing: + bgl.glEnable(bgl.GL_LINE_SMOOTH) + + colors = [] + if mode == 'MIX': + colors = draw_color_sets['red_white'] + elif mode == 'RGBA': + colors = draw_color_sets['yellow'] + elif mode == 'VECTOR': + colors = draw_color_sets['purple'] + elif mode == 'VALUE': + colors = draw_color_sets['grey'] + elif mode == 'SHADER': + colors = draw_color_sets['green'] + else: + colors = draw_color_sets['black'] + + m1x = self.mouse_path[0][0] + m1y = self.mouse_path[0][1] + m2x = self.mouse_path[-1][0] + m2y = self.mouse_path[-1][1] + + # circle outline + draw_circle(m1x, m1y, 6, colors[0]) + draw_circle(m2x, m2y, 6, colors[0]) + + draw_line(m1x, m1y, m2x, m2y, 4, colors[0]) # line outline + draw_line(m1x, m1y, m2x, m2y, 2, colors[1]) # line inner + + # circle inner + draw_circle(m1x, m1y, 5, colors[2]) + draw_circle(m2x, m2y, 5, colors[2]) + + # restore opengl defaults + bgl.glLineWidth(1) + bgl.glDisable(bgl.GL_BLEND) + bgl.glColor4f(0.0, 0.0, 0.0, 1.0) + + if settings.bgl_antialiasing: + bgl.glDisable(bgl.GL_LINE_SMOOTH) + def get_nodes_links(context): space = context.space_data @@ -168,36 +642,854 @@ def get_nodes_links(context): return nodes, links -class NodeToolBase: +# Addon prefs +class NWNodeWrangler(bpy.types.AddonPreferences): + bl_idname = __name__ + + merge_hide = EnumProperty( + name="Hide Mix nodes", + items=( + ("ALWAYS", "Always", "Always collapse the new merge nodes"), + ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"), + ("NEVER", "Never", "Never collapse the new merge nodes") + ), + default='NON_SHADER', + description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded") + merge_position = EnumProperty( + name="Mix Node Position", + items=( + ("CENTER", "Center", "Place the Mix node between the two nodes"), + ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node") + ), + default='CENTER', + description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes") + bgl_antialiasing = BoolProperty( + name="Line Antialiasing", + default=False, + description="Remove aliasing artifacts on lines drawn in interactive modes such as Lazy Connect (Alt+LMB) and Lazy Merge (Alt+RMB) - this may cause issues on some systems" + ) + + show_hotkey_list = BoolProperty( + name="Show Hotkey List", + default=False, + description="Expand this box into a list of all the hotkeys for functions in this addon" + ) + hotkey_list_filter = StringProperty( + name=" Filter by Name", + default="", + description="Show only hotkeys that have this text in their name" + ) + + def draw(self, context): + layout = self.layout + col = layout.column() + col.prop(self, "merge_position") + col.prop(self, "merge_hide") + col.prop(self, "bgl_antialiasing") + + box = col.box() + col = box.column(align=True) + + hotkey_button_name = "Show Hotkey List" + if self.show_hotkey_list: + hotkey_button_name = "Hide Hotkey List" + col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True) + if self.show_hotkey_list: + col.prop(self, "hotkey_list_filter", icon="VIEWZOOM") + col.separator() + for hotkey in kmi_defs: + if hotkey[6]: + hotkey_name = hotkey[6] + + if self.hotkey_list_filter.lower() in hotkey_name.lower(): + row = col.row(align=True) + row.label(hotkey_name) + keystr = nice_hotkey_name(hotkey[1]) + if hotkey[3]: + keystr = "Shift " + keystr + if hotkey[4]: + keystr = "Alt " + keystr + if hotkey[2]: + keystr = "Ctrl " + keystr + row.label(keystr) + + +class NWBase: @classmethod def poll(cls, context): space = context.space_data return space.type == 'NODE_EDITOR' and space.node_tree is not None -class MergeNodes(Operator, NodeToolBase): - bl_idname = "node.merge_nodes" +# OPERATORS +class NWLazyMix(Operator, NWBase): + """Add a Mix RGB/Shader node by interactively drawing lines between nodes""" + bl_idname = "node.nw_lazy_mix" + bl_label = "Mix Nodes" + bl_options = {'REGISTER', 'UNDO'} + + def modal(self, context, event): + context.area.tag_redraw() + nodes, links = get_nodes_links(context) + cont = True + + start_pos = [event.mouse_region_x, event.mouse_region_y] + + node1 = None + if not context.scene.NWBusyDrawing: + node1 = node_at_pos(nodes, context, event) + if node1: + context.scene.NWBusyDrawing = node1.name + else: + if context.scene.NWBusyDrawing != 'STOP': + node1 = nodes[context.scene.NWBusyDrawing] + + if event.type == 'MOUSEMOVE': + self.mouse_path.append((event.mouse_region_x, event.mouse_region_y)) + + elif event.type == 'RIGHTMOUSE': + end_pos = [event.mouse_region_x, event.mouse_region_y] + bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW') + + node2 = None + node2 = node_at_pos(nodes, context, event) + if node2: + context.scene.NWBusyDrawing = node2.name + + if node1 == node2: + cont = False + + if cont: + if node1 and node2: + for node in nodes: + node.select = False + node1.select = True + node2.select = True + + bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO") + + context.scene.NWBusyDrawing = "" + return {'FINISHED'} + + elif event.type == 'ESC': + print('cancelled') + bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW') + return {'CANCELLED'} + + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + if context.area.type == 'NODE_EDITOR': + # the arguments we pass the the callback + args = (self, context, 'MIX') + # Add the region OpenGL drawing callback + # draw in view space with 'POST_VIEW' and 'PRE_VIEW' + self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL') + + self.mouse_path = [] + + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "View3D not found, cannot run operator") + return {'CANCELLED'} + + +class NWLazyConnect(Operator, NWBase): + """Connect two nodes without clicking a specific socket (automatically determined""" + bl_idname = "node.nw_lazy_connect" + bl_label = "Lazy Connect" + bl_options = {'REGISTER', 'UNDO'} + + def modal(self, context, event): + context.area.tag_redraw() + nodes, links = get_nodes_links(context) + cont = True + + start_pos = [event.mouse_region_x, event.mouse_region_y] + + node1 = None + if not context.scene.NWBusyDrawing: + node1 = node_at_pos(nodes, context, event) + if node1: + context.scene.NWBusyDrawing = node1.name + else: + if context.scene.NWBusyDrawing != 'STOP': + node1 = nodes[context.scene.NWBusyDrawing] + + if event.type == 'MOUSEMOVE': + self.mouse_path.append((event.mouse_region_x, event.mouse_region_y)) + + elif event.type == 'RIGHTMOUSE': + end_pos = [event.mouse_region_x, event.mouse_region_y] + bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW') + + node2 = None + node2 = node_at_pos(nodes, context, event) + if node2: + context.scene.NWBusyDrawing = node2.name + + if node1 == node2: + cont = False + + link_success = False + if cont: + if node1 and node2: + original_sel = [] + original_unsel = [] + for node in nodes: + if node.select == True: + node.select = False + original_sel.append(node) + else: + original_unsel.append(node) + node1.select = True + node2.select = True + + link_success = autolink(node1, node2, links) + + for node in original_sel: + node.select = True + for node in original_unsel: + node.select = False + + if link_success: + hack_force_update(context, nodes) + context.scene.NWBusyDrawing = "" + return {'FINISHED'} + + elif event.type == 'ESC': + bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW') + return {'CANCELLED'} + + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + if context.area.type == 'NODE_EDITOR': + nodes, links = get_nodes_links(context) + node = node_at_pos(nodes, context, event) + if node: + context.scene.NWBusyDrawing = node.name + if node.outputs: + context.scene.NWDrawColType = node.outputs[0].type + else: + context.scene.NWDrawColType = 'x' + + # the arguments we pass the the callback + args = (self, context, context.scene.NWDrawColType) + # Add the region OpenGL drawing callback + # draw in view space with 'POST_VIEW' and 'PRE_VIEW' + self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL') + + self.mouse_path = [] + + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "View3D not found, cannot run operator") + return {'CANCELLED'} + + +class NWDeleteUnused(Operator, NWBase): + """Delete all nodes whose output is not used""" + bl_idname = 'node.nw_del_unused' + bl_label = 'Delete Unused Nodes' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + valid = False + if context.space_data: + if context.space_data.node_tree: + if context.space_data.node_tree.nodes: + valid = True + return valid + + def execute(self, context): + nodes, links = get_nodes_links(context) + end_types = 'OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \ + 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LAMP', \ + 'OUTPUT_WORLD', 'GROUP', 'GROUP_INPUT', 'GROUP_OUTPUT' + + # Store selection + selection = [] + for node in nodes: + if node.select == True: + selection.append(node.name) + + deleted_nodes = [] + temp_deleted_nodes = [] + del_unused_iterations = len(nodes) + for it in range(0, del_unused_iterations): + temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration + for node in nodes: + node.select = False + for node in nodes: + if is_end_node(node) and not node.type in end_types and node.type != 'FRAME': + node.select = True + deleted_nodes.append(node.name) + bpy.ops.node.delete() + + if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted + break + # get unique list of deleted nodes (iterations would count the same node more than once) + deleted_nodes = list(set(deleted_nodes)) + for n in deleted_nodes: + self.report({'INFO'}, "Node " + n + " deleted") + num_deleted = len(deleted_nodes) + n = ' node' + if num_deleted > 1: + n += 's' + if num_deleted: + self.report({'INFO'}, "Deleted " + str(num_deleted) + n) + else: + self.report({'INFO'}, "Nothing deleted") + + # Restore selection + nodes, links = get_nodes_links(context) + for node in nodes: + if node.name in selection: + node.select = True + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + +class NWSwapOutputs(Operator, NWBase): + """Swap the output connections of the two selected nodes""" + bl_idname = 'node.nw_swap_outputs' + bl_label = 'Swap Outputs' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + snode = context.space_data + if context.selected_nodes: + return len(context.selected_nodes) == 2 + else: + return False + + def execute(self, context): + nodes, links = get_nodes_links(context) + selected_nodes = context.selected_nodes + n1 = selected_nodes[0] + n2 = selected_nodes[1] + n1_outputs = [] + n2_outputs = [] + + out_index = 0 + for output in n1.outputs: + if output.links: + for link in output.links: + n1_outputs.append([out_index, link.to_socket]) + links.remove(link) + out_index += 1 + + out_index = 0 + for output in n2.outputs: + if output.links: + for link in output.links: + n2_outputs.append([out_index, link.to_socket]) + links.remove(link) + out_index += 1 + + for connection in n1_outputs: + try: + links.new(n2.outputs[connection[0]], connection[1]) + except: + self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets") + for connection in n2_outputs: + try: + links.new(n1.outputs[connection[0]], connection[1]) + except: + self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets") + + hack_force_update(context, nodes) + return {'FINISHED'} + + +class NWResetBG(Operator, NWBase): + """Reset the zoom and position of the background image""" + bl_idname = 'node.nw_bg_reset' + bl_label = 'Reset Backdrop' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + snode = context.space_data + return snode.tree_type == 'CompositorNodeTree' + + def execute(self, context): + context.space_data.backdrop_zoom = 1 + context.space_data.backdrop_x = 0 + context.space_data.backdrop_y = 0 + return {'FINISHED'} + + +class NWAddAttrNode(Operator, NWBase): + """Add an Attribute node with this name""" + bl_idname = 'node.nw_add_attr_node' + bl_label = 'Add UV map' + attr_name = StringProperty() + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute") + nodes, links = get_nodes_links(context) + nodes.active.attribute_name = self.attr_name + return {'FINISHED'} + + +class NWEmissionViewer(Operator, NWBase): + bl_idname = "node.nw_emission_viewer" + bl_label = "Emission Viewer" + bl_description = "Connect active node to Emission Shader for shadeless previews" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + space = context.space_data + valid = False + if space.type == 'NODE_EDITOR': + if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None and (context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD"): + valid = True + return valid + + def invoke(self, context, event): + shader_type = context.space_data.shader_type + if shader_type == 'OBJECT': + shader_output_type = "OUTPUT_MATERIAL" + shader_output_ident = "ShaderNodeOutputMaterial" + shader_viewer_ident = "ShaderNodeEmission" + elif shader_type == 'WORLD': + shader_output_type = "OUTPUT_WORLD" + shader_output_ident = "ShaderNodeOutputWorld" + shader_viewer_ident = "ShaderNodeBackground" + shader_types = [x[1] for x in shaders_shader_nodes_props] + mlocx = event.mouse_region_x + mlocy = event.mouse_region_y + select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False) + if 'FINISHED' in select_node: # only run if mouse click is on a node + nodes, links = get_nodes_links(context) + in_group = context.active_node != context.space_data.node_tree.nodes.active + active = nodes.active + valid = False + output_types = [x[1] for x in shaders_output_nodes_props] + if active: + if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group: + if active.select: + if active.type not in shader_types: + valid = True + if valid: + # get material_output node + materialout_exists = False + materialout = None # placeholder node + for node in nodes: + if node.type == shader_output_type: + materialout_exists = True + materialout = node + if not materialout: + materialout = nodes.new(shader_output_ident) + sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x)) + max_xloc_node = sorted_by_xloc[-1] + if max_xloc_node.name == 'Emission Viewer': + max_xloc_node = sorted_by_xloc[-2] + materialout.location.x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80 + sum_yloc = 0 + for node in nodes: + sum_yloc += node.location.y + materialout.location.y = sum_yloc / len(nodes) # put material output at average y location + materialout.select = False + # get Emission Viewer node + emission_exists = False + emission_placeholder = nodes[0] + for node in nodes: + if "Emission Viewer" in node.name: + emission_exists = True + emission_placeholder = node + + position = 0 + for link in links: # check if Emission Viewer is already connected to active node + if link.from_node.name == active.name and "Emission Viewer" in link.to_node.name and "Emission Viewer" in materialout.inputs[0].links[0].from_node.name: + num_outputs = len(link.from_node.outputs) + index = 0 + for output in link.from_node.outputs: + if link.from_socket == output: + position = index + index = index + 1 + position = position + 1 + if position >= num_outputs: + position = 0 + + # Store selection + selection = [] + for node in nodes: + if node.select == True: + selection.append(node.name) + + locx = active.location.x + locy = active.location.y + dimx = active.dimensions.x + dimy = active.dimensions.y + if not emission_exists: + emission = nodes.new(shader_viewer_ident) + emission.hide = True + emission.location = [materialout.location.x, (materialout.location.y + 40)] + emission.label = "Viewer" + emission.name = "Emission Viewer" + emission.use_custom_color = True + emission.color = (0.6, 0.5, 0.4) + else: + emission = emission_placeholder + + nodes.active = emission + links.new(active.outputs[position], emission.inputs[0]) + bpy.ops.node.nw_link_out() + + # Restore selection + emission.select = False + nodes.active = active + for node in nodes: + if node.name in selection: + node.select = True + else: # if active node is a shader, connect to output + if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group: + bpy.ops.node.nw_link_out() + + # ----Delete Emission Viewer---- + if [x for x in nodes if x.name == 'Emission Viewer']: + # Store selection + selection = [] + for node in nodes: + if node.select == True: + selection.append(node.name) + node.select = False + # Delete it + nodes['Emission Viewer'].select = True + bpy.ops.node.delete() + # Restore selection + for node in nodes: + if node.name in selection: + node.select = True + + return {'FINISHED'} + else: + return {'CANCELLED'} + + +class NWFrameSelected(Operator, NWBase): + bl_idname = "node.nw_frame_selected" + bl_label = "Frame Selected" + bl_description = "Add a frame node and parent the selected nodes to it" + bl_options = {'REGISTER', 'UNDO'} + label_prop = StringProperty(name='Label', default=' ', description='The visual name of the frame node') + color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6), + min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3) + + @classmethod + def poll(cls, context): + space = context.space_data + valid = False + if space.type == 'NODE_EDITOR': + if space.node_tree is not None: + valid = True + return valid + + def execute(self, context): + nodes, links = get_nodes_links(context) + selected = [] + for node in nodes: + if node.select == True: + selected.append(node) + + bpy.ops.node.add_node(type='NodeFrame') + frm = nodes.active + frm.label = self.label_prop + frm.use_custom_color = True + frm.color = self.color_prop + + for node in selected: + node.parent = frm + + return {'FINISHED'} + + +class NWReloadImages(Operator, NWBase): + bl_idname = "node.nw_reload_images" + bl_label = "Reload Images" + bl_description = "Update all the image nodes to match their files on disk" + + @classmethod + def poll(cls, context): + space = context.space_data + valid = False + if space.type == 'NODE_EDITOR': + if space.node_tree is not None: + valid = True + return valid + + def execute(self, context): + nodes, links = get_nodes_links(context) + image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"] + num_reloaded = 0 + for node in nodes: + if node.type in image_types: + if node.type == "TEXTURE": + if node.texture: # node has texture assigned + if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']: + if node.texture.image: # texture has image assigned + node.texture.image.reload() + num_reloaded += 1 + else: + if node.image: + node.image.reload() + num_reloaded += 1 + + if num_reloaded: + self.report({'INFO'}, "Reloaded images") + print("Reloaded " + str(num_reloaded) + " images") + hack_force_update(context, nodes) + return {'FINISHED'} + else: + self.report({'WARNING'}, "No images found to reload in this node tree") + return {'CANCELLED'} + + +class NWSwitchNodeType(Operator, NWBase): + """Switch type of selected nodes """ + bl_idname = "node.nw_swtch_node_type" + bl_label = "Switch Node Type" + bl_options = {'REGISTER', 'UNDO'} + + to_type = EnumProperty( + name="Switch to type", + items=list(shaders_input_nodes_props) + + list(shaders_output_nodes_props) + + list(shaders_shader_nodes_props) + + list(shaders_texture_nodes_props) + + list(shaders_color_nodes_props) + + list(shaders_vector_nodes_props) + + list(shaders_converter_nodes_props) + + list(shaders_layout_nodes_props) + + list(compo_input_nodes_props) + + list(compo_output_nodes_props) + + list(compo_color_nodes_props) + + list(compo_converter_nodes_props) + + list(compo_filter_nodes_props) + + list(compo_vector_nodes_props) + + list(compo_matte_nodes_props) + + list(compo_distort_nodes_props) + + list(compo_layout_nodes_props), + ) + + def execute(self, context): + nodes, links = get_nodes_links(context) + to_type = self.to_type + # Those types of nodes will not swap. + src_excludes = ('NodeFrame') + # Those attributes of nodes will be copied if possible + attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent', + 'show_options', 'show_preview', 'show_texture', + 'use_alpha', 'use_clamp', 'use_custom_color', 'location' + ) + selected = [n for n in nodes if n.select] + reselect = [] + for node in [n for n in selected if + n.rna_type.identifier not in src_excludes and + n.rna_type.identifier != to_type]: + new_node = nodes.new(to_type) + for attr in attrs_to_pass: + if hasattr(node, attr) and hasattr(new_node, attr): + setattr(new_node, attr, getattr(node, attr)) + # set image datablock of dst to image of src + if hasattr(node, 'image') and hasattr(new_node, 'image'): + if node.image: + new_node.image = node.image + # Special cases + if new_node.type == 'SWITCH': + new_node.hide = True + # Dictionaries: src_sockets and dst_sockets: + # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs). + # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs). + # in 'INPUTS' and 'OUTPUTS': + # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types. + # socket entry: + # (index_in_type, socket_index, socket_name, socket_default_value, socket_links) + src_sockets = { + 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None}, + 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None}, + } + dst_sockets = { + 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None}, + 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None}, + } + types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE' + types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE' + # check src node to set src_sockets values and dst node to set dst_sockets dict values + for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)): + # Check node's inputs and outputs and fill proper entries in "sockets" dict + for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')): + # enumerate in inputs, then in outputs + # find name, default value and links of socket + for i, socket in enumerate(in_out): + the_name = socket.name + dval = None + # Not every socket, especially in outputs has "default_value" + if hasattr(socket, 'default_value'): + dval = socket.default_value + socket_links = [] + for lnk in socket.links: + socket_links.append(lnk) + # check type of socket to fill proper keys. + for the_type in types_order_one: + if socket.type == the_type: + # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type] + # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links) + sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links)) + # Check which of the types in inputs/outputs is considered to be "main". + # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN'] + for type_check in types_order_one: + if sockets[in_out_name][type_check]: + sockets[in_out_name]['MAIN'] = type_check + break + + matches = { + 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []}, + 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []}, + } + + for inout, soctype in ( + ('INPUTS', 'MAIN',), + ('INPUTS', 'SHADER',), + ('INPUTS', 'RGBA',), + ('INPUTS', 'VECTOR',), + ('INPUTS', 'VALUE',), + ('OUTPUTS', 'MAIN',), + ('OUTPUTS', 'SHADER',), + ('OUTPUTS', 'RGBA',), + ('OUTPUTS', 'VECTOR',), + ('OUTPUTS', 'VALUE',), + ): + if src_sockets[inout][soctype] and dst_sockets[inout][soctype]: + if soctype == 'MAIN': + sc = src_sockets[inout][src_sockets[inout]['MAIN']] + dt = dst_sockets[inout][dst_sockets[inout]['MAIN']] + else: + sc = src_sockets[inout][soctype] + dt = dst_sockets[inout][soctype] + # start with 'dt' to determine number of possibilities. + for i, soc in enumerate(dt): + # if src main has enough entries - match them with dst main sockets by indexes. + if len(sc) > i: + matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3]))) + # add 'VALUE_NAME' criterion to inputs. + if inout == 'INPUTS' and soctype == 'VALUE': + for s in sc: + if s[2] == soc[2]: # if names match + # append src (index, dval), dst (index, dval) + matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3]))) + + # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible. + # This creates better links when relinking textures. + if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']: + matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR'] + + # Pass default values and RELINK: + for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'): + # INPUTS: Base on matches in proper order. + for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]: + # pass dvals + if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}: + new_node.inputs[dst_i].default_value = src_dval + # Special case: switch to math + if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\ + new_node.type == 'MATH' and\ + tp == 'MAIN': + new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2]) + new_node.inputs[dst_i].default_value = new_dst_dval + if node.type == 'MIX_RGB': + if node.blend_type in [o[0] for o in operations]: + new_node.operation = node.blend_type + # Special case: switch from math to some types + if node.type == 'MATH' and\ + new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\ + tp == 'MAIN': + for i in range(3): + new_node.inputs[dst_i].default_value[i] = src_dval + if new_node.type == 'MIX_RGB': + if node.operation in [t[0] for t in blend_types]: + new_node.blend_type = node.operation + # Set Fac of MIX_RGB to 1.0 + new_node.inputs[0].default_value = 1.0 + # make link only when dst matching input is not linked already. + if node.inputs[src_i].links and not new_node.inputs[dst_i].links: + in_src_link = node.inputs[src_i].links[0] + in_dst_socket = new_node.inputs[dst_i] + links.new(in_src_link.from_socket, in_dst_socket) + links.remove(in_src_link) + # OUTPUTS: Base on matches in proper order. + for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]: + for out_src_link in node.outputs[src_i].links: + out_dst_socket = new_node.outputs[dst_i] + links.new(out_dst_socket, out_src_link.to_socket) + # relink rest inputs if possible, no criteria + for src_inp in node.inputs: + for dst_inp in new_node.inputs: + if src_inp.links and not dst_inp.links: + src_link = src_inp.links[0] + links.new(src_link.from_socket, dst_inp) + links.remove(src_link) + # relink rest outputs if possible, base on node kind if any left. + for src_o in node.outputs: + for out_src_link in src_o.links: + for dst_o in new_node.outputs: + if src_o.type == dst_o.type: + links.new(dst_o, out_src_link.to_socket) + # relink rest outputs no criteria if any left. Link all from first output. + for src_o in node.outputs: + for out_src_link in src_o.links: + if new_node.outputs: + links.new(new_node.outputs[0], out_src_link.to_socket) + nodes.remove(node) + return {'FINISHED'} + + +class NWMergeNodes(Operator, NWBase): + bl_idname = "node.nw_merge_nodes" bl_label = "Merge Nodes" bl_description = "Merge Selected Nodes" bl_options = {'REGISTER', 'UNDO'} mode = EnumProperty( - name="mode", - description="All possible blend types and math operations", - items=blend_types + [op for op in operations if op not in blend_types], - ) + name="mode", + description="All possible blend types and math operations", + items=blend_types + [op for op in operations if op not in blend_types], + ) merge_type = EnumProperty( - name="merge type", - description="Type of Merge to be used", - items=( - ('AUTO', 'Auto', 'Automatic Output Type Detection'), - ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'), - ('MIX', 'Mix Node', 'Merge using Mix Nodes'), - ('MATH', 'Math Node', 'Merge using Math Nodes'), - ), - ) + name="merge type", + description="Type of Merge to be used", + items=( + ('AUTO', 'Auto', 'Automatic Output Type Detection'), + ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'), + ('MIX', 'Mix Node', 'Merge using Mix Nodes'), + ('MATH', 'Math Node', 'Merge using Math Nodes'), + ), + ) def execute(self, context): + settings = context.user_preferences.addons[__name__].preferences + merge_hide = settings.merge_hide + merge_position = settings.merge_position # 'center' or 'bottom' + + do_hide = False + do_hide_shader = False + if merge_hide == 'ALWAYS': + do_hide = True + do_hide_shader = True + elif merge_hide == 'NON_SHADER': + do_hide = True + tree_type = context.space_data.node_tree.type if tree_type == 'COMPOSITING': node_type = 'CompositorNode' @@ -214,10 +1506,10 @@ class MergeNodes(Operator, NodeToolBase): if node.select and node.outputs: if merge_type == 'AUTO': for (type, types_list, dst) in ( - ('SHADER', merge_shaders_types, selected_shader), + ('SHADER', ('MIX', 'ADD'), selected_shader), ('RGBA', [t[0] for t in blend_types], selected_mix), ('VALUE', [t[0] for t in operations], selected_math), - ): + ): output_type = node.outputs[0].type valid_mode = mode in types_list # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types. @@ -232,10 +1524,10 @@ class MergeNodes(Operator, NodeToolBase): dst.append([i, node.location.x, node.location.y]) else: for (type, types_list, dst) in ( - ('SHADER', merge_shaders_types, selected_shader), + ('SHADER', ('MIX', 'ADD'), selected_shader), ('MIX', [t[0] for t in blend_types], selected_mix), ('MATH', [t[0] for t in operations], selected_math), - ): + ): if merge_type == type and mode in types_list: dst.append([i, node.location.x, node.location.y]) # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time @@ -251,17 +1543,20 @@ class MergeNodes(Operator, NodeToolBase): # sort list by loc_x - reversed nodes_list.sort(key=lambda k: k[1], reverse=True) # get maximum loc_x - loc_x = nodes_list[0][1] + 350.0 + loc_x = nodes_list[0][1] + 250.0 nodes_list.sort(key=lambda k: k[2], reverse=True) - loc_y = nodes_list[len(nodes_list) - 1][2] - offset_y = 40.0 - if nodes_list == selected_shader: + if merge_position == 'CENTER': + 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) + else: + loc_y = nodes_list[len(nodes_list) - 1][2] + offset_y = 100 + if not do_hide: + offset_y = 200 + if nodes_list == selected_shader and not do_hide_shader: offset_y = 150.0 the_range = len(nodes_list) - 1 - do_hide = True if len(nodes_list) == 1: the_range = 1 - do_hide = False for i in range(the_range): if nodes_list == selected_mix: add_type = node_type + 'MixRGB' @@ -269,6 +1564,8 @@ class MergeNodes(Operator, NodeToolBase): add.blend_type = mode add.show_preview = False add.hide = do_hide + if do_hide: + loc_y = loc_y - 50 first = 1 second = 2 add.width_hidden = 100.0 @@ -277,6 +1574,8 @@ class MergeNodes(Operator, NodeToolBase): add = nodes.new(add_type) add.operation = mode add.hide = do_hide + if do_hide: + loc_y = loc_y - 50 first = 0 second = 1 add.width_hidden = 100.0 @@ -284,12 +1583,18 @@ class MergeNodes(Operator, NodeToolBase): if mode == 'MIX': add_type = node_type + 'MixShader' add = nodes.new(add_type) + add.hide = do_hide_shader + if do_hide_shader: + loc_y = loc_y - 50 first = 1 second = 2 add.width_hidden = 100.0 elif mode == 'ADD': add_type = node_type + 'AddShader' add = nodes.new(add_type) + add.hide = do_hide_shader + if do_hide_shader: + loc_y = loc_y - 50 first = 0 second = 1 add.width_hidden = 100.0 @@ -327,22 +1632,23 @@ class MergeNodes(Operator, NodeToolBase): return {'FINISHED'} -class BatchChangeNodes(Operator, NodeToolBase): - bl_idname = "node.batch_change" +class NWBatchChangeNodes(Operator, NWBase): + bl_idname = "node.nw_batch_change" bl_label = "Batch Change" bl_description = "Batch Change Blend Type and Math Operation" bl_options = {'REGISTER', 'UNDO'} blend_type = EnumProperty( - name="Blend Type", - items=blend_types + navs, - ) + name="Blend Type", + items=blend_types + navs, + ) operation = EnumProperty( - name="Operation", - items=operations + navs, - ) + name="Operation", + items=operations + navs, + ) def execute(self, context): + nodes, links = get_nodes_links(context) blend_type = self.blend_type operation = self.operation @@ -389,8 +1695,8 @@ class BatchChangeNodes(Operator, NodeToolBase): return {'FINISHED'} -class ChangeMixFactor(Operator, NodeToolBase): - bl_idname = "node.factor" +class NWChangeMixFactor(Operator, NWBase): + bl_idname = "node.nw_factor" bl_label = "Change Factor" bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes" bl_options = {'REGISTER', 'UNDO'} @@ -420,8 +1726,8 @@ class ChangeMixFactor(Operator, NodeToolBase): return {'FINISHED'} -class NodesCopySettings(Operator): - bl_idname = "node.copy_settings" +class NWCopySettings(Operator, NWBase): + bl_idname = "node.nw_copy_settings" bl_label = "Copy Settings" bl_description = "Copy Settings of Active Node to Selected Nodes" bl_options = {'REGISTER', 'UNDO'} @@ -458,7 +1764,7 @@ class NodesCopySettings(Operator): attributes = ( 'hide', 'show_preview', 'mute', 'label', 'use_custom_color', 'color', 'width', 'width_hidden', - ) + ) for attr in attributes: setattr(copied, attr, getattr(node, attr)) # Handle scenario when 'node' is in frame. 'copied' is in same frame then. @@ -498,20 +1804,20 @@ class NodesCopySettings(Operator): return {'FINISHED'} -class NodesCopyLabel(Operator, NodeToolBase): - bl_idname = "node.copy_label" +class NWCopyLabel(Operator, NWBase): + bl_idname = "node.nw_copy_label" bl_label = "Copy Label" bl_options = {'REGISTER', 'UNDO'} option = EnumProperty( - name="option", - description="Source of name of label", - items=( - ('FROM_ACTIVE', 'from active', 'from active node',), - ('FROM_NODE', 'from node', 'from node linked to selected node'), - ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'), - ) - ) + name="option", + description="Source of name of label", + items=( + ('FROM_ACTIVE', 'from active', 'from active node',), + ('FROM_NODE', 'from node', 'from node linked to selected node'), + ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'), + ) + ) def execute(self, context): nodes, links = get_nodes_links(context) @@ -542,8 +1848,8 @@ class NodesCopyLabel(Operator, NodeToolBase): return {'FINISHED'} -class NodesClearLabel(Operator, NodeToolBase): - bl_idname = "node.clear_label" +class NWClearLabel(Operator, NWBase): + bl_idname = "node.nw_clear_label" bl_label = "Clear Label" bl_options = {'REGISTER', 'UNDO'} @@ -563,8 +1869,41 @@ class NodesClearLabel(Operator, NodeToolBase): return context.window_manager.invoke_confirm(self, event) -class NodesAddTextureSetup(Operator): - bl_idname = "node.add_texture" +class NWModifyLabels(Operator, NWBase): + """Modify Labels of all selected nodes.""" + bl_idname = "node.nw_modify_labels" + bl_label = "Modify Labels" + bl_options = {'REGISTER', 'UNDO'} + + prepend = StringProperty( + name="Add to Beginning" + ) + append = StringProperty( + name="Add to End" + ) + replace_from = StringProperty( + name="Text to Replace" + ) + replace_to = StringProperty( + name="Replace with" + ) + + def execute(self, context): + nodes, links = get_nodes_links(context) + for node in [n for n in nodes if n.select]: + node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append + + return {'FINISHED'} + + def invoke(self, context, event): + self.prepend = "" + self.append = "" + self.remove = "" + return context.window_manager.invoke_props_dialog(self) + + +class NWAddTextureSetup(Operator, NWBase): + bl_idname = "node.nw_add_texture" bl_label = "Texture Setup" bl_description = "Add Texture Node Setup to Selected Shaders" bl_options = {'REGISTER', 'UNDO'} @@ -581,56 +1920,72 @@ class NodesAddTextureSetup(Operator): def execute(self, context): nodes, links = get_nodes_links(context) active = nodes.active + shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}] + texture_types = [x[1] for x in shaders_texture_nodes_props] valid = False if active: if active.select: - if active.type in { - 'BSDF_ANISOTROPIC', - 'BSDF_DIFFUSE', - 'BSDF_GLOSSY', - 'BSDF_GLASS', - 'BSDF_REFRACTION', - 'BSDF_TRANSLUCENT', - 'BSDF_TRANSPARENT', - 'BSDF_VELVET', - 'EMISSION', - 'AMBIENT_OCCLUSION', - }: + if active.type in shader_types or active.type in texture_types: if not active.inputs[0].is_linked: valid = True if valid: locx = active.location.x locy = active.location.y - tex = nodes.new('ShaderNodeTexImage') - tex.location = [locx - 200.0, locy + 28.0] + + xoffset = [500.0, 700.0] + isshader = True + if active.type not in shader_types: + xoffset = [290.0, 500.0] + isshader = False + + coordout = 2 + image_type = 'ShaderNodeTexImage' + + if (active.type in texture_types and active.type != 'TEX_IMAGE') or (active.type == 'BACKGROUND'): + coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated + if active.type == 'BACKGROUND': + image_type = 'ShaderNodeTexEnvironment' + + if isshader: + tex = nodes.new(image_type) + tex.location = [locx - 200.0, locy + 28.0] + map = nodes.new('ShaderNodeMapping') - map.location = [locx - 490.0, locy + 80.0] + map.location = [locx - xoffset[0], locy + 80.0] + map.width = 240 coord = nodes.new('ShaderNodeTexCoord') - coord.location = [locx - 700, locy + 40.0] + coord.location = [locx - xoffset[1], locy + 40.0] active.select = False - nodes.active = tex - links.new(tex.outputs[0], active.inputs[0]) - links.new(map.outputs[0], tex.inputs[0]) - links.new(coord.outputs[2], map.inputs[0]) + if isshader: + nodes.active = tex + links.new(tex.outputs[0], active.inputs[0]) + links.new(map.outputs[0], tex.inputs[0]) + links.new(coord.outputs[coordout], map.inputs[0]) + + else: + nodes.active = map + links.new(map.outputs[0], active.inputs[0]) + links.new(coord.outputs[coordout], map.inputs[0]) return {'FINISHED'} -class NodesAddReroutes(Operator, NodeToolBase): - bl_idname = "node.add_reroutes" +class NWAddReroutes(Operator, NWBase): + """Add Reroute Nodes and link them to outputs of selected nodes""" + bl_idname = "node.nw_add_reroutes" bl_label = "Add Reroutes" bl_description = "Add Reroutes to Outputs" bl_options = {'REGISTER', 'UNDO'} option = EnumProperty( - name="option", - items=[ - ('ALL', 'to all', 'Add to all outputs'), - ('LOOSE', 'to loose', 'Add only to loose outputs'), - ('LINKED', 'to linked', 'Add only to linked outputs'), - ] - ) + name="option", + items=[ + ('ALL', 'to all', 'Add to all outputs'), + ('LOOSE', 'to loose', 'Add only to loose outputs'), + ('LINKED', 'to linked', 'Add only to linked outputs'), + ] + ) def execute(self, context): tree_type = context.space_data.node_tree.type @@ -716,162 +2071,9 @@ class NodesAddReroutes(Operator, NodeToolBase): return {'FINISHED'} -class NodesSwap(Operator, NodeToolBase): - bl_idname = "node.swap_nodes" - bl_label = "Swap Nodes" - bl_options = {'REGISTER', 'UNDO'} - - option = EnumProperty( - items=[ - ('CompositorNodeSwitch', 'Switch', 'Switch'), - ('NodeReroute', 'Reroute', 'Reroute'), - ('NodeMixRGB', 'Mix Node', 'Mix Node'), - ('NodeMath', 'Math Node', 'Math Node'), - ('CompositorNodeAlphaOver', 'Alpha Over', 'Alpha Over'), - ('ShaderNodeMixShader', 'Mix Shader', 'Mix Shader'), - ('ShaderNodeAddShader', 'Add Shader', 'Add Shader'), - ('ShaderNodeBsdfDiffuse', 'Diffuse BSDF', 'Diffuse BSDF'), - ('ShaderNodeBsdfGlossy', 'Glossy BSDF', 'Glossy BSDF'), - ('ShaderNodeBsdfTransparent', 'Transparent BSDF', 'Transparent BSDF'), - ('ShaderNodeBsdfRefraction', 'Refraction BSDF', 'Refraction BSDF'), - ('ShaderNodeBsdfGlass', 'Glass BSDF', 'Glass BSDF'), - ('ShaderNodeBsdfTranslucent', 'Translucent BSDF', 'Translucent BSDF'), - ('ShaderNodeBsdfAnisotropic', 'Anisotropic BSDF', 'Anisotropic BSDF'), - ('ShaderNodeBsdfVelvet', 'Velvet BSDF', 'Velvet BSDF'), - ('ShaderNodeBsdfToon', 'Toon BSDF', 'Toon BSDF'), - ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'), - ('ShaderNodeEmission', 'Emission', 'Emission'), - ('ShaderNodeBackground', 'Background', 'Background'), - ('ShaderNodeAmbientOcclusion', 'Ambient Occlusion', 'Ambient Occlusion'), - ('ShaderNodeHoldout', 'Holdout', 'Holdout'), - ] - ) - - def execute(self, context): - nodes, links = get_nodes_links(context) - tree_type = context.space_data.tree_type - if tree_type == 'CompositorNodeTree': - prefix = 'Compositor' - elif tree_type == 'ShaderNodeTree': - prefix = 'Shader' - option = self.option - selected = [n for n in nodes if n.select] - reselect = [] - mode = None # will be used to set proper operation or blend type in new Math or Mix nodes. - # regular_shaders - global list. Entry: (identifier, type, name for humans) - # example: ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF') - swap_shaders = option in (s[0] for s in regular_shaders) - swap_merge_shaders = option in (s[0] for s in merge_shaders) - if swap_shaders or swap_merge_shaders: - # replace_types - list of node types that can be replaced using selected option - shaders = regular_shaders + merge_shaders - replace_types = [type[1] for type in shaders] - new_type = option - elif option == 'CompositorNodeSwitch': - replace_types = ('REROUTE', 'MIX_RGB', 'MATH', 'ALPHAOVER') - new_type = option - elif option == 'NodeReroute': - replace_types = ('SWITCH') - new_type = option - elif option == 'NodeMixRGB': - replace_types = ('REROUTE', 'SWITCH', 'MATH', 'ALPHAOVER') - new_type = prefix + option - elif option == 'NodeMath': - replace_types = ('REROUTE', 'SWITCH', 'MIX_RGB', 'ALPHAOVER') - new_type = prefix + option - elif option == 'CompositorNodeAlphaOver': - replace_types = ('REROUTE', 'SWITCH', 'MATH', 'MIX_RGB') - new_type = option - for node in selected: - if node.type in replace_types: - hide = node.hide - if node.type == 'REROUTE': - hide = True - new_node = nodes.new(new_type) - # if swap Mix to Math of vice-verca - try to set blend type or operation accordingly - if new_node.type in {'MIX_RGB', 'ALPHAOVER'}: - new_node.inputs[0].default_value = 1.0 - if node.type == 'MATH': - if node.operation in [entry[0] for entry in blend_types]: - if hasattr(new_node, 'blend_type'): - new_node.blend_type = node.operation - for i in range(2): - new_node.inputs[i+1].default_value = [node.inputs[i].default_value] * 3 + [1.0] - elif node.type in {'MIX_RGB', 'ALPHAOVER'}: - for i in range(3): - new_node.inputs[i].default_value = node.inputs[i].default_value - elif new_node.type == 'MATH': - if node.type in {'MIX_RGB', 'ALPHAOVER'}: - if hasattr(node, 'blend_type'): - if node.blend_type in [entry[0] for entry in operations]: - new_node.operation = node.blend_type - for i in range(2): - channels = [] - for c in range(3): - channels.append(node.inputs[i+1].default_value[c]) - new_node.inputs[i].default_value = max(channels) - old_inputs_count = len(node.inputs) - new_inputs_count = len(new_node.inputs) - replace = [] # entries - pairs: old input index, new input index. - if swap_shaders: - for old_i, old_input in enumerate(node.inputs): - for new_i, new_input in enumerate(new_node.inputs): - if old_input.name == new_input.name: - replace.append((old_i, new_i)) - new_input.default_value = old_input.default_value - break - elif option == 'ShaderNodeAddShader': - if node.type == 'ADD_SHADER': - replace = ((0, 0), (1, 1)) - elif node.type == 'MIX_SHADER': - replace = ((1, 0), (2, 1)) - elif option == 'ShaderNodeMixShader': - if node.type == 'ADD_SHADER': - replace = ((0, 1), (1, 2)) - elif node.type == 'MIX_SHADER': - replace = ((1, 1), (2, 2)) - elif new_inputs_count == 1: - replace = ((0, 0), ) - elif new_inputs_count == 2: - if old_inputs_count == 1: - replace = ((0, 0), ) - elif old_inputs_count == 2: - replace = ((0, 0), (1, 1)) - elif old_inputs_count == 3: - replace = ((1, 0), (2, 1)) - elif new_inputs_count == 3: - if old_inputs_count == 1: - replace = ((0, 1), ) - elif old_inputs_count == 2: - replace = ((0, 1), (1, 2)) - elif old_inputs_count == 3: - replace = ((0, 0), (1, 1), (2, 2)) - if replace: - for old_i, new_i in replace: - if node.inputs[old_i].links: - in_link = node.inputs[old_i].links[0] - links.new(in_link.from_socket, new_node.inputs[new_i]) - for out_link in node.outputs[0].links: - links.new(new_node.outputs[0], out_link.to_socket) - for attr in {'location', 'label', 'mute', 'show_preview', 'width_hidden', 'use_clamp'}: - if hasattr(node, attr) and hasattr(new_node, attr): - setattr(new_node, attr, getattr(node, attr)) - new_node.hide = hide - nodes.active = new_node - reselect.append(new_node) - bpy.ops.node.select_all(action="DESELECT") - node.select = True - bpy.ops.node.delete() - else: - reselect.append(node) - for node in reselect: - node.select = True - - return {'FINISHED'} - - -class NodesLinkActiveToSelected(Operator): - bl_idname = "node.link_active_to_selected" +class NWLinkActiveToSelected(Operator, NWBase): + """Link active node to selected nodes basing on various criteria""" + bl_idname = "node.nw_link_active_to_selected" bl_label = "Link Active Node to Selected" bl_options = {'REGISTER', 'UNDO'} @@ -951,20 +2153,20 @@ class NodesLinkActiveToSelected(Operator): return {'FINISHED'} -class AlignNodes(Operator, NodeToolBase): - bl_idname = "node.align_nodes" +class NWAlignNodes(Operator, NWBase): + bl_idname = "node.nw_align_nodes" bl_label = "Align nodes" bl_options = {'REGISTER', 'UNDO'} # option: 'Vertically', 'Horizontally' option = EnumProperty( - name="option", - description="Direction", - items=( - ('AXIS_X', "Align Vertically", 'Align Vertically'), - ('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'), - ) - ) + name="option", + description="Direction", + items=( + ('AXIS_X', "Align Vertically", 'Align Vertically'), + ('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'), + ) + ) def execute(self, context): nodes, links = get_nodes_links(context) @@ -972,6 +2174,8 @@ class AlignNodes(Operator, NodeToolBase): frames_reselect = [] # entry = frame node. will be used to reselect all selected frames active = nodes.active for i, node in enumerate(nodes): + total_w = 0.0 # total width of all nodes. Will be calculated later. + total_h = 0.0 # total height of all nodes. Will be calculated later if node.select: if node.type == 'FRAME': node.select = False @@ -979,43 +2183,19 @@ class AlignNodes(Operator, NodeToolBase): else: locx = node.location.x locy = node.location.y + width = node.dimensions[0] + height = node.dimensions[1] + total_w += width # add nodes[i] width to total width of all nodes + total_h += height # add nodes[i] height to total height of all nodes + # calculate relative locations parent = node.parent while parent is not None: locx += parent.location.x locy += parent.location.y parent = parent.parent - selected.append([i, locx, locy]) + selected.append([i, locx, locy, width, height]) count = len(selected) - # add reroute node then scale all to 0.0 and calculate widths and heights of nodes if count > 1: # aligning makes sense only if at least 2 nodes are selected - helper = nodes.new('NodeReroute') - helper.select = True - bpy.ops.transform.resize(value=(0.0, 0.0, 0.0)) - # store helper's location for further calculations - zero_x = helper.location.x - zero_y = helper.location.y - nodes.remove(helper) - # helper is deleted but its location is stored - # helper's width and height are 0.0. - # Check loc of other nodes in relation to helper to calculate their dimensions - # and append them to entries of "selected" - total_w = 0.0 # total width of all nodes. Will be calculated later. - total_h = 0.0 # total height of all nodes. Will be calculated later - for j, [i, x, y] in enumerate(selected): - locx = nodes[i].location.x - locy = nodes[i].location.y - # take node's parent (frame) into account. Get absolute location - parent = nodes[i].parent - while parent is not None: - locx += parent.location.x - locy += parent.location.y - parent = parent.parent - width = abs((zero_x - locx) * 2.0) - height = abs((zero_y - locy) * 2.0) - selected[j].append(width) # complete selected's entry for nodes[i] - selected[j].append(height) # complete selected's entry for nodes[i] - total_w += width # add nodes[i] width to total width of all nodes - total_h += height # add nodes[i] height to total height of all nodes selected_sorted_x = sorted(selected, key=lambda k: (k[1], -k[2])) selected_sorted_y = sorted(selected, key=lambda k: (-k[2], k[1])) min_x = selected_sorted_x[0][1] # min loc.x @@ -1071,18 +2251,18 @@ class AlignNodes(Operator, NodeToolBase): return {'FINISHED'} -class SelectParentChildren(Operator, NodeToolBase): - bl_idname = "node.select_parent_child" +class NWSelectParentChildren(Operator, NWBase): + bl_idname = "node.nw_select_parent_child" bl_label = "Select Parent or Children" bl_options = {'REGISTER', 'UNDO'} option = EnumProperty( - name="option", - items=( - ('PARENT', 'Select Parent', 'Select Parent Frame'), - ('CHILD', 'Select Children', 'Select members of selected frame'), - ) - ) + name="option", + items=( + ('PARENT', 'Select Parent', 'Select Parent Frame'), + ('CHILD', 'Select Children', 'Select members of selected frame'), + ) + ) def execute(self, context): nodes, links = get_nodes_links(context) @@ -1102,8 +2282,9 @@ class SelectParentChildren(Operator, NodeToolBase): return {'FINISHED'} -class DetachOutputs(Operator, NodeToolBase): - bl_idname = "node.detach_outputs" +class NWDetachOutputs(Operator, NWBase): + """Detach outputs of selected node leaving inluts liked""" + bl_idname = "node.nw_detach_outputs" bl_label = "Detach Outputs" bl_options = {'REGISTER', 'UNDO'} @@ -1123,519 +2304,869 @@ class DetachOutputs(Operator, NodeToolBase): return {'FINISHED'} -class LinkToOutputNode(Operator, NodeToolBase): - bl_idname = "node.link_to_output_node" - bl_label = "Link to Output Node" +class NWLinkToOutputNode(Operator, NWBase): + """Link to Composite node or Material Output node""" + bl_idname = "node.nw_link_out" + bl_label = "Connect to Output" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): space = context.space_data - valid = False - if (space.type == 'NODE_EDITOR' and - space.node_tree is not None and - context.active_node is not None - ): - valid = True - return valid + return (space.type == 'NODE_EDITOR' and space.node_tree is not None and context.active_node is not None) def execute(self, context): nodes, links = get_nodes_links(context) active = nodes.active output_node = None + tree_type = context.space_data.tree_type + output_types_shaders = [x[1] for x in shaders_output_nodes_props] + output_types_compo = ['COMPOSITE'] + output_types = output_types_shaders + output_types_compo for node in nodes: - if (node.type == 'OUTPUT_MATERIAL' or\ - node.type == 'OUTPUT_WORLD' or\ - node.type == 'OUTPUT_LAMP' or\ - node.type == 'COMPOSITE'): + if node.type in output_types: output_node = node break if not output_node: bpy.ops.node.select_all(action="DESELECT") - type = context.space_data.tree_type - if type == 'ShaderNodeTree': + if tree_type == 'ShaderNodeTree': output_node = nodes.new('ShaderNodeOutputMaterial') - elif type == 'CompositorNodeTree': + elif tree_type == 'CompositorNodeTree': output_node = nodes.new('CompositorNodeComposite') - output_node.location = active.location + Vector((300.0, 0.0)) - nodes.active = output_node + output_node.location.x = active.location.x + active.dimensions.x + 80 + output_node.location.y = active.location.y if (output_node and active.outputs): output_index = 0 for i, output in enumerate(active.outputs): if output.type == output_node.inputs[0].type: output_index = i break - links.new(active.outputs[output_index], output_node.inputs[0]) + + out_input_index = 0 + if tree_type == 'ShaderNodeTree': + if active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader + out_input_index = 2 + links.new(active.outputs[output_index], output_node.inputs[out_input_index]) + + hack_force_update(context, nodes) # viewport render does not update return {'FINISHED'} -############################################################# -# P A N E L S -############################################################# +# +# P A N E L +# -class EfficiencyToolsPanel(Panel, NodeToolBase): - bl_idname = "NODE_PT_efficiency_tools" +def drawlayout(context, layout, mode='non-panel'): + tree_type = context.space_data.tree_type + + col = layout.column(align=True) + col.menu(NWMergeNodesMenu.bl_idname) + col.separator() + + col = layout.column(align=True) + col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type") + col.separator() + + if tree_type == 'ShaderNodeTree': + col = layout.column(align=True) + col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL') + col.separator() + + col = layout.column(align=True) + col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED') + col.operator(NWSwapOutputs.bl_idname) + col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED') + col.separator() + + col = layout.column(align=True) + col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED') + col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER') + col.separator() + + col = layout.column(align=True) + if mode == 'panel': + row = col.row(align=True) + row.operator(NWClearLabel.bl_idname).option = True + row.operator(NWModifyLabels.bl_idname) + else: + col.operator(NWClearLabel.bl_idname).option = True + col.operator(NWModifyLabels.bl_idname) + col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change") + col.separator() + col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected") + col.separator() + + col = layout.column(align=True) + if tree_type == 'CompositorNodeTree': + col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS') + col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH') + col.separator() + + col = layout.column(align=True) + col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC') + col.separator() + + col = layout.column(align=True) + col.operator(NWDeleteUnused.bl_idname, icon='CANCEL') + col.separator() + + +class NodeWranglerPanel(Panel, NWBase): + bl_idname = "NODE_PT_nw_node_wrangler" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' - bl_label = "Efficiency Tools (Ctrl-SPACE)" + bl_label = "Node Wrangler" + + prepend = StringProperty( + name='prepend', + ) + append = StringProperty() + remove = StringProperty() def draw(self, context): - type = context.space_data.tree_type - layout = self.layout + self.layout.label(text="(Quick access: Ctrl+Space)") + drawlayout(context, self.layout, mode='panel') - box = layout.box() - box.menu(MergeNodesMenu.bl_idname) - if type == 'ShaderNodeTree': - box.operator(NodesAddTextureSetup.bl_idname, text="Add Image Texture (Ctrl T)") - box.menu(BatchChangeNodesMenu.bl_idname, text="Batch Change...") - box.menu(NodeAlignMenu.bl_idname, text="Align Nodes (Shift =)") - box.menu(CopyToSelectedMenu.bl_idname, text="Copy to Selected (Shift-C)") - box.operator(NodesClearLabel.bl_idname).option = True - box.operator(DetachOutputs.bl_idname) - box.menu(AddReroutesMenu.bl_idname, text="Add Reroutes ( / )") - box.menu(NodesSwapMenu.bl_idname, text="Swap Nodes (Shift-S)") - box.menu(LinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected ( \\ )") - box.operator(LinkToOutputNode.bl_idname) - - -############################################################# -# M E N U S -############################################################# -class EfficiencyToolsMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_node_tools_menu" - bl_label = "Efficiency Tools" +# +# M E N U S +# +class NodeWranglerMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_node_wrangler_menu" + bl_label = "Node Wrangler" def draw(self, context): - type = context.space_data.tree_type - layout = self.layout - layout.menu(MergeNodesMenu.bl_idname, text="Merge Selected Nodes") - if type == 'ShaderNodeTree': - layout.operator(NodesAddTextureSetup.bl_idname, text="Add Image Texture with coordinates") - layout.menu(BatchChangeNodesMenu.bl_idname, text="Batch Change") - layout.menu(NodeAlignMenu.bl_idname, text="Align Nodes") - layout.menu(CopyToSelectedMenu.bl_idname, text="Copy to Selected") - layout.operator(NodesClearLabel.bl_idname).option = True - layout.operator(DetachOutputs.bl_idname) - layout.menu(AddReroutesMenu.bl_idname, text="Add Reroutes") - layout.menu(NodesSwapMenu.bl_idname, text="Swap Nodes") - layout.menu(LinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected") - layout.operator(LinkToOutputNode.bl_idname) - - -class MergeNodesMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_merge_nodes_menu" + drawlayout(context, self.layout) + + +class NWMergeNodesMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_merge_nodes_menu" bl_label = "Merge Selected Nodes" def draw(self, context): type = context.space_data.tree_type layout = self.layout if type == 'ShaderNodeTree': - layout.menu(MergeShadersMenu.bl_idname, text="Use Shaders") - layout.menu(MergeMixMenu.bl_idname, text="Use Mix Nodes") - layout.menu(MergeMathMenu.bl_idname, text="Use Math Nodes") + layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders") + layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes") + layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes") -class MergeShadersMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_merge_shaders_menu" +class NWMergeShadersMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_merge_shaders_menu" bl_label = "Merge Selected Nodes using Shaders" def draw(self, context): layout = self.layout - for type in merge_shaders_types: - props = layout.operator(MergeNodes.bl_idname, text=type) + for type in ('MIX', 'ADD'): + props = layout.operator(NWMergeNodes.bl_idname, text=type) props.mode = type props.merge_type = 'SHADER' -class MergeMixMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_merge_mix_menu" +class NWMergeMixMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_merge_mix_menu" bl_label = "Merge Selected Nodes using Mix" def draw(self, context): layout = self.layout for type, name, description in blend_types: - props = layout.operator(MergeNodes.bl_idname, text=name) + props = layout.operator(NWMergeNodes.bl_idname, text=name) props.mode = type props.merge_type = 'MIX' -class MergeMathMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_merge_math_menu" +class NWMergeMathMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_merge_math_menu" bl_label = "Merge Selected Nodes using Math" def draw(self, context): layout = self.layout for type, name, description in operations: - props = layout.operator(MergeNodes.bl_idname, text=name) + props = layout.operator(NWMergeNodes.bl_idname, text=name) props.mode = type props.merge_type = 'MATH' -class BatchChangeNodesMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_batch_change_nodes_menu" +class NWBatchChangeNodesMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_batch_change_nodes_menu" bl_label = "Batch Change Selected Nodes" def draw(self, context): layout = self.layout - layout.menu(BatchChangeBlendTypeMenu.bl_idname) - layout.menu(BatchChangeOperationMenu.bl_idname) + layout.menu(NWBatchChangeBlendTypeMenu.bl_idname) + layout.menu(NWBatchChangeOperationMenu.bl_idname) -class BatchChangeBlendTypeMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_batch_change_blend_type_menu" +class NWBatchChangeBlendTypeMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_batch_change_blend_type_menu" bl_label = "Batch Change Blend Type" def draw(self, context): layout = self.layout for type, name, description in blend_types: - props = layout.operator(BatchChangeNodes.bl_idname, text=name) + props = layout.operator(NWBatchChangeNodes.bl_idname, text=name) props.blend_type = type props.operation = 'CURRENT' -class BatchChangeOperationMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_batch_change_operation_menu" +class NWBatchChangeOperationMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_batch_change_operation_menu" bl_label = "Batch Change Math Operation" def draw(self, context): layout = self.layout for type, name, description in operations: - props = layout.operator(BatchChangeNodes.bl_idname, text=name) + props = layout.operator(NWBatchChangeNodes.bl_idname, text=name) props.blend_type = 'CURRENT' props.operation = type -class CopyToSelectedMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_copy_node_properties_menu" +class NWCopyToSelectedMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_copy_node_properties_menu" bl_label = "Copy to Selected" def draw(self, context): layout = self.layout - layout.operator(NodesCopySettings.bl_idname, text="Settings from Active") - layout.menu(CopyLabelMenu.bl_idname) + layout.operator(NWCopySettings.bl_idname, text="Settings from Active") + layout.menu(NWCopyLabelMenu.bl_idname) -class CopyLabelMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_copy_label_menu" +class NWCopyLabelMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_copy_label_menu" bl_label = "Copy Label" def draw(self, context): layout = self.layout - layout.operator(NodesCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE' - layout.operator(NodesCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE' - layout.operator(NodesCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET' + layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE' + layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE' + layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET' -class AddReroutesMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_add_reroutes_menu" +class NWAddReroutesMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_add_reroutes_menu" bl_label = "Add Reroutes" bl_description = "Add Reroute Nodes to Selected Nodes' Outputs" def draw(self, context): layout = self.layout - layout.operator(NodesAddReroutes.bl_idname, text="to All Outputs").option = 'ALL' - layout.operator(NodesAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE' - layout.operator(NodesAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED' - - -class NodesSwapMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_swap_menu" - bl_label = "Swap Nodes" - - def draw(self, context): - type = context.space_data.tree_type - layout = self.layout - if type == 'ShaderNodeTree': - layout.menu(ShadersSwapMenu.bl_idname, text="Swap Shaders") - layout.operator(NodesSwap.bl_idname, text="Change to Mix Nodes").option = 'NodeMixRGB' - layout.operator(NodesSwap.bl_idname, text="Change to Math Nodes").option = 'NodeMath' - if type == 'CompositorNodeTree': - layout.operator(NodesSwap.bl_idname, text="Change to Alpha Over").option = 'CompositorNodeAlphaOver' - if type == 'CompositorNodeTree': - layout.operator(NodesSwap.bl_idname, text="Change to Switches").option = 'CompositorNodeSwitch' - layout.operator(NodesSwap.bl_idname, text="Change to Reroutes").option = 'NodeReroute' + layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL' + layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE' + layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED' -class ShadersSwapMenu(Menu): - bl_idname = "NODE_MT_shaders_swap_menu" - bl_label = "Swap Shaders" - - @classmethod - def poll(cls, context): - space = context.space_data - valid = False - if space.type == 'NODE_EDITOR': - if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None: - valid = True - return valid - - def draw(self, context): - layout = self.layout - shaders = merge_shaders + regular_shaders - for opt, type, txt in shaders: - layout.operator(NodesSwap.bl_idname, text=txt).option = opt - - -class LinkActiveToSelectedMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_link_active_to_selected_menu" +class NWLinkActiveToSelectedMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_link_active_to_selected_menu" bl_label = "Link Active to Selected" def draw(self, context): layout = self.layout - layout.menu(LinkStandardMenu.bl_idname) - layout.menu(LinkUseNodeNameMenu.bl_idname) - layout.menu(LinkUseOutputsNamesMenu.bl_idname) + layout.menu(NWLinkStandardMenu.bl_idname) + layout.menu(NWLinkUseNodeNameMenu.bl_idname) + layout.menu(NWLinkUseOutputsNamesMenu.bl_idname) -class LinkStandardMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_link_standard_menu" +class NWLinkStandardMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_link_standard_menu" bl_label = "To All Selected" def draw(self, context): layout = self.layout - props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") + props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links") props.replace = False props.use_node_name = False props.use_outputs_names = False - props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links") + props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links") props.replace = True props.use_node_name = False props.use_outputs_names = False -class LinkUseNodeNameMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_link_use_node_name_menu" +class NWLinkUseNodeNameMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_link_use_node_name_menu" bl_label = "Use Node Name/Label" def draw(self, context): layout = self.layout - props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") + props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links") props.replace = False props.use_node_name = True props.use_outputs_names = False - props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links") + props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links") props.replace = True props.use_node_name = True props.use_outputs_names = False -class LinkUseOutputsNamesMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_link_use_outputs_names_menu" +class NWLinkUseOutputsNamesMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_link_use_outputs_names_menu" bl_label = "Use Outputs Names" def draw(self, context): layout = self.layout - props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") + props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links") props.replace = False props.use_node_name = False props.use_outputs_names = True - props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links") + props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links") props.replace = True props.use_node_name = False props.use_outputs_names = True -class NodeAlignMenu(Menu, NodeToolBase): - bl_idname = "NODE_MT_node_align_menu" +class NWNodeAlignMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_node_align_menu" bl_label = "Align Nodes" def draw(self, context): layout = self.layout - layout.operator(AlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X' - layout.operator(AlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y' + layout.operator(NWAlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X' + layout.operator(NWAlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y' -############################################################# -# MENU ITEMS -############################################################# +# TODO, add to toolbar panel +class NWUVMenu(bpy.types.Menu): + bl_idname = "NODE_MT_nw_node_uvs_menu" + bl_label = "UV Maps" + + @classmethod + def poll(cls, context): + if context.area.spaces[0].node_tree: + if context.area.spaces[0].node_tree.type == 'SHADER': + return True + else: + return False + else: + return False + + def draw(self, context): + l = self.layout + nodes, links = get_nodes_links(context) + mat = context.object.active_material + + objs = [] + for obj in bpy.data.objects: + for slot in obj.material_slots: + if slot.material == mat: + objs.append(obj) + uvs = [] + for obj in objs: + if obj.data.uv_layers: + for uv in obj.data.uv_layers: + uvs.append(uv.name) + uvs = list(set(uvs)) # get a unique list + + if uvs: + for uv in uvs: + l.operator(NWAddAttrNode.bl_idname, text=uv).attr_name = uv + else: + l.label("No UV layers on objects with this material") + + +class NWVertColMenu(bpy.types.Menu): + bl_idname = "NODE_MT_nw_node_vertex_color_menu" + bl_label = "Vertex Colors" + + @classmethod + def poll(cls, context): + if context.area.spaces[0].node_tree: + if context.area.spaces[0].node_tree.type == 'SHADER': + return True + else: + return False + else: + return False + + def draw(self, context): + l = self.layout + nodes, links = get_nodes_links(context) + mat = context.object.active_material + + objs = [] + for obj in bpy.data.objects: + for slot in obj.material_slots: + if slot.material == mat: + objs.append(obj) + vcols = [] + for obj in objs: + if obj.data.vertex_colors: + for vcol in obj.data.vertex_colors: + vcols.append(vcol.name) + vcols = list(set(vcols)) # get a unique list + + if vcols: + for vcol in vcols: + l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol + else: + l.label("No Vertex Color layers on objects with this material") + + +class NWSwitchNodeTypeMenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_node_type_menu" + bl_label = "Switch Type to..." + + def draw(self, context): + layout = self.layout + tree = context.space_data.node_tree + if tree.type == 'SHADER': + layout.menu(NWSwitchShadersInputSubmenu.bl_idname) + layout.menu(NWSwitchShadersOutputSubmenu.bl_idname) + layout.menu(NWSwitchShadersShaderSubmenu.bl_idname) + layout.menu(NWSwitchShadersTextureSubmenu.bl_idname) + layout.menu(NWSwitchShadersColorSubmenu.bl_idname) + layout.menu(NWSwitchShadersVectorSubmenu.bl_idname) + layout.menu(NWSwitchShadersConverterSubmenu.bl_idname) + layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname) + if tree.type == 'COMPOSITING': + layout.menu(NWSwitchCompoInputSubmenu.bl_idname) + layout.menu(NWSwitchCompoOutputSubmenu.bl_idname) + layout.menu(NWSwitchCompoColorSubmenu.bl_idname) + layout.menu(NWSwitchCompoConverterSubmenu.bl_idname) + layout.menu(NWSwitchCompoFilterSubmenu.bl_idname) + layout.menu(NWSwitchCompoVectorSubmenu.bl_idname) + layout.menu(NWSwitchCompoMatteSubmenu.bl_idname) + layout.menu(NWSwitchCompoDistortSubmenu.bl_idname) + layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname) + + +class NWSwitchShadersInputSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_input_submenu" + bl_label = "Input" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_input_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchShadersOutputSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_output_submenu" + bl_label = "Output" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_output_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchShadersShaderSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu" + bl_label = "Shader" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_shader_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchShadersTextureSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu" + bl_label = "Texture" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_texture_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchShadersColorSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_color_submenu" + bl_label = "Color" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_color_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchShadersVectorSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu" + bl_label = "Vector" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_vector_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchShadersConverterSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu" + bl_label = "Converter" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_converter_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchShadersLayoutSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu" + bl_label = "Layout" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in shaders_layout_nodes_props: + if type != 'FRAME': + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoInputSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_input_submenu" + bl_label = "Input" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_input_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoOutputSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_output_submenu" + bl_label = "Output" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_output_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoColorSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_color_submenu" + bl_label = "Color" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_color_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoConverterSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_converter_submenu" + bl_label = "Converter" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_converter_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoFilterSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_filter_submenu" + bl_label = "Filter" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_filter_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoVectorSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_vector_submenu" + bl_label = "Vector" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_vector_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoMatteSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_matte_submenu" + bl_label = "Matte" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_matte_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoDistortSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_distort_submenu" + bl_label = "Distort" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_distort_nodes_props: + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +class NWSwitchCompoLayoutSubmenu(Menu, NWBase): + bl_idname = "NODE_MT_nw_switch_compo_layout_submenu" + bl_label = "Layout" + + def draw(self, context): + layout = self.layout + for ident, type, rna_name in compo_layout_nodes_props: + if type != 'FRAME': + props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name) + props.to_type = ident + + +# +# APPENDAGES TO EXISTING UI +# + def select_parent_children_buttons(self, context): layout = self.layout - layout.operator(SelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD' - layout.operator(SelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT' + layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD' + layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT' + + +def attr_nodes_menu_func(self, context): + col = self.layout.column(align=True) + col.menu("NODE_MT_nw_node_uvs_menu") + col.menu("NODE_MT_nw_node_vertex_color_menu") + col.separator() + + +def bgreset_menu_func(self, context): + self.layout.operator(NWResetBG.bl_idname) + -############################################################# +# # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS -############################################################# +# addon_keymaps = [] -# kmi_defs entry: (identifier, key, CTRL, SHIFT, ALT, props) +# kmi_defs entry: (identifier, key, CTRL, SHIFT, ALT, props, nice name) # props entry: (property name, property value) kmi_defs = ( # MERGE NODES - # MergeNodes with Ctrl (AUTO). - (MergeNodes.bl_idname, 'NUMPAD_0', True, False, False, - (('mode', 'MIX'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'ZERO', True, False, False, - (('mode', 'MIX'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, False, - (('mode', 'ADD'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'EQUAL', True, False, False, - (('mode', 'ADD'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, False, - (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'EIGHT', True, False, False, - (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, False, - (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'MINUS', True, False, False, - (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, False, - (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'SLASH', True, False, False, - (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),)), - (MergeNodes.bl_idname, 'COMMA', True, False, False, - (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'PERIOD', True, False, False, - (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),)), - # MergeNodes with Ctrl Alt (MIX) - (MergeNodes.bl_idname, 'NUMPAD_0', True, False, True, - (('mode', 'MIX'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'ZERO', True, False, True, - (('mode', 'MIX'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, True, - (('mode', 'ADD'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'EQUAL', True, False, True, - (('mode', 'ADD'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, True, - (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'EIGHT', True, False, True, - (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, True, - (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'MINUS', True, False, True, - (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, True, - (('mode', 'DIVIDE'), ('merge_type', 'MIX'),)), - (MergeNodes.bl_idname, 'SLASH', True, False, True, - (('mode', 'DIVIDE'), ('merge_type', 'MIX'),)), - # MergeNodes with Ctrl Shift (MATH) - (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, True, False, - (('mode', 'ADD'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'EQUAL', True, True, False, - (('mode', 'ADD'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, True, False, - (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'EIGHT', True, True, False, - (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, True, False, - (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'MINUS', True, True, False, - (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, True, False, - (('mode', 'DIVIDE'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'SLASH', True, True, False, - (('mode', 'DIVIDE'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'COMMA', True, True, False, - (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),)), - (MergeNodes.bl_idname, 'PERIOD', True, True, False, - (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),)), + # NWMergeNodes with Ctrl (AUTO). + (NWMergeNodes.bl_idname, 'NUMPAD_0', True, False, False, + (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"), + (NWMergeNodes.bl_idname, 'ZERO', True, False, False, + (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"), + (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, False, + (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"), + (NWMergeNodes.bl_idname, 'EQUAL', True, False, False, + (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"), + (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, False, + (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"), + (NWMergeNodes.bl_idname, 'EIGHT', True, False, False, + (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"), + (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, False, + (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"), + (NWMergeNodes.bl_idname, 'MINUS', True, False, False, + (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"), + (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, False, + (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"), + (NWMergeNodes.bl_idname, 'SLASH', True, False, False, + (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"), + (NWMergeNodes.bl_idname, 'COMMA', True, False, False, + (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"), + (NWMergeNodes.bl_idname, 'PERIOD', True, False, False, + (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"), + # NWMergeNodes with Ctrl Alt (MIX) + (NWMergeNodes.bl_idname, 'NUMPAD_0', True, False, True, + (('mode', 'MIX'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Mix)"), + (NWMergeNodes.bl_idname, 'ZERO', True, False, True, + (('mode', 'MIX'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Mix)"), + (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, True, + (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"), + (NWMergeNodes.bl_idname, 'EQUAL', True, False, True, + (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"), + (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, True, + (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"), + (NWMergeNodes.bl_idname, 'EIGHT', True, False, True, + (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"), + (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, True, + (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"), + (NWMergeNodes.bl_idname, 'MINUS', True, False, True, + (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"), + (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, True, + (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"), + (NWMergeNodes.bl_idname, 'SLASH', True, False, True, + (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"), + # NWMergeNodes with Ctrl Shift (MATH) + (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, True, False, + (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"), + (NWMergeNodes.bl_idname, 'EQUAL', True, True, False, + (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"), + (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, True, False, + (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"), + (NWMergeNodes.bl_idname, 'EIGHT', True, True, False, + (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"), + (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, True, False, + (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"), + (NWMergeNodes.bl_idname, 'MINUS', True, True, False, + (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"), + (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, True, False, + (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"), + (NWMergeNodes.bl_idname, 'SLASH', True, True, False, + (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"), + (NWMergeNodes.bl_idname, 'COMMA', True, True, False, + (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"), + (NWMergeNodes.bl_idname, 'PERIOD', True, True, False, + (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"), # BATCH CHANGE NODES - # BatchChangeNodes with Alt - (BatchChangeNodes.bl_idname, 'NUMPAD_0', False, False, True, - (('blend_type', 'MIX'), ('operation', 'CURRENT'),)), - (BatchChangeNodes.bl_idname, 'ZERO', False, False, True, - (('blend_type', 'MIX'), ('operation', 'CURRENT'),)), - (BatchChangeNodes.bl_idname, 'NUMPAD_PLUS', False, False, True, - (('blend_type', 'ADD'), ('operation', 'ADD'),)), - (BatchChangeNodes.bl_idname, 'EQUAL', False, False, True, - (('blend_type', 'ADD'), ('operation', 'ADD'),)), - (BatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', False, False, True, - (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),)), - (BatchChangeNodes.bl_idname, 'EIGHT', False, False, True, - (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),)), - (BatchChangeNodes.bl_idname, 'NUMPAD_MINUS', False, False, True, - (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),)), - (BatchChangeNodes.bl_idname, 'MINUS', False, False, True, - (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),)), - (BatchChangeNodes.bl_idname, 'NUMPAD_SLASH', False, False, True, - (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),)), - (BatchChangeNodes.bl_idname, 'SLASH', False, False, True, - (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),)), - (BatchChangeNodes.bl_idname, 'COMMA', False, False, True, - (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),)), - (BatchChangeNodes.bl_idname, 'PERIOD', False, False, True, - (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),)), - (BatchChangeNodes.bl_idname, 'DOWN_ARROW', False, False, True, - (('blend_type', 'NEXT'), ('operation', 'NEXT'),)), - (BatchChangeNodes.bl_idname, 'UP_ARROW', False, False, True, - (('blend_type', 'PREV'), ('operation', 'PREV'),)), + # NWBatchChangeNodes with Alt + (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', False, False, True, + (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"), + (NWBatchChangeNodes.bl_idname, 'ZERO', False, False, True, + (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"), + (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', False, False, True, + (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"), + (NWBatchChangeNodes.bl_idname, 'EQUAL', False, False, True, + (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"), + (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', False, False, True, + (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"), + (NWBatchChangeNodes.bl_idname, 'EIGHT', False, False, True, + (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"), + (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', False, False, True, + (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"), + (NWBatchChangeNodes.bl_idname, 'MINUS', False, False, True, + (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"), + (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', False, False, True, + (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"), + (NWBatchChangeNodes.bl_idname, 'SLASH', False, False, True, + (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"), + (NWBatchChangeNodes.bl_idname, 'COMMA', False, False, True, + (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"), + (NWBatchChangeNodes.bl_idname, 'PERIOD', False, False, True, + (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"), + (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', False, False, True, + (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"), + (NWBatchChangeNodes.bl_idname, 'UP_ARROW', False, False, True, + (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"), # LINK ACTIVE TO SELECTED # Don't use names, don't replace links (K) - (NodesLinkActiveToSelected.bl_idname, 'K', False, False, False, - (('replace', False), ('use_node_name', False), ('use_outputs_names', False),)), + (NWLinkActiveToSelected.bl_idname, 'K', False, False, False, + (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"), # Don't use names, replace links (Shift K) - (NodesLinkActiveToSelected.bl_idname, 'K', False, True, False, - (('replace', True), ('use_node_name', False), ('use_outputs_names', False),)), + (NWLinkActiveToSelected.bl_idname, 'K', False, True, False, + (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"), # Use node name, don't replace links (') - (NodesLinkActiveToSelected.bl_idname, 'QUOTE', False, False, False, - (('replace', False), ('use_node_name', True), ('use_outputs_names', False),)), + (NWLinkActiveToSelected.bl_idname, 'QUOTE', False, False, False, + (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"), + # Use node name, replace links (Shift ') + (NWLinkActiveToSelected.bl_idname, 'QUOTE', False, True, False, + (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"), + # Don't use names, don't replace links (;) + (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, False, False, + (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"), # Don't use names, replace links (') - (NodesLinkActiveToSelected.bl_idname, 'QUOTE', False, True, False, - (('replace', True), ('use_node_name', True), ('use_outputs_names', False),)), - (NodesLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, False, False, - (('replace', False), ('use_node_name', False), ('use_outputs_names', True),)), - # Don't use names, replace links (') - (NodesLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, True, False, - (('replace', True), ('use_node_name', False), ('use_outputs_names', True),)), + (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, True, False, + (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"), # CHANGE MIX FACTOR - (ChangeMixFactor.bl_idname, 'LEFT_ARROW', False, False, True, (('option', -0.1),)), - (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, False, True, (('option', 0.1),)), - (ChangeMixFactor.bl_idname, 'LEFT_ARROW', False, True, True, (('option', -0.01),)), - (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, True, True, (('option', 0.01),)), - (ChangeMixFactor.bl_idname, 'LEFT_ARROW', True, True, True, (('option', 0.0),)), - (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', True, True, True, (('option', 1.0),)), - (ChangeMixFactor.bl_idname, 'NUMPAD_0', True, True, True, (('option', 0.0),)), - (ChangeMixFactor.bl_idname, 'ZERO', True, True, True, (('option', 0.0),)), - (ChangeMixFactor.bl_idname, 'NUMPAD_1', True, True, True, (('option', 1.0),)), - (ChangeMixFactor.bl_idname, 'ONE', True, True, True, (('option', 1.0),)), + (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"), + (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"), + (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"), + (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"), + (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"), + (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"), + (NWChangeMixFactor.bl_idname, 'NUMPAD_0', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"), + (NWChangeMixFactor.bl_idname, 'ZERO', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"), + (NWChangeMixFactor.bl_idname, 'NUMPAD_1', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"), + (NWChangeMixFactor.bl_idname, 'ONE', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"), # CLEAR LABEL (Alt L) - (NodesClearLabel.bl_idname, 'L', False, False, True, (('option', False),)), + (NWClearLabel.bl_idname, 'L', False, False, True, (('option', False),), "Clear node labels"), + # MODIFY LABEL (Alt Shift L) + (NWModifyLabels.bl_idname, 'L', False, True, True, None, "Modify node labels"), + # Copy Label from active to selected + (NWCopyLabel.bl_idname, 'V', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"), # DETACH OUTPUTS (Alt Shift D) - (DetachOutputs.bl_idname, 'D', False, True, True, None), + (NWDetachOutputs.bl_idname, 'D', False, True, True, None, "Detach outputs"), # LINK TO OUTPUT NODE (O) - (LinkToOutputNode.bl_idname, 'O', False, False, False, None), + (NWLinkToOutputNode.bl_idname, 'O', False, False, False, None, "Link to output node"), # SELECT PARENT/CHILDREN # Select Children - (SelectParentChildren.bl_idname, 'RIGHT_BRACKET', False, False, False, (('option', 'CHILD'),)), + (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', False, False, False, (('option', 'CHILD'),), "Select children"), # Select Parent - (SelectParentChildren.bl_idname, 'LEFT_BRACKET', False, False, False, (('option', 'PARENT'),)), + (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', False, False, False, (('option', 'PARENT'),), "Select Parent"), # Add Texture Setup - (NodesAddTextureSetup.bl_idname, 'T', True, False, False, None), - # Copy Label from active to selected - (NodesCopyLabel.bl_idname, 'V', False, True, False, (('option', 'FROM_ACTIVE'),)), + (NWAddTextureSetup.bl_idname, 'T', True, False, False, None, "Add texture setup"), + # Reset backdrop + (NWResetBG.bl_idname, 'Z', False, False, False, None, "Reset backdrop image zoom"), + # Delete unused + (NWDeleteUnused.bl_idname, 'X', False, False, True, None, "Delete unused nodes"), + # Frame Seleted + (NWFrameSelected.bl_idname, 'P', False, True, False, None, "Frame selected nodes"), + # Swap Outputs + (NWSwapOutputs.bl_idname, 'S', False, False, True, None, "Swap Outputs"), + # Emission Viewer + (NWEmissionViewer.bl_idname, 'LEFTMOUSE', True, True, False, None, "Connect to Cycles Viewer node"), + # Reload Images + (NWReloadImages.bl_idname, 'R', False, False, True, None, "Reload images"), + # Lazy Mix + (NWLazyMix.bl_idname, 'RIGHTMOUSE', False, False, True, None, "Lazy Mix"), + # Lazy Connect + (NWLazyConnect.bl_idname, 'RIGHTMOUSE', True, False, False, None, "Lazy Connect"), # MENUS - ('wm.call_menu', 'SPACE', True, False, False, (('name', EfficiencyToolsMenu.bl_idname),)), - ('wm.call_menu', 'SLASH', False, False, False, (('name', AddReroutesMenu.bl_idname),)), - ('wm.call_menu', 'NUMPAD_SLASH', False, False, False, (('name', AddReroutesMenu.bl_idname),)), - ('wm.call_menu', 'EQUAL', False, True, False, (('name', NodeAlignMenu.bl_idname),)), - ('wm.call_menu', 'BACK_SLASH', False, False, False, (('name', LinkActiveToSelectedMenu.bl_idname),)), - ('wm.call_menu', 'C', False, True, False, (('name', CopyToSelectedMenu.bl_idname),)), - ('wm.call_menu', 'S', False, True, False, (('name', NodesSwapMenu.bl_idname),)), - ) + ('wm.call_menu', 'SPACE', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"), + ('wm.call_menu', 'SLASH', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"), + ('wm.call_menu', 'NUMPAD_SLASH', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"), + ('wm.call_menu', 'EQUAL', False, True, False, (('name', NWNodeAlignMenu.bl_idname),), "Node alignment menu"), + ('wm.call_menu', 'BACK_SLASH', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"), + ('wm.call_menu', 'C', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"), + ('wm.call_menu', 'S', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"), +) def register(): + # props + bpy.types.Scene.NWBusyDrawing = StringProperty( + name="Busy Drawing!", + default="", + description="An internal property used to store only the first mouse position") + bpy.types.Scene.NWDrawColType = StringProperty( + name="Color Type!", + default="x", + description="An internal property used to store the line color") + bpy.utils.register_module(__name__) + + # keymaps km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name='Node Editor', space_type="NODE_EDITOR") - for (identifier, key, CTRL, SHIFT, ALT, props) in kmi_defs: + for (identifier, key, CTRL, SHIFT, ALT, props, nicename) in kmi_defs: kmi = km.keymap_items.new(identifier, key, 'PRESS', ctrl=CTRL, shift=SHIFT, alt=ALT) if props: for prop, value in props: setattr(kmi.properties, prop, value) addon_keymaps.append((km, kmi)) + # menu items bpy.types.NODE_MT_select.append(select_parent_children_buttons) + bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func) + bpy.types.NODE_PT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func) + bpy.types.NODE_PT_backdrop.append(bgreset_menu_func) def unregister(): + # props + del bpy.types.Scene.NWBusyDrawing + del bpy.types.Scene.NWDrawColType + bpy.utils.unregister_module(__name__) - bpy.types.NODE_MT_select.remove(select_parent_children_buttons) + + # keymaps for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear() + # menuitems + bpy.types.NODE_MT_select.remove(select_parent_children_buttons) + bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func) + bpy.types.NODE_PT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func) + bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func) + if __name__ == "__main__": - register() \ No newline at end of file + register()