io_scene_gltf2: correct tip (gives doc generation error)
[blender-addons.git] / node_wrangler.py
blob0c0a2b405caf3607b73225f31c04eeb23f41e438
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Node Wrangler",
21 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
22 "version": (3, 35),
23 "blender": (2, 80, 0),
24 "location": "Node Editor Toolbar or Ctrl-Space",
25 "description": "Various tools to enhance and speed up node-based workflow",
26 "warning": "",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Nodes/Nodes_Efficiency_Tools",
29 "category": "Node",
32 import bpy, blf, bgl
33 from bpy.types import Operator, Panel, Menu
34 from bpy.props import (
35 FloatProperty,
36 EnumProperty,
37 BoolProperty,
38 IntProperty,
39 StringProperty,
40 FloatVectorProperty,
41 CollectionProperty,
43 from bpy_extras.io_utils import ImportHelper, ExportHelper
44 from mathutils import Vector
45 from math import cos, sin, pi, hypot
46 from os import path
47 from glob import glob
48 from copy import copy
49 from itertools import chain
50 import re
51 from collections import namedtuple
53 #################
54 # rl_outputs:
55 # list of outputs of Input Render Layer
56 # with attributes determinig if pass is used,
57 # and MultiLayer EXR outputs names and corresponding render engines
59 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles)
60 RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_internal', 'in_cycles'])
61 rl_outputs = (
62 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
63 RL_entry('use_pass_color', 'Color', 'Color', True, False),
64 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
65 RL_entry('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False),
66 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
67 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
68 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
69 RL_entry('use_pass_emit', 'Emit', 'Emit', True, False),
70 RL_entry('use_pass_environment', 'Environment', 'Env', True, False),
71 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
72 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
73 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
74 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', True, False),
75 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', True, True),
76 RL_entry('use_pass_mist', 'Mist', 'Mist', True, False),
77 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
78 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', True, True),
79 RL_entry('use_pass_reflection', 'Reflect', 'Reflect', True, False),
80 RL_entry('use_pass_refraction', 'Refract', 'Refract', True, False),
81 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', True, True),
82 RL_entry('use_pass_specular', 'Specular', 'Spec', True, False),
83 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', False, True),
84 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', False, True),
85 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
86 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
87 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
88 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
89 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
90 RL_entry('use_pass_vector', 'Speed', 'Vector', True, True),
91 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
94 # shader nodes
95 # (rna_type.identifier, type, rna_type.name)
96 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
97 shaders_input_nodes_props = (
98 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
99 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
100 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
101 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
102 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
103 ('ShaderNodeRGB', 'RGB', 'RGB'),
104 ('ShaderNodeValue', 'VALUE', 'Value'),
105 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
106 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
107 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
108 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
109 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
110 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
111 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
112 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
114 # (rna_type.identifier, type, rna_type.name)
115 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
116 shaders_output_nodes_props = (
117 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
118 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
119 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
121 # (rna_type.identifier, type, rna_type.name)
122 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
123 shaders_shader_nodes_props = (
124 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
125 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
126 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
127 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
128 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
129 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
130 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
131 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
132 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
133 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
134 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
135 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
136 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
137 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
138 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
139 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
140 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
141 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
142 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
143 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
145 # (rna_type.identifier, type, rna_type.name)
146 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
147 shaders_texture_nodes_props = (
148 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
149 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
150 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
151 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
152 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
153 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
154 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
155 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
156 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
157 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
158 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
159 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
161 # (rna_type.identifier, type, rna_type.name)
162 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
163 shaders_color_nodes_props = (
164 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
165 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
166 ('ShaderNodeInvert', 'INVERT', 'Invert'),
167 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
168 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
169 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
170 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
172 # (rna_type.identifier, type, rna_type.name)
173 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
174 shaders_vector_nodes_props = (
175 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
176 ('ShaderNodeBump', 'BUMP', 'Bump'),
177 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
178 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
179 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
180 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
182 # (rna_type.identifier, type, rna_type.name)
183 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
184 shaders_converter_nodes_props = (
185 ('ShaderNodeMath', 'MATH', 'Math'),
186 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
187 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
188 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
189 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
190 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
191 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
192 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
193 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
194 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
195 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
196 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
198 # (rna_type.identifier, type, rna_type.name)
199 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
200 shaders_layout_nodes_props = (
201 ('NodeFrame', 'FRAME', 'Frame'),
202 ('NodeReroute', 'REROUTE', 'Reroute'),
205 # compositing nodes
206 # (rna_type.identifier, type, rna_type.name)
207 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
208 compo_input_nodes_props = (
209 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
210 ('CompositorNodeImage', 'IMAGE', 'Image'),
211 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
212 ('CompositorNodeMask', 'MASK', 'Mask'),
213 ('CompositorNodeRGB', 'RGB', 'RGB'),
214 ('CompositorNodeValue', 'VALUE', 'Value'),
215 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
216 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
217 ('CompositorNodeTime', 'TIME', 'Time'),
218 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
220 # (rna_type.identifier, type, rna_type.name)
221 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
222 compo_output_nodes_props = (
223 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
224 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
225 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
226 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
227 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
229 # (rna_type.identifier, type, rna_type.name)
230 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
231 compo_color_nodes_props = (
232 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
233 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
234 ('CompositorNodeInvert', 'INVERT', 'Invert'),
235 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
236 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
237 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
238 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
239 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
240 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
241 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
242 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
243 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
245 # (rna_type.identifier, type, rna_type.name)
246 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
247 compo_converter_nodes_props = (
248 ('CompositorNodeMath', 'MATH', 'Math'),
249 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
250 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
251 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
252 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
253 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
254 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
255 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
256 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
257 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
258 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
259 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
260 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
261 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
263 # (rna_type.identifier, type, rna_type.name)
264 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
265 compo_filter_nodes_props = (
266 ('CompositorNodeBlur', 'BLUR', 'Blur'),
267 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
268 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
269 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
270 ('CompositorNodeFilter', 'FILTER', 'Filter'),
271 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
272 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
273 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
274 ('CompositorNodeGlare', 'GLARE', 'Glare'),
275 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
276 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
277 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
278 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
280 # (rna_type.identifier, type, rna_type.name)
281 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
282 compo_vector_nodes_props = (
283 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
284 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
285 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
286 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
287 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
289 # (rna_type.identifier, type, rna_type.name)
290 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
291 compo_matte_nodes_props = (
292 ('CompositorNodeKeying', 'KEYING', 'Keying'),
293 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
294 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
295 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
296 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
297 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
298 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
299 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
300 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
301 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
302 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
303 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
305 # (rna_type.identifier, type, rna_type.name)
306 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
307 compo_distort_nodes_props = (
308 ('CompositorNodeScale', 'SCALE', 'Scale'),
309 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
310 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
311 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
312 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
313 ('CompositorNodeFlip', 'FLIP', 'Flip'),
314 ('CompositorNodeCrop', 'CROP', 'Crop'),
315 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
316 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
317 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
318 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
319 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
320 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
322 # (rna_type.identifier, type, rna_type.name)
323 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
324 compo_layout_nodes_props = (
325 ('NodeFrame', 'FRAME', 'Frame'),
326 ('NodeReroute', 'REROUTE', 'Reroute'),
327 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
329 # Blender Render material nodes
330 # (rna_type.identifier, type, rna_type.name)
331 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
332 blender_mat_input_nodes_props = (
333 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
334 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
335 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
336 ('ShaderNodeValue', 'VALUE', 'Value'),
337 ('ShaderNodeRGB', 'RGB', 'RGB'),
338 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
339 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
340 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
343 # (rna_type.identifier, type, rna_type.name)
344 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
345 blender_mat_output_nodes_props = (
346 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
349 # (rna_type.identifier, type, rna_type.name)
350 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
351 blender_mat_color_nodes_props = (
352 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
353 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
354 ('ShaderNodeInvert', 'INVERT', 'Invert'),
355 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
358 # (rna_type.identifier, type, rna_type.name)
359 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
360 blender_mat_vector_nodes_props = (
361 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
362 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
363 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
366 # (rna_type.identifier, type, rna_type.name)
367 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
368 blender_mat_converter_nodes_props = (
369 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
370 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
371 ('ShaderNodeMath', 'MATH', 'Math'),
372 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
373 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
374 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
375 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
376 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
377 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
380 # (rna_type.identifier, type, rna_type.name)
381 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
382 blender_mat_layout_nodes_props = (
383 ('NodeReroute', 'REROUTE', 'Reroute'),
386 # Texture Nodes
387 # (rna_type.identifier, type, rna_type.name)
388 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
389 texture_input_nodes_props = (
390 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
391 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
392 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
393 ('TextureNodeImage', 'IMAGE', 'Image'),
396 # (rna_type.identifier, type, rna_type.name)
397 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
398 texture_output_nodes_props = (
399 ('TextureNodeOutput', 'OUTPUT', 'Output'),
400 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
403 # (rna_type.identifier, type, rna_type.name)
404 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
405 texture_color_nodes_props = (
406 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
407 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
408 ('TextureNodeInvert', 'INVERT', 'Invert'),
409 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
410 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
411 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
414 # (rna_type.identifier, type, rna_type.name)
415 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
416 texture_pattern_nodes_props = (
417 ('TextureNodeChecker', 'CHECKER', 'Checker'),
418 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
421 # (rna_type.identifier, type, rna_type.name)
422 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
423 texture_textures_nodes_props = (
424 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
425 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
426 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
427 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
428 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
429 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
430 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
431 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
432 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
433 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
436 # (rna_type.identifier, type, rna_type.name)
437 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
438 texture_converter_nodes_props = (
439 ('TextureNodeMath', 'MATH', 'Math'),
440 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
441 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
442 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
443 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
446 # (rna_type.identifier, type, rna_type.name)
447 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
448 texture_distort_nodes_props = (
449 ('TextureNodeScale', 'SCALE', 'Scale'),
450 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
451 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
452 ('TextureNodeAt', 'AT', 'At'),
455 # (rna_type.identifier, type, rna_type.name)
456 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
457 texture_layout_nodes_props = (
458 ('NodeReroute', 'REROUTE', 'Reroute'),
461 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
462 # used list, not tuple for easy merging with other lists.
463 blend_types = [
464 ('MIX', 'Mix', 'Mix Mode'),
465 ('ADD', 'Add', 'Add Mode'),
466 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
467 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
468 ('SCREEN', 'Screen', 'Screen Mode'),
469 ('DIVIDE', 'Divide', 'Divide Mode'),
470 ('DIFFERENCE', 'Difference', 'Difference Mode'),
471 ('DARKEN', 'Darken', 'Darken Mode'),
472 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
473 ('OVERLAY', 'Overlay', 'Overlay Mode'),
474 ('DODGE', 'Dodge', 'Dodge Mode'),
475 ('BURN', 'Burn', 'Burn Mode'),
476 ('HUE', 'Hue', 'Hue Mode'),
477 ('SATURATION', 'Saturation', 'Saturation Mode'),
478 ('VALUE', 'Value', 'Value Mode'),
479 ('COLOR', 'Color', 'Color Mode'),
480 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
481 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
484 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
485 # used list, not tuple for easy merging with other lists.
486 operations = [
487 ('ADD', 'Add', 'Add Mode'),
488 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
489 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
490 ('DIVIDE', 'Divide', 'Divide Mode'),
491 ('SINE', 'Sine', 'Sine Mode'),
492 ('COSINE', 'Cosine', 'Cosine Mode'),
493 ('TANGENT', 'Tangent', 'Tangent Mode'),
494 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
495 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
496 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
497 ('POWER', 'Power', 'Power Mode'),
498 ('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
499 ('MINIMUM', 'Minimum', 'Minimum Mode'),
500 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
501 ('ROUND', 'Round', 'Round Mode'),
502 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
503 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
504 ('MODULO', 'Modulo', 'Modulo Mode'),
505 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
508 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
509 # used list, not tuple for easy merging with other lists.
510 navs = [
511 ('CURRENT', 'Current', 'Leave at current state'),
512 ('NEXT', 'Next', 'Next blend type/operation'),
513 ('PREV', 'Prev', 'Previous blend type/operation'),
516 draw_color_sets = {
517 "red_white": (
518 (1.0, 1.0, 1.0, 0.7),
519 (1.0, 0.0, 0.0, 0.7),
520 (0.8, 0.2, 0.2, 1.0)
522 "green": (
523 (0.0, 0.0, 0.0, 1.0),
524 (0.38, 0.77, 0.38, 1.0),
525 (0.38, 0.77, 0.38, 1.0)
527 "yellow": (
528 (0.0, 0.0, 0.0, 1.0),
529 (0.77, 0.77, 0.16, 1.0),
530 (0.77, 0.77, 0.16, 1.0)
532 "purple": (
533 (0.0, 0.0, 0.0, 1.0),
534 (0.38, 0.38, 0.77, 1.0),
535 (0.38, 0.38, 0.77, 1.0)
537 "grey": (
538 (0.0, 0.0, 0.0, 1.0),
539 (0.63, 0.63, 0.63, 1.0),
540 (0.63, 0.63, 0.63, 1.0)
542 "black": (
543 (1.0, 1.0, 1.0, 0.7),
544 (0.0, 0.0, 0.0, 0.7),
545 (0.2, 0.2, 0.2, 1.0)
550 def is_cycles_or_eevee(context):
551 return context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}
554 def nice_hotkey_name(punc):
555 # convert the ugly string name into the actual character
556 pairs = (
557 ('LEFTMOUSE', "LMB"),
558 ('MIDDLEMOUSE', "MMB"),
559 ('RIGHTMOUSE', "RMB"),
560 ('WHEELUPMOUSE', "Wheel Up"),
561 ('WHEELDOWNMOUSE', "Wheel Down"),
562 ('WHEELINMOUSE', "Wheel In"),
563 ('WHEELOUTMOUSE', "Wheel Out"),
564 ('ZERO', "0"),
565 ('ONE', "1"),
566 ('TWO', "2"),
567 ('THREE', "3"),
568 ('FOUR', "4"),
569 ('FIVE', "5"),
570 ('SIX', "6"),
571 ('SEVEN', "7"),
572 ('EIGHT', "8"),
573 ('NINE', "9"),
574 ('OSKEY', "Super"),
575 ('RET', "Enter"),
576 ('LINE_FEED', "Enter"),
577 ('SEMI_COLON', ";"),
578 ('PERIOD', "."),
579 ('COMMA', ","),
580 ('QUOTE', '"'),
581 ('MINUS', "-"),
582 ('SLASH', "/"),
583 ('BACK_SLASH', "\\"),
584 ('EQUAL', "="),
585 ('NUMPAD_1', "Numpad 1"),
586 ('NUMPAD_2', "Numpad 2"),
587 ('NUMPAD_3', "Numpad 3"),
588 ('NUMPAD_4', "Numpad 4"),
589 ('NUMPAD_5', "Numpad 5"),
590 ('NUMPAD_6', "Numpad 6"),
591 ('NUMPAD_7', "Numpad 7"),
592 ('NUMPAD_8', "Numpad 8"),
593 ('NUMPAD_9', "Numpad 9"),
594 ('NUMPAD_0', "Numpad 0"),
595 ('NUMPAD_PERIOD', "Numpad ."),
596 ('NUMPAD_SLASH', "Numpad /"),
597 ('NUMPAD_ASTERIX', "Numpad *"),
598 ('NUMPAD_MINUS', "Numpad -"),
599 ('NUMPAD_ENTER', "Numpad Enter"),
600 ('NUMPAD_PLUS', "Numpad +"),
602 nice_punc = False
603 for (ugly, nice) in pairs:
604 if punc == ugly:
605 nice_punc = nice
606 break
607 if not nice_punc:
608 nice_punc = punc.replace("_", " ").title()
609 return nice_punc
612 def force_update(context):
613 context.space_data.node_tree.update_tag()
616 def dpifac():
617 prefs = bpy.context.user_preferences.system
618 return prefs.dpi * prefs.pixel_size / 72
621 def node_mid_pt(node, axis):
622 if axis == 'x':
623 d = node.location.x + (node.dimensions.x / 2)
624 elif axis == 'y':
625 d = node.location.y - (node.dimensions.y / 2)
626 else:
627 d = 0
628 return d
631 def autolink(node1, node2, links):
632 link_made = False
634 for outp in node1.outputs:
635 for inp in node2.inputs:
636 if not inp.is_linked and inp.name == outp.name:
637 link_made = True
638 links.new(outp, inp)
639 return True
641 for outp in node1.outputs:
642 for inp in node2.inputs:
643 if not inp.is_linked and inp.type == outp.type:
644 link_made = True
645 links.new(outp, inp)
646 return True
648 # force some connection even if the type doesn't match
649 for outp in node1.outputs:
650 for inp in node2.inputs:
651 if not inp.is_linked:
652 link_made = True
653 links.new(outp, inp)
654 return True
656 # even if no sockets are open, force one of matching type
657 for outp in node1.outputs:
658 for inp in node2.inputs:
659 if inp.type == outp.type:
660 link_made = True
661 links.new(outp, inp)
662 return True
664 # do something!
665 for outp in node1.outputs:
666 for inp in node2.inputs:
667 link_made = True
668 links.new(outp, inp)
669 return True
671 print("Could not make a link from " + node1.name + " to " + node2.name)
672 return link_made
675 def node_at_pos(nodes, context, event):
676 nodes_near_mouse = []
677 nodes_under_mouse = []
678 target_node = None
680 store_mouse_cursor(context, event)
681 x, y = context.space_data.cursor_location
682 x = x
683 y = y
685 # Make a list of each corner (and middle of border) for each node.
686 # Will be sorted to find nearest point and thus nearest node
687 node_points_with_dist = []
688 for node in nodes:
689 skipnode = False
690 if node.type != 'FRAME': # no point trying to link to a frame node
691 locx = node.location.x
692 locy = node.location.y
693 dimx = node.dimensions.x/dpifac()
694 dimy = node.dimensions.y/dpifac()
695 if node.parent:
696 locx += node.parent.location.x
697 locy += node.parent.location.y
698 if node.parent.parent:
699 locx += node.parent.parent.location.x
700 locy += node.parent.parent.location.y
701 if node.parent.parent.parent:
702 locx += node.parent.parent.parent.location.x
703 locy += node.parent.parent.parent.location.y
704 if node.parent.parent.parent.parent:
705 # Support three levels or parenting
706 # There's got to be a better way to do this...
707 skipnode = True
708 if not skipnode:
709 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
710 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
711 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
712 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
714 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
715 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
716 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
717 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
719 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
721 for node in nodes:
722 if node.type != 'FRAME' and skipnode == False:
723 locx = node.location.x
724 locy = node.location.y
725 dimx = node.dimensions.x/dpifac()
726 dimy = node.dimensions.y/dpifac()
727 if node.parent:
728 locx += node.parent.location.x
729 locy += node.parent.location.y
730 if (locx <= x <= locx + dimx) and \
731 (locy - dimy <= y <= locy):
732 nodes_under_mouse.append(node)
734 if len(nodes_under_mouse) == 1:
735 if nodes_under_mouse[0] != nearest_node:
736 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
737 else:
738 target_node = nearest_node # else use the nearest node
739 else:
740 target_node = nearest_node
741 return target_node
744 def store_mouse_cursor(context, event):
745 space = context.space_data
746 v2d = context.region.view2d
747 tree = space.edit_tree
749 # convert mouse position to the View2D for later node placement
750 if context.region.type == 'WINDOW':
751 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
752 else:
753 space.cursor_location = tree.view_center
756 def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]):
757 shademodel_state = bgl.Buffer(bgl.GL_INT, 1)
758 bgl.glGetIntegerv(bgl.GL_SHADE_MODEL, shademodel_state)
760 bgl.glEnable(bgl.GL_BLEND)
761 bgl.glLineWidth(size * dpifac())
762 bgl.glShadeModel(bgl.GL_SMOOTH)
763 bgl.glEnable(bgl.GL_LINE_SMOOTH)
765 bgl.glBegin(bgl.GL_LINE_STRIP)
766 try:
767 bgl.glColor4f(colour[0]+(1.0-colour[0])/4, colour[1]+(1.0-colour[1])/4, colour[2]+(1.0-colour[2])/4, colour[3]+(1.0-colour[3])/4)
768 bgl.glVertex2f(x1, y1)
769 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
770 bgl.glVertex2f(x2, y2)
771 except:
772 pass
773 bgl.glEnd()
775 bgl.glShadeModel(shademodel_state[0])
776 bgl.glDisable(bgl.GL_LINE_SMOOTH)
779 def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]):
780 bgl.glEnable(bgl.GL_LINE_SMOOTH)
781 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
782 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
783 radius = radius * dpifac()
784 sides = 12
785 for i in range(sides + 1):
786 cosine = radius * cos(i * 2 * pi / sides) + mx
787 sine = radius * sin(i * 2 * pi / sides) + my
788 bgl.glVertex2f(cosine, sine)
789 bgl.glEnd()
790 bgl.glDisable(bgl.GL_LINE_SMOOTH)
793 def draw_rounded_node_border(node, radius=8, colour=[1.0, 1.0, 1.0, 0.7]):
794 bgl.glEnable(bgl.GL_BLEND)
795 bgl.glEnable(bgl.GL_LINE_SMOOTH)
797 area_width = bpy.context.area.width - (16*dpifac()) - 1
798 bottom_bar = (16*dpifac()) + 1
799 sides = 16
800 radius = radius*dpifac()
801 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
803 nlocx = (node.location.x+1)*dpifac()
804 nlocy = (node.location.y+1)*dpifac()
805 ndimx = node.dimensions.x
806 ndimy = node.dimensions.y
807 # This is a stupid way to do this... TODO use while loop
808 if node.parent:
809 nlocx += node.parent.location.x
810 nlocy += node.parent.location.y
811 if node.parent.parent:
812 nlocx += node.parent.parent.location.x
813 nlocy += node.parent.parent.location.y
814 if node.parent.parent.parent:
815 nlocx += node.parent.parent.parent.location.x
816 nlocy += node.parent.parent.parent.location.y
818 if node.hide:
819 nlocx += -1
820 nlocy += 5
821 if node.type == 'REROUTE':
822 #nlocx += 1
823 nlocy -= 1
824 ndimx = 0
825 ndimy = 0
826 radius += 6
828 # Top left corner
829 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
830 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
831 bgl.glVertex2f(mx,my)
832 for i in range(sides+1):
833 if (4<=i<=8):
834 if my > bottom_bar and mx < area_width:
835 cosine = radius * cos(i * 2 * pi / sides) + mx
836 sine = radius * sin(i * 2 * pi / sides) + my
837 bgl.glVertex2f(cosine, sine)
838 bgl.glEnd()
840 # Top right corner
841 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
842 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
843 bgl.glVertex2f(mx,my)
844 for i in range(sides+1):
845 if (0<=i<=4):
846 if my > bottom_bar and mx < area_width:
847 cosine = radius * cos(i * 2 * pi / sides) + mx
848 sine = radius * sin(i * 2 * pi / sides) + my
849 bgl.glVertex2f(cosine, sine)
850 bgl.glEnd()
852 # Bottom left corner
853 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
854 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
855 bgl.glVertex2f(mx,my)
856 for i in range(sides+1):
857 if (8<=i<=12):
858 if my > bottom_bar and mx < area_width:
859 cosine = radius * cos(i * 2 * pi / sides) + mx
860 sine = radius * sin(i * 2 * pi / sides) + my
861 bgl.glVertex2f(cosine, sine)
862 bgl.glEnd()
864 # Bottom right corner
865 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
866 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
867 bgl.glVertex2f(mx,my)
868 for i in range(sides+1):
869 if (12<=i<=16):
870 if my > bottom_bar and mx < area_width:
871 cosine = radius * cos(i * 2 * pi / sides) + mx
872 sine = radius * sin(i * 2 * pi / sides) + my
873 bgl.glVertex2f(cosine, sine)
874 bgl.glEnd()
877 # Left edge
878 bgl.glBegin(bgl.GL_QUADS)
879 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
880 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
881 m1y = max(m1y, bottom_bar)
882 m2y = max(m2y, bottom_bar)
883 if m1x < area_width and m2x < area_width:
884 bgl.glVertex2f(m2x-radius,m2y) # draw order is important, start with bottom left and go anti-clockwise
885 bgl.glVertex2f(m2x,m2y)
886 bgl.glVertex2f(m1x,m1y)
887 bgl.glVertex2f(m1x-radius,m1y)
888 bgl.glEnd()
890 # Top edge
891 bgl.glBegin(bgl.GL_QUADS)
892 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
893 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
894 m1x = min(m1x, area_width)
895 m2x = min(m2x, area_width)
896 if m1y > bottom_bar and m2y > bottom_bar:
897 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
898 bgl.glVertex2f(m2x,m2y)
899 bgl.glVertex2f(m2x,m1y+radius)
900 bgl.glVertex2f(m1x,m1y+radius)
901 bgl.glEnd()
903 # Right edge
904 bgl.glBegin(bgl.GL_QUADS)
905 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
906 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
907 m1y = max(m1y, bottom_bar)
908 m2y = max(m2y, bottom_bar)
909 if m1x < area_width and m2x < area_width:
910 bgl.glVertex2f(m2x,m2y) # draw order is important, start with bottom left and go anti-clockwise
911 bgl.glVertex2f(m2x+radius,m2y)
912 bgl.glVertex2f(m1x+radius,m1y)
913 bgl.glVertex2f(m1x,m1y)
914 bgl.glEnd()
916 # Bottom edge
917 bgl.glBegin(bgl.GL_QUADS)
918 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
919 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
920 m1x = min(m1x, area_width)
921 m2x = min(m2x, area_width)
922 if m1y > bottom_bar and m2y > bottom_bar:
923 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
924 bgl.glVertex2f(m2x,m2y)
925 bgl.glVertex2f(m2x,m1y-radius)
926 bgl.glVertex2f(m1x,m1y-radius)
927 bgl.glEnd()
930 # Restore defaults
931 bgl.glDisable(bgl.GL_BLEND)
932 bgl.glDisable(bgl.GL_LINE_SMOOTH)
935 def draw_callback_nodeoutline(self, context, mode):
936 if self.mouse_path:
937 nodes, links = get_nodes_links(context)
938 bgl.glEnable(bgl.GL_LINE_SMOOTH)
940 if mode == "LINK":
941 col_outer = [1.0, 0.2, 0.2, 0.4]
942 col_inner = [0.0, 0.0, 0.0, 0.5]
943 col_circle_inner = [0.3, 0.05, 0.05, 1.0]
944 elif mode == "LINKMENU":
945 col_outer = [0.4, 0.6, 1.0, 0.4]
946 col_inner = [0.0, 0.0, 0.0, 0.5]
947 col_circle_inner = [0.08, 0.15, .3, 1.0]
948 elif mode == "MIX":
949 col_outer = [0.2, 1.0, 0.2, 0.4]
950 col_inner = [0.0, 0.0, 0.0, 0.5]
951 col_circle_inner = [0.05, 0.3, 0.05, 1.0]
953 m1x = self.mouse_path[0][0]
954 m1y = self.mouse_path[0][1]
955 m2x = self.mouse_path[-1][0]
956 m2y = self.mouse_path[-1][1]
958 n1 = nodes[context.scene.NWLazySource]
959 n2 = nodes[context.scene.NWLazyTarget]
961 if n1 == n2:
962 col_outer = [0.4, 0.4, 0.4, 0.4]
963 col_inner = [0.0, 0.0, 0.0, 0.5]
964 col_circle_inner = [0.2, 0.2, 0.2, 1.0]
966 draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
967 draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
968 draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
969 draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
971 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
972 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
974 # circle outline
975 draw_circle(m1x, m1y, 7, col_outer)
976 draw_circle(m2x, m2y, 7, col_outer)
978 # circle inner
979 draw_circle(m1x, m1y, 5, col_circle_inner)
980 draw_circle(m2x, m2y, 5, col_circle_inner)
982 # restore opengl defaults
983 bgl.glLineWidth(1)
984 bgl.glDisable(bgl.GL_BLEND)
985 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
987 bgl.glDisable(bgl.GL_LINE_SMOOTH)
990 def get_nodes_links(context):
991 tree = context.space_data.node_tree
993 # Get nodes from currently edited tree.
994 # If user is editing a group, space_data.node_tree is still the base level (outside group).
995 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
996 # the same as context.active_node, the user is in a group.
997 # Check recursively until we find the real active node_tree:
998 if tree.nodes.active:
999 while tree.nodes.active != context.active_node:
1000 tree = tree.nodes.active.node_tree
1002 return tree.nodes, tree.links
1004 # Principled prefs
1005 class NWPrincipledPreferences(bpy.types.PropertyGroup):
1006 base_color: StringProperty(
1007 name='Base Color',
1008 default='diffuse diff albedo base col color',
1009 description='Naming Components for Base Color maps')
1010 sss_color: StringProperty(
1011 name='Subsurface Color',
1012 default='sss subsurface',
1013 description='Naming Components for Subsurface Color maps')
1014 metallic: StringProperty(
1015 name='Metallic',
1016 default='metallic metalness metal mtl',
1017 description='Naming Components for metallness maps')
1018 specular: StringProperty(
1019 name='Specular',
1020 default='specularity specular spec spc',
1021 description='Naming Components for Specular maps')
1022 normal: StringProperty(
1023 name='Normal',
1024 default='normal nor nrm nrml norm',
1025 description='Naming Components for Normal maps')
1026 bump: StringProperty(
1027 name='Bump',
1028 default='bump bmp',
1029 description='Naming Components for bump maps')
1030 rough: StringProperty(
1031 name='Roughness',
1032 default='roughness rough rgh',
1033 description='Naming Components for roughness maps')
1034 gloss: StringProperty(
1035 name='Gloss',
1036 default='gloss glossy glossyness',
1037 description='Naming Components for glossy maps')
1038 displacement: StringProperty(
1039 name='Displacement',
1040 default='displacement displace disp dsp height heightmap',
1041 description='Naming Components for displacement maps')
1043 # Addon prefs
1044 class NWNodeWrangler(bpy.types.AddonPreferences):
1045 bl_idname = __name__
1047 merge_hide: EnumProperty(
1048 name="Hide Mix nodes",
1049 items=(
1050 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1051 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1052 ("NEVER", "Never", "Never collapse the new merge nodes")
1054 default='NON_SHADER',
1055 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1056 merge_position: EnumProperty(
1057 name="Mix Node Position",
1058 items=(
1059 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1060 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1062 default='CENTER',
1063 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1065 show_hotkey_list: BoolProperty(
1066 name="Show Hotkey List",
1067 default=False,
1068 description="Expand this box into a list of all the hotkeys for functions in this addon"
1070 hotkey_list_filter: StringProperty(
1071 name=" Filter by Name",
1072 default="",
1073 description="Show only hotkeys that have this text in their name"
1075 show_principled_lists: BoolProperty(
1076 name="Show Principled naming tags",
1077 default=False,
1078 description="Expand this box into a list of all naming tags for principled texture setup"
1080 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
1082 def draw(self, context):
1083 layout = self.layout
1084 col = layout.column()
1085 col.prop(self, "merge_position")
1086 col.prop(self, "merge_hide")
1088 box = layout.box()
1089 col = box.column(align=True)
1090 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
1091 if self.show_principled_lists:
1092 tags = self.principled_tags
1094 col.prop(tags, "base_color")
1095 col.prop(tags, "sss_color")
1096 col.prop(tags, "metallic")
1097 col.prop(tags, "specular")
1098 col.prop(tags, "rough")
1099 col.prop(tags, "gloss")
1100 col.prop(tags, "normal")
1101 col.prop(tags, "bump")
1102 col.prop(tags, "displacement")
1104 box = layout.box()
1105 col = box.column(align=True)
1106 hotkey_button_name = "Show Hotkey List"
1107 if self.show_hotkey_list:
1108 hotkey_button_name = "Hide Hotkey List"
1109 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
1110 if self.show_hotkey_list:
1111 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
1112 col.separator()
1113 for hotkey in kmi_defs:
1114 if hotkey[7]:
1115 hotkey_name = hotkey[7]
1117 if self.hotkey_list_filter.lower() in hotkey_name.lower():
1118 row = col.row(align=True)
1119 row.label(text=hotkey_name)
1120 keystr = nice_hotkey_name(hotkey[1])
1121 if hotkey[4]:
1122 keystr = "Shift " + keystr
1123 if hotkey[5]:
1124 keystr = "Alt " + keystr
1125 if hotkey[3]:
1126 keystr = "Ctrl " + keystr
1127 row.label(text=keystr)
1131 def nw_check(context):
1132 space = context.space_data
1133 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree"]
1135 valid = False
1136 if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
1137 valid = True
1139 return valid
1141 class NWBase:
1142 @classmethod
1143 def poll(cls, context):
1144 return nw_check(context)
1147 # OPERATORS
1148 class NWLazyMix(Operator, NWBase):
1149 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1150 bl_idname = "node.nw_lazy_mix"
1151 bl_label = "Mix Nodes"
1152 bl_options = {'REGISTER', 'UNDO'}
1154 def modal(self, context, event):
1155 context.area.tag_redraw()
1156 nodes, links = get_nodes_links(context)
1157 cont = True
1159 start_pos = [event.mouse_region_x, event.mouse_region_y]
1161 node1 = None
1162 if not context.scene.NWBusyDrawing:
1163 node1 = node_at_pos(nodes, context, event)
1164 if node1:
1165 context.scene.NWBusyDrawing = node1.name
1166 else:
1167 if context.scene.NWBusyDrawing != 'STOP':
1168 node1 = nodes[context.scene.NWBusyDrawing]
1170 context.scene.NWLazySource = node1.name
1171 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1173 if event.type == 'MOUSEMOVE':
1174 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1176 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1177 end_pos = [event.mouse_region_x, event.mouse_region_y]
1178 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1180 node2 = None
1181 node2 = node_at_pos(nodes, context, event)
1182 if node2:
1183 context.scene.NWBusyDrawing = node2.name
1185 if node1 == node2:
1186 cont = False
1188 if cont:
1189 if node1 and node2:
1190 for node in nodes:
1191 node.select = False
1192 node1.select = True
1193 node2.select = True
1195 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
1197 context.scene.NWBusyDrawing = ""
1198 return {'FINISHED'}
1200 elif event.type == 'ESC':
1201 print('cancelled')
1202 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1203 return {'CANCELLED'}
1205 return {'RUNNING_MODAL'}
1207 def invoke(self, context, event):
1208 if context.area.type == 'NODE_EDITOR':
1209 # the arguments we pass the the callback
1210 args = (self, context, 'MIX')
1211 # Add the region OpenGL drawing callback
1212 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1213 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1215 self.mouse_path = []
1217 context.window_manager.modal_handler_add(self)
1218 return {'RUNNING_MODAL'}
1219 else:
1220 self.report({'WARNING'}, "View3D not found, cannot run operator")
1221 return {'CANCELLED'}
1224 class NWLazyConnect(Operator, NWBase):
1225 """Connect two nodes without clicking a specific socket (automatically determined"""
1226 bl_idname = "node.nw_lazy_connect"
1227 bl_label = "Lazy Connect"
1228 bl_options = {'REGISTER', 'UNDO'}
1229 with_menu: BoolProperty()
1231 def modal(self, context, event):
1232 context.area.tag_redraw()
1233 nodes, links = get_nodes_links(context)
1234 cont = True
1236 start_pos = [event.mouse_region_x, event.mouse_region_y]
1238 node1 = None
1239 if not context.scene.NWBusyDrawing:
1240 node1 = node_at_pos(nodes, context, event)
1241 if node1:
1242 context.scene.NWBusyDrawing = node1.name
1243 else:
1244 if context.scene.NWBusyDrawing != 'STOP':
1245 node1 = nodes[context.scene.NWBusyDrawing]
1247 context.scene.NWLazySource = node1.name
1248 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1250 if event.type == 'MOUSEMOVE':
1251 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1253 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1254 end_pos = [event.mouse_region_x, event.mouse_region_y]
1255 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1257 node2 = None
1258 node2 = node_at_pos(nodes, context, event)
1259 if node2:
1260 context.scene.NWBusyDrawing = node2.name
1262 if node1 == node2:
1263 cont = False
1265 link_success = False
1266 if cont:
1267 if node1 and node2:
1268 original_sel = []
1269 original_unsel = []
1270 for node in nodes:
1271 if node.select == True:
1272 node.select = False
1273 original_sel.append(node)
1274 else:
1275 original_unsel.append(node)
1276 node1.select = True
1277 node2.select = True
1279 #link_success = autolink(node1, node2, links)
1280 if self.with_menu:
1281 if len(node1.outputs) > 1 and node2.inputs:
1282 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1283 elif len(node1.outputs) == 1:
1284 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1285 else:
1286 link_success = autolink(node1, node2, links)
1288 for node in original_sel:
1289 node.select = True
1290 for node in original_unsel:
1291 node.select = False
1293 if link_success:
1294 force_update(context)
1295 context.scene.NWBusyDrawing = ""
1296 return {'FINISHED'}
1298 elif event.type == 'ESC':
1299 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1300 return {'CANCELLED'}
1302 return {'RUNNING_MODAL'}
1304 def invoke(self, context, event):
1305 if context.area.type == 'NODE_EDITOR':
1306 nodes, links = get_nodes_links(context)
1307 node = node_at_pos(nodes, context, event)
1308 if node:
1309 context.scene.NWBusyDrawing = node.name
1311 # the arguments we pass the the callback
1312 mode = "LINK"
1313 if self.with_menu:
1314 mode = "LINKMENU"
1315 args = (self, context, mode)
1316 # Add the region OpenGL drawing callback
1317 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1318 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1320 self.mouse_path = []
1322 context.window_manager.modal_handler_add(self)
1323 return {'RUNNING_MODAL'}
1324 else:
1325 self.report({'WARNING'}, "View3D not found, cannot run operator")
1326 return {'CANCELLED'}
1329 class NWDeleteUnused(Operator, NWBase):
1330 """Delete all nodes whose output is not used"""
1331 bl_idname = 'node.nw_del_unused'
1332 bl_label = 'Delete Unused Nodes'
1333 bl_options = {'REGISTER', 'UNDO'}
1335 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1336 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1338 def is_unused_node(self, node):
1339 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1340 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1341 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1342 if node.type in end_types:
1343 return False
1345 for output in node.outputs:
1346 if output.links:
1347 return False
1348 return True
1350 @classmethod
1351 def poll(cls, context):
1352 valid = False
1353 if nw_check(context):
1354 if context.space_data.node_tree.nodes:
1355 valid = True
1356 return valid
1358 def execute(self, context):
1359 nodes, links = get_nodes_links(context)
1361 # Store selection
1362 selection = []
1363 for node in nodes:
1364 if node.select == True:
1365 selection.append(node.name)
1367 for node in nodes:
1368 node.select = False
1370 deleted_nodes = []
1371 temp_deleted_nodes = []
1372 del_unused_iterations = len(nodes)
1373 for it in range(0, del_unused_iterations):
1374 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1375 for node in nodes:
1376 if self.is_unused_node(node):
1377 node.select = True
1378 deleted_nodes.append(node.name)
1379 bpy.ops.node.delete()
1381 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1382 break
1384 if self.delete_frames:
1385 repeat = True
1386 while repeat:
1387 frames_in_use = []
1388 frames = []
1389 repeat = False
1390 for node in nodes:
1391 if node.parent:
1392 frames_in_use.append(node.parent)
1393 for node in nodes:
1394 if node.type == 'FRAME' and node not in frames_in_use:
1395 frames.append(node)
1396 if node.parent:
1397 repeat = True # repeat for nested frames
1398 for node in frames:
1399 if node not in frames_in_use:
1400 node.select = True
1401 deleted_nodes.append(node.name)
1402 bpy.ops.node.delete()
1404 if self.delete_muted:
1405 for node in nodes:
1406 if node.mute:
1407 node.select = True
1408 deleted_nodes.append(node.name)
1409 bpy.ops.node.delete_reconnect()
1411 # get unique list of deleted nodes (iterations would count the same node more than once)
1412 deleted_nodes = list(set(deleted_nodes))
1413 for n in deleted_nodes:
1414 self.report({'INFO'}, "Node " + n + " deleted")
1415 num_deleted = len(deleted_nodes)
1416 n = ' node'
1417 if num_deleted > 1:
1418 n += 's'
1419 if num_deleted:
1420 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1421 else:
1422 self.report({'INFO'}, "Nothing deleted")
1424 # Restore selection
1425 nodes, links = get_nodes_links(context)
1426 for node in nodes:
1427 if node.name in selection:
1428 node.select = True
1429 return {'FINISHED'}
1431 def invoke(self, context, event):
1432 return context.window_manager.invoke_confirm(self, event)
1435 class NWSwapLinks(Operator, NWBase):
1436 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1437 bl_idname = 'node.nw_swap_links'
1438 bl_label = 'Swap Links'
1439 bl_options = {'REGISTER', 'UNDO'}
1441 @classmethod
1442 def poll(cls, context):
1443 valid = False
1444 if nw_check(context):
1445 if context.selected_nodes:
1446 valid = len(context.selected_nodes) <= 2
1447 return valid
1449 def execute(self, context):
1450 nodes, links = get_nodes_links(context)
1451 selected_nodes = context.selected_nodes
1452 n1 = selected_nodes[0]
1454 # Swap outputs
1455 if len(selected_nodes) == 2:
1456 n2 = selected_nodes[1]
1457 if n1.outputs and n2.outputs:
1458 n1_outputs = []
1459 n2_outputs = []
1461 out_index = 0
1462 for output in n1.outputs:
1463 if output.links:
1464 for link in output.links:
1465 n1_outputs.append([out_index, link.to_socket])
1466 links.remove(link)
1467 out_index += 1
1469 out_index = 0
1470 for output in n2.outputs:
1471 if output.links:
1472 for link in output.links:
1473 n2_outputs.append([out_index, link.to_socket])
1474 links.remove(link)
1475 out_index += 1
1477 for connection in n1_outputs:
1478 try:
1479 links.new(n2.outputs[connection[0]], connection[1])
1480 except:
1481 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1482 for connection in n2_outputs:
1483 try:
1484 links.new(n1.outputs[connection[0]], connection[1])
1485 except:
1486 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1487 else:
1488 if n1.outputs or n2.outputs:
1489 self.report({'WARNING'}, "One of the nodes has no outputs!")
1490 else:
1491 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1493 # Swap Inputs
1494 elif len(selected_nodes) == 1:
1495 if n1.inputs:
1496 types = []
1498 for i1 in n1.inputs:
1499 if i1.is_linked:
1500 similar_types = 0
1501 for i2 in n1.inputs:
1502 if i1.type == i2.type and i2.is_linked:
1503 similar_types += 1
1504 types.append ([i1, similar_types, i])
1505 i += 1
1506 types.sort(key=lambda k: k[1], reverse=True)
1508 if types:
1509 t = types[0]
1510 if t[1] == 2:
1511 for i2 in n1.inputs:
1512 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1513 pair = [t[0], i2]
1514 i1f = pair[0].links[0].from_socket
1515 i1t = pair[0].links[0].to_socket
1516 i2f = pair[1].links[0].from_socket
1517 i2t = pair[1].links[0].to_socket
1518 links.new(i1f, i2t)
1519 links.new(i2f, i1t)
1520 if t[1] == 1:
1521 if len(types) == 1:
1522 fs = t[0].links[0].from_socket
1523 i = t[2]
1524 links.remove(t[0].links[0])
1525 if i+1 == len(n1.inputs):
1526 i = -1
1527 i += 1
1528 while n1.inputs[i].is_linked:
1529 i += 1
1530 links.new(fs, n1.inputs[i])
1531 elif len(types) == 2:
1532 i1f = types[0][0].links[0].from_socket
1533 i1t = types[0][0].links[0].to_socket
1534 i2f = types[1][0].links[0].from_socket
1535 i2t = types[1][0].links[0].to_socket
1536 links.new(i1f, i2t)
1537 links.new(i2f, i1t)
1539 else:
1540 self.report({'WARNING'}, "This node has no input connections to swap!")
1541 else:
1542 self.report({'WARNING'}, "This node has no inputs to swap!")
1544 force_update(context)
1545 return {'FINISHED'}
1548 class NWResetBG(Operator, NWBase):
1549 """Reset the zoom and position of the background image"""
1550 bl_idname = 'node.nw_bg_reset'
1551 bl_label = 'Reset Backdrop'
1552 bl_options = {'REGISTER', 'UNDO'}
1554 @classmethod
1555 def poll(cls, context):
1556 valid = False
1557 if nw_check(context):
1558 snode = context.space_data
1559 valid = snode.tree_type == 'CompositorNodeTree'
1560 return valid
1562 def execute(self, context):
1563 context.space_data.backdrop_zoom = 1
1564 context.space_data.backdrop_offset[0] = 0
1565 context.space_data.backdrop_offset[1] = 0
1566 return {'FINISHED'}
1569 class NWAddAttrNode(Operator, NWBase):
1570 """Add an Attribute node with this name"""
1571 bl_idname = 'node.nw_add_attr_node'
1572 bl_label = 'Add UV map'
1573 bl_options = {'REGISTER', 'UNDO'}
1575 attr_name: StringProperty()
1577 def execute(self, context):
1578 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1579 nodes, links = get_nodes_links(context)
1580 nodes.active.attribute_name = self.attr_name
1581 return {'FINISHED'}
1584 class NWEmissionViewer(Operator, NWBase):
1585 bl_idname = "node.nw_emission_viewer"
1586 bl_label = "Emission Viewer"
1587 bl_description = "Connect active node to Emission Shader for shadeless previews"
1588 bl_options = {'REGISTER', 'UNDO'}
1590 @classmethod
1591 def poll(cls, context):
1592 is_cycles = is_cycles_or_eevee(context)
1593 if nw_check(context):
1594 space = context.space_data
1595 if space.tree_type == 'ShaderNodeTree' and is_cycles:
1596 if context.active_node:
1597 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1598 return True
1599 else:
1600 return True
1601 return False
1603 def invoke(self, context, event):
1604 space = context.space_data
1605 shader_type = space.shader_type
1606 if shader_type == 'OBJECT':
1607 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1608 shader_output_type = "OUTPUT_MATERIAL"
1609 shader_output_ident = "ShaderNodeOutputMaterial"
1610 shader_viewer_ident = "ShaderNodeEmission"
1611 else:
1612 shader_output_type = "OUTPUT_LIGHT"
1613 shader_output_ident = "ShaderNodeOutputLight"
1614 shader_viewer_ident = "ShaderNodeEmission"
1616 elif shader_type == 'WORLD':
1617 shader_output_type = "OUTPUT_WORLD"
1618 shader_output_ident = "ShaderNodeOutputWorld"
1619 shader_viewer_ident = "ShaderNodeBackground"
1620 shader_types = [x[1] for x in shaders_shader_nodes_props]
1621 mlocx = event.mouse_region_x
1622 mlocy = event.mouse_region_y
1623 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1624 if 'FINISHED' in select_node: # only run if mouse click is on a node
1625 nodes, links = get_nodes_links(context)
1626 in_group = context.active_node != space.node_tree.nodes.active
1627 active = nodes.active
1628 output_types = [x[1] for x in shaders_output_nodes_props]
1629 valid = False
1630 if active:
1631 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1632 for out in active.outputs:
1633 if not out.hide:
1634 valid = True
1635 break
1636 if valid:
1637 # get material_output node, store selection, deselect all
1638 materialout = None # placeholder node
1639 selection = []
1640 for node in nodes:
1641 if node.type == shader_output_type:
1642 materialout = node
1643 if node.select:
1644 selection.append(node.name)
1645 node.select = False
1646 if not materialout:
1647 # get right-most location
1648 sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
1649 max_xloc_node = sorted_by_xloc[-1]
1650 if max_xloc_node.name == 'Emission Viewer':
1651 max_xloc_node = sorted_by_xloc[-2]
1653 # get average y location
1654 sum_yloc = 0
1655 for node in nodes:
1656 sum_yloc += node.location.y
1658 new_locx = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1659 new_locy = sum_yloc / len(nodes)
1661 materialout = nodes.new(shader_output_ident)
1662 materialout.location.x = new_locx
1663 materialout.location.y = new_locy
1664 materialout.select = False
1665 # Analyze outputs, add "Emission Viewer" if needed, make links
1666 out_i = None
1667 valid_outputs = []
1668 for i, out in enumerate(active.outputs):
1669 if not out.hide:
1670 valid_outputs.append(i)
1671 if valid_outputs:
1672 out_i = valid_outputs[0] # Start index of node's outputs
1673 for i, valid_i in enumerate(valid_outputs):
1674 for out_link in active.outputs[valid_i].links:
1675 if "Emission Viewer" in out_link.to_node.name or (out_link.to_node == materialout and out_link.to_socket == materialout.inputs[0]):
1676 if i < len(valid_outputs) - 1:
1677 out_i = valid_outputs[i + 1]
1678 else:
1679 out_i = valid_outputs[0]
1680 make_links = [] # store sockets for new links
1681 if active.outputs:
1682 # If output type not 'SHADER' - "Emission Viewer" needed
1683 if active.outputs[out_i].type != 'SHADER':
1684 # get Emission Viewer node
1685 emission_exists = False
1686 emission_placeholder = nodes[0]
1687 for node in nodes:
1688 if "Emission Viewer" in node.name:
1689 emission_exists = True
1690 emission_placeholder = node
1691 if not emission_exists:
1692 emission = nodes.new(shader_viewer_ident)
1693 emission.hide = True
1694 emission.location = [materialout.location.x, (materialout.location.y + 40)]
1695 emission.label = "Viewer"
1696 emission.name = "Emission Viewer"
1697 emission.use_custom_color = True
1698 emission.color = (0.6, 0.5, 0.4)
1699 emission.select = False
1700 else:
1701 emission = emission_placeholder
1702 make_links.append((active.outputs[out_i], emission.inputs[0]))
1704 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
1705 if emission.outputs[0].links.__len__() > 0:
1706 if not emission.outputs[0].links[0].to_node == materialout:
1707 make_links.append((emission.outputs[0], materialout.inputs[0]))
1708 else:
1709 make_links.append((emission.outputs[0], materialout.inputs[0]))
1711 # Set brightness of viewer to compensate for Film and CM exposure
1712 intensity = 1/context.scene.cycles.film_exposure # Film exposure is a multiplier
1713 intensity /= pow(2, (context.scene.view_settings.exposure)) # CM exposure is measured in stops/EVs (2^x)
1714 emission.inputs[1].default_value = intensity
1716 else:
1717 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
1718 make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
1719 for node in nodes:
1720 if node.name == 'Emission Viewer':
1721 node.select = True
1722 bpy.ops.node.delete()
1723 for li_from, li_to in make_links:
1724 links.new(li_from, li_to)
1725 # Restore selection
1726 nodes.active = active
1727 for node in nodes:
1728 if node.name in selection:
1729 node.select = True
1730 force_update(context)
1731 return {'FINISHED'}
1732 else:
1733 return {'CANCELLED'}
1736 class NWFrameSelected(Operator, NWBase):
1737 bl_idname = "node.nw_frame_selected"
1738 bl_label = "Frame Selected"
1739 bl_description = "Add a frame node and parent the selected nodes to it"
1740 bl_options = {'REGISTER', 'UNDO'}
1742 label_prop: StringProperty(
1743 name='Label',
1744 description='The visual name of the frame node',
1745 default=' '
1747 color_prop: FloatVectorProperty(
1748 name="Color",
1749 description="The color of the frame node",
1750 default=(0.6, 0.6, 0.6),
1751 min=0, max=1, step=1, precision=3,
1752 subtype='COLOR_GAMMA', size=3
1755 def execute(self, context):
1756 nodes, links = get_nodes_links(context)
1757 selected = []
1758 for node in nodes:
1759 if node.select == True:
1760 selected.append(node)
1762 bpy.ops.node.add_node(type='NodeFrame')
1763 frm = nodes.active
1764 frm.label = self.label_prop
1765 frm.use_custom_color = True
1766 frm.color = self.color_prop
1768 for node in selected:
1769 node.parent = frm
1771 return {'FINISHED'}
1774 class NWReloadImages(Operator, NWBase):
1775 bl_idname = "node.nw_reload_images"
1776 bl_label = "Reload Images"
1777 bl_description = "Update all the image nodes to match their files on disk"
1779 def execute(self, context):
1780 nodes, links = get_nodes_links(context)
1781 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1782 num_reloaded = 0
1783 for node in nodes:
1784 if node.type in image_types:
1785 if node.type == "TEXTURE":
1786 if node.texture: # node has texture assigned
1787 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1788 if node.texture.image: # texture has image assigned
1789 node.texture.image.reload()
1790 num_reloaded += 1
1791 else:
1792 if node.image:
1793 node.image.reload()
1794 num_reloaded += 1
1796 if num_reloaded:
1797 self.report({'INFO'}, "Reloaded images")
1798 print("Reloaded " + str(num_reloaded) + " images")
1799 force_update(context)
1800 return {'FINISHED'}
1801 else:
1802 self.report({'WARNING'}, "No images found to reload in this node tree")
1803 return {'CANCELLED'}
1806 class NWSwitchNodeType(Operator, NWBase):
1807 """Switch type of selected nodes """
1808 bl_idname = "node.nw_swtch_node_type"
1809 bl_label = "Switch Node Type"
1810 bl_options = {'REGISTER', 'UNDO'}
1812 to_type: EnumProperty(
1813 name="Switch to type",
1814 items=list(shaders_input_nodes_props) +
1815 list(shaders_output_nodes_props) +
1816 list(shaders_shader_nodes_props) +
1817 list(shaders_texture_nodes_props) +
1818 list(shaders_color_nodes_props) +
1819 list(shaders_vector_nodes_props) +
1820 list(shaders_converter_nodes_props) +
1821 list(shaders_layout_nodes_props) +
1822 list(compo_input_nodes_props) +
1823 list(compo_output_nodes_props) +
1824 list(compo_color_nodes_props) +
1825 list(compo_converter_nodes_props) +
1826 list(compo_filter_nodes_props) +
1827 list(compo_vector_nodes_props) +
1828 list(compo_matte_nodes_props) +
1829 list(compo_distort_nodes_props) +
1830 list(compo_layout_nodes_props) +
1831 list(blender_mat_input_nodes_props) +
1832 list(blender_mat_output_nodes_props) +
1833 list(blender_mat_color_nodes_props) +
1834 list(blender_mat_vector_nodes_props) +
1835 list(blender_mat_converter_nodes_props) +
1836 list(blender_mat_layout_nodes_props) +
1837 list(texture_input_nodes_props) +
1838 list(texture_output_nodes_props) +
1839 list(texture_color_nodes_props) +
1840 list(texture_pattern_nodes_props) +
1841 list(texture_textures_nodes_props) +
1842 list(texture_converter_nodes_props) +
1843 list(texture_distort_nodes_props) +
1844 list(texture_layout_nodes_props)
1847 def execute(self, context):
1848 nodes, links = get_nodes_links(context)
1849 to_type = self.to_type
1850 # Those types of nodes will not swap.
1851 src_excludes = ('NodeFrame')
1852 # Those attributes of nodes will be copied if possible
1853 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1854 'show_options', 'show_preview', 'show_texture',
1855 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1857 selected = [n for n in nodes if n.select]
1858 reselect = []
1859 for node in [n for n in selected if
1860 n.rna_type.identifier not in src_excludes and
1861 n.rna_type.identifier != to_type]:
1862 new_node = nodes.new(to_type)
1863 for attr in attrs_to_pass:
1864 if hasattr(node, attr) and hasattr(new_node, attr):
1865 setattr(new_node, attr, getattr(node, attr))
1866 # set image datablock of dst to image of src
1867 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1868 if node.image:
1869 new_node.image = node.image
1870 # Special cases
1871 if new_node.type == 'SWITCH':
1872 new_node.hide = True
1873 # Dictionaries: src_sockets and dst_sockets:
1874 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1875 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1876 # in 'INPUTS' and 'OUTPUTS':
1877 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1878 # socket entry:
1879 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1880 src_sockets = {
1881 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1882 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1884 dst_sockets = {
1885 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1886 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1888 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1889 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1890 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1891 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1892 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1893 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1894 # enumerate in inputs, then in outputs
1895 # find name, default value and links of socket
1896 for i, socket in enumerate(in_out):
1897 the_name = socket.name
1898 dval = None
1899 # Not every socket, especially in outputs has "default_value"
1900 if hasattr(socket, 'default_value'):
1901 dval = socket.default_value
1902 socket_links = []
1903 for lnk in socket.links:
1904 socket_links.append(lnk)
1905 # check type of socket to fill proper keys.
1906 for the_type in types_order_one:
1907 if socket.type == the_type:
1908 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1909 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1910 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1911 # Check which of the types in inputs/outputs is considered to be "main".
1912 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1913 for type_check in types_order_one:
1914 if sockets[in_out_name][type_check]:
1915 sockets[in_out_name]['MAIN'] = type_check
1916 break
1918 matches = {
1919 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1920 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1923 for inout, soctype in (
1924 ('INPUTS', 'MAIN',),
1925 ('INPUTS', 'SHADER',),
1926 ('INPUTS', 'RGBA',),
1927 ('INPUTS', 'VECTOR',),
1928 ('INPUTS', 'VALUE',),
1929 ('OUTPUTS', 'MAIN',),
1930 ('OUTPUTS', 'SHADER',),
1931 ('OUTPUTS', 'RGBA',),
1932 ('OUTPUTS', 'VECTOR',),
1933 ('OUTPUTS', 'VALUE',),
1935 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1936 if soctype == 'MAIN':
1937 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1938 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1939 else:
1940 sc = src_sockets[inout][soctype]
1941 dt = dst_sockets[inout][soctype]
1942 # start with 'dt' to determine number of possibilities.
1943 for i, soc in enumerate(dt):
1944 # if src main has enough entries - match them with dst main sockets by indexes.
1945 if len(sc) > i:
1946 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1947 # add 'VALUE_NAME' criterion to inputs.
1948 if inout == 'INPUTS' and soctype == 'VALUE':
1949 for s in sc:
1950 if s[2] == soc[2]: # if names match
1951 # append src (index, dval), dst (index, dval)
1952 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1954 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1955 # This creates better links when relinking textures.
1956 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1957 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1959 # Pass default values and RELINK:
1960 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1961 # INPUTS: Base on matches in proper order.
1962 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1963 # pass dvals
1964 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1965 new_node.inputs[dst_i].default_value = src_dval
1966 # Special case: switch to math
1967 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1968 new_node.type == 'MATH' and\
1969 tp == 'MAIN':
1970 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1971 new_node.inputs[dst_i].default_value = new_dst_dval
1972 if node.type == 'MIX_RGB':
1973 if node.blend_type in [o[0] for o in operations]:
1974 new_node.operation = node.blend_type
1975 # Special case: switch from math to some types
1976 if node.type == 'MATH' and\
1977 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1978 tp == 'MAIN':
1979 for i in range(3):
1980 new_node.inputs[dst_i].default_value[i] = src_dval
1981 if new_node.type == 'MIX_RGB':
1982 if node.operation in [t[0] for t in blend_types]:
1983 new_node.blend_type = node.operation
1984 # Set Fac of MIX_RGB to 1.0
1985 new_node.inputs[0].default_value = 1.0
1986 # make link only when dst matching input is not linked already.
1987 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1988 in_src_link = node.inputs[src_i].links[0]
1989 in_dst_socket = new_node.inputs[dst_i]
1990 links.new(in_src_link.from_socket, in_dst_socket)
1991 links.remove(in_src_link)
1992 # OUTPUTS: Base on matches in proper order.
1993 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1994 for out_src_link in node.outputs[src_i].links:
1995 out_dst_socket = new_node.outputs[dst_i]
1996 links.new(out_dst_socket, out_src_link.to_socket)
1997 # relink rest inputs if possible, no criteria
1998 for src_inp in node.inputs:
1999 for dst_inp in new_node.inputs:
2000 if src_inp.links and not dst_inp.links:
2001 src_link = src_inp.links[0]
2002 links.new(src_link.from_socket, dst_inp)
2003 links.remove(src_link)
2004 # relink rest outputs if possible, base on node kind if any left.
2005 for src_o in node.outputs:
2006 for out_src_link in src_o.links:
2007 for dst_o in new_node.outputs:
2008 if src_o.type == dst_o.type:
2009 links.new(dst_o, out_src_link.to_socket)
2010 # relink rest outputs no criteria if any left. Link all from first output.
2011 for src_o in node.outputs:
2012 for out_src_link in src_o.links:
2013 if new_node.outputs:
2014 links.new(new_node.outputs[0], out_src_link.to_socket)
2015 nodes.remove(node)
2016 force_update(context)
2017 return {'FINISHED'}
2020 class NWMergeNodes(Operator, NWBase):
2021 bl_idname = "node.nw_merge_nodes"
2022 bl_label = "Merge Nodes"
2023 bl_description = "Merge Selected Nodes"
2024 bl_options = {'REGISTER', 'UNDO'}
2026 mode: EnumProperty(
2027 name="mode",
2028 description="All possible blend types and math operations",
2029 items=blend_types + [op for op in operations if op not in blend_types],
2031 merge_type: EnumProperty(
2032 name="merge type",
2033 description="Type of Merge to be used",
2034 items=(
2035 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2036 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2037 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2038 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2039 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2040 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2044 def execute(self, context):
2045 settings = context.user_preferences.addons[__name__].preferences
2046 merge_hide = settings.merge_hide
2047 merge_position = settings.merge_position # 'center' or 'bottom'
2049 do_hide = False
2050 do_hide_shader = False
2051 if merge_hide == 'ALWAYS':
2052 do_hide = True
2053 do_hide_shader = True
2054 elif merge_hide == 'NON_SHADER':
2055 do_hide = True
2057 tree_type = context.space_data.node_tree.type
2058 if tree_type == 'COMPOSITING':
2059 node_type = 'CompositorNode'
2060 elif tree_type == 'SHADER':
2061 node_type = 'ShaderNode'
2062 elif tree_type == 'TEXTURE':
2063 node_type = 'TextureNode'
2064 nodes, links = get_nodes_links(context)
2065 mode = self.mode
2066 merge_type = self.merge_type
2067 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2068 # 'ZCOMBINE' works only if mode == 'MIX'
2069 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2070 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2071 merge_type = 'MIX'
2072 mode = 'MIX'
2073 selected_mix = [] # entry = [index, loc]
2074 selected_shader = [] # entry = [index, loc]
2075 selected_math = [] # entry = [index, loc]
2076 selected_z = [] # entry = [index, loc]
2077 selected_alphaover = [] # entry = [index, loc]
2079 for i, node in enumerate(nodes):
2080 if node.select and node.outputs:
2081 if merge_type == 'AUTO':
2082 for (type, types_list, dst) in (
2083 ('SHADER', ('MIX', 'ADD'), selected_shader),
2084 ('RGBA', [t[0] for t in blend_types], selected_mix),
2085 ('VALUE', [t[0] for t in operations], selected_math),
2087 output_type = node.outputs[0].type
2088 valid_mode = mode in types_list
2089 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2090 # Cheat that output type is 'RGBA',
2091 # and that 'MIX' exists in math operations list.
2092 # This way when selected_mix list is analyzed:
2093 # Node data will be appended even though it doesn't meet requirements.
2094 if output_type != 'SHADER' and mode == 'MIX':
2095 output_type = 'RGBA'
2096 valid_mode = True
2097 if output_type == type and valid_mode:
2098 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2099 else:
2100 for (type, types_list, dst) in (
2101 ('SHADER', ('MIX', 'ADD'), selected_shader),
2102 ('MIX', [t[0] for t in blend_types], selected_mix),
2103 ('MATH', [t[0] for t in operations], selected_math),
2104 ('ZCOMBINE', ('MIX', ), selected_z),
2105 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2107 if merge_type == type and mode in types_list:
2108 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2109 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2110 # use only 'Mix' nodes for merging.
2111 # For that we add selected_math list to selected_mix list and clear selected_math.
2112 if selected_mix and selected_math and merge_type == 'AUTO':
2113 selected_mix += selected_math
2114 selected_math = []
2116 for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
2117 if nodes_list:
2118 count_before = len(nodes)
2119 # sort list by loc_x - reversed
2120 nodes_list.sort(key=lambda k: k[1], reverse=True)
2121 # get maximum loc_x
2122 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2123 nodes_list.sort(key=lambda k: k[2], reverse=True)
2124 if merge_position == 'CENTER':
2125 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)
2126 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2127 if do_hide:
2128 loc_y += 40
2129 else:
2130 loc_y += 80
2131 else:
2132 loc_y = nodes_list[len(nodes_list) - 1][2]
2133 offset_y = 100
2134 if not do_hide:
2135 offset_y = 200
2136 if nodes_list == selected_shader and not do_hide_shader:
2137 offset_y = 150.0
2138 the_range = len(nodes_list) - 1
2139 if len(nodes_list) == 1:
2140 the_range = 1
2141 for i in range(the_range):
2142 if nodes_list == selected_mix:
2143 add_type = node_type + 'MixRGB'
2144 add = nodes.new(add_type)
2145 add.blend_type = mode
2146 if mode != 'MIX':
2147 add.inputs[0].default_value = 1.0
2148 add.show_preview = False
2149 add.hide = do_hide
2150 if do_hide:
2151 loc_y = loc_y - 50
2152 first = 1
2153 second = 2
2154 add.width_hidden = 100.0
2155 elif nodes_list == selected_math:
2156 add_type = node_type + 'Math'
2157 add = nodes.new(add_type)
2158 add.operation = mode
2159 add.hide = do_hide
2160 if do_hide:
2161 loc_y = loc_y - 50
2162 first = 0
2163 second = 1
2164 add.width_hidden = 100.0
2165 elif nodes_list == selected_shader:
2166 if mode == 'MIX':
2167 add_type = node_type + 'MixShader'
2168 add = nodes.new(add_type)
2169 add.hide = do_hide_shader
2170 if do_hide_shader:
2171 loc_y = loc_y - 50
2172 first = 1
2173 second = 2
2174 add.width_hidden = 100.0
2175 elif mode == 'ADD':
2176 add_type = node_type + 'AddShader'
2177 add = nodes.new(add_type)
2178 add.hide = do_hide_shader
2179 if do_hide_shader:
2180 loc_y = loc_y - 50
2181 first = 0
2182 second = 1
2183 add.width_hidden = 100.0
2184 elif nodes_list == selected_z:
2185 add = nodes.new('CompositorNodeZcombine')
2186 add.show_preview = False
2187 add.hide = do_hide
2188 if do_hide:
2189 loc_y = loc_y - 50
2190 first = 0
2191 second = 2
2192 add.width_hidden = 100.0
2193 elif nodes_list == selected_alphaover:
2194 add = nodes.new('CompositorNodeAlphaOver')
2195 add.show_preview = False
2196 add.hide = do_hide
2197 if do_hide:
2198 loc_y = loc_y - 50
2199 first = 1
2200 second = 2
2201 add.width_hidden = 100.0
2202 add.location = loc_x, loc_y
2203 loc_y += offset_y
2204 add.select = True
2205 count_adds = i + 1
2206 count_after = len(nodes)
2207 index = count_after - 1
2208 first_selected = nodes[nodes_list[0][0]]
2209 # "last" node has been added as first, so its index is count_before.
2210 last_add = nodes[count_before]
2211 # Special case:
2212 # Two nodes were selected and first selected has no output links, second selected has output links.
2213 # Then add links from last add to all links 'to_socket' of out links of second selected.
2214 if len(nodes_list) == 2:
2215 if not first_selected.outputs[0].links:
2216 second_selected = nodes[nodes_list[1][0]]
2217 for ss_link in second_selected.outputs[0].links:
2218 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2219 # Create list of invalid indexes.
2220 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2221 # Link only if "to_node" index not in invalid indexes list.
2222 if ss_link.to_node not in [nodes[i] for i in invalid_i]:
2223 links.new(last_add.outputs[0], ss_link.to_socket)
2224 # add links from last_add to all links 'to_socket' of out links of first selected.
2225 for fs_link in first_selected.outputs[0].links:
2226 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2227 # Create list of invalid indexes.
2228 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2229 # Link only if "to_node" index not in invalid indexes list.
2230 if fs_link.to_node not in [nodes[i] for i in invalid_i]:
2231 links.new(last_add.outputs[0], fs_link.to_socket)
2232 # add link from "first" selected and "first" add node
2233 node_to = nodes[count_after - 1]
2234 links.new(first_selected.outputs[0], node_to.inputs[first])
2235 if node_to.type == 'ZCOMBINE':
2236 for fs_out in first_selected.outputs:
2237 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
2238 links.new(fs_out, node_to.inputs[1])
2239 break
2240 # add links between added ADD nodes and between selected and ADD nodes
2241 for i in range(count_adds):
2242 if i < count_adds - 1:
2243 node_from = nodes[index]
2244 node_to = nodes[index - 1]
2245 node_to_input_i = first
2246 node_to_z_i = 1 # if z combine - link z to first z input
2247 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2248 if node_to.type == 'ZCOMBINE':
2249 for from_out in node_from.outputs:
2250 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2251 links.new(from_out, node_to.inputs[node_to_z_i])
2252 if len(nodes_list) > 1:
2253 node_from = nodes[nodes_list[i + 1][0]]
2254 node_to = nodes[index]
2255 node_to_input_i = second
2256 node_to_z_i = 3 # if z combine - link z to second z input
2257 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2258 if node_to.type == 'ZCOMBINE':
2259 for from_out in node_from.outputs:
2260 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2261 links.new(from_out, node_to.inputs[node_to_z_i])
2262 index -= 1
2263 # set "last" of added nodes as active
2264 nodes.active = last_add
2265 for i, x, y, dx, h in nodes_list:
2266 nodes[i].select = False
2268 return {'FINISHED'}
2271 class NWBatchChangeNodes(Operator, NWBase):
2272 bl_idname = "node.nw_batch_change"
2273 bl_label = "Batch Change"
2274 bl_description = "Batch Change Blend Type and Math Operation"
2275 bl_options = {'REGISTER', 'UNDO'}
2277 blend_type: EnumProperty(
2278 name="Blend Type",
2279 items=blend_types + navs,
2281 operation: EnumProperty(
2282 name="Operation",
2283 items=operations + navs,
2286 def execute(self, context):
2288 nodes, links = get_nodes_links(context)
2289 blend_type = self.blend_type
2290 operation = self.operation
2291 for node in context.selected_nodes:
2292 if node.type == 'MIX_RGB':
2293 if not blend_type in [nav[0] for nav in navs]:
2294 node.blend_type = blend_type
2295 else:
2296 if blend_type == 'NEXT':
2297 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2298 #index = blend_types.index(node.blend_type)
2299 if index == len(blend_types) - 1:
2300 node.blend_type = blend_types[0][0]
2301 else:
2302 node.blend_type = blend_types[index + 1][0]
2304 if blend_type == 'PREV':
2305 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2306 if index == 0:
2307 node.blend_type = blend_types[len(blend_types) - 1][0]
2308 else:
2309 node.blend_type = blend_types[index - 1][0]
2311 if node.type == 'MATH':
2312 if not operation in [nav[0] for nav in navs]:
2313 node.operation = operation
2314 else:
2315 if operation == 'NEXT':
2316 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2317 #index = operations.index(node.operation)
2318 if index == len(operations) - 1:
2319 node.operation = operations[0][0]
2320 else:
2321 node.operation = operations[index + 1][0]
2323 if operation == 'PREV':
2324 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2325 #index = operations.index(node.operation)
2326 if index == 0:
2327 node.operation = operations[len(operations) - 1][0]
2328 else:
2329 node.operation = operations[index - 1][0]
2331 return {'FINISHED'}
2334 class NWChangeMixFactor(Operator, NWBase):
2335 bl_idname = "node.nw_factor"
2336 bl_label = "Change Factor"
2337 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2338 bl_options = {'REGISTER', 'UNDO'}
2340 # option: Change factor.
2341 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2342 # Else - change factor by option value.
2343 option: FloatProperty()
2345 def execute(self, context):
2346 nodes, links = get_nodes_links(context)
2347 option = self.option
2348 selected = [] # entry = index
2349 for si, node in enumerate(nodes):
2350 if node.select:
2351 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2352 selected.append(si)
2354 for si in selected:
2355 fac = nodes[si].inputs[0]
2356 nodes[si].hide = False
2357 if option in {0.0, 1.0}:
2358 fac.default_value = option
2359 else:
2360 fac.default_value += option
2362 return {'FINISHED'}
2365 class NWCopySettings(Operator, NWBase):
2366 bl_idname = "node.nw_copy_settings"
2367 bl_label = "Copy Settings"
2368 bl_description = "Copy Settings of Active Node to Selected Nodes"
2369 bl_options = {'REGISTER', 'UNDO'}
2371 @classmethod
2372 def poll(cls, context):
2373 valid = False
2374 if nw_check(context):
2375 if context.active_node is not None and context.active_node.type is not 'FRAME':
2376 valid = True
2377 return valid
2379 def execute(self, context):
2380 node_active = context.active_node
2381 node_selected = context.selected_nodes
2383 # Error handling
2384 if not (len(node_selected) > 1):
2385 self.report({'ERROR'}, "2 nodes must be selected at least")
2386 return {'CANCELLED'}
2388 # Check if active node is in the selection
2389 selected_node_names = [n.name for n in node_selected]
2390 if node_active.name not in selected_node_names:
2391 self.report({'ERROR'}, "No active node")
2392 return {'CANCELLED'}
2394 # Get nodes in selection by type
2395 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2397 if not (len(valid_nodes) > 1) and node_active:
2398 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2399 return {'CANCELLED'}
2401 if len(valid_nodes) != len(node_selected):
2402 # Report nodes that are not valid
2403 valid_node_names = [n.name for n in valid_nodes]
2404 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2405 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2407 # Reference original
2408 orig = node_active
2409 #node_selected_names = [n.name for n in node_selected]
2411 # Output list
2412 success_names = []
2414 # Deselect all nodes
2415 for i in node_selected:
2416 i.select = False
2418 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2419 # Run through all other nodes
2420 for node in valid_nodes[1:]:
2422 # Check for frame node
2423 parent = node.parent if node.parent else None
2424 node_loc = [node.location.x, node.location.y]
2426 # Select original to duplicate
2427 orig.select = True
2429 # Duplicate selected node
2430 bpy.ops.node.duplicate()
2431 new_node = context.selected_nodes[0]
2433 # Deselect copy
2434 new_node.select = False
2436 # Properties to copy
2437 node_tree = node.id_data
2438 props_to_copy = 'bl_idname name location height width'.split(' ')
2440 # Input and outputs
2441 reconnections = []
2442 mappings = chain.from_iterable([node.inputs, node.outputs])
2443 for i in (i for i in mappings if i.is_linked):
2444 for L in i.links:
2445 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2447 # Properties
2448 props = {j: getattr(node, j) for j in props_to_copy}
2449 props_to_copy.pop(0)
2451 for prop in props_to_copy:
2452 setattr(new_node, prop, props[prop])
2454 # Get the node tree to remove the old node
2455 nodes = node_tree.nodes
2456 nodes.remove(node)
2457 new_node.name = props['name']
2459 if parent:
2460 new_node.parent = parent
2461 new_node.location = node_loc
2463 for str_from, str_to in reconnections:
2464 node_tree.links.new(eval(str_from), eval(str_to))
2466 success_names.append(new_node.name)
2468 orig.select = True
2469 node_tree.nodes.active = orig
2470 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2471 return {'FINISHED'}
2474 class NWCopyLabel(Operator, NWBase):
2475 bl_idname = "node.nw_copy_label"
2476 bl_label = "Copy Label"
2477 bl_options = {'REGISTER', 'UNDO'}
2479 option: EnumProperty(
2480 name="option",
2481 description="Source of name of label",
2482 items=(
2483 ('FROM_ACTIVE', 'from active', 'from active node',),
2484 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2485 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2489 def execute(self, context):
2490 nodes, links = get_nodes_links(context)
2491 option = self.option
2492 active = nodes.active
2493 if option == 'FROM_ACTIVE':
2494 if active:
2495 src_label = active.label
2496 for node in [n for n in nodes if n.select and nodes.active != n]:
2497 node.label = src_label
2498 elif option == 'FROM_NODE':
2499 selected = [n for n in nodes if n.select]
2500 for node in selected:
2501 for input in node.inputs:
2502 if input.links:
2503 src = input.links[0].from_node
2504 node.label = src.label
2505 break
2506 elif option == 'FROM_SOCKET':
2507 selected = [n for n in nodes if n.select]
2508 for node in selected:
2509 for input in node.inputs:
2510 if input.links:
2511 src = input.links[0].from_socket
2512 node.label = src.name
2513 break
2515 return {'FINISHED'}
2518 class NWClearLabel(Operator, NWBase):
2519 bl_idname = "node.nw_clear_label"
2520 bl_label = "Clear Label"
2521 bl_options = {'REGISTER', 'UNDO'}
2523 option: BoolProperty()
2525 def execute(self, context):
2526 nodes, links = get_nodes_links(context)
2527 for node in [n for n in nodes if n.select]:
2528 node.label = ''
2530 return {'FINISHED'}
2532 def invoke(self, context, event):
2533 if self.option:
2534 return self.execute(context)
2535 else:
2536 return context.window_manager.invoke_confirm(self, event)
2539 class NWModifyLabels(Operator, NWBase):
2540 """Modify Labels of all selected nodes"""
2541 bl_idname = "node.nw_modify_labels"
2542 bl_label = "Modify Labels"
2543 bl_options = {'REGISTER', 'UNDO'}
2545 prepend: StringProperty(
2546 name="Add to Beginning"
2548 append: StringProperty(
2549 name="Add to End"
2551 replace_from: StringProperty(
2552 name="Text to Replace"
2554 replace_to: StringProperty(
2555 name="Replace with"
2558 def execute(self, context):
2559 nodes, links = get_nodes_links(context)
2560 for node in [n for n in nodes if n.select]:
2561 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2563 return {'FINISHED'}
2565 def invoke(self, context, event):
2566 self.prepend = ""
2567 self.append = ""
2568 self.remove = ""
2569 return context.window_manager.invoke_props_dialog(self)
2572 class NWAddTextureSetup(Operator, NWBase):
2573 bl_idname = "node.nw_add_texture"
2574 bl_label = "Texture Setup"
2575 bl_description = "Add Texture Node Setup to Selected Shaders"
2576 bl_options = {'REGISTER', 'UNDO'}
2578 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2580 @classmethod
2581 def poll(cls, context):
2582 valid = False
2583 if nw_check(context):
2584 space = context.space_data
2585 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2586 valid = True
2587 return valid
2589 def execute(self, context):
2590 nodes, links = get_nodes_links(context)
2591 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2592 texture_types = [x[1] for x in shaders_texture_nodes_props]
2593 selected_nodes = [n for n in nodes if n.select]
2594 for t_node in selected_nodes:
2595 valid = False
2596 input_index = 0
2597 if t_node.inputs:
2598 for index, i in enumerate(t_node.inputs):
2599 if not i.is_linked:
2600 valid = True
2601 input_index = index
2602 break
2603 if valid:
2604 locx = t_node.location.x
2605 locy = t_node.location.y - t_node.dimensions.y/2
2607 xoffset = [500, 700]
2608 is_texture = False
2609 if t_node.type in texture_types + ['MAPPING']:
2610 xoffset = [290, 500]
2611 is_texture = True
2613 coordout = 2
2614 image_type = 'ShaderNodeTexImage'
2616 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
2617 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2618 if t_node.type == 'BACKGROUND':
2619 image_type = 'ShaderNodeTexEnvironment'
2621 if not is_texture:
2622 tex = nodes.new(image_type)
2623 tex.location = [locx - 200, locy + 112]
2624 nodes.active = tex
2625 links.new(tex.outputs[0], t_node.inputs[input_index])
2627 t_node.select = False
2628 if self.add_mapping or is_texture:
2629 if t_node.type != 'MAPPING':
2630 m = nodes.new('ShaderNodeMapping')
2631 m.location = [locx - xoffset[0], locy + 141]
2632 m.width = 240
2633 else:
2634 m = t_node
2635 coord = nodes.new('ShaderNodeTexCoord')
2636 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
2638 if not is_texture:
2639 links.new(m.outputs[0], tex.inputs[0])
2640 links.new(coord.outputs[coordout], m.inputs[0])
2641 else:
2642 nodes.active = m
2643 links.new(m.outputs[0], t_node.inputs[input_index])
2644 links.new(coord.outputs[coordout], m.inputs[0])
2645 else:
2646 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
2647 return {'FINISHED'}
2650 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2651 bl_idname = "node.nw_add_textures_for_principled"
2652 bl_label = "Principled Texture Setup"
2653 bl_description = "Add Texture Node Setup for Principled BSDF"
2654 bl_options = {'REGISTER', 'UNDO'}
2656 directory: StringProperty(
2657 name='Directory',
2658 subtype='DIR_PATH',
2659 default='',
2660 description='Folder to search in for image files'
2662 files: CollectionProperty(
2663 type=bpy.types.OperatorFileListElement,
2664 options={'HIDDEN', 'SKIP_SAVE'}
2667 order = [
2668 "filepath",
2669 "files",
2672 @classmethod
2673 def poll(cls, context):
2674 valid = False
2675 if nw_check(context):
2676 space = context.space_data
2677 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2678 valid = True
2679 return valid
2681 def execute(self, context):
2682 # Check if everything is ok
2683 if not self.directory:
2684 self.report({'INFO'}, 'No Folder Selected')
2685 return {'CANCELLED'}
2686 if not self.files[:]:
2687 self.report({'INFO'}, 'No Files Selected')
2688 return {'CANCELLED'}
2690 nodes, links = get_nodes_links(context)
2691 active_node = nodes.active
2692 if not active_node.bl_idname == 'ShaderNodeBsdfPrincipled':
2693 self.report({'INFO'}, 'Select Principled BSDF')
2694 return {'CANCELLED'}
2696 # Helper_functions
2697 def split_into__components(fname):
2698 # Split filename into components
2699 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2700 # Remove extension
2701 fname = path.splitext(fname)[0]
2702 # Remove digits
2703 fname = ''.join(i for i in fname if not i.isdigit())
2704 # Separate CamelCase by space
2705 fname = re.sub("([a-z])([A-Z])","\g<1> \g<2>",fname)
2706 # Replace common separators with SPACE
2707 seperators = ['_', '.', '-', '__', '--', '#']
2708 for sep in seperators:
2709 fname = fname.replace(sep, ' ')
2711 components = fname.split(' ')
2712 components = [c.lower() for c in components]
2713 return components
2715 # Filter textures names for texturetypes in filenames
2716 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2717 tags = context.user_preferences.addons[__name__].preferences.principled_tags
2718 normal_abbr = tags.normal.split(' ')
2719 bump_abbr = tags.bump.split(' ')
2720 gloss_abbr = tags.gloss.split(' ')
2721 rough_abbr = tags.rough.split(' ')
2722 socketnames = [
2723 ['Displacement', tags.displacement.split(' '), None],
2724 ['Base Color', tags.base_color.split(' '), None],
2725 ['Subsurface Color', tags.sss_color.split(' '), None],
2726 ['Metallic', tags.metallic.split(' '), None],
2727 ['Specular', tags.specular.split(' '), None],
2728 ['Roughness', rough_abbr + gloss_abbr, None],
2729 ['Normal', normal_abbr + bump_abbr, None],
2732 # Look through texture_types and set value as filename of first matched file
2733 def match_files_to_socket_names():
2734 for sname in socketnames:
2735 for file in self.files:
2736 fname = file.name
2737 filenamecomponents = split_into__components(fname)
2738 matches = set(sname[1]).intersection(set(filenamecomponents))
2739 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2740 if matches:
2741 sname[2] = fname
2742 break
2744 match_files_to_socket_names()
2745 # Remove socketnames without found files
2746 socketnames = [s for s in socketnames if s[2]
2747 and path.exists(self.directory+s[2])]
2748 if not socketnames:
2749 self.report({'INFO'}, 'No matching images found')
2750 print('No matching images found')
2751 return {'CANCELLED'}
2753 # Add found images
2754 print('\nMatched Textures:')
2755 texture_nodes = []
2756 disp_texture = None
2757 normal_node = None
2758 roughness_node = None
2759 for i, sname in enumerate(socketnames):
2760 print(i, sname[0], sname[2])
2762 # DISPLACEMENT NODES
2763 if sname[0] == 'Displacement':
2764 disp_texture = nodes.new(type='ShaderNodeTexImage')
2765 img = bpy.data.images.load(self.directory+sname[2])
2766 disp_texture.image = img
2767 disp_texture.label = 'Displacement'
2768 disp_texture.color_space = 'NONE'
2770 # Add displacement offset nodes
2771 disp_node = nodes.new(type='ShaderNodeDisplacement')
2772 disp_node.location = active_node.location + Vector((0, -560))
2773 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
2775 # TODO Turn on true displacement in the material
2776 # Too complicated for now
2778 # Find output node
2779 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2780 if output_node:
2781 if not output_node[0].inputs[2].is_linked:
2782 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
2784 continue
2786 if not active_node.inputs[sname[0]].is_linked:
2787 # No texture node connected -> add texture node with new image
2788 texture_node = nodes.new(type='ShaderNodeTexImage')
2789 img = bpy.data.images.load(self.directory+sname[2])
2790 texture_node.image = img
2792 # NORMAL NODES
2793 if sname[0] == 'Normal':
2794 # Test if new texture node is normal or bump map
2795 fname_components = split_into__components(sname[2])
2796 match_normal = set(normal_abbr).intersection(set(fname_components))
2797 match_bump = set(bump_abbr).intersection(set(fname_components))
2798 if match_normal:
2799 # If Normal add normal node in between
2800 normal_node = nodes.new(type='ShaderNodeNormalMap')
2801 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
2802 elif match_bump:
2803 # If Bump add bump node in between
2804 normal_node = nodes.new(type='ShaderNodeBump')
2805 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
2807 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
2808 normal_node_texture = texture_node
2810 elif sname[0] == 'Roughness':
2811 # Test if glossy or roughness map
2812 fname_components = split_into__components(sname[2])
2813 match_rough = set(rough_abbr).intersection(set(fname_components))
2814 match_gloss = set(gloss_abbr).intersection(set(fname_components))
2816 if match_rough:
2817 # If Roughness nothing to to
2818 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2820 elif match_gloss:
2821 # If Gloss Map add invert node
2822 invert_node = nodes.new(type='ShaderNodeInvert')
2823 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
2825 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
2826 roughness_node = texture_node
2828 else:
2829 # This is a simple connection Texture --> Input slot
2830 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2832 # Use non-color for all but 'Base Color' Textures
2833 if not sname[0] in ['Base Color']:
2834 texture_node.color_space = 'NONE'
2836 else:
2837 # If already texture connected. add to node list for alignment
2838 texture_node = active_node.inputs[sname[0]].links[0].from_node
2840 # This are all connected texture nodes
2841 texture_nodes.append(texture_node)
2842 texture_node.label = sname[0]
2844 if disp_texture:
2845 texture_nodes.append(disp_texture)
2847 # Alignment
2848 for i, texture_node in enumerate(texture_nodes):
2849 offset = Vector((-550, (i * -280) + 200))
2850 texture_node.location = active_node.location + offset
2852 if normal_node:
2853 # Extra alignment if normal node was added
2854 normal_node.location = normal_node_texture.location + Vector((300, 0))
2856 if roughness_node:
2857 # Alignment of invert node if glossy map
2858 invert_node.location = roughness_node.location + Vector((300, 0))
2860 # Add texture input + mapping
2861 mapping = nodes.new(type='ShaderNodeMapping')
2862 mapping.location = active_node.location + Vector((-1050, 0))
2863 if len(texture_nodes) > 1:
2864 # If more than one texture add reroute node in between
2865 reroute = nodes.new(type='NodeReroute')
2866 texture_nodes.append(reroute)
2867 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
2868 reroute.location = tex_coords + Vector((-50, -120))
2869 for texture_node in texture_nodes:
2870 link = links.new(texture_node.inputs[0], reroute.outputs[0])
2871 link = links.new(reroute.inputs[0], mapping.outputs[0])
2872 else:
2873 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
2875 # Connect texture_coordiantes to mapping node
2876 texture_input = nodes.new(type='ShaderNodeTexCoord')
2877 texture_input.location = mapping.location + Vector((-200, 0))
2878 link = links.new(mapping.inputs[0], texture_input.outputs[2])
2880 # Create frame around tex coords and mapping
2881 frame = nodes.new(type='NodeFrame')
2882 frame.label = 'Mapping'
2883 mapping.parent = frame
2884 texture_input.parent = frame
2885 frame.update()
2887 # Create frame around texture nodes
2888 frame = nodes.new(type='NodeFrame')
2889 frame.label = 'Textures'
2890 for tnode in texture_nodes:
2891 tnode.parent = frame
2892 frame.update()
2894 # Just to be sure
2895 active_node.select = False
2896 nodes.update()
2897 links.update()
2898 force_update(context)
2899 return {'FINISHED'}
2902 class NWAddReroutes(Operator, NWBase):
2903 """Add Reroute Nodes and link them to outputs of selected nodes"""
2904 bl_idname = "node.nw_add_reroutes"
2905 bl_label = "Add Reroutes"
2906 bl_description = "Add Reroutes to Outputs"
2907 bl_options = {'REGISTER', 'UNDO'}
2909 option: EnumProperty(
2910 name="option",
2911 items=[
2912 ('ALL', 'to all', 'Add to all outputs'),
2913 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2914 ('LINKED', 'to linked', 'Add only to linked outputs'),
2918 def execute(self, context):
2919 tree_type = context.space_data.node_tree.type
2920 option = self.option
2921 nodes, links = get_nodes_links(context)
2922 # output valid when option is 'all' or when 'loose' output has no links
2923 valid = False
2924 post_select = [] # nodes to be selected after execution
2925 # create reroutes and recreate links
2926 for node in [n for n in nodes if n.select]:
2927 if node.outputs:
2928 x = node.location.x
2929 y = node.location.y
2930 width = node.width
2931 # unhide 'REROUTE' nodes to avoid issues with location.y
2932 if node.type == 'REROUTE':
2933 node.hide = False
2934 # When node is hidden - width_hidden not usable.
2935 # Hack needed to calculate real width
2936 if node.hide:
2937 bpy.ops.node.select_all(action='DESELECT')
2938 helper = nodes.new('NodeReroute')
2939 helper.select = True
2940 node.select = True
2941 # resize node and helper to zero. Then check locations to calculate width
2942 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2943 width = 2.0 * (helper.location.x - node.location.x)
2944 # restore node location
2945 node.location = x, y
2946 # delete helper
2947 node.select = False
2948 # only helper is selected now
2949 bpy.ops.node.delete()
2950 x = node.location.x + width + 20.0
2951 if node.type != 'REROUTE':
2952 y -= 35.0
2953 y_offset = -22.0
2954 loc = x, y
2955 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2956 for out_i, output in enumerate(node.outputs):
2957 pass_used = False # initial value to be analyzed if 'R_LAYERS'
2958 # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
2959 if node.type != 'R_LAYERS':
2960 pass_used = True
2961 else: # if 'R_LAYERS' check if output represent used render pass
2962 node_scene = node.scene
2963 node_layer = node.layer
2964 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2965 if output.name == 'Alpha':
2966 pass_used = True
2967 else:
2968 # check entries in global 'rl_outputs' variable
2969 #for render_pass, output_name, exr_name, in_internal, in_cycles in rl_outputs:
2970 for rlo in rl_outputs:
2971 if output.name == rlo.output_name or output.name == rlo.exr_output_name:
2972 pass_used = getattr(node_scene.render.layers[node_layer], rlo.render_pass)
2973 break
2974 if pass_used:
2975 valid = ((option == 'ALL') or
2976 (option == 'LOOSE' and not output.links) or
2977 (option == 'LINKED' and output.links))
2978 # Add reroutes only if valid, but offset location in all cases.
2979 if valid:
2980 n = nodes.new('NodeReroute')
2981 nodes.active = n
2982 for link in output.links:
2983 links.new(n.outputs[0], link.to_socket)
2984 links.new(output, n.inputs[0])
2985 n.location = loc
2986 post_select.append(n)
2987 reroutes_count += 1
2988 y += y_offset
2989 loc = x, y
2990 # disselect the node so that after execution of script only newly created nodes are selected
2991 node.select = False
2992 # nicer reroutes distribution along y when node.hide
2993 if node.hide:
2994 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2995 for reroute in [r for r in nodes if r.select]:
2996 reroute.location.y -= y_translate
2997 for node in post_select:
2998 node.select = True
3000 return {'FINISHED'}
3003 class NWLinkActiveToSelected(Operator, NWBase):
3004 """Link active node to selected nodes basing on various criteria"""
3005 bl_idname = "node.nw_link_active_to_selected"
3006 bl_label = "Link Active Node to Selected"
3007 bl_options = {'REGISTER', 'UNDO'}
3009 replace: BoolProperty()
3010 use_node_name: BoolProperty()
3011 use_outputs_names: BoolProperty()
3013 @classmethod
3014 def poll(cls, context):
3015 valid = False
3016 if nw_check(context):
3017 if context.active_node is not None:
3018 if context.active_node.select:
3019 valid = True
3020 return valid
3022 def execute(self, context):
3023 nodes, links = get_nodes_links(context)
3024 replace = self.replace
3025 use_node_name = self.use_node_name
3026 use_outputs_names = self.use_outputs_names
3027 active = nodes.active
3028 selected = [node for node in nodes if node.select and node != active]
3029 outputs = [] # Only usable outputs of active nodes will be stored here.
3030 for out in active.outputs:
3031 if active.type != 'R_LAYERS':
3032 outputs.append(out)
3033 else:
3034 # 'R_LAYERS' node type needs special handling.
3035 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3036 # Only outputs that represent used passes should be taken into account
3037 # Check if pass represented by output is used.
3038 # global 'rl_outputs' list will be used for that
3039 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
3040 pass_used = False # initial value. Will be set to True if pass is used
3041 if out.name == 'Alpha':
3042 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3043 pass_used = True
3044 elif out.name == out_name:
3045 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3046 pass_used = getattr(active.scene.render.layers[active.layer], render_pass)
3047 break
3048 if pass_used:
3049 outputs.append(out)
3050 doit = True # Will be changed to False when links successfully added to previous output.
3051 for out in outputs:
3052 if doit:
3053 for node in selected:
3054 dst_name = node.name # Will be compared with src_name if needed.
3055 # When node has label - use it as dst_name
3056 if node.label:
3057 dst_name = node.label
3058 valid = True # Initial value. Will be changed to False if names don't match.
3059 src_name = dst_name # If names not used - this asignment will keep valid = True.
3060 if use_node_name:
3061 # Set src_name to source node name or label
3062 src_name = active.name
3063 if active.label:
3064 src_name = active.label
3065 elif use_outputs_names:
3066 src_name = (out.name, )
3067 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
3068 if out.name in {out_name, exr_name}:
3069 src_name = (out_name, exr_name)
3070 if dst_name not in src_name:
3071 valid = False
3072 if valid:
3073 for input in node.inputs:
3074 if input.type == out.type or node.type == 'REROUTE':
3075 if replace or not input.is_linked:
3076 links.new(out, input)
3077 if not use_node_name and not use_outputs_names:
3078 doit = False
3079 break
3081 return {'FINISHED'}
3084 class NWAlignNodes(Operator, NWBase):
3085 '''Align the selected nodes neatly in a row/column'''
3086 bl_idname = "node.nw_align_nodes"
3087 bl_label = "Align Nodes"
3088 bl_options = {'REGISTER', 'UNDO'}
3089 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3091 def execute(self, context):
3092 nodes, links = get_nodes_links(context)
3093 margin = self.margin
3095 selection = []
3096 for node in nodes:
3097 if node.select and node.type != 'FRAME':
3098 selection.append(node)
3100 # If no nodes are selected, align all nodes
3101 active_loc = None
3102 if not selection:
3103 selection = nodes
3104 elif nodes.active in selection:
3105 active_loc = copy(nodes.active.location) # make a copy, not a reference
3107 # Check if nodes should be laid out horizontally or vertically
3108 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3109 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3110 x_range = max(x_locs) - min(x_locs)
3111 y_range = max(y_locs) - min(y_locs)
3112 mid_x = (max(x_locs) + min(x_locs)) / 2
3113 mid_y = (max(y_locs) + min(y_locs)) / 2
3114 horizontal = x_range > y_range
3116 # Sort selection by location of node mid-point
3117 if horizontal:
3118 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3119 else:
3120 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3122 # Alignment
3123 current_pos = 0
3124 for node in selection:
3125 current_margin = margin
3126 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3128 if horizontal:
3129 node.location.x = current_pos
3130 current_pos += current_margin + node.dimensions.x
3131 node.location.y = mid_y + (node.dimensions.y / 2)
3132 else:
3133 node.location.y = current_pos
3134 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3135 node.location.x = mid_x - (node.dimensions.x / 2)
3137 # If active node is selected, center nodes around it
3138 if active_loc is not None:
3139 active_loc_diff = active_loc - nodes.active.location
3140 for node in selection:
3141 node.location += active_loc_diff
3142 else: # Position nodes centered around where they used to be
3143 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
3144 new_mid = (max(locs) + min(locs)) / 2
3145 for node in selection:
3146 if horizontal:
3147 node.location.x += (mid_x - new_mid)
3148 else:
3149 node.location.y += (mid_y - new_mid)
3151 return {'FINISHED'}
3154 class NWSelectParentChildren(Operator, NWBase):
3155 bl_idname = "node.nw_select_parent_child"
3156 bl_label = "Select Parent or Children"
3157 bl_options = {'REGISTER', 'UNDO'}
3159 option: EnumProperty(
3160 name="option",
3161 items=(
3162 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3163 ('CHILD', 'Select Children', 'Select members of selected frame'),
3167 def execute(self, context):
3168 nodes, links = get_nodes_links(context)
3169 option = self.option
3170 selected = [node for node in nodes if node.select]
3171 if option == 'PARENT':
3172 for sel in selected:
3173 parent = sel.parent
3174 if parent:
3175 parent.select = True
3176 else: # option == 'CHILD'
3177 for sel in selected:
3178 children = [node for node in nodes if node.parent == sel]
3179 for kid in children:
3180 kid.select = True
3182 return {'FINISHED'}
3185 class NWDetachOutputs(Operator, NWBase):
3186 """Detach outputs of selected node leaving inputs linked"""
3187 bl_idname = "node.nw_detach_outputs"
3188 bl_label = "Detach Outputs"
3189 bl_options = {'REGISTER', 'UNDO'}
3191 def execute(self, context):
3192 nodes, links = get_nodes_links(context)
3193 selected = context.selected_nodes
3194 bpy.ops.node.duplicate_move_keep_inputs()
3195 new_nodes = context.selected_nodes
3196 bpy.ops.node.select_all(action="DESELECT")
3197 for node in selected:
3198 node.select = True
3199 bpy.ops.node.delete_reconnect()
3200 for new_node in new_nodes:
3201 new_node.select = True
3202 bpy.ops.transform.translate('INVOKE_DEFAULT')
3204 return {'FINISHED'}
3207 class NWLinkToOutputNode(Operator, NWBase):
3208 """Link to Composite node or Material Output node"""
3209 bl_idname = "node.nw_link_out"
3210 bl_label = "Connect to Output"
3211 bl_options = {'REGISTER', 'UNDO'}
3213 @classmethod
3214 def poll(cls, context):
3215 valid = False
3216 if nw_check(context):
3217 if context.active_node is not None:
3218 for out in context.active_node.outputs:
3219 if not out.hide:
3220 valid = True
3221 break
3222 return valid
3224 def execute(self, context):
3225 nodes, links = get_nodes_links(context)
3226 active = nodes.active
3227 output_node = None
3228 output_index = None
3229 tree_type = context.space_data.tree_type
3230 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
3231 output_types_compo = ['COMPOSITE']
3232 output_types_blender_mat = ['OUTPUT']
3233 output_types_textures = ['OUTPUT']
3234 output_types = output_types_shaders + output_types_compo + output_types_blender_mat
3235 for node in nodes:
3236 if node.type in output_types:
3237 output_node = node
3238 break
3239 if not output_node:
3240 bpy.ops.node.select_all(action="DESELECT")
3241 if tree_type == 'ShaderNodeTree':
3242 if is_cycles_or_eevee(context):
3243 output_node = nodes.new('ShaderNodeOutputMaterial')
3244 else:
3245 output_node = nodes.new('ShaderNodeOutput')
3246 elif tree_type == 'CompositorNodeTree':
3247 output_node = nodes.new('CompositorNodeComposite')
3248 elif tree_type == 'TextureNodeTree':
3249 output_node = nodes.new('TextureNodeOutput')
3250 output_node.location.x = active.location.x + active.dimensions.x + 80
3251 output_node.location.y = active.location.y
3252 if (output_node and active.outputs):
3253 for i, output in enumerate(active.outputs):
3254 if not output.hide:
3255 output_index = i
3256 break
3257 for i, output in enumerate(active.outputs):
3258 if output.type == output_node.inputs[0].type and not output.hide:
3259 output_index = i
3260 break
3262 out_input_index = 0
3263 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3264 if active.outputs[output_index].name == 'Volume':
3265 out_input_index = 1
3266 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3267 out_input_index = 2
3268 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3270 force_update(context) # viewport render does not update
3272 return {'FINISHED'}
3275 class NWMakeLink(Operator, NWBase):
3276 """Make a link from one socket to another"""
3277 bl_idname = 'node.nw_make_link'
3278 bl_label = 'Make Link'
3279 bl_options = {'REGISTER', 'UNDO'}
3280 from_socket: IntProperty()
3281 to_socket: IntProperty()
3283 def execute(self, context):
3284 nodes, links = get_nodes_links(context)
3286 n1 = nodes[context.scene.NWLazySource]
3287 n2 = nodes[context.scene.NWLazyTarget]
3289 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3291 force_update(context)
3293 return {'FINISHED'}
3296 class NWCallInputsMenu(Operator, NWBase):
3297 """Link from this output"""
3298 bl_idname = 'node.nw_call_inputs_menu'
3299 bl_label = 'Make Link'
3300 bl_options = {'REGISTER', 'UNDO'}
3301 from_socket: IntProperty()
3303 def execute(self, context):
3304 nodes, links = get_nodes_links(context)
3306 context.scene.NWSourceSocket = self.from_socket
3308 n1 = nodes[context.scene.NWLazySource]
3309 n2 = nodes[context.scene.NWLazyTarget]
3310 if len(n2.inputs) > 1:
3311 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3312 elif len(n2.inputs) == 1:
3313 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3314 return {'FINISHED'}
3317 class NWAddSequence(Operator, ImportHelper):
3318 """Add an Image Sequence"""
3319 bl_idname = 'node.nw_add_sequence'
3320 bl_label = 'Import Image Sequence'
3321 bl_options = {'REGISTER', 'UNDO'}
3323 directory: StringProperty(
3324 subtype="DIR_PATH"
3326 filename: StringProperty(
3327 subtype="FILE_NAME"
3329 files: CollectionProperty(
3330 type=bpy.types.OperatorFileListElement,
3331 options={'HIDDEN', 'SKIP_SAVE'}
3334 def execute(self, context):
3335 nodes, links = get_nodes_links(context)
3336 directory = self.directory
3337 filename = self.filename
3338 files = self.files
3339 tree = context.space_data.node_tree
3341 # DEBUG
3342 # print ("\nDIR:", directory)
3343 # print ("FN:", filename)
3344 # print ("Fs:", list(f.name for f in files), '\n')
3346 if tree.type == 'SHADER':
3347 node_type = "ShaderNodeTexImage"
3348 elif tree.type == 'COMPOSITING':
3349 node_type = "CompositorNodeImage"
3350 else:
3351 self.report({'ERROR'}, "Unsupported Node Tree type!")
3352 return {'CANCELLED'}
3354 if not files[0].name and not filename:
3355 self.report({'ERROR'}, "No file chosen")
3356 return {'CANCELLED'}
3357 elif files[0].name and (not filename or not path.exists(directory+filename)):
3358 # User has selected multiple files without an active one, or the active one is non-existant
3359 filename = files[0].name
3361 if not path.exists(directory+filename):
3362 self.report({'ERROR'}, filename+" does not exist!")
3363 return {'CANCELLED'}
3365 without_ext = '.'.join(filename.split('.')[:-1])
3367 # if last digit isn't a number, it's not a sequence
3368 if not without_ext[-1].isdigit():
3369 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3370 return {'CANCELLED'}
3373 extension = filename.split('.')[-1]
3374 reverse = without_ext[::-1] # reverse string
3376 count_numbers = 0
3377 for char in reverse:
3378 if char.isdigit():
3379 count_numbers += 1
3380 else:
3381 break
3383 without_num = without_ext[:count_numbers*-1]
3385 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3387 num_frames = len(files)
3389 nodes_list = [node for node in nodes]
3390 if nodes_list:
3391 nodes_list.sort(key=lambda k: k.location.x)
3392 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3393 yloc = 0
3394 for node in nodes:
3395 node.select = False
3396 yloc += node_mid_pt(node, 'y')
3397 yloc = yloc/len(nodes)
3398 else:
3399 xloc = 0
3400 yloc = 0
3402 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3404 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3405 node = nodes.active
3406 node.label = name_with_hashes
3408 img = bpy.data.images.load(directory+(without_ext+'.'+extension))
3409 img.source = 'SEQUENCE'
3410 img.name = name_with_hashes
3411 node.image = img
3412 image_user = node.image_user if tree.type == 'SHADER' else node
3413 image_user.frame_offset = int(files[0][len(without_num)+len(directory):-1*(len(extension)+1)]) - 1 # separate the number from the file name of the first file
3414 image_user.frame_duration = num_frames
3416 return {'FINISHED'}
3419 class NWAddMultipleImages(Operator, ImportHelper):
3420 """Add multiple images at once"""
3421 bl_idname = 'node.nw_add_multiple_images'
3422 bl_label = 'Open Selected Images'
3423 bl_options = {'REGISTER', 'UNDO'}
3424 directory: StringProperty(
3425 subtype="DIR_PATH"
3427 files: CollectionProperty(
3428 type=bpy.types.OperatorFileListElement,
3429 options={'HIDDEN', 'SKIP_SAVE'}
3432 def execute(self, context):
3433 nodes, links = get_nodes_links(context)
3435 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3437 if context.space_data.node_tree.type == 'SHADER':
3438 node_type = "ShaderNodeTexImage"
3439 elif context.space_data.node_tree.type == 'COMPOSITING':
3440 node_type = "CompositorNodeImage"
3441 else:
3442 self.report({'ERROR'}, "Unsupported Node Tree type!")
3443 return {'CANCELLED'}
3445 new_nodes = []
3446 for f in self.files:
3447 fname = f.name
3449 node = nodes.new(node_type)
3450 new_nodes.append(node)
3451 node.label = fname
3452 node.hide = True
3453 node.width_hidden = 100
3454 node.location.x = xloc
3455 node.location.y = yloc
3456 yloc -= 40
3458 img = bpy.data.images.load(self.directory+fname)
3459 node.image = img
3461 # shift new nodes up to center of tree
3462 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3463 for node in nodes:
3464 if node in new_nodes:
3465 node.select = True
3466 node.location.y += (list_size/2)
3467 else:
3468 node.select = False
3469 return {'FINISHED'}
3472 class NWViewerFocus(bpy.types.Operator):
3473 """Set the viewer tile center to the mouse position"""
3474 bl_idname = "node.nw_viewer_focus"
3475 bl_label = "Viewer Focus"
3477 x: bpy.props.IntProperty()
3478 y: bpy.props.IntProperty()
3480 @classmethod
3481 def poll(cls, context):
3482 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3484 def execute(self, context):
3485 return {'FINISHED'}
3487 def invoke(self, context, event):
3488 render = context.scene.render
3489 space = context.space_data
3490 percent = render.resolution_percentage*0.01
3492 nodes, links = get_nodes_links(context)
3493 viewers = [n for n in nodes if n.type == 'VIEWER']
3495 if viewers:
3496 mlocx = event.mouse_region_x
3497 mlocy = event.mouse_region_y
3498 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
3500 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3501 region_x = context.region.width
3502 region_y = context.region.height
3504 region_center_x = context.region.width / 2
3505 region_center_y = context.region.height / 2
3507 bd_x = render.resolution_x * percent * space.backdrop_zoom
3508 bd_y = render.resolution_y * percent * space.backdrop_zoom
3510 backdrop_center_x = (bd_x / 2) - space.backdrop_x
3511 backdrop_center_y = (bd_y / 2) - space.backdrop_y
3513 margin_x = region_center_x - backdrop_center_x
3514 margin_y = region_center_y - backdrop_center_y
3516 abs_mouse_x = (mlocx - margin_x) / bd_x
3517 abs_mouse_y = (mlocy - margin_y) / bd_y
3519 for node in viewers:
3520 node.center_x = abs_mouse_x
3521 node.center_y = abs_mouse_y
3522 else:
3523 return {'PASS_THROUGH'}
3525 return self.execute(context)
3528 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3529 """Save the current viewer node to an image file"""
3530 bl_idname = "node.nw_save_viewer"
3531 bl_label = "Save This Image"
3532 filepath: StringProperty(subtype="FILE_PATH")
3533 filename_ext: EnumProperty(
3534 name="Format",
3535 description="Choose the file format to save to",
3536 items=(('.bmp', "PNG", ""),
3537 ('.rgb', 'IRIS', ""),
3538 ('.png', 'PNG', ""),
3539 ('.jpg', 'JPEG', ""),
3540 ('.jp2', 'JPEG2000', ""),
3541 ('.tga', 'TARGA', ""),
3542 ('.cin', 'CINEON', ""),
3543 ('.dpx', 'DPX', ""),
3544 ('.exr', 'OPEN_EXR', ""),
3545 ('.hdr', 'HDR', ""),
3546 ('.tif', 'TIFF', "")),
3547 default='.png',
3550 @classmethod
3551 def poll(cls, context):
3552 valid = False
3553 if nw_check(context):
3554 if context.space_data.tree_type == 'CompositorNodeTree':
3555 if "Viewer Node" in [i.name for i in bpy.data.images]:
3556 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3557 valid = True
3558 return valid
3560 def execute(self, context):
3561 fp = self.filepath
3562 if fp:
3563 formats = {
3564 '.bmp': 'BMP',
3565 '.rgb': 'IRIS',
3566 '.png': 'PNG',
3567 '.jpg': 'JPEG',
3568 '.jpeg': 'JPEG',
3569 '.jp2': 'JPEG2000',
3570 '.tga': 'TARGA',
3571 '.cin': 'CINEON',
3572 '.dpx': 'DPX',
3573 '.exr': 'OPEN_EXR',
3574 '.hdr': 'HDR',
3575 '.tiff': 'TIFF',
3576 '.tif': 'TIFF'}
3577 basename, ext = path.splitext(fp)
3578 old_render_format = context.scene.render.image_settings.file_format
3579 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3580 context.area.type = "IMAGE_EDITOR"
3581 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3582 context.area.spaces[0].image.save_render(fp)
3583 context.area.type = "NODE_EDITOR"
3584 context.scene.render.image_settings.file_format = old_render_format
3585 return {'FINISHED'}
3588 class NWResetNodes(bpy.types.Operator):
3589 """Reset Nodes in Selection"""
3590 bl_idname = "node.nw_reset_nodes"
3591 bl_label = "Reset Nodes"
3592 bl_options = {'REGISTER', 'UNDO'}
3594 @classmethod
3595 def poll(cls, context):
3596 space = context.space_data
3597 return space.type == 'NODE_EDITOR'
3599 def execute(self, context):
3600 node_active = context.active_node
3601 node_selected = context.selected_nodes
3602 node_ignore = ["FRAME","REROUTE", "GROUP"]
3604 # Check if one node is selected at least
3605 if not (len(node_selected) > 0):
3606 self.report({'ERROR'}, "1 node must be selected at least")
3607 return {'CANCELLED'}
3609 active_node_name = node_active.name if node_active.select else None
3610 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3612 # Create output lists
3613 selected_node_names = [n.name for n in node_selected]
3614 success_names = []
3616 # Reset all valid children in a frame
3617 node_active_is_frame = False
3618 if len(node_selected) == 1 and node_active.type == "FRAME":
3619 node_tree = node_active.id_data
3620 children = [n for n in node_tree.nodes if n.parent == node_active]
3621 if children:
3622 valid_nodes = [n for n in children if n.type not in node_ignore]
3623 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3624 node_active_is_frame = True
3626 # Check if valid nodes in selection
3627 if not (len(valid_nodes) > 0):
3628 # Check for frames only
3629 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3630 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3631 self.report({'ERROR'}, "Please select only 1 frame to reset")
3632 else:
3633 self.report({'ERROR'}, "No valid node(s) in selection")
3634 return {'CANCELLED'}
3636 # Report nodes that are not valid
3637 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3638 valid_node_names = [n.name for n in valid_nodes]
3639 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3640 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3642 # Deselect all nodes
3643 for i in node_selected:
3644 i.select = False
3646 # Run through all valid nodes
3647 for node in valid_nodes:
3649 parent = node.parent if node.parent else None
3650 node_loc = [node.location.x, node.location.y]
3652 node_tree = node.id_data
3653 props_to_copy = 'bl_idname name location height width'.split(' ')
3655 reconnections = []
3656 mappings = chain.from_iterable([node.inputs, node.outputs])
3657 for i in (i for i in mappings if i.is_linked):
3658 for L in i.links:
3659 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3661 props = {j: getattr(node, j) for j in props_to_copy}
3663 new_node = node_tree.nodes.new(props['bl_idname'])
3664 props_to_copy.pop(0)
3666 for prop in props_to_copy:
3667 setattr(new_node, prop, props[prop])
3669 nodes = node_tree.nodes
3670 nodes.remove(node)
3671 new_node.name = props['name']
3673 if parent:
3674 new_node.parent = parent
3675 new_node.location = node_loc
3677 for str_from, str_to in reconnections:
3678 node_tree.links.new(eval(str_from), eval(str_to))
3680 new_node.select = False
3681 success_names.append(new_node.name)
3683 # Reselect all nodes
3684 if selected_node_names and node_active_is_frame is False:
3685 for i in selected_node_names:
3686 node_tree.nodes[i].select = True
3688 if active_node_name is not None:
3689 node_tree.nodes[active_node_name].select = True
3690 node_tree.nodes.active = node_tree.nodes[active_node_name]
3692 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3693 return {'FINISHED'}
3697 # P A N E L
3700 def drawlayout(context, layout, mode='non-panel'):
3701 tree_type = context.space_data.tree_type
3703 col = layout.column(align=True)
3704 col.menu(NWMergeNodesMenu.bl_idname)
3705 col.separator()
3707 col = layout.column(align=True)
3708 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3709 col.separator()
3711 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3712 col = layout.column(align=True)
3713 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3714 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3715 col.separator()
3717 col = layout.column(align=True)
3718 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3719 col.operator(NWSwapLinks.bl_idname)
3720 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3721 col.separator()
3723 col = layout.column(align=True)
3724 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3725 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3726 col.separator()
3728 col = layout.column(align=True)
3729 if mode == 'panel':
3730 row = col.row(align=True)
3731 row.operator(NWClearLabel.bl_idname).option = True
3732 row.operator(NWModifyLabels.bl_idname)
3733 else:
3734 col.operator(NWClearLabel.bl_idname).option = True
3735 col.operator(NWModifyLabels.bl_idname)
3736 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3737 col.separator()
3738 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3739 col.separator()
3741 col = layout.column(align=True)
3742 if tree_type == 'CompositorNodeTree':
3743 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
3744 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
3745 col.separator()
3747 col = layout.column(align=True)
3748 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3749 col.separator()
3751 col = layout.column(align=True)
3752 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
3753 col.separator()
3755 col = layout.column(align=True)
3756 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3757 col.separator()
3760 class NodeWranglerPanel(Panel, NWBase):
3761 bl_idname = "NODE_PT_nw_node_wrangler"
3762 bl_space_type = 'NODE_EDITOR'
3763 bl_label = "Node Wrangler"
3764 bl_region_type = "TOOLS"
3765 bl_category = "Node Wrangler"
3767 prepend: StringProperty(
3768 name='prepend',
3770 append: StringProperty()
3771 remove: StringProperty()
3773 def draw(self, context):
3774 self.layout.label(text="(Quick access: Ctrl+Space)")
3775 drawlayout(context, self.layout, mode='panel')
3779 # M E N U S
3781 class NodeWranglerMenu(Menu, NWBase):
3782 bl_idname = "NODE_MT_nw_node_wrangler_menu"
3783 bl_label = "Node Wrangler"
3785 def draw(self, context):
3786 drawlayout(context, self.layout)
3789 class NWMergeNodesMenu(Menu, NWBase):
3790 bl_idname = "NODE_MT_nw_merge_nodes_menu"
3791 bl_label = "Merge Selected Nodes"
3793 def draw(self, context):
3794 type = context.space_data.tree_type
3795 layout = self.layout
3796 if type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3797 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
3798 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
3799 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3800 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
3801 props.mode = 'MIX'
3802 props.merge_type = 'ZCOMBINE'
3803 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3804 props.mode = 'MIX'
3805 props.merge_type = 'ALPHAOVER'
3808 class NWMergeShadersMenu(Menu, NWBase):
3809 bl_idname = "NODE_MT_nw_merge_shaders_menu"
3810 bl_label = "Merge Selected Nodes using Shaders"
3812 def draw(self, context):
3813 layout = self.layout
3814 for type in ('MIX', 'ADD'):
3815 props = layout.operator(NWMergeNodes.bl_idname, text=type)
3816 props.mode = type
3817 props.merge_type = 'SHADER'
3820 class NWMergeMixMenu(Menu, NWBase):
3821 bl_idname = "NODE_MT_nw_merge_mix_menu"
3822 bl_label = "Merge Selected Nodes using Mix"
3824 def draw(self, context):
3825 layout = self.layout
3826 for type, name, description in blend_types:
3827 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3828 props.mode = type
3829 props.merge_type = 'MIX'
3832 class NWConnectionListOutputs(Menu, NWBase):
3833 bl_idname = "NODE_MT_nw_connection_list_out"
3834 bl_label = "From:"
3836 def draw(self, context):
3837 layout = self.layout
3838 nodes, links = get_nodes_links(context)
3840 n1 = nodes[context.scene.NWLazySource]
3842 if n1.type == "R_LAYERS":
3843 index=0
3844 for o in n1.outputs:
3845 if o.enabled: # Check which passes the render layer has enabled
3846 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3847 index+=1
3848 else:
3849 index=0
3850 for o in n1.outputs:
3851 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3852 index+=1
3855 class NWConnectionListInputs(Menu, NWBase):
3856 bl_idname = "NODE_MT_nw_connection_list_in"
3857 bl_label = "To:"
3859 def draw(self, context):
3860 layout = self.layout
3861 nodes, links = get_nodes_links(context)
3863 n2 = nodes[context.scene.NWLazyTarget]
3865 index = 0
3866 for i in n2.inputs:
3867 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
3868 op.from_socket = context.scene.NWSourceSocket
3869 op.to_socket = index
3870 index+=1
3873 class NWMergeMathMenu(Menu, NWBase):
3874 bl_idname = "NODE_MT_nw_merge_math_menu"
3875 bl_label = "Merge Selected Nodes using Math"
3877 def draw(self, context):
3878 layout = self.layout
3879 for type, name, description in operations:
3880 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3881 props.mode = type
3882 props.merge_type = 'MATH'
3885 class NWBatchChangeNodesMenu(Menu, NWBase):
3886 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3887 bl_label = "Batch Change Selected Nodes"
3889 def draw(self, context):
3890 layout = self.layout
3891 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3892 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3895 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3896 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3897 bl_label = "Batch Change Blend Type"
3899 def draw(self, context):
3900 layout = self.layout
3901 for type, name, description in blend_types:
3902 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3903 props.blend_type = type
3904 props.operation = 'CURRENT'
3907 class NWBatchChangeOperationMenu(Menu, NWBase):
3908 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3909 bl_label = "Batch Change Math Operation"
3911 def draw(self, context):
3912 layout = self.layout
3913 for type, name, description in operations:
3914 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3915 props.blend_type = 'CURRENT'
3916 props.operation = type
3919 class NWCopyToSelectedMenu(Menu, NWBase):
3920 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
3921 bl_label = "Copy to Selected"
3923 def draw(self, context):
3924 layout = self.layout
3925 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
3926 layout.menu(NWCopyLabelMenu.bl_idname)
3929 class NWCopyLabelMenu(Menu, NWBase):
3930 bl_idname = "NODE_MT_nw_copy_label_menu"
3931 bl_label = "Copy Label"
3933 def draw(self, context):
3934 layout = self.layout
3935 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
3936 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
3937 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
3940 class NWAddReroutesMenu(Menu, NWBase):
3941 bl_idname = "NODE_MT_nw_add_reroutes_menu"
3942 bl_label = "Add Reroutes"
3943 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
3945 def draw(self, context):
3946 layout = self.layout
3947 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
3948 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
3949 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
3952 class NWLinkActiveToSelectedMenu(Menu, NWBase):
3953 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
3954 bl_label = "Link Active to Selected"
3956 def draw(self, context):
3957 layout = self.layout
3958 layout.menu(NWLinkStandardMenu.bl_idname)
3959 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
3960 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
3963 class NWLinkStandardMenu(Menu, NWBase):
3964 bl_idname = "NODE_MT_nw_link_standard_menu"
3965 bl_label = "To All Selected"
3967 def draw(self, context):
3968 layout = self.layout
3969 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3970 props.replace = False
3971 props.use_node_name = False
3972 props.use_outputs_names = False
3973 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3974 props.replace = True
3975 props.use_node_name = False
3976 props.use_outputs_names = False
3979 class NWLinkUseNodeNameMenu(Menu, NWBase):
3980 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
3981 bl_label = "Use Node Name/Label"
3983 def draw(self, context):
3984 layout = self.layout
3985 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3986 props.replace = False
3987 props.use_node_name = True
3988 props.use_outputs_names = False
3989 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3990 props.replace = True
3991 props.use_node_name = True
3992 props.use_outputs_names = False
3995 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
3996 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
3997 bl_label = "Use Outputs Names"
3999 def draw(self, context):
4000 layout = self.layout
4001 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4002 props.replace = False
4003 props.use_node_name = False
4004 props.use_outputs_names = True
4005 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4006 props.replace = True
4007 props.use_node_name = False
4008 props.use_outputs_names = True
4011 class NWVertColMenu(bpy.types.Menu):
4012 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
4013 bl_label = "Vertex Colors"
4015 @classmethod
4016 def poll(cls, context):
4017 valid = False
4018 if nw_check(context):
4019 snode = context.space_data
4020 valid = snode.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context)
4021 return valid
4023 def draw(self, context):
4024 l = self.layout
4025 nodes, links = get_nodes_links(context)
4026 mat = context.object.active_material
4028 objs = []
4029 for obj in bpy.data.objects:
4030 for slot in obj.material_slots:
4031 if slot.material == mat:
4032 objs.append(obj)
4033 vcols = []
4034 for obj in objs:
4035 if obj.data.vertex_colors:
4036 for vcol in obj.data.vertex_colors:
4037 vcols.append(vcol.name)
4038 vcols = list(set(vcols)) # get a unique list
4040 if vcols:
4041 for vcol in vcols:
4042 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
4043 else:
4044 l.label("No Vertex Color layers on objects with this material")
4047 class NWSwitchNodeTypeMenu(Menu, NWBase):
4048 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4049 bl_label = "Switch Type to..."
4051 def draw(self, context):
4052 layout = self.layout
4053 tree = context.space_data.node_tree
4054 if tree.type == 'SHADER':
4055 if is_cycles_or_eevee(context):
4056 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
4057 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
4058 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
4059 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
4060 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
4061 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
4062 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
4063 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
4064 else:
4065 layout.menu(NWSwitchMatInputSubmenu.bl_idname)
4066 layout.menu(NWSwitchMatOutputSubmenu.bl_idname)
4067 layout.menu(NWSwitchMatColorSubmenu.bl_idname)
4068 layout.menu(NWSwitchMatVectorSubmenu.bl_idname)
4069 layout.menu(NWSwitchMatConverterSubmenu.bl_idname)
4070 layout.menu(NWSwitchMatLayoutSubmenu.bl_idname)
4071 if tree.type == 'COMPOSITING':
4072 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
4073 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
4074 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
4075 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
4076 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
4077 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
4078 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
4079 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
4080 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
4081 if tree.type == 'TEXTURE':
4082 layout.menu(NWSwitchTexInputSubmenu.bl_idname)
4083 layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
4084 layout.menu(NWSwitchTexColorSubmenu.bl_idname)
4085 layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
4086 layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
4087 layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
4088 layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
4089 layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
4092 class NWSwitchShadersInputSubmenu(Menu, NWBase):
4093 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
4094 bl_label = "Input"
4096 def draw(self, context):
4097 layout = self.layout
4098 for ident, node_type, rna_name in sorted(shaders_input_nodes_props, key=lambda k: k[2]):
4099 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4100 props.to_type = ident
4103 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
4104 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
4105 bl_label = "Output"
4107 def draw(self, context):
4108 layout = self.layout
4109 for ident, node_type, rna_name in sorted(shaders_output_nodes_props, key=lambda k: k[2]):
4110 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4111 props.to_type = ident
4114 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
4115 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
4116 bl_label = "Shader"
4118 def draw(self, context):
4119 layout = self.layout
4120 for ident, node_type, rna_name in sorted(shaders_shader_nodes_props, key=lambda k: k[2]):
4121 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4122 props.to_type = ident
4125 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
4126 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
4127 bl_label = "Texture"
4129 def draw(self, context):
4130 layout = self.layout
4131 for ident, node_type, rna_name in sorted(shaders_texture_nodes_props, key=lambda k: k[2]):
4132 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4133 props.to_type = ident
4136 class NWSwitchShadersColorSubmenu(Menu, NWBase):
4137 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
4138 bl_label = "Color"
4140 def draw(self, context):
4141 layout = self.layout
4142 for ident, node_type, rna_name in sorted(shaders_color_nodes_props, key=lambda k: k[2]):
4143 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4144 props.to_type = ident
4147 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
4148 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
4149 bl_label = "Vector"
4151 def draw(self, context):
4152 layout = self.layout
4153 for ident, node_type, rna_name in sorted(shaders_vector_nodes_props, key=lambda k: k[2]):
4154 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4155 props.to_type = ident
4158 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
4159 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
4160 bl_label = "Converter"
4162 def draw(self, context):
4163 layout = self.layout
4164 for ident, node_type, rna_name in sorted(shaders_converter_nodes_props, key=lambda k: k[2]):
4165 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4166 props.to_type = ident
4169 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
4170 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
4171 bl_label = "Layout"
4173 def draw(self, context):
4174 layout = self.layout
4175 for ident, node_type, rna_name in sorted(shaders_layout_nodes_props, key=lambda k: k[2]):
4176 if node_type != 'FRAME':
4177 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4178 props.to_type = ident
4181 class NWSwitchCompoInputSubmenu(Menu, NWBase):
4182 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
4183 bl_label = "Input"
4185 def draw(self, context):
4186 layout = self.layout
4187 for ident, node_type, rna_name in sorted(compo_input_nodes_props, key=lambda k: k[2]):
4188 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4189 props.to_type = ident
4192 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
4193 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
4194 bl_label = "Output"
4196 def draw(self, context):
4197 layout = self.layout
4198 for ident, node_type, rna_name in sorted(compo_output_nodes_props, key=lambda k: k[2]):
4199 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4200 props.to_type = ident
4203 class NWSwitchCompoColorSubmenu(Menu, NWBase):
4204 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
4205 bl_label = "Color"
4207 def draw(self, context):
4208 layout = self.layout
4209 for ident, node_type, rna_name in sorted(compo_color_nodes_props, key=lambda k: k[2]):
4210 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4211 props.to_type = ident
4214 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
4215 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
4216 bl_label = "Converter"
4218 def draw(self, context):
4219 layout = self.layout
4220 for ident, node_type, rna_name in sorted(compo_converter_nodes_props, key=lambda k: k[2]):
4221 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4222 props.to_type = ident
4225 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
4226 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
4227 bl_label = "Filter"
4229 def draw(self, context):
4230 layout = self.layout
4231 for ident, node_type, rna_name in sorted(compo_filter_nodes_props, key=lambda k: k[2]):
4232 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4233 props.to_type = ident
4236 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
4237 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
4238 bl_label = "Vector"
4240 def draw(self, context):
4241 layout = self.layout
4242 for ident, node_type, rna_name in sorted(compo_vector_nodes_props, key=lambda k: k[2]):
4243 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4244 props.to_type = ident
4247 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
4248 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
4249 bl_label = "Matte"
4251 def draw(self, context):
4252 layout = self.layout
4253 for ident, node_type, rna_name in sorted(compo_matte_nodes_props, key=lambda k: k[2]):
4254 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4255 props.to_type = ident
4258 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
4259 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
4260 bl_label = "Distort"
4262 def draw(self, context):
4263 layout = self.layout
4264 for ident, node_type, rna_name in sorted(compo_distort_nodes_props, key=lambda k: k[2]):
4265 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4266 props.to_type = ident
4269 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
4270 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
4271 bl_label = "Layout"
4273 def draw(self, context):
4274 layout = self.layout
4275 for ident, node_type, rna_name in sorted(compo_layout_nodes_props, key=lambda k: k[2]):
4276 if node_type != 'FRAME':
4277 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4278 props.to_type = ident
4281 class NWSwitchMatInputSubmenu(Menu, NWBase):
4282 bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
4283 bl_label = "Input"
4285 def draw(self, context):
4286 layout = self.layout
4287 for ident, node_type, rna_name in sorted(blender_mat_input_nodes_props, key=lambda k: k[2]):
4288 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4289 props.to_type = ident
4292 class NWSwitchMatOutputSubmenu(Menu, NWBase):
4293 bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
4294 bl_label = "Output"
4296 def draw(self, context):
4297 layout = self.layout
4298 for ident, node_type, rna_name in sorted(blender_mat_output_nodes_props, key=lambda k: k[2]):
4299 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4300 props.to_type = ident
4303 class NWSwitchMatColorSubmenu(Menu, NWBase):
4304 bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
4305 bl_label = "Color"
4307 def draw(self, context):
4308 layout = self.layout
4309 for ident, node_type, rna_name in sorted(blender_mat_color_nodes_props, key=lambda k: k[2]):
4310 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4311 props.to_type = ident
4314 class NWSwitchMatVectorSubmenu(Menu, NWBase):
4315 bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
4316 bl_label = "Vector"
4318 def draw(self, context):
4319 layout = self.layout
4320 for ident, node_type, rna_name in sorted(blender_mat_vector_nodes_props, key=lambda k: k[2]):
4321 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4322 props.to_type = ident
4325 class NWSwitchMatConverterSubmenu(Menu, NWBase):
4326 bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
4327 bl_label = "Converter"
4329 def draw(self, context):
4330 layout = self.layout
4331 for ident, node_type, rna_name in sorted(blender_mat_converter_nodes_props, key=lambda k: k[2]):
4332 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4333 props.to_type = ident
4336 class NWSwitchMatLayoutSubmenu(Menu, NWBase):
4337 bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
4338 bl_label = "Layout"
4340 def draw(self, context):
4341 layout = self.layout
4342 for ident, node_type, rna_name in sorted(blender_mat_layout_nodes_props, key=lambda k: k[2]):
4343 if node_type != 'FRAME':
4344 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4345 props.to_type = ident
4348 class NWSwitchTexInputSubmenu(Menu, NWBase):
4349 bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
4350 bl_label = "Input"
4352 def draw(self, context):
4353 layout = self.layout
4354 for ident, node_type, rna_name in sorted(texture_input_nodes_props, key=lambda k: k[2]):
4355 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4356 props.to_type = ident
4359 class NWSwitchTexOutputSubmenu(Menu, NWBase):
4360 bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
4361 bl_label = "Output"
4363 def draw(self, context):
4364 layout = self.layout
4365 for ident, node_type, rna_name in sorted(texture_output_nodes_props, key=lambda k: k[2]):
4366 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4367 props.to_type = ident
4370 class NWSwitchTexColorSubmenu(Menu, NWBase):
4371 bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
4372 bl_label = "Color"
4374 def draw(self, context):
4375 layout = self.layout
4376 for ident, node_type, rna_name in sorted(texture_color_nodes_props, key=lambda k: k[2]):
4377 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4378 props.to_type = ident
4381 class NWSwitchTexPatternSubmenu(Menu, NWBase):
4382 bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
4383 bl_label = "Pattern"
4385 def draw(self, context):
4386 layout = self.layout
4387 for ident, node_type, rna_name in sorted(texture_pattern_nodes_props, key=lambda k: k[2]):
4388 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4389 props.to_type = ident
4392 class NWSwitchTexTexturesSubmenu(Menu, NWBase):
4393 bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
4394 bl_label = "Textures"
4396 def draw(self, context):
4397 layout = self.layout
4398 for ident, node_type, rna_name in sorted(texture_textures_nodes_props, key=lambda k: k[2]):
4399 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4400 props.to_type = ident
4403 class NWSwitchTexConverterSubmenu(Menu, NWBase):
4404 bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
4405 bl_label = "Converter"
4407 def draw(self, context):
4408 layout = self.layout
4409 for ident, node_type, rna_name in sorted(texture_converter_nodes_props, key=lambda k: k[2]):
4410 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4411 props.to_type = ident
4414 class NWSwitchTexDistortSubmenu(Menu, NWBase):
4415 bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
4416 bl_label = "Distort"
4418 def draw(self, context):
4419 layout = self.layout
4420 for ident, node_type, rna_name in sorted(texture_distort_nodes_props, key=lambda k: k[2]):
4421 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4422 props.to_type = ident
4425 class NWSwitchTexLayoutSubmenu(Menu, NWBase):
4426 bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
4427 bl_label = "Layout"
4429 def draw(self, context):
4430 layout = self.layout
4431 for ident, node_type, rna_name in sorted(texture_layout_nodes_props, key=lambda k: k[2]):
4432 if node_type != 'FRAME':
4433 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4434 props.to_type = ident
4438 # APPENDAGES TO EXISTING UI
4442 def select_parent_children_buttons(self, context):
4443 layout = self.layout
4444 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4445 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4448 def attr_nodes_menu_func(self, context):
4449 col = self.layout.column(align=True)
4450 col.menu("NODE_MT_nw_node_vertex_color_menu")
4451 col.separator()
4454 def multipleimages_menu_func(self, context):
4455 col = self.layout.column(align=True)
4456 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4457 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4458 col.separator()
4461 def bgreset_menu_func(self, context):
4462 self.layout.operator(NWResetBG.bl_idname)
4465 def save_viewer_menu_func(self, context):
4466 if nw_check(context):
4467 if context.space_data.tree_type == 'CompositorNodeTree':
4468 if context.scene.node_tree.nodes.active:
4469 if context.scene.node_tree.nodes.active.type == "VIEWER":
4470 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4473 def reset_nodes_button(self, context):
4474 node_active = context.active_node
4475 node_selected = context.selected_nodes
4476 node_ignore = ["FRAME","REROUTE", "GROUP"]
4478 # Check if active node is in the selection and respective type
4479 if (len(node_selected) == 1) and node_active.select and node_active.type not in node_ignore:
4480 row = self.layout.row()
4481 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4482 self.layout.separator()
4484 elif (len(node_selected) == 1) and node_active.select and node_active.type == "FRAME":
4485 row = self.layout.row()
4486 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4487 self.layout.separator()
4491 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4494 addon_keymaps = []
4495 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4496 # props entry: (property name, property value)
4497 kmi_defs = (
4498 # MERGE NODES
4499 # NWMergeNodes with Ctrl (AUTO).
4500 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4501 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4502 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4503 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4504 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4505 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4506 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4507 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4508 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4509 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4510 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4511 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4512 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4513 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4514 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4515 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4516 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4517 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4518 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4519 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4520 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4521 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4522 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4523 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4524 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4525 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4526 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4527 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4528 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4529 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4530 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4531 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4532 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4533 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4534 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4535 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4536 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4537 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4538 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4539 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4540 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4541 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4542 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4543 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4544 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4545 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4546 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4547 # NWMergeNodes with Ctrl Shift (MATH)
4548 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4549 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4550 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4551 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4552 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4553 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4554 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4555 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4556 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4557 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4558 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4559 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4560 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4561 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4562 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4563 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4564 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4565 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4566 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4567 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4568 # BATCH CHANGE NODES
4569 # NWBatchChangeNodes with Alt
4570 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4571 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4572 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4573 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4574 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4575 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4576 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4577 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4578 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4579 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4580 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4581 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4582 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4583 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4584 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4585 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4586 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4587 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4588 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4589 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4590 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4591 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4592 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4593 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4594 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4595 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4596 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4597 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4598 # LINK ACTIVE TO SELECTED
4599 # Don't use names, don't replace links (K)
4600 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4601 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4602 # Don't use names, replace links (Shift K)
4603 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4604 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4605 # Use node name, don't replace links (')
4606 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4607 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4608 # Use node name, replace links (Shift ')
4609 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4610 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4611 # Don't use names, don't replace links (;)
4612 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4613 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4614 # Don't use names, replace links (')
4615 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4616 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4617 # CHANGE MIX FACTOR
4618 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4619 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4620 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4621 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4622 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4623 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4624 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4625 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4626 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4627 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4628 # CLEAR LABEL (Alt L)
4629 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4630 # MODIFY LABEL (Alt Shift L)
4631 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4632 # Copy Label from active to selected
4633 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4634 # DETACH OUTPUTS (Alt Shift D)
4635 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4636 # LINK TO OUTPUT NODE (O)
4637 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4638 # SELECT PARENT/CHILDREN
4639 # Select Children
4640 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4641 # Select Parent
4642 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4643 # Add Texture Setup
4644 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4645 # Add Principled BSDF Texture Setup
4646 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4647 # Reset backdrop
4648 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4649 # Delete unused
4650 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4651 # Frame Selected
4652 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4653 # Swap Outputs
4654 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4655 # Emission Viewer
4656 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4657 # Reload Images
4658 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4659 # Lazy Mix
4660 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, None, "Lazy Mix"),
4661 # Lazy Connect
4662 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, False, False, None, "Lazy Connect"),
4663 # Lazy Connect with Menu
4664 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4665 # Viewer Tile Center
4666 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4667 # Align Nodes
4668 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4669 # Reset Nodes (Back Space)
4670 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4671 # MENUS
4672 ('wm.call_menu', 'SPACE', 'PRESS', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"),
4673 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4674 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4675 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4676 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4677 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4681 classes = (
4682 NWPrincipledPreferences,
4683 NWNodeWrangler,
4684 NWLazyMix,
4685 NWLazyConnect,
4686 NWDeleteUnused,
4687 NWSwapLinks,
4688 NWResetBG,
4689 NWAddAttrNode,
4690 NWEmissionViewer,
4691 NWFrameSelected,
4692 NWReloadImages,
4693 NWSwitchNodeType,
4694 NWMergeNodes,
4695 NWBatchChangeNodes,
4696 NWChangeMixFactor,
4697 NWCopySettings,
4698 NWCopyLabel,
4699 NWClearLabel,
4700 NWModifyLabels,
4701 NWAddTextureSetup,
4702 NWAddPrincipledSetup,
4703 NWAddReroutes,
4704 NWLinkActiveToSelected,
4705 NWAlignNodes,
4706 NWSelectParentChildren,
4707 NWDetachOutputs,
4708 NWLinkToOutputNode,
4709 NWMakeLink,
4710 NWCallInputsMenu,
4711 NWAddSequence,
4712 NWAddMultipleImages,
4713 NWViewerFocus,
4714 NWSaveViewer,
4715 NWResetNodes,
4716 NodeWranglerPanel,
4717 NodeWranglerMenu,
4718 NWMergeNodesMenu,
4719 NWMergeShadersMenu,
4720 NWMergeMixMenu,
4721 NWConnectionListOutputs,
4722 NWConnectionListInputs,
4723 NWMergeMathMenu,
4724 NWBatchChangeNodesMenu,
4725 NWBatchChangeBlendTypeMenu,
4726 NWBatchChangeOperationMenu,
4727 NWCopyToSelectedMenu,
4728 NWCopyLabelMenu,
4729 NWAddReroutesMenu,
4730 NWLinkActiveToSelectedMenu,
4731 NWLinkStandardMenu,
4732 NWLinkUseNodeNameMenu,
4733 NWLinkUseOutputsNamesMenu,
4734 NWVertColMenu,
4735 NWSwitchNodeTypeMenu,
4736 NWSwitchShadersInputSubmenu,
4737 NWSwitchShadersOutputSubmenu,
4738 NWSwitchShadersShaderSubmenu,
4739 NWSwitchShadersTextureSubmenu,
4740 NWSwitchShadersColorSubmenu,
4741 NWSwitchShadersVectorSubmenu,
4742 NWSwitchShadersConverterSubmenu,
4743 NWSwitchShadersLayoutSubmenu,
4744 NWSwitchCompoInputSubmenu,
4745 NWSwitchCompoOutputSubmenu,
4746 NWSwitchCompoColorSubmenu,
4747 NWSwitchCompoConverterSubmenu,
4748 NWSwitchCompoFilterSubmenu,
4749 NWSwitchCompoVectorSubmenu,
4750 NWSwitchCompoMatteSubmenu,
4751 NWSwitchCompoDistortSubmenu,
4752 NWSwitchCompoLayoutSubmenu,
4753 NWSwitchMatInputSubmenu,
4754 NWSwitchMatOutputSubmenu,
4755 NWSwitchMatColorSubmenu,
4756 NWSwitchMatVectorSubmenu,
4757 NWSwitchMatConverterSubmenu,
4758 NWSwitchMatLayoutSubmenu,
4759 NWSwitchTexInputSubmenu,
4760 NWSwitchTexOutputSubmenu,
4761 NWSwitchTexColorSubmenu,
4762 NWSwitchTexPatternSubmenu,
4763 NWSwitchTexTexturesSubmenu,
4764 NWSwitchTexConverterSubmenu,
4765 NWSwitchTexDistortSubmenu,
4766 NWSwitchTexLayoutSubmenu,
4769 def register():
4770 from bpy.utils import register_class
4772 # props
4773 bpy.types.Scene.NWBusyDrawing = StringProperty(
4774 name="Busy Drawing!",
4775 default="",
4776 description="An internal property used to store only the first mouse position")
4777 bpy.types.Scene.NWLazySource = StringProperty(
4778 name="Lazy Source!",
4779 default="x",
4780 description="An internal property used to store the first node in a Lazy Connect operation")
4781 bpy.types.Scene.NWLazyTarget = StringProperty(
4782 name="Lazy Target!",
4783 default="x",
4784 description="An internal property used to store the last node in a Lazy Connect operation")
4785 bpy.types.Scene.NWSourceSocket = IntProperty(
4786 name="Source Socket!",
4787 default=0,
4788 description="An internal property used to store the source socket in a Lazy Connect operation")
4790 for cls in classes:
4791 register_class(cls)
4793 # keymaps
4794 addon_keymaps.clear()
4795 kc = bpy.context.window_manager.keyconfigs.addon
4796 if kc:
4797 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
4798 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
4799 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
4800 if props:
4801 for prop, value in props:
4802 setattr(kmi.properties, prop, value)
4803 addon_keymaps.append((km, kmi))
4805 # menu items
4806 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
4807 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4808 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
4809 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
4810 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4811 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4812 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
4813 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
4816 def unregister():
4817 from bpy.utils import unregister_class
4819 # props
4820 del bpy.types.Scene.NWBusyDrawing
4821 del bpy.types.Scene.NWLazySource
4822 del bpy.types.Scene.NWLazyTarget
4823 del bpy.types.Scene.NWSourceSocket
4825 # keymaps
4826 for km, kmi in addon_keymaps:
4827 km.keymap_items.remove(kmi)
4828 addon_keymaps.clear()
4830 # menuitems
4831 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
4832 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4833 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
4834 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
4835 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4836 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
4837 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
4838 bpy.types.NODE_MT_node.remove(reset_nodes_button)
4840 for cls in classes:
4841 unregister_class(cls)
4843 if __name__ == "__main__":
4844 register()