remove '.' from descriptions
[blender-addons.git] / node_efficiency_tools.py
blobff11cae7aac8864f5354fe7b02271475bee33491
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 (aka Nodes Efficiency Tools)",
21 'author': "Bartek Skorupa, Greg Zaal",
22 'version': (3, 4),
23 'blender': (2, 70, 0),
24 'location': "Node Editor Properties Panel 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 'tracker_url': "https://developer.blender.org/T33543",
30 'category': "Node",
33 import bpy, blf, bgl
34 from bpy.types import Operator, Panel, Menu
35 from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty, FloatVectorProperty, CollectionProperty
36 from bpy_extras.io_utils import ImportHelper
37 from mathutils import Vector
38 from math import cos, sin, pi, sqrt
39 from os import listdir
41 #################
42 # rl_outputs:
43 # list of outputs of Input Render Layer
44 # with attributes determinig if pass is used,
45 # and MultiLayer EXR outputs names and corresponding render engines
47 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles)
48 rl_outputs = (
49 ('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
50 ('use_pass_color', 'Color', 'Color', True, False),
51 ('use_pass_combined', 'Image', 'Combined', True, True),
52 ('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False),
53 ('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
54 ('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
55 ('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
56 ('use_pass_emit', 'Emit', 'Emit', True, False),
57 ('use_pass_environment', 'Environment', 'Env', True, False),
58 ('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
59 ('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
60 ('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
61 ('use_pass_indirect', 'Indirect', 'Indirect', True, False),
62 ('use_pass_material_index', 'IndexMA', 'IndexMA', True, True),
63 ('use_pass_mist', 'Mist', 'Mist', True, False),
64 ('use_pass_normal', 'Normal', 'Normal', True, True),
65 ('use_pass_object_index', 'IndexOB', 'IndexOB', True, True),
66 ('use_pass_reflection', 'Reflect', 'Reflect', True, False),
67 ('use_pass_refraction', 'Refract', 'Refract', True, False),
68 ('use_pass_shadow', 'Shadow', 'Shadow', True, True),
69 ('use_pass_specular', 'Specular', 'Spec', True, False),
70 ('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', False, True),
71 ('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', False, True),
72 ('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
73 ('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
74 ('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
75 ('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
76 ('use_pass_uv', 'UV', 'UV', True, True),
77 ('use_pass_vector', 'Speed', 'Vector', True, True),
78 ('use_pass_z', 'Z', 'Depth', True, True),
81 # shader nodes
82 # (rna_type.identifier, type, rna_type.name)
83 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
84 shaders_input_nodes_props = (
85 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
86 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
87 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
88 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
89 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
90 ('ShaderNodeRGB', 'RGB', 'RGB'),
91 ('ShaderNodeValue', 'VALUE', 'Value'),
92 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
93 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
94 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
95 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
96 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
97 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
98 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
100 # (rna_type.identifier, type, rna_type.name)
101 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
102 shaders_output_nodes_props = (
103 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
104 ('ShaderNodeOutputLamp', 'OUTPUT_LAMP', 'Lamp Output'),
105 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
107 # (rna_type.identifier, type, rna_type.name)
108 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
109 shaders_shader_nodes_props = (
110 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
111 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
112 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
113 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
114 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
115 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
116 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
117 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
118 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
119 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
120 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
121 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
122 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
123 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
124 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
125 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
126 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
127 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
128 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
130 # (rna_type.identifier, type, rna_type.name)
131 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
132 shaders_texture_nodes_props = (
133 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image'),
134 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment'),
135 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky'),
136 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise'),
137 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave'),
138 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
139 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
140 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient'),
141 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic'),
142 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker'),
143 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick')
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_color_nodes_props = (
148 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
149 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
150 ('ShaderNodeInvert', 'INVERT', 'Invert'),
151 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
152 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
153 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
154 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
156 # (rna_type.identifier, type, rna_type.name)
157 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
158 shaders_vector_nodes_props = (
159 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
160 ('ShaderNodeBump', 'BUMP', 'Bump'),
161 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
162 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
163 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
164 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
166 # (rna_type.identifier, type, rna_type.name)
167 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
168 shaders_converter_nodes_props = (
169 ('ShaderNodeMath', 'MATH', 'Math'),
170 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
171 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
172 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
173 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
174 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
175 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
176 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
177 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
178 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
180 # (rna_type.identifier, type, rna_type.name)
181 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
182 shaders_layout_nodes_props = (
183 ('NodeFrame', 'FRAME', 'Frame'),
184 ('NodeReroute', 'REROUTE', 'Reroute'),
187 # compositing nodes
188 # (rna_type.identifier, type, rna_type.name)
189 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
190 compo_input_nodes_props = (
191 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
192 ('CompositorNodeImage', 'IMAGE', 'Image'),
193 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
194 ('CompositorNodeMask', 'MASK', 'Mask'),
195 ('CompositorNodeRGB', 'RGB', 'RGB'),
196 ('CompositorNodeValue', 'VALUE', 'Value'),
197 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
198 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
199 ('CompositorNodeTime', 'TIME', 'Time'),
200 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
202 # (rna_type.identifier, type, rna_type.name)
203 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
204 compo_output_nodes_props = (
205 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
206 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
207 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
208 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
209 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
211 # (rna_type.identifier, type, rna_type.name)
212 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
213 compo_color_nodes_props = (
214 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
215 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
216 ('CompositorNodeInvert', 'INVERT', 'Invert'),
217 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
218 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
219 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
220 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
221 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
222 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
223 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
224 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
225 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
227 # (rna_type.identifier, type, rna_type.name)
228 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
229 compo_converter_nodes_props = (
230 ('CompositorNodeMath', 'MATH', 'Math'),
231 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
232 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
233 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
234 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
235 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
236 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
237 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
238 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
239 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
240 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
241 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
242 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
243 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
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_filter_nodes_props = (
248 ('CompositorNodeBlur', 'BLUR', 'Blur'),
249 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
250 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
251 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
252 ('CompositorNodeFilter', 'FILTER', 'Filter'),
253 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
254 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
255 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
256 ('CompositorNodeGlare', 'GLARE', 'Glare'),
257 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
258 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
259 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
261 # (rna_type.identifier, type, rna_type.name)
262 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
263 compo_vector_nodes_props = (
264 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
265 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
266 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
267 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
268 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
270 # (rna_type.identifier, type, rna_type.name)
271 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
272 compo_matte_nodes_props = (
273 ('CompositorNodeKeying', 'KEYING', 'Keying'),
274 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
275 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
276 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
277 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
278 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
279 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
280 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
281 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
282 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
283 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
284 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
286 # (rna_type.identifier, type, rna_type.name)
287 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
288 compo_distort_nodes_props = (
289 ('CompositorNodeScale', 'SCALE', 'Scale'),
290 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
291 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
292 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
293 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
294 ('CompositorNodeFlip', 'FLIP', 'Flip'),
295 ('CompositorNodeCrop', 'CROP', 'Crop'),
296 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
297 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
298 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
299 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
300 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
302 # (rna_type.identifier, type, rna_type.name)
303 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
304 compo_layout_nodes_props = (
305 ('NodeFrame', 'FRAME', 'Frame'),
306 ('NodeReroute', 'REROUTE', 'Reroute'),
307 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
310 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
311 # used list, not tuple for easy merging with other lists.
312 blend_types = [
313 ('MIX', 'Mix', 'Mix Mode'),
314 ('ADD', 'Add', 'Add Mode'),
315 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
316 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
317 ('SCREEN', 'Screen', 'Screen Mode'),
318 ('DIVIDE', 'Divide', 'Divide Mode'),
319 ('DIFFERENCE', 'Difference', 'Difference Mode'),
320 ('DARKEN', 'Darken', 'Darken Mode'),
321 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
322 ('OVERLAY', 'Overlay', 'Overlay Mode'),
323 ('DODGE', 'Dodge', 'Dodge Mode'),
324 ('BURN', 'Burn', 'Burn Mode'),
325 ('HUE', 'Hue', 'Hue Mode'),
326 ('SATURATION', 'Saturation', 'Saturation Mode'),
327 ('VALUE', 'Value', 'Value Mode'),
328 ('COLOR', 'Color', 'Color Mode'),
329 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
330 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
333 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
334 # used list, not tuple for easy merging with other lists.
335 operations = [
336 ('ADD', 'Add', 'Add Mode'),
337 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
338 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
339 ('DIVIDE', 'Divide', 'Divide Mode'),
340 ('SINE', 'Sine', 'Sine Mode'),
341 ('COSINE', 'Cosine', 'Cosine Mode'),
342 ('TANGENT', 'Tangent', 'Tangent Mode'),
343 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
344 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
345 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
346 ('POWER', 'Power', 'Power Mode'),
347 ('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
348 ('MINIMUM', 'Minimum', 'Minimum Mode'),
349 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
350 ('ROUND', 'Round', 'Round Mode'),
351 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
352 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
355 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
356 # used list, not tuple for easy merging with other lists.
357 navs = [
358 ('CURRENT', 'Current', 'Leave at current state'),
359 ('NEXT', 'Next', 'Next blend type/operation'),
360 ('PREV', 'Prev', 'Previous blend type/operation'),
363 draw_color_sets = {
364 "red_white": (
365 (1.0, 1.0, 1.0, 0.7),
366 (1.0, 0.0, 0.0, 0.7),
367 (0.8, 0.2, 0.2, 1.0)
369 "green": (
370 (0.0, 0.0, 0.0, 1.0),
371 (0.38, 0.77, 0.38, 1.0),
372 (0.38, 0.77, 0.38, 1.0)
374 "yellow": (
375 (0.0, 0.0, 0.0, 1.0),
376 (0.77, 0.77, 0.16, 1.0),
377 (0.77, 0.77, 0.16, 1.0)
379 "purple": (
380 (0.0, 0.0, 0.0, 1.0),
381 (0.38, 0.38, 0.77, 1.0),
382 (0.38, 0.38, 0.77, 1.0)
384 "grey": (
385 (0.0, 0.0, 0.0, 1.0),
386 (0.63, 0.63, 0.63, 1.0),
387 (0.63, 0.63, 0.63, 1.0)
389 "black": (
390 (1.0, 1.0, 1.0, 0.7),
391 (0.0, 0.0, 0.0, 0.7),
392 (0.2, 0.2, 0.2, 1.0)
397 def nice_hotkey_name(punc):
398 # convert the ugly string name into the actual character
399 pairs = (
400 ('LEFTMOUSE', "LMB"),
401 ('MIDDLEMOUSE', "MMB"),
402 ('RIGHTMOUSE', "RMB"),
403 ('SELECTMOUSE', "Select"),
404 ('WHEELUPMOUSE', "Wheel Up"),
405 ('WHEELDOWNMOUSE', "Wheel Down"),
406 ('WHEELINMOUSE', "Wheel In"),
407 ('WHEELOUTMOUSE', "Wheel Out"),
408 ('ZERO', "0"),
409 ('ONE', "1"),
410 ('TWO', "2"),
411 ('THREE', "3"),
412 ('FOUR', "4"),
413 ('FIVE', "5"),
414 ('SIX', "6"),
415 ('SEVEN', "7"),
416 ('EIGHT', "8"),
417 ('NINE', "9"),
418 ('OSKEY', "Super"),
419 ('RET', "Enter"),
420 ('LINE_FEED', "Enter"),
421 ('SEMI_COLON', ";"),
422 ('PERIOD', "."),
423 ('COMMA', ","),
424 ('QUOTE', '"'),
425 ('MINUS', "-"),
426 ('SLASH', "/"),
427 ('BACK_SLASH', "\\"),
428 ('EQUAL', "="),
429 ('NUMPAD_1', "Numpad 1"),
430 ('NUMPAD_2', "Numpad 2"),
431 ('NUMPAD_3', "Numpad 3"),
432 ('NUMPAD_4', "Numpad 4"),
433 ('NUMPAD_5', "Numpad 5"),
434 ('NUMPAD_6', "Numpad 6"),
435 ('NUMPAD_7', "Numpad 7"),
436 ('NUMPAD_8', "Numpad 8"),
437 ('NUMPAD_9', "Numpad 9"),
438 ('NUMPAD_0', "Numpad 0"),
439 ('NUMPAD_PERIOD', "Numpad ."),
440 ('NUMPAD_SLASH', "Numpad /"),
441 ('NUMPAD_ASTERIX', "Numpad *"),
442 ('NUMPAD_MINUS', "Numpad -"),
443 ('NUMPAD_ENTER', "Numpad Enter"),
444 ('NUMPAD_PLUS', "Numpad +"),
446 nice_punc = False
447 for (ugly, nice) in pairs:
448 if punc == ugly:
449 nice_punc = nice
450 break
451 if not nice_punc:
452 nice_punc = punc.replace("_", " ").title()
453 return nice_punc
456 def hack_force_update(context, nodes):
457 if context.space_data.tree_type == "ShaderNodeTree":
458 node = nodes.new('ShaderNodeMath')
459 node.inputs[0].default_value = 0.0
460 nodes.remove(node)
461 return False
464 def dpifac():
465 return bpy.context.user_preferences.system.dpi/72
468 def is_end_node(node):
469 bool = True
470 for output in node.outputs:
471 if output.links:
472 bool = False
473 break
474 return bool
477 def node_mid_pt(node, axis):
478 if axis == 'x':
479 d = node.location.x + (node.dimensions.x / 2)
480 elif axis == 'y':
481 d = node.location.y - (node.dimensions.y / 2)
482 else:
483 d = 0
484 return d
487 def autolink(node1, node2, links):
488 link_made = False
490 for outp in node1.outputs:
491 for inp in node2.inputs:
492 if not inp.is_linked and inp.name == outp.name:
493 link_made = True
494 links.new(outp, inp)
495 return True
497 for outp in node1.outputs:
498 for inp in node2.inputs:
499 if not inp.is_linked and inp.type == outp.type:
500 link_made = True
501 links.new(outp, inp)
502 return True
504 # force some connection even if the type doesn't match
505 for outp in node1.outputs:
506 for inp in node2.inputs:
507 if not inp.is_linked:
508 link_made = True
509 links.new(outp, inp)
510 return True
512 # even if no sockets are open, force one of matching type
513 for outp in node1.outputs:
514 for inp in node2.inputs:
515 if inp.type == outp.type:
516 link_made = True
517 links.new(outp, inp)
518 return True
520 # do something!
521 for outp in node1.outputs:
522 for inp in node2.inputs:
523 link_made = True
524 links.new(outp, inp)
525 return True
527 print("Could not make a link from " + node1.name + " to " + node2.name)
528 return link_made
531 def node_at_pos(nodes, context, event):
532 nodes_near_mouse = []
533 nodes_under_mouse = []
534 target_node = None
536 store_mouse_cursor(context, event)
537 x, y = context.space_data.cursor_location
538 x = x
539 y = y
541 # Make a list of each corner (and middle of border) for each node.
542 # Will be sorted to find nearest point and thus nearest node
543 node_points_with_dist = []
544 for node in nodes:
545 skipnode = False
546 if node.type != 'FRAME': # no point trying to link to a frame node
547 locx = node.location.x
548 locy = node.location.y
549 dimx = node.dimensions.x/dpifac()
550 dimy = node.dimensions.y/dpifac()
551 if node.parent:
552 locx += node.parent.location.x
553 locy += node.parent.location.y
554 if node.parent.parent:
555 locx += node.parent.parent.location.x
556 locy += node.parent.parent.location.y
557 if node.parent.parent.parent:
558 locx += node.parent.parent.parent.location.x
559 locy += node.parent.parent.parent.location.y
560 if node.parent.parent.parent.parent:
561 # Support three levels or parenting
562 # There's got to be a better way to do this...
563 skipnode = True
564 if not skipnode:
565 node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - locy) ** 2)]) # Top Left
566 node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - locy) ** 2)]) # Top Right
567 node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - (locy-dimy)) ** 2)]) # Bottom Left
568 node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - (locy-dimy)) ** 2)]) # Bottom Right
570 node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - locy) ** 2)]) # Mid Top
571 node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - (locy-dimy)) ** 2)]) # Mid Bottom
572 node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Mid Left
573 node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Mid Right
575 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
577 for node in nodes:
578 if node.type != 'FRAME' and skipnode == False:
579 locx = node.location.x
580 locy = node.location.y
581 dimx = node.dimensions.x/dpifac()
582 dimy = node.dimensions.y/dpifac()
583 if node.parent:
584 locx += node.parent.location.x
585 locy += node.parent.location.y
586 if (locx <= x <= locx + dimx) and \
587 (locy - dimy <= y <= locy):
588 nodes_under_mouse.append(node)
590 if len(nodes_under_mouse) == 1:
591 if nodes_under_mouse[0] != nearest_node:
592 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
593 else:
594 target_node = nearest_node # else use the nearest node
595 else:
596 target_node = nearest_node
597 return target_node
600 def store_mouse_cursor(context, event):
601 space = context.space_data
602 v2d = context.region.view2d
603 tree = space.edit_tree
605 # convert mouse position to the View2D for later node placement
606 if context.region.type == 'WINDOW':
607 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
608 else:
609 space.cursor_location = tree.view_center
612 def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]):
613 bgl.glEnable(bgl.GL_BLEND)
614 bgl.glLineWidth(size)
615 bgl.glShadeModel(bgl.GL_SMOOTH)
617 bgl.glBegin(bgl.GL_LINE_STRIP)
618 try:
619 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)
620 bgl.glVertex2f(x1, y1)
621 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
622 bgl.glVertex2f(x2, y2)
623 except:
624 pass
625 bgl.glEnd()
626 bgl.glShadeModel(bgl.GL_FLAT)
629 def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]):
630 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
631 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
632 radius = radius
633 sides = 32
634 for i in range(sides + 1):
635 cosine = radius * cos(i * 2 * pi / sides) + mx
636 sine = radius * sin(i * 2 * pi / sides) + my
637 bgl.glVertex2f(cosine, sine)
638 bgl.glEnd()
641 def draw_rounded_node_border(node, radius=8, colour=[1.0, 1.0, 1.0, 0.7]):
642 bgl.glEnable(bgl.GL_BLEND)
643 settings = bpy.context.user_preferences.addons[__name__].preferences
644 if settings.bgl_antialiasing:
645 bgl.glEnable(bgl.GL_LINE_SMOOTH)
646 sides = 16
647 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
649 nlocx = (node.location.x+1)*dpifac()
650 nlocy = (node.location.y+1)*dpifac()
651 ndimx = node.dimensions.x
652 ndimy = node.dimensions.y
653 if node.parent:
654 nlocx += node.parent.location.x
655 nlocy += node.parent.location.y
656 if node.parent.parent:
657 nlocx += node.parent.parent.location.x
658 nlocy += node.parent.parent.location.y
659 if node.parent.parent.parent:
660 nlocx += node.parent.parent.parent.location.x
661 nlocy += node.parent.parent.parent.location.y
664 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
665 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy)
666 bgl.glVertex2f(mx,my)
667 for i in range(sides+1):
668 if (4<=i<=8):
669 if mx != 12000 and my != 12000: # nodes that go over the view border give 12000 as coords
670 cosine = radius * cos(i * 2 * pi / sides) + mx
671 sine = radius * sin(i * 2 * pi / sides) + my
672 bgl.glVertex2f(cosine, sine)
673 bgl.glEnd()
675 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
676 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy)
677 bgl.glVertex2f(mx,my)
678 for i in range(sides+1):
679 if (0<=i<=4):
680 if mx != 12000 and my != 12000:
681 cosine = radius * cos(i * 2 * pi / sides) + mx
682 sine = radius * sin(i * 2 * pi / sides) + my
683 bgl.glVertex2f(cosine, sine)
685 bgl.glEnd()
686 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
687 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy)
688 bgl.glVertex2f(mx,my)
689 for i in range(sides+1):
690 if (8<=i<=12):
691 if mx != 12000 and my != 12000:
692 cosine = radius * cos(i * 2 * pi / sides) + mx
693 sine = radius * sin(i * 2 * pi / sides) + my
694 bgl.glVertex2f(cosine, sine)
695 bgl.glEnd()
697 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
698 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy)
699 bgl.glVertex2f(mx,my)
700 for i in range(sides+1):
701 if (12<=i<=16):
702 if mx != 12000 and my != 12000:
703 cosine = radius * cos(i * 2 * pi / sides) + mx
704 sine = radius * sin(i * 2 * pi / sides) + my
705 bgl.glVertex2f(cosine, sine)
706 bgl.glEnd()
709 bgl.glBegin(bgl.GL_QUADS)
710 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy)
711 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy)
712 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
713 bgl.glVertex2f(m2x-radius,m2y) # draw order is important, start with bottom left and go anti-clockwise
714 bgl.glVertex2f(m2x,m2y)
715 bgl.glVertex2f(m1x,m1y)
716 bgl.glVertex2f(m1x-radius,m1y)
717 bgl.glEnd()
719 bgl.glBegin(bgl.GL_QUADS)
720 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy)
721 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy)
722 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
723 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
724 bgl.glVertex2f(m2x,m2y)
725 bgl.glVertex2f(m2x,m1y+radius)
726 bgl.glVertex2f(m1x,m1y+radius)
727 bgl.glEnd()
729 bgl.glBegin(bgl.GL_QUADS)
730 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy)
731 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy)
732 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
733 bgl.glVertex2f(m2x,m2y) # draw order is important, start with bottom left and go anti-clockwise
734 bgl.glVertex2f(m2x+radius,m2y)
735 bgl.glVertex2f(m1x+radius,m1y)
736 bgl.glVertex2f(m1x,m1y)
737 bgl.glEnd()
739 bgl.glBegin(bgl.GL_QUADS)
740 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy)
741 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy)
742 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
743 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
744 bgl.glVertex2f(m2x,m2y)
745 bgl.glVertex2f(m2x,m1y-radius)
746 bgl.glVertex2f(m1x,m1y-radius)
747 bgl.glEnd()
749 bgl.glDisable(bgl.GL_BLEND)
750 if settings.bgl_antialiasing:
751 bgl.glDisable(bgl.GL_LINE_SMOOTH)
754 def draw_callback_mixnodes(self, context, mode):
755 if self.mouse_path:
756 nodes = context.space_data.node_tree.nodes
757 settings = context.user_preferences.addons[__name__].preferences
758 if settings.bgl_antialiasing:
759 bgl.glEnable(bgl.GL_LINE_SMOOTH)
761 if mode == "LINK":
762 col_outer = [1.0, 0.2, 0.2, 0.4]
763 col_inner = [0.0, 0.0, 0.0, 0.5]
764 col_circle_inner = [0.3, 0.05, 0.05, 1.0]
765 if mode == "LINKMENU":
766 col_outer = [0.4, 0.6, 1.0, 0.4]
767 col_inner = [0.0, 0.0, 0.0, 0.5]
768 col_circle_inner = [0.08, 0.15, .3, 1.0]
769 elif mode == "MIX":
770 col_outer = [0.2, 1.0, 0.2, 0.4]
771 col_inner = [0.0, 0.0, 0.0, 0.5]
772 col_circle_inner = [0.05, 0.3, 0.05, 1.0]
774 m1x = self.mouse_path[0][0]
775 m1y = self.mouse_path[0][1]
776 m2x = self.mouse_path[-1][0]
777 m2y = self.mouse_path[-1][1]
779 n1 = nodes[context.scene.NWLazySource]
780 n2 = nodes[context.scene.NWLazyTarget]
782 draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
783 draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
784 draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
785 draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
787 draw_line(m1x, m1y, m2x, m2y, 4, col_outer) # line outline
788 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
790 # circle outline
791 draw_circle(m1x, m1y, 6, col_outer)
792 draw_circle(m2x, m2y, 6, col_outer)
794 # circle inner
795 draw_circle(m1x, m1y, 5, col_circle_inner)
796 draw_circle(m2x, m2y, 5, col_circle_inner)
798 # restore opengl defaults
799 bgl.glLineWidth(1)
800 bgl.glDisable(bgl.GL_BLEND)
801 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
803 if settings.bgl_antialiasing:
804 bgl.glDisable(bgl.GL_LINE_SMOOTH)
807 def get_nodes_links(context):
808 space = context.space_data
809 tree = space.node_tree
810 nodes = tree.nodes
811 links = tree.links
812 active = nodes.active
813 context_active = context.active_node
814 # check if we are working on regular node tree or node group is currently edited.
815 # if group is edited - active node of space_tree is the group
816 # if context.active_node != space active node - it means that the group is being edited.
817 # in such case we set "nodes" to be nodes of this group, "links" to be links of this group
818 # if context.active_node == space.active_node it means that we are not currently editing group
819 is_main_tree = True
820 if active:
821 is_main_tree = context_active == active
822 if not is_main_tree: # if group is currently edited
823 tree = active.node_tree
824 nodes = tree.nodes
825 links = tree.links
827 return nodes, links
830 # Addon prefs
831 class NWNodeWrangler(bpy.types.AddonPreferences):
832 bl_idname = __name__
834 merge_hide = EnumProperty(
835 name="Hide Mix nodes",
836 items=(
837 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
838 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
839 ("NEVER", "Never", "Never collapse the new merge nodes")
841 default='NON_SHADER',
842 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded")
843 merge_position = EnumProperty(
844 name="Mix Node Position",
845 items=(
846 ("CENTER", "Center", "Place the Mix node between the two nodes"),
847 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
849 default='CENTER',
850 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes")
851 bgl_antialiasing = BoolProperty(
852 name="Line Antialiasing",
853 default=False,
854 description="Remove aliasing artifacts on lines drawn in interactive modes such as Lazy Connect (Alt+LMB) and Lazy Merge (Alt+RMB) - this may cause issues on some systems"
857 show_hotkey_list = BoolProperty(
858 name="Show Hotkey List",
859 default=False,
860 description="Expand this box into a list of all the hotkeys for functions in this addon"
862 hotkey_list_filter = StringProperty(
863 name=" Filter by Name",
864 default="",
865 description="Show only hotkeys that have this text in their name"
868 def draw(self, context):
869 layout = self.layout
870 col = layout.column()
871 col.prop(self, "merge_position")
872 col.prop(self, "merge_hide")
873 col.prop(self, "bgl_antialiasing")
875 box = col.box()
876 col = box.column(align=True)
878 hotkey_button_name = "Show Hotkey List"
879 if self.show_hotkey_list:
880 hotkey_button_name = "Hide Hotkey List"
881 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
882 if self.show_hotkey_list:
883 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
884 col.separator()
885 for hotkey in kmi_defs:
886 if hotkey[6]:
887 hotkey_name = hotkey[6]
889 if self.hotkey_list_filter.lower() in hotkey_name.lower():
890 row = col.row(align=True)
891 row.label(hotkey_name)
892 keystr = nice_hotkey_name(hotkey[1])
893 if hotkey[3]:
894 keystr = "Shift " + keystr
895 if hotkey[4]:
896 keystr = "Alt " + keystr
897 if hotkey[2]:
898 keystr = "Ctrl " + keystr
899 row.label(keystr)
902 class NWBase:
903 @classmethod
904 def poll(cls, context):
905 space = context.space_data
906 return space.type == 'NODE_EDITOR' and space.node_tree is not None
909 # OPERATORS
910 class NWLazyMix(Operator, NWBase):
911 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
912 bl_idname = "node.nw_lazy_mix"
913 bl_label = "Mix Nodes"
914 bl_options = {'REGISTER', 'UNDO'}
916 def modal(self, context, event):
917 context.area.tag_redraw()
918 nodes, links = get_nodes_links(context)
919 cont = True
921 start_pos = [event.mouse_region_x, event.mouse_region_y]
923 node1 = None
924 if not context.scene.NWBusyDrawing:
925 node1 = node_at_pos(nodes, context, event)
926 if node1:
927 context.scene.NWBusyDrawing = node1.name
928 else:
929 if context.scene.NWBusyDrawing != 'STOP':
930 node1 = nodes[context.scene.NWBusyDrawing]
932 context.scene.NWLazySource = node1.name
933 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
935 if event.type == 'MOUSEMOVE':
936 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
938 elif event.type == 'RIGHTMOUSE':
939 end_pos = [event.mouse_region_x, event.mouse_region_y]
940 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
942 node2 = None
943 node2 = node_at_pos(nodes, context, event)
944 if node2:
945 context.scene.NWBusyDrawing = node2.name
947 if node1 == node2:
948 cont = False
950 if cont:
951 if node1 and node2:
952 for node in nodes:
953 node.select = False
954 node1.select = True
955 node2.select = True
957 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
959 context.scene.NWBusyDrawing = ""
960 return {'FINISHED'}
962 elif event.type == 'ESC':
963 print('cancelled')
964 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
965 return {'CANCELLED'}
967 return {'RUNNING_MODAL'}
969 def invoke(self, context, event):
970 if context.area.type == 'NODE_EDITOR':
971 # the arguments we pass the the callback
972 args = (self, context, 'MIX')
973 # Add the region OpenGL drawing callback
974 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
975 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
977 self.mouse_path = []
979 context.window_manager.modal_handler_add(self)
980 return {'RUNNING_MODAL'}
981 else:
982 self.report({'WARNING'}, "View3D not found, cannot run operator")
983 return {'CANCELLED'}
986 class NWLazyConnect(Operator, NWBase):
987 """Connect two nodes without clicking a specific socket (automatically determined"""
988 bl_idname = "node.nw_lazy_connect"
989 bl_label = "Lazy Connect"
990 bl_options = {'REGISTER', 'UNDO'}
991 with_menu = BoolProperty()
993 def modal(self, context, event):
994 context.area.tag_redraw()
995 nodes, links = get_nodes_links(context)
996 cont = True
998 start_pos = [event.mouse_region_x, event.mouse_region_y]
1000 node1 = None
1001 if not context.scene.NWBusyDrawing:
1002 node1 = node_at_pos(nodes, context, event)
1003 if node1:
1004 context.scene.NWBusyDrawing = node1.name
1005 else:
1006 if context.scene.NWBusyDrawing != 'STOP':
1007 node1 = nodes[context.scene.NWBusyDrawing]
1009 context.scene.NWLazySource = node1.name
1010 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1012 if event.type == 'MOUSEMOVE':
1013 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1015 elif event.type == 'RIGHTMOUSE':
1016 end_pos = [event.mouse_region_x, event.mouse_region_y]
1017 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1019 node2 = None
1020 node2 = node_at_pos(nodes, context, event)
1021 if node2:
1022 context.scene.NWBusyDrawing = node2.name
1024 if node1 == node2:
1025 cont = False
1027 link_success = False
1028 if cont:
1029 if node1 and node2:
1030 original_sel = []
1031 original_unsel = []
1032 for node in nodes:
1033 if node.select == True:
1034 node.select = False
1035 original_sel.append(node)
1036 else:
1037 original_unsel.append(node)
1038 node1.select = True
1039 node2.select = True
1041 #link_success = autolink(node1, node2, links)
1042 if self.with_menu:
1043 if len(node1.outputs) > 1 and node2.inputs:
1044 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1045 elif len(node1.outputs) == 1:
1046 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1047 else:
1048 link_success = autolink(node1, node2, links)
1050 for node in original_sel:
1051 node.select = True
1052 for node in original_unsel:
1053 node.select = False
1055 if link_success:
1056 hack_force_update(context, nodes)
1057 context.scene.NWBusyDrawing = ""
1058 return {'FINISHED'}
1060 elif event.type == 'ESC':
1061 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1062 return {'CANCELLED'}
1064 return {'RUNNING_MODAL'}
1066 def invoke(self, context, event):
1067 if context.area.type == 'NODE_EDITOR':
1068 nodes, links = get_nodes_links(context)
1069 node = node_at_pos(nodes, context, event)
1070 if node:
1071 context.scene.NWBusyDrawing = node.name
1073 # the arguments we pass the the callback
1074 mode = "LINK"
1075 if self.with_menu:
1076 mode = "LINKMENU"
1077 args = (self, context, mode)
1078 # Add the region OpenGL drawing callback
1079 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1080 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
1082 self.mouse_path = []
1084 context.window_manager.modal_handler_add(self)
1085 return {'RUNNING_MODAL'}
1086 else:
1087 self.report({'WARNING'}, "View3D not found, cannot run operator")
1088 return {'CANCELLED'}
1091 class NWDeleteUnused(Operator, NWBase):
1092 """Delete all nodes whose output is not used"""
1093 bl_idname = 'node.nw_del_unused'
1094 bl_label = 'Delete Unused Nodes'
1095 bl_options = {'REGISTER', 'UNDO'}
1097 @classmethod
1098 def poll(cls, context):
1099 valid = False
1100 if context.space_data:
1101 if context.space_data.node_tree:
1102 if context.space_data.node_tree.nodes:
1103 valid = True
1104 return valid
1106 def execute(self, context):
1107 nodes, links = get_nodes_links(context)
1108 end_types = 'OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1109 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LAMP', \
1110 'OUTPUT_WORLD', 'GROUP', 'GROUP_INPUT', 'GROUP_OUTPUT'
1112 # Store selection
1113 selection = []
1114 for node in nodes:
1115 if node.select == True:
1116 selection.append(node.name)
1118 deleted_nodes = []
1119 temp_deleted_nodes = []
1120 del_unused_iterations = len(nodes)
1121 for it in range(0, del_unused_iterations):
1122 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1123 for node in nodes:
1124 node.select = False
1125 for node in nodes:
1126 if is_end_node(node) and not node.type in end_types and node.type != 'FRAME':
1127 node.select = True
1128 deleted_nodes.append(node.name)
1129 bpy.ops.node.delete()
1131 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1132 break
1133 # get unique list of deleted nodes (iterations would count the same node more than once)
1134 deleted_nodes = list(set(deleted_nodes))
1135 for n in deleted_nodes:
1136 self.report({'INFO'}, "Node " + n + " deleted")
1137 num_deleted = len(deleted_nodes)
1138 n = ' node'
1139 if num_deleted > 1:
1140 n += 's'
1141 if num_deleted:
1142 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1143 else:
1144 self.report({'INFO'}, "Nothing deleted")
1146 # Restore selection
1147 nodes, links = get_nodes_links(context)
1148 for node in nodes:
1149 if node.name in selection:
1150 node.select = True
1151 return {'FINISHED'}
1153 def invoke(self, context, event):
1154 return context.window_manager.invoke_confirm(self, event)
1157 class NWSwapLinks(Operator, NWBase):
1158 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1159 bl_idname = 'node.nw_swap_links'
1160 bl_label = 'Swap Links'
1161 bl_options = {'REGISTER', 'UNDO'}
1163 @classmethod
1164 def poll(cls, context):
1165 snode = context.space_data
1166 if context.selected_nodes:
1167 return len(context.selected_nodes) <= 2
1168 else:
1169 return False
1171 def execute(self, context):
1172 nodes, links = get_nodes_links(context)
1173 selected_nodes = context.selected_nodes
1174 n1 = selected_nodes[0]
1176 # Swap outputs
1177 if len(selected_nodes) == 2:
1178 n2 = selected_nodes[1]
1179 if n1.outputs and n2.outputs:
1180 n1_outputs = []
1181 n2_outputs = []
1183 out_index = 0
1184 for output in n1.outputs:
1185 if output.links:
1186 for link in output.links:
1187 n1_outputs.append([out_index, link.to_socket])
1188 links.remove(link)
1189 out_index += 1
1191 out_index = 0
1192 for output in n2.outputs:
1193 if output.links:
1194 for link in output.links:
1195 n2_outputs.append([out_index, link.to_socket])
1196 links.remove(link)
1197 out_index += 1
1199 for connection in n1_outputs:
1200 try:
1201 links.new(n2.outputs[connection[0]], connection[1])
1202 except:
1203 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1204 for connection in n2_outputs:
1205 try:
1206 links.new(n1.outputs[connection[0]], connection[1])
1207 except:
1208 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1209 else:
1210 if n1.outputs or n2.outputs:
1211 self.report({'WARNING'}, "One of the nodes has no outputs!")
1212 else:
1213 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1215 # Swap Inputs
1216 elif len(selected_nodes) == 1:
1217 if n1.inputs:
1218 types = []
1220 for i1 in n1.inputs:
1221 if i1.is_linked:
1222 similar_types = 0
1223 for i2 in n1.inputs:
1224 if i1.type == i2.type and i2.is_linked:
1225 similar_types += 1
1226 types.append ([i1, similar_types, i])
1227 i += 1
1228 types.sort(key=lambda k: k[1], reverse=True)
1230 if types:
1231 t = types[0]
1232 if t[1] == 2:
1233 for i2 in n1.inputs:
1234 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1235 pair = [t[0], i2]
1236 i1f = pair[0].links[0].from_socket
1237 i1t = pair[0].links[0].to_socket
1238 i2f = pair[1].links[0].from_socket
1239 i2t = pair[1].links[0].to_socket
1240 links.new(i1f, i2t)
1241 links.new(i2f, i1t)
1242 if t[1] == 1:
1243 if len(types) == 1:
1244 fs = t[0].links[0].from_socket
1245 i = t[2]
1246 links.remove(t[0].links[0])
1247 if i+1 == len(n1.inputs):
1248 i = -1
1249 i += 1
1250 while n1.inputs[i].is_linked:
1251 i += 1
1252 links.new(fs, n1.inputs[i])
1253 elif len(types) == 2:
1254 i1f = types[0][0].links[0].from_socket
1255 i1t = types[0][0].links[0].to_socket
1256 i2f = types[1][0].links[0].from_socket
1257 i2t = types[1][0].links[0].to_socket
1258 links.new(i1f, i2t)
1259 links.new(i2f, i1t)
1261 else:
1262 self.report({'WARNING'}, "This node has no input connections to swap!")
1263 else:
1264 self.report({'WARNING'}, "This node has no inputs to swap!")
1266 hack_force_update(context, nodes)
1267 return {'FINISHED'}
1270 class NWResetBG(Operator, NWBase):
1271 """Reset the zoom and position of the background image"""
1272 bl_idname = 'node.nw_bg_reset'
1273 bl_label = 'Reset Backdrop'
1274 bl_options = {'REGISTER', 'UNDO'}
1276 @classmethod
1277 def poll(cls, context):
1278 snode = context.space_data
1279 return snode.tree_type == 'CompositorNodeTree'
1281 def execute(self, context):
1282 context.space_data.backdrop_zoom = 1
1283 context.space_data.backdrop_x = 0
1284 context.space_data.backdrop_y = 0
1285 return {'FINISHED'}
1288 class NWAddAttrNode(Operator, NWBase):
1289 """Add an Attribute node with this name"""
1290 bl_idname = 'node.nw_add_attr_node'
1291 bl_label = 'Add UV map'
1292 attr_name = StringProperty()
1293 bl_options = {'REGISTER', 'UNDO'}
1295 def execute(self, context):
1296 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1297 nodes, links = get_nodes_links(context)
1298 nodes.active.attribute_name = self.attr_name
1299 return {'FINISHED'}
1302 class NWEmissionViewer(Operator, NWBase):
1303 bl_idname = "node.nw_emission_viewer"
1304 bl_label = "Emission Viewer"
1305 bl_description = "Connect active node to Emission Shader for shadeless previews"
1306 bl_options = {'REGISTER', 'UNDO'}
1308 @classmethod
1309 def poll(cls, context):
1310 space = context.space_data
1311 valid = False
1312 if space.type == 'NODE_EDITOR':
1313 if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None and (context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD"):
1314 valid = True
1315 return valid
1317 def invoke(self, context, event):
1318 shader_type = context.space_data.shader_type
1319 if shader_type == 'OBJECT':
1320 shader_output_type = "OUTPUT_MATERIAL"
1321 shader_output_ident = "ShaderNodeOutputMaterial"
1322 shader_viewer_ident = "ShaderNodeEmission"
1323 elif shader_type == 'WORLD':
1324 shader_output_type = "OUTPUT_WORLD"
1325 shader_output_ident = "ShaderNodeOutputWorld"
1326 shader_viewer_ident = "ShaderNodeBackground"
1327 shader_types = [x[1] for x in shaders_shader_nodes_props]
1328 mlocx = event.mouse_region_x
1329 mlocy = event.mouse_region_y
1330 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1331 if 'FINISHED' in select_node: # only run if mouse click is on a node
1332 nodes, links = get_nodes_links(context)
1333 in_group = context.active_node != context.space_data.node_tree.nodes.active
1334 active = nodes.active
1335 valid = False
1336 output_types = [x[1] for x in shaders_output_nodes_props]
1337 if active:
1338 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1339 if active.select:
1340 if active.type not in shader_types:
1341 valid = True
1342 if valid:
1343 # get material_output node
1344 materialout_exists = False
1345 materialout = None # placeholder node
1346 for node in nodes:
1347 if node.type == shader_output_type:
1348 materialout_exists = True
1349 materialout = node
1350 if not materialout:
1351 materialout = nodes.new(shader_output_ident)
1352 sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
1353 max_xloc_node = sorted_by_xloc[-1]
1354 if max_xloc_node.name == 'Emission Viewer':
1355 max_xloc_node = sorted_by_xloc[-2]
1356 materialout.location.x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1357 sum_yloc = 0
1358 for node in nodes:
1359 sum_yloc += node.location.y
1360 materialout.location.y = sum_yloc / len(nodes) # put material output at average y location
1361 materialout.select = False
1362 # get Emission Viewer node
1363 emission_exists = False
1364 emission_placeholder = nodes[0]
1365 for node in nodes:
1366 if "Emission Viewer" in node.name:
1367 emission_exists = True
1368 emission_placeholder = node
1370 position = 0
1371 for link in links: # check if Emission Viewer is already connected to active node
1372 if link.from_node.name == active.name and "Emission Viewer" in link.to_node.name and "Emission Viewer" in materialout.inputs[0].links[0].from_node.name:
1373 num_outputs = len(link.from_node.outputs)
1374 index = 0
1375 for output in link.from_node.outputs:
1376 if link.from_socket == output:
1377 position = index
1378 index = index + 1
1379 position = position + 1
1380 if position >= num_outputs:
1381 position = 0
1383 # Store selection
1384 selection = []
1385 for node in nodes:
1386 if node.select == True:
1387 selection.append(node.name)
1389 locx = active.location.x
1390 locy = active.location.y
1391 dimx = active.dimensions.x
1392 dimy = active.dimensions.y
1393 if not emission_exists:
1394 emission = nodes.new(shader_viewer_ident)
1395 emission.hide = True
1396 emission.location = [materialout.location.x, (materialout.location.y + 40)]
1397 emission.label = "Viewer"
1398 emission.name = "Emission Viewer"
1399 emission.use_custom_color = True
1400 emission.color = (0.6, 0.5, 0.4)
1401 else:
1402 emission = emission_placeholder
1404 nodes.active = emission
1405 links.new(active.outputs[position], emission.inputs[0])
1406 bpy.ops.node.nw_link_out()
1408 # Restore selection
1409 emission.select = False
1410 nodes.active = active
1411 for node in nodes:
1412 if node.name in selection:
1413 node.select = True
1414 else: # if active node is a shader, connect to output
1415 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1416 bpy.ops.node.nw_link_out()
1418 # ----Delete Emission Viewer----
1419 if [x for x in nodes if x.name == 'Emission Viewer']:
1420 # Store selection
1421 selection = []
1422 for node in nodes:
1423 if node.select == True:
1424 selection.append(node.name)
1425 node.select = False
1426 # Delete it
1427 nodes['Emission Viewer'].select = True
1428 bpy.ops.node.delete()
1429 # Restore selection
1430 for node in nodes:
1431 if node.name in selection:
1432 node.select = True
1434 return {'FINISHED'}
1435 else:
1436 return {'CANCELLED'}
1439 class NWFrameSelected(Operator, NWBase):
1440 bl_idname = "node.nw_frame_selected"
1441 bl_label = "Frame Selected"
1442 bl_description = "Add a frame node and parent the selected nodes to it"
1443 bl_options = {'REGISTER', 'UNDO'}
1444 label_prop = StringProperty(name='Label', default=' ', description='The visual name of the frame node')
1445 color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6),
1446 min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3)
1448 @classmethod
1449 def poll(cls, context):
1450 space = context.space_data
1451 valid = False
1452 if space.type == 'NODE_EDITOR':
1453 if space.node_tree is not None:
1454 valid = True
1455 return valid
1457 def execute(self, context):
1458 nodes, links = get_nodes_links(context)
1459 selected = []
1460 for node in nodes:
1461 if node.select == True:
1462 selected.append(node)
1464 bpy.ops.node.add_node(type='NodeFrame')
1465 frm = nodes.active
1466 frm.label = self.label_prop
1467 frm.use_custom_color = True
1468 frm.color = self.color_prop
1470 for node in selected:
1471 node.parent = frm
1473 return {'FINISHED'}
1476 class NWReloadImages(Operator, NWBase):
1477 bl_idname = "node.nw_reload_images"
1478 bl_label = "Reload Images"
1479 bl_description = "Update all the image nodes to match their files on disk"
1481 @classmethod
1482 def poll(cls, context):
1483 space = context.space_data
1484 valid = False
1485 if space.type == 'NODE_EDITOR':
1486 if space.node_tree is not None:
1487 valid = True
1488 return valid
1490 def execute(self, context):
1491 nodes, links = get_nodes_links(context)
1492 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1493 num_reloaded = 0
1494 for node in nodes:
1495 if node.type in image_types:
1496 if node.type == "TEXTURE":
1497 if node.texture: # node has texture assigned
1498 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1499 if node.texture.image: # texture has image assigned
1500 node.texture.image.reload()
1501 num_reloaded += 1
1502 else:
1503 if node.image:
1504 node.image.reload()
1505 num_reloaded += 1
1507 if num_reloaded:
1508 self.report({'INFO'}, "Reloaded images")
1509 print("Reloaded " + str(num_reloaded) + " images")
1510 hack_force_update(context, nodes)
1511 return {'FINISHED'}
1512 else:
1513 self.report({'WARNING'}, "No images found to reload in this node tree")
1514 return {'CANCELLED'}
1517 class NWSwitchNodeType(Operator, NWBase):
1518 """Switch type of selected nodes """
1519 bl_idname = "node.nw_swtch_node_type"
1520 bl_label = "Switch Node Type"
1521 bl_options = {'REGISTER', 'UNDO'}
1523 to_type = EnumProperty(
1524 name="Switch to type",
1525 items=list(shaders_input_nodes_props) +
1526 list(shaders_output_nodes_props) +
1527 list(shaders_shader_nodes_props) +
1528 list(shaders_texture_nodes_props) +
1529 list(shaders_color_nodes_props) +
1530 list(shaders_vector_nodes_props) +
1531 list(shaders_converter_nodes_props) +
1532 list(shaders_layout_nodes_props) +
1533 list(compo_input_nodes_props) +
1534 list(compo_output_nodes_props) +
1535 list(compo_color_nodes_props) +
1536 list(compo_converter_nodes_props) +
1537 list(compo_filter_nodes_props) +
1538 list(compo_vector_nodes_props) +
1539 list(compo_matte_nodes_props) +
1540 list(compo_distort_nodes_props) +
1541 list(compo_layout_nodes_props),
1544 def execute(self, context):
1545 nodes, links = get_nodes_links(context)
1546 to_type = self.to_type
1547 # Those types of nodes will not swap.
1548 src_excludes = ('NodeFrame')
1549 # Those attributes of nodes will be copied if possible
1550 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1551 'show_options', 'show_preview', 'show_texture',
1552 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1554 selected = [n for n in nodes if n.select]
1555 reselect = []
1556 for node in [n for n in selected if
1557 n.rna_type.identifier not in src_excludes and
1558 n.rna_type.identifier != to_type]:
1559 new_node = nodes.new(to_type)
1560 for attr in attrs_to_pass:
1561 if hasattr(node, attr) and hasattr(new_node, attr):
1562 setattr(new_node, attr, getattr(node, attr))
1563 # set image datablock of dst to image of src
1564 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1565 if node.image:
1566 new_node.image = node.image
1567 # Special cases
1568 if new_node.type == 'SWITCH':
1569 new_node.hide = True
1570 # Dictionaries: src_sockets and dst_sockets:
1571 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1572 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1573 # in 'INPUTS' and 'OUTPUTS':
1574 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1575 # socket entry:
1576 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1577 src_sockets = {
1578 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1579 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1581 dst_sockets = {
1582 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1583 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1585 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1586 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1587 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1588 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1589 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1590 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1591 # enumerate in inputs, then in outputs
1592 # find name, default value and links of socket
1593 for i, socket in enumerate(in_out):
1594 the_name = socket.name
1595 dval = None
1596 # Not every socket, especially in outputs has "default_value"
1597 if hasattr(socket, 'default_value'):
1598 dval = socket.default_value
1599 socket_links = []
1600 for lnk in socket.links:
1601 socket_links.append(lnk)
1602 # check type of socket to fill proper keys.
1603 for the_type in types_order_one:
1604 if socket.type == the_type:
1605 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1606 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1607 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1608 # Check which of the types in inputs/outputs is considered to be "main".
1609 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1610 for type_check in types_order_one:
1611 if sockets[in_out_name][type_check]:
1612 sockets[in_out_name]['MAIN'] = type_check
1613 break
1615 matches = {
1616 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1617 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1620 for inout, soctype in (
1621 ('INPUTS', 'MAIN',),
1622 ('INPUTS', 'SHADER',),
1623 ('INPUTS', 'RGBA',),
1624 ('INPUTS', 'VECTOR',),
1625 ('INPUTS', 'VALUE',),
1626 ('OUTPUTS', 'MAIN',),
1627 ('OUTPUTS', 'SHADER',),
1628 ('OUTPUTS', 'RGBA',),
1629 ('OUTPUTS', 'VECTOR',),
1630 ('OUTPUTS', 'VALUE',),
1632 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1633 if soctype == 'MAIN':
1634 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1635 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1636 else:
1637 sc = src_sockets[inout][soctype]
1638 dt = dst_sockets[inout][soctype]
1639 # start with 'dt' to determine number of possibilities.
1640 for i, soc in enumerate(dt):
1641 # if src main has enough entries - match them with dst main sockets by indexes.
1642 if len(sc) > i:
1643 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1644 # add 'VALUE_NAME' criterion to inputs.
1645 if inout == 'INPUTS' and soctype == 'VALUE':
1646 for s in sc:
1647 if s[2] == soc[2]: # if names match
1648 # append src (index, dval), dst (index, dval)
1649 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1651 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1652 # This creates better links when relinking textures.
1653 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1654 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1656 # Pass default values and RELINK:
1657 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1658 # INPUTS: Base on matches in proper order.
1659 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1660 # pass dvals
1661 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1662 new_node.inputs[dst_i].default_value = src_dval
1663 # Special case: switch to math
1664 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1665 new_node.type == 'MATH' and\
1666 tp == 'MAIN':
1667 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1668 new_node.inputs[dst_i].default_value = new_dst_dval
1669 if node.type == 'MIX_RGB':
1670 if node.blend_type in [o[0] for o in operations]:
1671 new_node.operation = node.blend_type
1672 # Special case: switch from math to some types
1673 if node.type == 'MATH' and\
1674 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1675 tp == 'MAIN':
1676 for i in range(3):
1677 new_node.inputs[dst_i].default_value[i] = src_dval
1678 if new_node.type == 'MIX_RGB':
1679 if node.operation in [t[0] for t in blend_types]:
1680 new_node.blend_type = node.operation
1681 # Set Fac of MIX_RGB to 1.0
1682 new_node.inputs[0].default_value = 1.0
1683 # make link only when dst matching input is not linked already.
1684 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1685 in_src_link = node.inputs[src_i].links[0]
1686 in_dst_socket = new_node.inputs[dst_i]
1687 links.new(in_src_link.from_socket, in_dst_socket)
1688 links.remove(in_src_link)
1689 # OUTPUTS: Base on matches in proper order.
1690 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1691 for out_src_link in node.outputs[src_i].links:
1692 out_dst_socket = new_node.outputs[dst_i]
1693 links.new(out_dst_socket, out_src_link.to_socket)
1694 # relink rest inputs if possible, no criteria
1695 for src_inp in node.inputs:
1696 for dst_inp in new_node.inputs:
1697 if src_inp.links and not dst_inp.links:
1698 src_link = src_inp.links[0]
1699 links.new(src_link.from_socket, dst_inp)
1700 links.remove(src_link)
1701 # relink rest outputs if possible, base on node kind if any left.
1702 for src_o in node.outputs:
1703 for out_src_link in src_o.links:
1704 for dst_o in new_node.outputs:
1705 if src_o.type == dst_o.type:
1706 links.new(dst_o, out_src_link.to_socket)
1707 # relink rest outputs no criteria if any left. Link all from first output.
1708 for src_o in node.outputs:
1709 for out_src_link in src_o.links:
1710 if new_node.outputs:
1711 links.new(new_node.outputs[0], out_src_link.to_socket)
1712 nodes.remove(node)
1713 return {'FINISHED'}
1716 class NWMergeNodes(Operator, NWBase):
1717 bl_idname = "node.nw_merge_nodes"
1718 bl_label = "Merge Nodes"
1719 bl_description = "Merge Selected Nodes"
1720 bl_options = {'REGISTER', 'UNDO'}
1722 mode = EnumProperty(
1723 name="mode",
1724 description="All possible blend types and math operations",
1725 items=blend_types + [op for op in operations if op not in blend_types],
1727 merge_type = EnumProperty(
1728 name="merge type",
1729 description="Type of Merge to be used",
1730 items=(
1731 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1732 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1733 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1734 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1735 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes')
1739 def execute(self, context):
1740 settings = context.user_preferences.addons[__name__].preferences
1741 merge_hide = settings.merge_hide
1742 merge_position = settings.merge_position # 'center' or 'bottom'
1744 do_hide = False
1745 do_hide_shader = False
1746 if merge_hide == 'ALWAYS':
1747 do_hide = True
1748 do_hide_shader = True
1749 elif merge_hide == 'NON_SHADER':
1750 do_hide = True
1752 tree_type = context.space_data.node_tree.type
1753 if tree_type == 'COMPOSITING':
1754 node_type = 'CompositorNode'
1755 elif tree_type == 'SHADER':
1756 node_type = 'ShaderNode'
1757 nodes, links = get_nodes_links(context)
1758 mode = self.mode
1759 merge_type = self.merge_type
1760 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1761 # 'ZCOMBINE' works only if mode == 'MIX'
1762 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1763 if merge_type == 'ZCOMBINE' and tree_type != 'COMPOSITING':
1764 mode = None
1765 selected_mix = [] # entry = [index, loc]
1766 selected_shader = [] # entry = [index, loc]
1767 selected_math = [] # entry = [index, loc]
1768 selected_z = [] # entry = [index, loc]
1770 for i, node in enumerate(nodes):
1771 if node.select and node.outputs:
1772 if merge_type == 'AUTO':
1773 for (type, types_list, dst) in (
1774 ('SHADER', ('MIX', 'ADD'), selected_shader),
1775 ('RGBA', [t[0] for t in blend_types], selected_mix),
1776 ('VALUE', [t[0] for t in operations], selected_math),
1778 output_type = node.outputs[0].type
1779 valid_mode = mode in types_list
1780 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1781 # Cheat that output type is 'RGBA',
1782 # and that 'MIX' exists in math operations list.
1783 # This way when selected_mix list is analyzed:
1784 # Node data will be appended even though it doesn't meet requirements.
1785 if output_type != 'SHADER' and mode == 'MIX':
1786 output_type = 'RGBA'
1787 valid_mode = True
1788 if output_type == type and valid_mode:
1789 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1790 else:
1791 for (type, types_list, dst) in (
1792 ('SHADER', ('MIX', 'ADD'), selected_shader),
1793 ('MIX', [t[0] for t in blend_types], selected_mix),
1794 ('MATH', [t[0] for t in operations], selected_math),
1795 ('ZCOMBINE', ('MIX', ), selected_z),
1797 if merge_type == type and mode in types_list:
1798 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1799 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1800 # use only 'Mix' nodes for merging.
1801 # For that we add selected_math list to selected_mix list and clear selected_math.
1802 if selected_mix and selected_math and merge_type == 'AUTO':
1803 selected_mix += selected_math
1804 selected_math = []
1806 for nodes_list in [selected_mix, selected_shader, selected_math, selected_z]:
1807 if nodes_list:
1808 count_before = len(nodes)
1809 # sort list by loc_x - reversed
1810 nodes_list.sort(key=lambda k: k[1], reverse=True)
1811 # get maximum loc_x
1812 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
1813 nodes_list.sort(key=lambda k: k[2], reverse=True)
1814 if merge_position == 'CENTER':
1815 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)
1816 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
1817 if do_hide:
1818 loc_y += 40
1819 else:
1820 loc_y += 80
1821 else:
1822 loc_y = nodes_list[len(nodes_list) - 1][2]
1823 offset_y = 100
1824 if not do_hide:
1825 offset_y = 200
1826 if nodes_list == selected_shader and not do_hide_shader:
1827 offset_y = 150.0
1828 the_range = len(nodes_list) - 1
1829 if len(nodes_list) == 1:
1830 the_range = 1
1831 for i in range(the_range):
1832 if nodes_list == selected_mix:
1833 add_type = node_type + 'MixRGB'
1834 add = nodes.new(add_type)
1835 add.blend_type = mode
1836 add.show_preview = False
1837 add.hide = do_hide
1838 if do_hide:
1839 loc_y = loc_y - 50
1840 first = 1
1841 second = 2
1842 add.width_hidden = 100.0
1843 elif nodes_list == selected_math:
1844 add_type = node_type + 'Math'
1845 add = nodes.new(add_type)
1846 add.operation = mode
1847 add.hide = do_hide
1848 if do_hide:
1849 loc_y = loc_y - 50
1850 first = 0
1851 second = 1
1852 add.width_hidden = 100.0
1853 elif nodes_list == selected_shader:
1854 if mode == 'MIX':
1855 add_type = node_type + 'MixShader'
1856 add = nodes.new(add_type)
1857 add.hide = do_hide_shader
1858 if do_hide_shader:
1859 loc_y = loc_y - 50
1860 first = 1
1861 second = 2
1862 add.width_hidden = 100.0
1863 elif mode == 'ADD':
1864 add_type = node_type + 'AddShader'
1865 add = nodes.new(add_type)
1866 add.hide = do_hide_shader
1867 if do_hide_shader:
1868 loc_y = loc_y - 50
1869 first = 0
1870 second = 1
1871 add.width_hidden = 100.0
1872 elif nodes_list == selected_z:
1873 add = nodes.new('CompositorNodeZcombine')
1874 add.show_preview = False
1875 add.hide = do_hide
1876 if do_hide:
1877 loc_y = loc_y - 50
1878 first = 0
1879 second = 2
1880 add.width_hidden = 100.0
1881 add.location = loc_x, loc_y
1882 loc_y += offset_y
1883 add.select = True
1884 count_adds = i + 1
1885 count_after = len(nodes)
1886 index = count_after - 1
1887 first_selected = nodes[nodes_list[0][0]]
1888 # "last" node has been added as first, so its index is count_before.
1889 last_add = nodes[count_before]
1890 # add links from last_add to all links 'to_socket' of out links of first selected.
1891 for fs_link in first_selected.outputs[0].links:
1892 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
1893 # Create list of invalid indexes.
1894 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
1895 # Link only if "to_node" index not in invalid indexes list.
1896 if fs_link.to_node not in [nodes[i] for i in invalid_i]:
1897 links.new(last_add.outputs[0], fs_link.to_socket)
1898 # add link from "first" selected and "first" add node
1899 node_to = nodes[count_after - 1]
1900 links.new(first_selected.outputs[0], node_to.inputs[first])
1901 if node_to.type == 'ZCOMBINE':
1902 for fs_out in first_selected.outputs:
1903 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
1904 links.new(fs_out, node_to.inputs[1])
1905 break
1906 # add links between added ADD nodes and between selected and ADD nodes
1907 for i in range(count_adds):
1908 if i < count_adds - 1:
1909 node_from = nodes[index]
1910 node_to = nodes[index - 1]
1911 node_to_input_i = first
1912 node_to_z_i = 1 # if z combine - link z to first z input
1913 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
1914 if node_to.type == 'ZCOMBINE':
1915 for from_out in node_from.outputs:
1916 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
1917 links.new(from_out, node_to.inputs[node_to_z_i])
1918 if len(nodes_list) > 1:
1919 node_from = nodes[nodes_list[i + 1][0]]
1920 node_to = nodes[index]
1921 node_to_input_i = second
1922 node_to_z_i = 3 # if z combine - link z to second z input
1923 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
1924 if node_to.type == 'ZCOMBINE':
1925 for from_out in node_from.outputs:
1926 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
1927 links.new(from_out, node_to.inputs[node_to_z_i])
1928 index -= 1
1929 # set "last" of added nodes as active
1930 nodes.active = last_add
1931 for i, x, y, dx, h in nodes_list:
1932 nodes[i].select = False
1934 return {'FINISHED'}
1937 class NWBatchChangeNodes(Operator, NWBase):
1938 bl_idname = "node.nw_batch_change"
1939 bl_label = "Batch Change"
1940 bl_description = "Batch Change Blend Type and Math Operation"
1941 bl_options = {'REGISTER', 'UNDO'}
1943 blend_type = EnumProperty(
1944 name="Blend Type",
1945 items=blend_types + navs,
1947 operation = EnumProperty(
1948 name="Operation",
1949 items=operations + navs,
1952 def execute(self, context):
1954 nodes, links = get_nodes_links(context)
1955 blend_type = self.blend_type
1956 operation = self.operation
1957 for node in context.selected_nodes:
1958 if node.type == 'MIX_RGB':
1959 if not blend_type in [nav[0] for nav in navs]:
1960 node.blend_type = blend_type
1961 else:
1962 if blend_type == 'NEXT':
1963 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1964 #index = blend_types.index(node.blend_type)
1965 if index == len(blend_types) - 1:
1966 node.blend_type = blend_types[0][0]
1967 else:
1968 node.blend_type = blend_types[index + 1][0]
1970 if blend_type == 'PREV':
1971 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1972 if index == 0:
1973 node.blend_type = blend_types[len(blend_types) - 1][0]
1974 else:
1975 node.blend_type = blend_types[index - 1][0]
1977 if node.type == 'MATH':
1978 if not operation in [nav[0] for nav in navs]:
1979 node.operation = operation
1980 else:
1981 if operation == 'NEXT':
1982 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1983 #index = operations.index(node.operation)
1984 if index == len(operations) - 1:
1985 node.operation = operations[0][0]
1986 else:
1987 node.operation = operations[index + 1][0]
1989 if operation == 'PREV':
1990 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1991 #index = operations.index(node.operation)
1992 if index == 0:
1993 node.operation = operations[len(operations) - 1][0]
1994 else:
1995 node.operation = operations[index - 1][0]
1997 return {'FINISHED'}
2000 class NWChangeMixFactor(Operator, NWBase):
2001 bl_idname = "node.nw_factor"
2002 bl_label = "Change Factor"
2003 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2004 bl_options = {'REGISTER', 'UNDO'}
2006 # option: Change factor.
2007 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2008 # Else - change factor by option value.
2009 option = FloatProperty()
2011 def execute(self, context):
2012 nodes, links = get_nodes_links(context)
2013 option = self.option
2014 selected = [] # entry = index
2015 for si, node in enumerate(nodes):
2016 if node.select:
2017 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2018 selected.append(si)
2020 for si in selected:
2021 fac = nodes[si].inputs[0]
2022 nodes[si].hide = False
2023 if option in {0.0, 1.0}:
2024 fac.default_value = option
2025 else:
2026 fac.default_value += option
2028 return {'FINISHED'}
2031 class NWCopySettings(Operator, NWBase):
2032 bl_idname = "node.nw_copy_settings"
2033 bl_label = "Copy Settings"
2034 bl_description = "Copy Settings of Active Node to Selected Nodes"
2035 bl_options = {'REGISTER', 'UNDO'}
2037 @classmethod
2038 def poll(cls, context):
2039 space = context.space_data
2040 valid = False
2041 if (space.type == 'NODE_EDITOR' and
2042 space.node_tree is not None and
2043 context.active_node is not None and
2044 context.active_node.type is not 'FRAME'
2046 valid = True
2047 return valid
2049 def execute(self, context):
2050 nodes, links = get_nodes_links(context)
2051 selected = [n for n in nodes if n.select]
2052 reselect = [] # duplicated nodes will be selected after execution
2053 active = nodes.active
2054 if active.select:
2055 reselect.append(active)
2057 for node in selected:
2058 if node.type == active.type and node != active:
2059 # duplicate active, relink links as in 'node', append copy to 'reselect', delete node
2060 bpy.ops.node.select_all(action='DESELECT')
2061 nodes.active = active
2062 active.select = True
2063 bpy.ops.node.duplicate()
2064 copied = nodes.active
2065 # Copied active should however inherit some properties from 'node'
2066 attributes = (
2067 'hide', 'show_preview', 'mute', 'label',
2068 'use_custom_color', 'color', 'width', 'width_hidden',
2070 for attr in attributes:
2071 setattr(copied, attr, getattr(node, attr))
2072 # Handle scenario when 'node' is in frame. 'copied' is in same frame then.
2073 if copied.parent:
2074 bpy.ops.node.parent_clear()
2075 locx = node.location.x
2076 locy = node.location.y
2077 # get absolute node location
2078 parent = node.parent
2079 while parent:
2080 locx += parent.location.x
2081 locy += parent.location.y
2082 parent = parent.parent
2083 copied.location = [locx, locy]
2084 # reconnect links from node to copied
2085 for i, input in enumerate(node.inputs):
2086 if input.links:
2087 link = input.links[0]
2088 links.new(link.from_socket, copied.inputs[i])
2089 for out, output in enumerate(node.outputs):
2090 if output.links:
2091 out_links = output.links
2092 for link in out_links:
2093 links.new(copied.outputs[out], link.to_socket)
2094 bpy.ops.node.select_all(action='DESELECT')
2095 node.select = True
2096 bpy.ops.node.delete()
2097 reselect.append(copied)
2098 else: # If selected wasn't copied, need to reselect it afterwards.
2099 reselect.append(node)
2100 # clean up
2101 bpy.ops.node.select_all(action='DESELECT')
2102 for node in reselect:
2103 node.select = True
2104 nodes.active = active
2106 return {'FINISHED'}
2109 class NWCopyLabel(Operator, NWBase):
2110 bl_idname = "node.nw_copy_label"
2111 bl_label = "Copy Label"
2112 bl_options = {'REGISTER', 'UNDO'}
2114 option = EnumProperty(
2115 name="option",
2116 description="Source of name of label",
2117 items=(
2118 ('FROM_ACTIVE', 'from active', 'from active node',),
2119 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2120 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2124 def execute(self, context):
2125 nodes, links = get_nodes_links(context)
2126 option = self.option
2127 active = nodes.active
2128 if option == 'FROM_ACTIVE':
2129 if active:
2130 src_label = active.label
2131 for node in [n for n in nodes if n.select and nodes.active != n]:
2132 node.label = src_label
2133 elif option == 'FROM_NODE':
2134 selected = [n for n in nodes if n.select]
2135 for node in selected:
2136 for input in node.inputs:
2137 if input.links:
2138 src = input.links[0].from_node
2139 node.label = src.label
2140 break
2141 elif option == 'FROM_SOCKET':
2142 selected = [n for n in nodes if n.select]
2143 for node in selected:
2144 for input in node.inputs:
2145 if input.links:
2146 src = input.links[0].from_socket
2147 node.label = src.name
2148 break
2150 return {'FINISHED'}
2153 class NWClearLabel(Operator, NWBase):
2154 bl_idname = "node.nw_clear_label"
2155 bl_label = "Clear Label"
2156 bl_options = {'REGISTER', 'UNDO'}
2158 option = BoolProperty()
2160 def execute(self, context):
2161 nodes, links = get_nodes_links(context)
2162 for node in [n for n in nodes if n.select]:
2163 node.label = ''
2165 return {'FINISHED'}
2167 def invoke(self, context, event):
2168 if self.option:
2169 return self.execute(context)
2170 else:
2171 return context.window_manager.invoke_confirm(self, event)
2174 class NWModifyLabels(Operator, NWBase):
2175 """Modify Labels of all selected nodes."""
2176 bl_idname = "node.nw_modify_labels"
2177 bl_label = "Modify Labels"
2178 bl_options = {'REGISTER', 'UNDO'}
2180 prepend = StringProperty(
2181 name="Add to Beginning"
2183 append = StringProperty(
2184 name="Add to End"
2186 replace_from = StringProperty(
2187 name="Text to Replace"
2189 replace_to = StringProperty(
2190 name="Replace with"
2193 def execute(self, context):
2194 nodes, links = get_nodes_links(context)
2195 for node in [n for n in nodes if n.select]:
2196 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2198 return {'FINISHED'}
2200 def invoke(self, context, event):
2201 self.prepend = ""
2202 self.append = ""
2203 self.remove = ""
2204 return context.window_manager.invoke_props_dialog(self)
2207 class NWAddTextureSetup(Operator, NWBase):
2208 bl_idname = "node.nw_add_texture"
2209 bl_label = "Texture Setup"
2210 bl_description = "Add Texture Node Setup to Selected Shaders"
2211 bl_options = {'REGISTER', 'UNDO'}
2213 @classmethod
2214 def poll(cls, context):
2215 space = context.space_data
2216 valid = False
2217 if space.type == 'NODE_EDITOR':
2218 if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None:
2219 valid = True
2220 return valid
2222 def execute(self, context):
2223 nodes, links = get_nodes_links(context)
2224 active = nodes.active
2225 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2226 texture_types = [x[1] for x in shaders_texture_nodes_props]
2227 valid = False
2228 if active:
2229 if active.select:
2230 if active.type in shader_types or active.type in texture_types:
2231 if not active.inputs[0].is_linked:
2232 valid = True
2233 if valid:
2234 locx = active.location.x
2235 locy = active.location.y
2237 xoffset = [500.0, 700.0]
2238 isshader = True
2239 if active.type not in shader_types:
2240 xoffset = [290.0, 500.0]
2241 isshader = False
2243 coordout = 2
2244 image_type = 'ShaderNodeTexImage'
2246 if (active.type in texture_types and active.type != 'TEX_IMAGE') or (active.type == 'BACKGROUND'):
2247 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2248 if active.type == 'BACKGROUND':
2249 image_type = 'ShaderNodeTexEnvironment'
2251 if isshader:
2252 tex = nodes.new(image_type)
2253 tex.location = [locx - 200.0, locy + 28.0]
2255 map = nodes.new('ShaderNodeMapping')
2256 map.location = [locx - xoffset[0], locy + 80.0]
2257 map.width = 240
2258 coord = nodes.new('ShaderNodeTexCoord')
2259 coord.location = [locx - xoffset[1], locy + 40.0]
2260 active.select = False
2262 if isshader:
2263 nodes.active = tex
2264 links.new(tex.outputs[0], active.inputs[0])
2265 links.new(map.outputs[0], tex.inputs[0])
2266 links.new(coord.outputs[coordout], map.inputs[0])
2268 else:
2269 nodes.active = map
2270 links.new(map.outputs[0], active.inputs[0])
2271 links.new(coord.outputs[coordout], map.inputs[0])
2273 return {'FINISHED'}
2276 class NWAddReroutes(Operator, NWBase):
2277 """Add Reroute Nodes and link them to outputs of selected nodes"""
2278 bl_idname = "node.nw_add_reroutes"
2279 bl_label = "Add Reroutes"
2280 bl_description = "Add Reroutes to Outputs"
2281 bl_options = {'REGISTER', 'UNDO'}
2283 option = EnumProperty(
2284 name="option",
2285 items=[
2286 ('ALL', 'to all', 'Add to all outputs'),
2287 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2288 ('LINKED', 'to linked', 'Add only to linked outputs'),
2292 def execute(self, context):
2293 tree_type = context.space_data.node_tree.type
2294 option = self.option
2295 nodes, links = get_nodes_links(context)
2296 # output valid when option is 'all' or when 'loose' output has no links
2297 valid = False
2298 post_select = [] # nodes to be selected after execution
2299 # create reroutes and recreate links
2300 for node in [n for n in nodes if n.select]:
2301 if node.outputs:
2302 x = node.location.x
2303 y = node.location.y
2304 width = node.width
2305 # unhide 'REROUTE' nodes to avoid issues with location.y
2306 if node.type == 'REROUTE':
2307 node.hide = False
2308 # When node is hidden - width_hidden not usable.
2309 # Hack needed to calculate real width
2310 if node.hide:
2311 bpy.ops.node.select_all(action='DESELECT')
2312 helper = nodes.new('NodeReroute')
2313 helper.select = True
2314 node.select = True
2315 # resize node and helper to zero. Then check locations to calculate width
2316 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2317 width = 2.0 * (helper.location.x - node.location.x)
2318 # restore node location
2319 node.location = x, y
2320 # delete helper
2321 node.select = False
2322 # only helper is selected now
2323 bpy.ops.node.delete()
2324 x = node.location.x + width + 20.0
2325 if node.type != 'REROUTE':
2326 y -= 35.0
2327 y_offset = -22.0
2328 loc = x, y
2329 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2330 for out_i, output in enumerate(node.outputs):
2331 pass_used = False # initial value to be analyzed if 'R_LAYERS'
2332 # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
2333 if node.type != 'R_LAYERS':
2334 pass_used = True
2335 else: # if 'R_LAYERS' check if output represent used render pass
2336 node_scene = node.scene
2337 node_layer = node.layer
2338 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2339 if output.name == 'Alpha':
2340 pass_used = True
2341 else:
2342 # check entries in global 'rl_outputs' variable
2343 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
2344 if output.name == out_name:
2345 pass_used = getattr(node_scene.render.layers[node_layer], render_pass)
2346 break
2347 if pass_used:
2348 valid = ((option == 'ALL') or
2349 (option == 'LOOSE' and not output.links) or
2350 (option == 'LINKED' and output.links))
2351 # Add reroutes only if valid, but offset location in all cases.
2352 if valid:
2353 n = nodes.new('NodeReroute')
2354 nodes.active = n
2355 for link in output.links:
2356 links.new(n.outputs[0], link.to_socket)
2357 links.new(output, n.inputs[0])
2358 n.location = loc
2359 post_select.append(n)
2360 reroutes_count += 1
2361 y += y_offset
2362 loc = x, y
2363 # disselect the node so that after execution of script only newly created nodes are selected
2364 node.select = False
2365 # nicer reroutes distribution along y when node.hide
2366 if node.hide:
2367 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2368 for reroute in [r for r in nodes if r.select]:
2369 reroute.location.y -= y_translate
2370 for node in post_select:
2371 node.select = True
2373 return {'FINISHED'}
2376 class NWLinkActiveToSelected(Operator, NWBase):
2377 """Link active node to selected nodes basing on various criteria"""
2378 bl_idname = "node.nw_link_active_to_selected"
2379 bl_label = "Link Active Node to Selected"
2380 bl_options = {'REGISTER', 'UNDO'}
2382 replace = BoolProperty()
2383 use_node_name = BoolProperty()
2384 use_outputs_names = BoolProperty()
2386 @classmethod
2387 def poll(cls, context):
2388 space = context.space_data
2389 valid = False
2390 if space.type == 'NODE_EDITOR':
2391 if space.node_tree is not None and context.active_node is not None:
2392 if context.active_node.select:
2393 valid = True
2394 return valid
2396 def execute(self, context):
2397 nodes, links = get_nodes_links(context)
2398 replace = self.replace
2399 use_node_name = self.use_node_name
2400 use_outputs_names = self.use_outputs_names
2401 active = nodes.active
2402 selected = [node for node in nodes if node.select and node != active]
2403 outputs = [] # Only usable outputs of active nodes will be stored here.
2404 for out in active.outputs:
2405 if active.type != 'R_LAYERS':
2406 outputs.append(out)
2407 else:
2408 # 'R_LAYERS' node type needs special handling.
2409 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2410 # Only outputs that represent used passes should be taken into account
2411 # Check if pass represented by output is used.
2412 # global 'rl_outputs' list will be used for that
2413 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
2414 pass_used = False # initial value. Will be set to True if pass is used
2415 if out.name == 'Alpha':
2416 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2417 pass_used = True
2418 elif out.name == out_name:
2419 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2420 pass_used = getattr(active.scene.render.layers[active.layer], render_pass)
2421 break
2422 if pass_used:
2423 outputs.append(out)
2424 doit = True # Will be changed to False when links successfully added to previous output.
2425 for out in outputs:
2426 if doit:
2427 for node in selected:
2428 dst_name = node.name # Will be compared with src_name if needed.
2429 # When node has label - use it as dst_name
2430 if node.label:
2431 dst_name = node.label
2432 valid = True # Initial value. Will be changed to False if names don't match.
2433 src_name = dst_name # If names not used - this asignment will keep valid = True.
2434 if use_node_name:
2435 # Set src_name to source node name or label
2436 src_name = active.name
2437 if active.label:
2438 src_name = active.label
2439 elif use_outputs_names:
2440 src_name = (out.name, )
2441 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
2442 if out.name in {out_name, exr_name}:
2443 src_name = (out_name, exr_name)
2444 if dst_name not in src_name:
2445 valid = False
2446 if valid:
2447 for input in node.inputs:
2448 if input.type == out.type or node.type == 'REROUTE':
2449 if replace or not input.is_linked:
2450 links.new(out, input)
2451 if not use_node_name and not use_outputs_names:
2452 doit = False
2453 break
2455 return {'FINISHED'}
2458 class NWAlignNodes(Operator, NWBase):
2459 bl_idname = "node.nw_align_nodes"
2460 bl_label = "Align nodes"
2461 bl_options = {'REGISTER', 'UNDO'}
2463 # option: 'Vertically', 'Horizontally'
2464 option = EnumProperty(
2465 name="option",
2466 description="Direction",
2467 items=(
2468 ('AXIS_X', "Align Vertically", 'Align Vertically'),
2469 ('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'),
2473 def execute(self, context):
2474 nodes, links = get_nodes_links(context)
2475 selected = [] # entry = [index, loc.x, loc.y, width, height]
2476 frames_reselect = [] # entry = frame node. will be used to reselect all selected frames
2477 active = nodes.active
2478 for i, node in enumerate(nodes):
2479 total_w = 0.0 # total width of all nodes. Will be calculated later.
2480 total_h = 0.0 # total height of all nodes. Will be calculated later
2481 if node.select:
2482 if node.type == 'FRAME':
2483 node.select = False
2484 frames_reselect.append(i)
2485 else:
2486 locx = node.location.x
2487 locy = node.location.y
2488 width = node.dimensions[0]
2489 height = node.dimensions[1]
2490 total_w += width # add nodes[i] width to total width of all nodes
2491 total_h += height # add nodes[i] height to total height of all nodes
2492 # calculate relative locations
2493 parent = node.parent
2494 while parent is not None:
2495 locx += parent.location.x
2496 locy += parent.location.y
2497 parent = parent.parent
2498 selected.append([i, locx, locy, width, height])
2499 count = len(selected)
2500 if count > 1: # aligning makes sense only if at least 2 nodes are selected
2501 selected_sorted_x = sorted(selected, key=lambda k: (k[1], -k[2]))
2502 selected_sorted_y = sorted(selected, key=lambda k: (-k[2], k[1]))
2503 min_x = selected_sorted_x[0][1] # min loc.x
2504 min_x_loc_y = selected_sorted_x[0][2] # loc y of node with min loc x
2505 min_x_w = selected_sorted_x[0][3] # width of node with max loc x
2506 max_x = selected_sorted_x[count - 1][1] # max loc.x
2507 max_x_loc_y = selected_sorted_x[count - 1][2] # loc y of node with max loc.x
2508 max_x_w = selected_sorted_x[count - 1][3] # width of node with max loc.x
2509 min_y = selected_sorted_y[0][2] # min loc.y
2510 min_y_loc_x = selected_sorted_y[0][1] # loc.x of node with min loc.y
2511 min_y_h = selected_sorted_y[0][4] # height of node with min loc.y
2512 min_y_w = selected_sorted_y[0][3] # width of node with min loc.y
2513 max_y = selected_sorted_y[count - 1][2] # max loc.y
2514 max_y_loc_x = selected_sorted_y[count - 1][1] # loc x of node with max loc.y
2515 max_y_w = selected_sorted_y[count - 1][3] # width of node with max loc.y
2516 max_y_h = selected_sorted_y[count - 1][4] # height of node with max loc.y
2518 if self.option == 'AXIS_Y': # Horizontally. Equivelent of s -> x -> 0 with even spacing.
2519 loc_x = min_x
2520 #loc_y = (max_x_loc_y + min_x_loc_y) / 2.0
2521 loc_y = (max_y - max_y_h / 2.0 + min_y - min_y_h / 2.0) / 2.0
2522 offset_x = (max_x - min_x - total_w + max_x_w) / (count - 1)
2523 for i, x, y, w, h in selected_sorted_x:
2524 nodes[i].location.x = loc_x
2525 nodes[i].location.y = loc_y + h / 2.0
2526 parent = nodes[i].parent
2527 while parent is not None:
2528 nodes[i].location.x -= parent.location.x
2529 nodes[i].location.y -= parent.location.y
2530 parent = parent.parent
2531 loc_x += offset_x + w
2532 else: # if self.option == 'AXIS_Y'
2533 loc_x = (max_x + max_x_w / 2.0 + min_x + min_x_w / 2.0) / 2.0
2534 loc_y = min_y
2535 offset_y = (max_y - min_y + total_h - min_y_h) / (count - 1)
2536 for i, x, y, w, h in selected_sorted_y:
2537 nodes[i].location.x = loc_x - w / 2.0
2538 nodes[i].location.y = loc_y
2539 parent = nodes[i].parent
2540 while parent is not None:
2541 nodes[i].location.x -= parent.location.x
2542 nodes[i].location.y -= parent.location.y
2543 parent = parent.parent
2544 loc_y += offset_y - h
2546 # reselect selected frames
2547 for i in frames_reselect:
2548 nodes[i].select = True
2549 # restore active node
2550 nodes.active = active
2552 return {'FINISHED'}
2555 class NWSelectParentChildren(Operator, NWBase):
2556 bl_idname = "node.nw_select_parent_child"
2557 bl_label = "Select Parent or Children"
2558 bl_options = {'REGISTER', 'UNDO'}
2560 option = EnumProperty(
2561 name="option",
2562 items=(
2563 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2564 ('CHILD', 'Select Children', 'Select members of selected frame'),
2568 def execute(self, context):
2569 nodes, links = get_nodes_links(context)
2570 option = self.option
2571 selected = [node for node in nodes if node.select]
2572 if option == 'PARENT':
2573 for sel in selected:
2574 parent = sel.parent
2575 if parent:
2576 parent.select = True
2577 else: # option == 'CHILD'
2578 for sel in selected:
2579 children = [node for node in nodes if node.parent == sel]
2580 for kid in children:
2581 kid.select = True
2583 return {'FINISHED'}
2586 class NWDetachOutputs(Operator, NWBase):
2587 """Detach outputs of selected node leaving inluts liked"""
2588 bl_idname = "node.nw_detach_outputs"
2589 bl_label = "Detach Outputs"
2590 bl_options = {'REGISTER', 'UNDO'}
2592 def execute(self, context):
2593 nodes, links = get_nodes_links(context)
2594 selected = context.selected_nodes
2595 bpy.ops.node.duplicate_move_keep_inputs()
2596 new_nodes = context.selected_nodes
2597 bpy.ops.node.select_all(action="DESELECT")
2598 for node in selected:
2599 node.select = True
2600 bpy.ops.node.delete_reconnect()
2601 for new_node in new_nodes:
2602 new_node.select = True
2603 bpy.ops.transform.translate('INVOKE_DEFAULT')
2605 return {'FINISHED'}
2608 class NWLinkToOutputNode(Operator, NWBase):
2609 """Link to Composite node or Material Output node"""
2610 bl_idname = "node.nw_link_out"
2611 bl_label = "Connect to Output"
2612 bl_options = {'REGISTER', 'UNDO'}
2614 @classmethod
2615 def poll(cls, context):
2616 space = context.space_data
2617 return (space.type == 'NODE_EDITOR' and space.node_tree is not None and context.active_node is not None)
2619 def execute(self, context):
2620 nodes, links = get_nodes_links(context)
2621 active = nodes.active
2622 output_node = None
2623 tree_type = context.space_data.tree_type
2624 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
2625 output_types_compo = ['COMPOSITE']
2626 output_types = output_types_shaders + output_types_compo
2627 for node in nodes:
2628 if node.type in output_types:
2629 output_node = node
2630 break
2631 if not output_node:
2632 bpy.ops.node.select_all(action="DESELECT")
2633 if tree_type == 'ShaderNodeTree':
2634 output_node = nodes.new('ShaderNodeOutputMaterial')
2635 elif tree_type == 'CompositorNodeTree':
2636 output_node = nodes.new('CompositorNodeComposite')
2637 output_node.location.x = active.location.x + active.dimensions.x + 80
2638 output_node.location.y = active.location.y
2639 if (output_node and active.outputs):
2640 output_index = 0
2641 for i, output in enumerate(active.outputs):
2642 if output.type == output_node.inputs[0].type:
2643 output_index = i
2644 break
2646 out_input_index = 0
2647 if tree_type == 'ShaderNodeTree':
2648 if active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
2649 out_input_index = 2
2650 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
2652 hack_force_update(context, nodes) # viewport render does not update
2654 return {'FINISHED'}
2657 class NWMakeLink(Operator, NWBase):
2658 """Make a link from one socket to another"""
2659 bl_idname = 'node.nw_make_link'
2660 bl_label = 'Make Link'
2661 bl_options = {'REGISTER', 'UNDO'}
2662 from_socket = IntProperty()
2663 to_socket = IntProperty()
2665 @classmethod
2666 def poll(cls, context):
2667 snode = context.space_data
2668 return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None)
2670 def execute(self, context):
2671 nodes, links = get_nodes_links(context)
2673 n1 = nodes[context.scene.NWLazySource]
2674 n2 = nodes[context.scene.NWLazyTarget]
2676 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2678 hack_force_update(context, nodes)
2680 return {'FINISHED'}
2683 class NWCallInputsMenu(Operator, NWBase):
2684 """Link from this output"""
2685 bl_idname = 'node.nw_call_inputs_menu'
2686 bl_label = 'Make Link'
2687 bl_options = {'REGISTER', 'UNDO'}
2688 from_socket = IntProperty()
2690 @classmethod
2691 def poll(cls, context):
2692 snode = context.space_data
2693 return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None)
2695 def execute(self, context):
2696 nodes, links = get_nodes_links(context)
2698 context.scene.NWSourceSocket = self.from_socket
2700 n1 = nodes[context.scene.NWLazySource]
2701 n2 = nodes[context.scene.NWLazyTarget]
2702 if len(n2.inputs) > 1:
2703 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
2704 elif len(n2.inputs) == 1:
2705 links.new(n1.outputs[self.from_socket], n2.inputs[0])
2706 return {'FINISHED'}
2709 class NWAddSequence(Operator, ImportHelper):
2710 """Add an Image Sequence"""
2711 bl_idname = 'node.nw_add_sequence'
2712 bl_label = 'Import Image Sequence'
2713 bl_options = {'REGISTER', 'UNDO'}
2714 directory = StringProperty(subtype="DIR_PATH")
2715 filename = StringProperty(subtype="FILE_NAME")
2717 @classmethod
2718 def poll(cls, context):
2719 snode = context.space_data
2720 return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None)
2722 def execute(self, context):
2723 nodes, links = get_nodes_links(context)
2724 directory = self.directory
2725 filename = self.filename
2728 if context.space_data.node_tree.type == 'SHADER':
2729 node_type = "ShaderNodeTexImage"
2730 elif context.space_data.node_tree.type == 'COMPOSITING':
2731 node_type = "CompositorNodeImage"
2732 else:
2733 self.report({'ERROR'}, "Unsupported Node Tree type!")
2734 return {'CANCELLED'}
2736 # if last digit isn't a number, it's not a sequence
2737 without_ext = '.'.join(filename.split('.')[:-1])
2738 if without_ext[-1].isdigit():
2739 without_ext = without_ext[:-1] + '1'
2740 else:
2741 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
2742 return {'CANCELLED'}
2744 reverse = without_ext[::-1] # reverse string
2745 newreverse = ""
2746 non_numbers = ""
2747 count_numbers = 0
2748 stop = False
2749 for char in reverse:
2750 if char.isdigit() and stop==False:
2751 count_numbers += 1
2752 newreverse += '0' # replace numbers of image sequence with zeros
2753 else:
2754 stop = True
2755 newreverse += char
2756 non_numbers = char + non_numbers
2758 newreverse = '1' + newreverse[1:]
2759 without_ext = newreverse[::-1] # reverse string
2761 # print (without_ext+'.'+filename.split('.')[-1])
2762 # print (non_numbers)
2763 extension = filename.split('.')[-1]
2765 num_frames = len(list(f for f in listdir(directory) if f.startswith(non_numbers)))
2767 for x in range(count_numbers):
2768 non_numbers += '#'
2770 nodes_list = [node for node in nodes]
2771 if nodes_list:
2772 nodes_list.sort(key=lambda k: k.location.x)
2773 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
2774 yloc = 0
2775 for node in nodes:
2776 node.select = False
2777 yloc += node_mid_pt(node, 'y')
2778 yloc = yloc/len(nodes)
2779 else:
2780 xloc = 0
2781 yloc = 0
2783 node = nodes.new(node_type)
2784 node.location.x = xloc
2785 node.location.y = yloc + 110
2786 node.label = non_numbers+'.'+extension
2788 img = bpy.data.images.load(directory+(without_ext+'.'+extension))
2789 img.source = 'SEQUENCE'
2790 node.image = img
2791 if context.space_data.node_tree.type == 'SHADER':
2792 node.image_user.frame_duration = num_frames
2793 else:
2794 node.frame_duration = num_frames
2796 return {'FINISHED'}
2799 class NWAddMultipleImages(Operator, ImportHelper):
2800 """Add multiple images at once"""
2801 bl_idname = 'node.nw_add_multiple_images'
2802 bl_label = 'Open Selected Images'
2803 bl_options = {'REGISTER', 'UNDO'}
2804 directory = StringProperty(subtype="DIR_PATH")
2805 files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
2807 @classmethod
2808 def poll(cls, context):
2809 snode = context.space_data
2810 return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None)
2812 def execute(self, context):
2813 nodes, links = get_nodes_links(context)
2814 nodes_list = [node for node in nodes]
2815 if nodes_list:
2816 nodes_list.sort(key=lambda k: k.location.x)
2817 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
2818 yloc = 0
2819 for node in nodes:
2820 node.select = False
2821 yloc += node_mid_pt(node, 'y')
2822 yloc = yloc/len(nodes)
2823 else:
2824 xloc = 0
2825 yloc = 0
2827 if context.space_data.node_tree.type == 'SHADER':
2828 node_type = "ShaderNodeTexImage"
2829 elif context.space_data.node_tree.type == 'COMPOSITING':
2830 node_type = "CompositorNodeImage"
2831 else:
2832 self.report({'ERROR'}, "Unsupported Node Tree type!")
2833 return {'CANCELLED'}
2835 new_nodes = []
2836 for f in self.files:
2837 fname = f.name
2839 node = nodes.new(node_type)
2840 new_nodes.append(node)
2841 node.label = fname
2842 node.hide = True
2843 node.width_hidden = 100
2844 node.location.x = xloc
2845 node.location.y = yloc
2846 yloc -= 40
2848 img = bpy.data.images.load(self.directory+fname)
2849 node.image = img
2851 # shift new nodes up to center of tree
2852 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
2853 for node in new_nodes:
2854 node.select = True
2855 node.location.y += (list_size/2)
2856 return {'FINISHED'}
2860 # P A N E L
2863 def drawlayout(context, layout, mode='non-panel'):
2864 tree_type = context.space_data.tree_type
2866 col = layout.column(align=True)
2867 col.menu(NWMergeNodesMenu.bl_idname)
2868 col.separator()
2870 col = layout.column(align=True)
2871 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
2872 col.separator()
2874 if tree_type == 'ShaderNodeTree':
2875 col = layout.column(align=True)
2876 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
2877 col.separator()
2879 col = layout.column(align=True)
2880 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
2881 col.operator(NWSwapLinks.bl_idname)
2882 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
2883 col.separator()
2885 col = layout.column(align=True)
2886 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
2887 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
2888 col.separator()
2890 col = layout.column(align=True)
2891 if mode == 'panel':
2892 row = col.row(align=True)
2893 row.operator(NWClearLabel.bl_idname).option = True
2894 row.operator(NWModifyLabels.bl_idname)
2895 else:
2896 col.operator(NWClearLabel.bl_idname).option = True
2897 col.operator(NWModifyLabels.bl_idname)
2898 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
2899 col.separator()
2900 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
2901 col.separator()
2903 col = layout.column(align=True)
2904 if tree_type == 'CompositorNodeTree':
2905 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
2906 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
2907 col.separator()
2909 col = layout.column(align=True)
2910 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
2911 col.separator()
2913 col = layout.column(align=True)
2914 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
2915 col.separator()
2918 class NodeWranglerPanel(Panel, NWBase):
2919 bl_idname = "NODE_PT_nw_node_wrangler"
2920 bl_space_type = 'NODE_EDITOR'
2921 bl_region_type = 'UI'
2922 bl_label = "Node Wrangler"
2924 prepend = StringProperty(
2925 name='prepend',
2927 append = StringProperty()
2928 remove = StringProperty()
2930 def draw(self, context):
2931 self.layout.label(text="(Quick access: Ctrl+Space)")
2932 drawlayout(context, self.layout, mode='panel')
2936 # M E N U S
2938 class NodeWranglerMenu(Menu, NWBase):
2939 bl_idname = "NODE_MT_nw_node_wrangler_menu"
2940 bl_label = "Node Wrangler"
2942 def draw(self, context):
2943 drawlayout(context, self.layout)
2946 class NWMergeNodesMenu(Menu, NWBase):
2947 bl_idname = "NODE_MT_nw_merge_nodes_menu"
2948 bl_label = "Merge Selected Nodes"
2950 def draw(self, context):
2951 type = context.space_data.tree_type
2952 layout = self.layout
2953 if type == 'ShaderNodeTree':
2954 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
2955 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
2956 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
2957 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
2958 props.mode = 'MIX'
2959 props.merge_type = 'ZCOMBINE'
2962 class NWMergeShadersMenu(Menu, NWBase):
2963 bl_idname = "NODE_MT_nw_merge_shaders_menu"
2964 bl_label = "Merge Selected Nodes using Shaders"
2966 def draw(self, context):
2967 layout = self.layout
2968 for type in ('MIX', 'ADD'):
2969 props = layout.operator(NWMergeNodes.bl_idname, text=type)
2970 props.mode = type
2971 props.merge_type = 'SHADER'
2974 class NWMergeMixMenu(Menu, NWBase):
2975 bl_idname = "NODE_MT_nw_merge_mix_menu"
2976 bl_label = "Merge Selected Nodes using Mix"
2978 def draw(self, context):
2979 layout = self.layout
2980 for type, name, description in blend_types:
2981 props = layout.operator(NWMergeNodes.bl_idname, text=name)
2982 props.mode = type
2983 props.merge_type = 'MIX'
2986 class NWConnectionListOutputs(Menu, NWBase):
2987 bl_idname = "NODE_MT_nw_connection_list_out"
2988 bl_label = "From:"
2990 def draw(self, context):
2991 layout = self.layout
2992 nodes, links = get_nodes_links(context)
2994 n1 = nodes[context.scene.NWLazySource]
2996 if n1.type == "R_LAYERS":
2997 index=0
2998 for o in n1.outputs:
2999 if o.enabled: # Check which passes the render layer has enabled
3000 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3001 index+=1
3002 else:
3003 index=0
3004 for o in n1.outputs:
3005 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3006 index+=1
3009 class NWConnectionListInputs(Menu, NWBase):
3010 bl_idname = "NODE_MT_nw_connection_list_in"
3011 bl_label = "To:"
3013 def draw(self, context):
3014 layout = self.layout
3015 nodes, links = get_nodes_links(context)
3017 n2 = nodes[context.scene.NWLazyTarget]
3019 #print (self.from_socket)
3021 index = 0
3022 for i in n2.inputs:
3023 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
3024 op.from_socket = context.scene.NWSourceSocket
3025 op.to_socket = index
3026 index+=1
3029 class NWMergeMathMenu(Menu, NWBase):
3030 bl_idname = "NODE_MT_nw_merge_math_menu"
3031 bl_label = "Merge Selected Nodes using Math"
3033 def draw(self, context):
3034 layout = self.layout
3035 for type, name, description in operations:
3036 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3037 props.mode = type
3038 props.merge_type = 'MATH'
3041 class NWBatchChangeNodesMenu(Menu, NWBase):
3042 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3043 bl_label = "Batch Change Selected Nodes"
3045 def draw(self, context):
3046 layout = self.layout
3047 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3048 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3051 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3052 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3053 bl_label = "Batch Change Blend Type"
3055 def draw(self, context):
3056 layout = self.layout
3057 for type, name, description in blend_types:
3058 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3059 props.blend_type = type
3060 props.operation = 'CURRENT'
3063 class NWBatchChangeOperationMenu(Menu, NWBase):
3064 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3065 bl_label = "Batch Change Math Operation"
3067 def draw(self, context):
3068 layout = self.layout
3069 for type, name, description in operations:
3070 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3071 props.blend_type = 'CURRENT'
3072 props.operation = type
3075 class NWCopyToSelectedMenu(Menu, NWBase):
3076 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
3077 bl_label = "Copy to Selected"
3079 def draw(self, context):
3080 layout = self.layout
3081 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
3082 layout.menu(NWCopyLabelMenu.bl_idname)
3085 class NWCopyLabelMenu(Menu, NWBase):
3086 bl_idname = "NODE_MT_nw_copy_label_menu"
3087 bl_label = "Copy Label"
3089 def draw(self, context):
3090 layout = self.layout
3091 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
3092 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
3093 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
3096 class NWAddReroutesMenu(Menu, NWBase):
3097 bl_idname = "NODE_MT_nw_add_reroutes_menu"
3098 bl_label = "Add Reroutes"
3099 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
3101 def draw(self, context):
3102 layout = self.layout
3103 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
3104 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
3105 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
3108 class NWLinkActiveToSelectedMenu(Menu, NWBase):
3109 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
3110 bl_label = "Link Active to Selected"
3112 def draw(self, context):
3113 layout = self.layout
3114 layout.menu(NWLinkStandardMenu.bl_idname)
3115 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
3116 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
3119 class NWLinkStandardMenu(Menu, NWBase):
3120 bl_idname = "NODE_MT_nw_link_standard_menu"
3121 bl_label = "To All Selected"
3123 def draw(self, context):
3124 layout = self.layout
3125 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3126 props.replace = False
3127 props.use_node_name = False
3128 props.use_outputs_names = False
3129 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3130 props.replace = True
3131 props.use_node_name = False
3132 props.use_outputs_names = False
3135 class NWLinkUseNodeNameMenu(Menu, NWBase):
3136 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
3137 bl_label = "Use Node Name/Label"
3139 def draw(self, context):
3140 layout = self.layout
3141 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3142 props.replace = False
3143 props.use_node_name = True
3144 props.use_outputs_names = False
3145 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3146 props.replace = True
3147 props.use_node_name = True
3148 props.use_outputs_names = False
3151 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
3152 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
3153 bl_label = "Use Outputs Names"
3155 def draw(self, context):
3156 layout = self.layout
3157 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3158 props.replace = False
3159 props.use_node_name = False
3160 props.use_outputs_names = True
3161 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3162 props.replace = True
3163 props.use_node_name = False
3164 props.use_outputs_names = True
3167 class NWNodeAlignMenu(Menu, NWBase):
3168 bl_idname = "NODE_MT_nw_node_align_menu"
3169 bl_label = "Align Nodes"
3171 def draw(self, context):
3172 layout = self.layout
3173 layout.operator(NWAlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X'
3174 layout.operator(NWAlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y'
3177 class NWVertColMenu(bpy.types.Menu):
3178 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
3179 bl_label = "Vertex Colors"
3181 @classmethod
3182 def poll(cls, context):
3183 if context.area.spaces[0].node_tree:
3184 if context.area.spaces[0].node_tree.type == 'SHADER':
3185 return True
3186 else:
3187 return False
3188 else:
3189 return False
3191 def draw(self, context):
3192 l = self.layout
3193 nodes, links = get_nodes_links(context)
3194 mat = context.object.active_material
3196 objs = []
3197 for obj in bpy.data.objects:
3198 for slot in obj.material_slots:
3199 if slot.material == mat:
3200 objs.append(obj)
3201 vcols = []
3202 for obj in objs:
3203 if obj.data.vertex_colors:
3204 for vcol in obj.data.vertex_colors:
3205 vcols.append(vcol.name)
3206 vcols = list(set(vcols)) # get a unique list
3208 if vcols:
3209 for vcol in vcols:
3210 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
3211 else:
3212 l.label("No Vertex Color layers on objects with this material")
3215 class NWSwitchNodeTypeMenu(Menu, NWBase):
3216 bl_idname = "NODE_MT_nw_switch_node_type_menu"
3217 bl_label = "Switch Type to..."
3219 def draw(self, context):
3220 layout = self.layout
3221 tree = context.space_data.node_tree
3222 if tree.type == 'SHADER':
3223 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
3224 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
3225 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
3226 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
3227 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
3228 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
3229 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
3230 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
3231 if tree.type == 'COMPOSITING':
3232 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
3233 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
3234 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
3235 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
3236 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
3237 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
3238 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
3239 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
3240 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
3243 class NWSwitchShadersInputSubmenu(Menu, NWBase):
3244 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
3245 bl_label = "Input"
3247 def draw(self, context):
3248 layout = self.layout
3249 for ident, type, rna_name in shaders_input_nodes_props:
3250 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3251 props.to_type = ident
3254 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
3255 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
3256 bl_label = "Output"
3258 def draw(self, context):
3259 layout = self.layout
3260 for ident, type, rna_name in shaders_output_nodes_props:
3261 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3262 props.to_type = ident
3265 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
3266 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
3267 bl_label = "Shader"
3269 def draw(self, context):
3270 layout = self.layout
3271 for ident, type, rna_name in shaders_shader_nodes_props:
3272 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3273 props.to_type = ident
3276 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
3277 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
3278 bl_label = "Texture"
3280 def draw(self, context):
3281 layout = self.layout
3282 for ident, type, rna_name in shaders_texture_nodes_props:
3283 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3284 props.to_type = ident
3287 class NWSwitchShadersColorSubmenu(Menu, NWBase):
3288 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
3289 bl_label = "Color"
3291 def draw(self, context):
3292 layout = self.layout
3293 for ident, type, rna_name in shaders_color_nodes_props:
3294 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3295 props.to_type = ident
3298 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
3299 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
3300 bl_label = "Vector"
3302 def draw(self, context):
3303 layout = self.layout
3304 for ident, type, rna_name in shaders_vector_nodes_props:
3305 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3306 props.to_type = ident
3309 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
3310 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
3311 bl_label = "Converter"
3313 def draw(self, context):
3314 layout = self.layout
3315 for ident, type, rna_name in shaders_converter_nodes_props:
3316 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3317 props.to_type = ident
3320 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
3321 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
3322 bl_label = "Layout"
3324 def draw(self, context):
3325 layout = self.layout
3326 for ident, type, rna_name in shaders_layout_nodes_props:
3327 if type != 'FRAME':
3328 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3329 props.to_type = ident
3332 class NWSwitchCompoInputSubmenu(Menu, NWBase):
3333 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
3334 bl_label = "Input"
3336 def draw(self, context):
3337 layout = self.layout
3338 for ident, type, rna_name in compo_input_nodes_props:
3339 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3340 props.to_type = ident
3343 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
3344 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
3345 bl_label = "Output"
3347 def draw(self, context):
3348 layout = self.layout
3349 for ident, type, rna_name in compo_output_nodes_props:
3350 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3351 props.to_type = ident
3354 class NWSwitchCompoColorSubmenu(Menu, NWBase):
3355 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
3356 bl_label = "Color"
3358 def draw(self, context):
3359 layout = self.layout
3360 for ident, type, rna_name in compo_color_nodes_props:
3361 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3362 props.to_type = ident
3365 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
3366 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
3367 bl_label = "Converter"
3369 def draw(self, context):
3370 layout = self.layout
3371 for ident, type, rna_name in compo_converter_nodes_props:
3372 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3373 props.to_type = ident
3376 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
3377 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
3378 bl_label = "Filter"
3380 def draw(self, context):
3381 layout = self.layout
3382 for ident, type, rna_name in compo_filter_nodes_props:
3383 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3384 props.to_type = ident
3387 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
3388 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
3389 bl_label = "Vector"
3391 def draw(self, context):
3392 layout = self.layout
3393 for ident, type, rna_name in compo_vector_nodes_props:
3394 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3395 props.to_type = ident
3398 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
3399 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
3400 bl_label = "Matte"
3402 def draw(self, context):
3403 layout = self.layout
3404 for ident, type, rna_name in compo_matte_nodes_props:
3405 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3406 props.to_type = ident
3409 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
3410 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
3411 bl_label = "Distort"
3413 def draw(self, context):
3414 layout = self.layout
3415 for ident, type, rna_name in compo_distort_nodes_props:
3416 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3417 props.to_type = ident
3420 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
3421 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
3422 bl_label = "Layout"
3424 def draw(self, context):
3425 layout = self.layout
3426 for ident, type, rna_name in compo_layout_nodes_props:
3427 if type != 'FRAME':
3428 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3429 props.to_type = ident
3433 # APPENDAGES TO EXISTING UI
3437 def select_parent_children_buttons(self, context):
3438 layout = self.layout
3439 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
3440 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
3443 def attr_nodes_menu_func(self, context):
3444 col = self.layout.column(align=True)
3445 col.menu("NODE_MT_nw_node_vertex_color_menu")
3446 col.separator()
3449 def multipleimages_menu_func(self, context):
3450 col = self.layout.column(align=True)
3451 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
3452 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
3453 col.separator()
3456 def bgreset_menu_func(self, context):
3457 self.layout.operator(NWResetBG.bl_idname)
3461 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
3464 addon_keymaps = []
3465 # kmi_defs entry: (identifier, key, CTRL, SHIFT, ALT, props, nice name)
3466 # props entry: (property name, property value)
3467 kmi_defs = (
3468 # MERGE NODES
3469 # NWMergeNodes with Ctrl (AUTO).
3470 (NWMergeNodes.bl_idname, 'NUMPAD_0', True, False, False,
3471 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
3472 (NWMergeNodes.bl_idname, 'ZERO', True, False, False,
3473 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
3474 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, False,
3475 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
3476 (NWMergeNodes.bl_idname, 'EQUAL', True, False, False,
3477 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
3478 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, False,
3479 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
3480 (NWMergeNodes.bl_idname, 'EIGHT', True, False, False,
3481 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
3482 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, False,
3483 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
3484 (NWMergeNodes.bl_idname, 'MINUS', True, False, False,
3485 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
3486 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, False,
3487 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
3488 (NWMergeNodes.bl_idname, 'SLASH', True, False, False,
3489 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
3490 (NWMergeNodes.bl_idname, 'COMMA', True, False, False,
3491 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
3492 (NWMergeNodes.bl_idname, 'PERIOD', True, False, False,
3493 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
3494 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', True, False, False,
3495 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
3496 # NWMergeNodes with Ctrl Alt (MIX)
3497 (NWMergeNodes.bl_idname, 'NUMPAD_0', True, False, True,
3498 (('mode', 'MIX'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Mix)"),
3499 (NWMergeNodes.bl_idname, 'ZERO', True, False, True,
3500 (('mode', 'MIX'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Mix)"),
3501 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, True,
3502 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
3503 (NWMergeNodes.bl_idname, 'EQUAL', True, False, True,
3504 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
3505 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, True,
3506 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
3507 (NWMergeNodes.bl_idname, 'EIGHT', True, False, True,
3508 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
3509 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, True,
3510 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
3511 (NWMergeNodes.bl_idname, 'MINUS', True, False, True,
3512 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
3513 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, True,
3514 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
3515 (NWMergeNodes.bl_idname, 'SLASH', True, False, True,
3516 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
3517 # NWMergeNodes with Ctrl Shift (MATH)
3518 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, True, False,
3519 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
3520 (NWMergeNodes.bl_idname, 'EQUAL', True, True, False,
3521 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
3522 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, True, False,
3523 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
3524 (NWMergeNodes.bl_idname, 'EIGHT', True, True, False,
3525 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
3526 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, True, False,
3527 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
3528 (NWMergeNodes.bl_idname, 'MINUS', True, True, False,
3529 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
3530 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, True, False,
3531 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
3532 (NWMergeNodes.bl_idname, 'SLASH', True, True, False,
3533 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
3534 (NWMergeNodes.bl_idname, 'COMMA', True, True, False,
3535 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
3536 (NWMergeNodes.bl_idname, 'PERIOD', True, True, False,
3537 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
3538 # BATCH CHANGE NODES
3539 # NWBatchChangeNodes with Alt
3540 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', False, False, True,
3541 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
3542 (NWBatchChangeNodes.bl_idname, 'ZERO', False, False, True,
3543 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
3544 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', False, False, True,
3545 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
3546 (NWBatchChangeNodes.bl_idname, 'EQUAL', False, False, True,
3547 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
3548 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', False, False, True,
3549 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
3550 (NWBatchChangeNodes.bl_idname, 'EIGHT', False, False, True,
3551 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
3552 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', False, False, True,
3553 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
3554 (NWBatchChangeNodes.bl_idname, 'MINUS', False, False, True,
3555 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
3556 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', False, False, True,
3557 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
3558 (NWBatchChangeNodes.bl_idname, 'SLASH', False, False, True,
3559 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
3560 (NWBatchChangeNodes.bl_idname, 'COMMA', False, False, True,
3561 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
3562 (NWBatchChangeNodes.bl_idname, 'PERIOD', False, False, True,
3563 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
3564 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', False, False, True,
3565 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
3566 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', False, False, True,
3567 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
3568 # LINK ACTIVE TO SELECTED
3569 # Don't use names, don't replace links (K)
3570 (NWLinkActiveToSelected.bl_idname, 'K', False, False, False,
3571 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
3572 # Don't use names, replace links (Shift K)
3573 (NWLinkActiveToSelected.bl_idname, 'K', False, True, False,
3574 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
3575 # Use node name, don't replace links (')
3576 (NWLinkActiveToSelected.bl_idname, 'QUOTE', False, False, False,
3577 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
3578 # Use node name, replace links (Shift ')
3579 (NWLinkActiveToSelected.bl_idname, 'QUOTE', False, True, False,
3580 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
3581 # Don't use names, don't replace links (;)
3582 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, False, False,
3583 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
3584 # Don't use names, replace links (')
3585 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, True, False,
3586 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
3587 # CHANGE MIX FACTOR
3588 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
3589 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
3590 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
3591 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
3592 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
3593 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
3594 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
3595 (NWChangeMixFactor.bl_idname, 'ZERO', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
3596 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
3597 (NWChangeMixFactor.bl_idname, 'ONE', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
3598 # CLEAR LABEL (Alt L)
3599 (NWClearLabel.bl_idname, 'L', False, False, True, (('option', False),), "Clear node labels"),
3600 # MODIFY LABEL (Alt Shift L)
3601 (NWModifyLabels.bl_idname, 'L', False, True, True, None, "Modify node labels"),
3602 # Copy Label from active to selected
3603 (NWCopyLabel.bl_idname, 'V', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
3604 # DETACH OUTPUTS (Alt Shift D)
3605 (NWDetachOutputs.bl_idname, 'D', False, True, True, None, "Detach outputs"),
3606 # LINK TO OUTPUT NODE (O)
3607 (NWLinkToOutputNode.bl_idname, 'O', False, False, False, None, "Link to output node"),
3608 # SELECT PARENT/CHILDREN
3609 # Select Children
3610 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', False, False, False, (('option', 'CHILD'),), "Select children"),
3611 # Select Parent
3612 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', False, False, False, (('option', 'PARENT'),), "Select Parent"),
3613 # Add Texture Setup
3614 (NWAddTextureSetup.bl_idname, 'T', True, False, False, None, "Add texture setup"),
3615 # Reset backdrop
3616 (NWResetBG.bl_idname, 'Z', False, False, False, None, "Reset backdrop image zoom"),
3617 # Delete unused
3618 (NWDeleteUnused.bl_idname, 'X', False, False, True, None, "Delete unused nodes"),
3619 # Frame Seleted
3620 (NWFrameSelected.bl_idname, 'P', False, True, False, None, "Frame selected nodes"),
3621 # Swap Outputs
3622 (NWSwapLinks.bl_idname, 'S', False, False, True, None, "Swap Outputs"),
3623 # Emission Viewer
3624 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', True, True, False, None, "Connect to Cycles Viewer node"),
3625 # Reload Images
3626 (NWReloadImages.bl_idname, 'R', False, False, True, None, "Reload images"),
3627 # Lazy Mix
3628 (NWLazyMix.bl_idname, 'RIGHTMOUSE', False, False, True, None, "Lazy Mix"),
3629 # Lazy Connect
3630 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', True, False, False, None, "Lazy Connect"),
3631 # Lazy Connect with Menu
3632 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
3633 # MENUS
3634 ('wm.call_menu', 'SPACE', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"),
3635 ('wm.call_menu', 'SLASH', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
3636 ('wm.call_menu', 'NUMPAD_SLASH', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
3637 ('wm.call_menu', 'EQUAL', False, True, False, (('name', NWNodeAlignMenu.bl_idname),), "Node alignment menu"),
3638 ('wm.call_menu', 'BACK_SLASH', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
3639 ('wm.call_menu', 'C', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
3640 ('wm.call_menu', 'S', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
3644 def register():
3645 # props
3646 bpy.types.Scene.NWBusyDrawing = StringProperty(
3647 name="Busy Drawing!",
3648 default="",
3649 description="An internal property used to store only the first mouse position")
3650 bpy.types.Scene.NWLazySource = StringProperty(
3651 name="Lazy Source!",
3652 default="x",
3653 description="An internal property used to store the first node in a Lazy Connect operation")
3654 bpy.types.Scene.NWLazyTarget = StringProperty(
3655 name="Lazy Target!",
3656 default="x",
3657 description="An internal property used to store the last node in a Lazy Connect operation")
3658 bpy.types.Scene.NWSourceSocket = IntProperty(
3659 name="Source Socket!",
3660 default=0,
3661 description="An internal property used to store the source socket in a Lazy Connect operation")
3663 bpy.utils.register_module(__name__)
3665 # keymaps
3666 km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
3667 for (identifier, key, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
3668 kmi = km.keymap_items.new(identifier, key, 'PRESS', ctrl=CTRL, shift=SHIFT, alt=ALT)
3669 if props:
3670 for prop, value in props:
3671 setattr(kmi.properties, prop, value)
3672 addon_keymaps.append((km, kmi))
3674 # menu items
3675 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
3676 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
3677 bpy.types.NODE_PT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
3678 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
3679 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
3680 bpy.types.NODE_PT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
3681 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
3682 bpy.types.NODE_PT_category_CMP_INPUT.prepend(multipleimages_menu_func)
3685 def unregister():
3686 # props
3687 del bpy.types.Scene.NWBusyDrawing
3688 del bpy.types.Scene.NWLazySource
3689 del bpy.types.Scene.NWLazyTarget
3690 del bpy.types.Scene.NWSourceSocket
3692 bpy.utils.unregister_module(__name__)
3694 # keymaps
3695 for km, kmi in addon_keymaps:
3696 km.keymap_items.remove(kmi)
3697 addon_keymaps.clear()
3699 # menuitems
3700 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
3701 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
3702 bpy.types.NODE_PT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
3703 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
3704 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
3705 bpy.types.NODE_PT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
3706 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
3707 bpy.types.NODE_PT_category_CMP_INPUT.remove(multipleimages_menu_func)
3709 if __name__ == "__main__":
3710 register()