Fix T38788: zero area faces raised exception with overhang test
[blender-addons.git] / node_efficiency_tools.py
blob9d53e77985ff696e6fca17eef7741974c9616d4b
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, 2),
23 'blender': (2, 69, 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
36 from mathutils import Vector
37 from math import cos, sin, pi, sqrt
39 #################
40 # rl_outputs:
41 # list of outputs of Input Render Layer
42 # with attributes determinig if pass is used,
43 # and MultiLayer EXR outputs names and corresponding render engines
45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles)
46 rl_outputs = (
47 ('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
48 ('use_pass_color', 'Color', 'Color', True, False),
49 ('use_pass_combined', 'Image', 'Combined', True, True),
50 ('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False),
51 ('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
52 ('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
53 ('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
54 ('use_pass_emit', 'Emit', 'Emit', True, False),
55 ('use_pass_environment', 'Environment', 'Env', True, False),
56 ('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
57 ('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
58 ('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
59 ('use_pass_indirect', 'Indirect', 'Indirect', True, False),
60 ('use_pass_material_index', 'IndexMA', 'IndexMA', True, True),
61 ('use_pass_mist', 'Mist', 'Mist', True, False),
62 ('use_pass_normal', 'Normal', 'Normal', True, True),
63 ('use_pass_object_index', 'IndexOB', 'IndexOB', True, True),
64 ('use_pass_reflection', 'Reflect', 'Reflect', True, False),
65 ('use_pass_refraction', 'Refract', 'Refract', True, False),
66 ('use_pass_shadow', 'Shadow', 'Shadow', True, True),
67 ('use_pass_specular', 'Specular', 'Spec', True, False),
68 ('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', False, True),
69 ('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', False, True),
70 ('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
71 ('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
72 ('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
73 ('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
74 ('use_pass_uv', 'UV', 'UV', True, True),
75 ('use_pass_vector', 'Speed', 'Vector', True, True),
76 ('use_pass_z', 'Z', 'Depth', True, True),
79 # shader nodes
80 # (rna_type.identifier, type, rna_type.name)
81 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
82 shaders_input_nodes_props = (
83 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
84 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
85 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
86 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
87 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
88 ('ShaderNodeRGB', 'RGB', 'RGB'),
89 ('ShaderNodeValue', 'VALUE', 'Value'),
90 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
91 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
92 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
93 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
94 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
95 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
96 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
98 # (rna_type.identifier, type, rna_type.name)
99 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
100 shaders_output_nodes_props = (
101 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
102 ('ShaderNodeOutputLamp', 'OUTPUT_LAMP', 'Lamp Output'),
103 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
105 # (rna_type.identifier, type, rna_type.name)
106 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
107 shaders_shader_nodes_props = (
108 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
109 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
110 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
111 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
112 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
113 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
114 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
115 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
116 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
117 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
118 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
119 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
120 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
121 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
122 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
123 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
124 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
125 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
126 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
128 # (rna_type.identifier, type, rna_type.name)
129 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
130 shaders_texture_nodes_props = (
131 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image'),
132 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment'),
133 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky'),
134 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise'),
135 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave'),
136 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
137 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
138 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient'),
139 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic'),
140 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker'),
141 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick')
143 # (rna_type.identifier, type, rna_type.name)
144 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
145 shaders_color_nodes_props = (
146 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
147 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
148 ('ShaderNodeInvert', 'INVERT', 'Invert'),
149 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
150 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
151 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
152 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
154 # (rna_type.identifier, type, rna_type.name)
155 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
156 shaders_vector_nodes_props = (
157 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
158 ('ShaderNodeBump', 'BUMP', 'Bump'),
159 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
160 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
161 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
162 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
164 # (rna_type.identifier, type, rna_type.name)
165 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
166 shaders_converter_nodes_props = (
167 ('ShaderNodeMath', 'MATH', 'Math'),
168 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
169 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
170 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
171 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
172 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
173 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
174 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
175 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
176 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
178 # (rna_type.identifier, type, rna_type.name)
179 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
180 shaders_layout_nodes_props = (
181 ('NodeFrame', 'FRAME', 'Frame'),
182 ('NodeReroute', 'REROUTE', 'Reroute'),
185 # compositing nodes
186 # (rna_type.identifier, type, rna_type.name)
187 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
188 compo_input_nodes_props = (
189 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
190 ('CompositorNodeImage', 'IMAGE', 'Image'),
191 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
192 ('CompositorNodeMask', 'MASK', 'Mask'),
193 ('CompositorNodeRGB', 'RGB', 'RGB'),
194 ('CompositorNodeValue', 'VALUE', 'Value'),
195 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
196 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
197 ('CompositorNodeTime', 'TIME', 'Time'),
198 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
200 # (rna_type.identifier, type, rna_type.name)
201 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
202 compo_output_nodes_props = (
203 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
204 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
205 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
206 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
207 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
209 # (rna_type.identifier, type, rna_type.name)
210 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
211 compo_color_nodes_props = (
212 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
213 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
214 ('CompositorNodeInvert', 'INVERT', 'Invert'),
215 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
216 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
217 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
218 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
219 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
220 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
221 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
222 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
223 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
225 # (rna_type.identifier, type, rna_type.name)
226 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
227 compo_converter_nodes_props = (
228 ('CompositorNodeMath', 'MATH', 'Math'),
229 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
230 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
231 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
232 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
233 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
234 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
235 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
236 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
237 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
238 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
239 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
240 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
241 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
243 # (rna_type.identifier, type, rna_type.name)
244 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
245 compo_filter_nodes_props = (
246 ('CompositorNodeBlur', 'BLUR', 'Blur'),
247 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
248 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
249 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
250 ('CompositorNodeFilter', 'FILTER', 'Filter'),
251 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
252 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
253 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
254 ('CompositorNodeGlare', 'GLARE', 'Glare'),
255 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
256 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
257 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
259 # (rna_type.identifier, type, rna_type.name)
260 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
261 compo_vector_nodes_props = (
262 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
263 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
264 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
265 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
266 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
268 # (rna_type.identifier, type, rna_type.name)
269 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
270 compo_matte_nodes_props = (
271 ('CompositorNodeKeying', 'KEYING', 'Keying'),
272 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
273 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
274 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
275 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
276 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
277 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
278 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
279 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
280 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
281 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
282 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
284 # (rna_type.identifier, type, rna_type.name)
285 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
286 compo_distort_nodes_props = (
287 ('CompositorNodeScale', 'SCALE', 'Scale'),
288 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
289 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
290 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
291 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
292 ('CompositorNodeFlip', 'FLIP', 'Flip'),
293 ('CompositorNodeCrop', 'CROP', 'Crop'),
294 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
295 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
296 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
297 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
298 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
300 # (rna_type.identifier, type, rna_type.name)
301 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
302 compo_layout_nodes_props = (
303 ('NodeFrame', 'FRAME', 'Frame'),
304 ('NodeReroute', 'REROUTE', 'Reroute'),
305 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
308 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
309 # used list, not tuple for easy merging with other lists.
310 blend_types = [
311 ('MIX', 'Mix', 'Mix Mode'),
312 ('ADD', 'Add', 'Add Mode'),
313 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
314 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
315 ('SCREEN', 'Screen', 'Screen Mode'),
316 ('DIVIDE', 'Divide', 'Divide Mode'),
317 ('DIFFERENCE', 'Difference', 'Difference Mode'),
318 ('DARKEN', 'Darken', 'Darken Mode'),
319 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
320 ('OVERLAY', 'Overlay', 'Overlay Mode'),
321 ('DODGE', 'Dodge', 'Dodge Mode'),
322 ('BURN', 'Burn', 'Burn Mode'),
323 ('HUE', 'Hue', 'Hue Mode'),
324 ('SATURATION', 'Saturation', 'Saturation Mode'),
325 ('VALUE', 'Value', 'Value Mode'),
326 ('COLOR', 'Color', 'Color Mode'),
327 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
328 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
331 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
332 # used list, not tuple for easy merging with other lists.
333 operations = [
334 ('ADD', 'Add', 'Add Mode'),
335 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
336 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
337 ('DIVIDE', 'Divide', 'Divide Mode'),
338 ('SINE', 'Sine', 'Sine Mode'),
339 ('COSINE', 'Cosine', 'Cosine Mode'),
340 ('TANGENT', 'Tangent', 'Tangent Mode'),
341 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
342 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
343 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
344 ('POWER', 'Power', 'Power Mode'),
345 ('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
346 ('MINIMUM', 'Minimum', 'Minimum Mode'),
347 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
348 ('ROUND', 'Round', 'Round Mode'),
349 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
350 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
353 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
354 # used list, not tuple for easy merging with other lists.
355 navs = [
356 ('CURRENT', 'Current', 'Leave at current state'),
357 ('NEXT', 'Next', 'Next blend type/operation'),
358 ('PREV', 'Prev', 'Previous blend type/operation'),
361 draw_color_sets = {
362 "red_white": (
363 (1.0, 1.0, 1.0, 0.7),
364 (1.0, 0.0, 0.0, 0.7),
365 (0.8, 0.2, 0.2, 1.0)
367 "green": (
368 (0.0, 0.0, 0.0, 1.0),
369 (0.38, 0.77, 0.38, 1.0),
370 (0.38, 0.77, 0.38, 1.0)
372 "yellow": (
373 (0.0, 0.0, 0.0, 1.0),
374 (0.77, 0.77, 0.16, 1.0),
375 (0.77, 0.77, 0.16, 1.0)
377 "purple": (
378 (0.0, 0.0, 0.0, 1.0),
379 (0.38, 0.38, 0.77, 1.0),
380 (0.38, 0.38, 0.77, 1.0)
382 "grey": (
383 (0.0, 0.0, 0.0, 1.0),
384 (0.63, 0.63, 0.63, 1.0),
385 (0.63, 0.63, 0.63, 1.0)
387 "black": (
388 (1.0, 1.0, 1.0, 0.7),
389 (0.0, 0.0, 0.0, 0.7),
390 (0.2, 0.2, 0.2, 1.0)
395 def nice_hotkey_name(punc):
396 # convert the ugly string name into the actual character
397 pairs = (
398 ('LEFTMOUSE', "LMB"),
399 ('MIDDLEMOUSE', "MMB"),
400 ('RIGHTMOUSE', "RMB"),
401 ('SELECTMOUSE', "Select"),
402 ('WHEELUPMOUSE', "Wheel Up"),
403 ('WHEELDOWNMOUSE', "Wheel Down"),
404 ('WHEELINMOUSE', "Wheel In"),
405 ('WHEELOUTMOUSE', "Wheel Out"),
406 ('ZERO', "0"),
407 ('ONE', "1"),
408 ('TWO', "2"),
409 ('THREE', "3"),
410 ('FOUR', "4"),
411 ('FIVE', "5"),
412 ('SIX', "6"),
413 ('SEVEN', "7"),
414 ('EIGHT', "8"),
415 ('NINE', "9"),
416 ('OSKEY', "Super"),
417 ('RET', "Enter"),
418 ('LINE_FEED', "Enter"),
419 ('SEMI_COLON', ";"),
420 ('PERIOD', "."),
421 ('COMMA', ","),
422 ('QUOTE', '"'),
423 ('MINUS', "-"),
424 ('SLASH', "/"),
425 ('BACK_SLASH', "\\"),
426 ('EQUAL', "="),
427 ('NUMPAD_1', "Numpad 1"),
428 ('NUMPAD_2', "Numpad 2"),
429 ('NUMPAD_3', "Numpad 3"),
430 ('NUMPAD_4', "Numpad 4"),
431 ('NUMPAD_5', "Numpad 5"),
432 ('NUMPAD_6', "Numpad 6"),
433 ('NUMPAD_7', "Numpad 7"),
434 ('NUMPAD_8', "Numpad 8"),
435 ('NUMPAD_9', "Numpad 9"),
436 ('NUMPAD_0', "Numpad 0"),
437 ('NUMPAD_PERIOD', "Numpad ."),
438 ('NUMPAD_SLASH', "Numpad /"),
439 ('NUMPAD_ASTERIX', "Numpad *"),
440 ('NUMPAD_MINUS', "Numpad -"),
441 ('NUMPAD_ENTER', "Numpad Enter"),
442 ('NUMPAD_PLUS', "Numpad +"),
444 nice_punc = False
445 for (ugly, nice) in pairs:
446 if punc == ugly:
447 nice_punc = nice
448 break
449 if not nice_punc:
450 nice_punc = punc.replace("_", " ").title()
451 return nice_punc
454 def hack_force_update(context, nodes):
455 if context.space_data.tree_type == "ShaderNodeTree":
456 node = nodes.new('ShaderNodeMath')
457 node.inputs[0].default_value = 0.0
458 nodes.remove(node)
459 return False
462 def dpifac():
463 return bpy.context.user_preferences.system.dpi/72
466 def is_end_node(node):
467 bool = True
468 for output in node.outputs:
469 if output.links:
470 bool = False
471 break
472 return bool
475 def node_mid_pt(node, axis):
476 if axis == 'x':
477 d = node.location.x + (node.dimensions.x / 2)
478 elif axis == 'y':
479 d = node.location.y - (node.dimensions.y / 2)
480 else:
481 d = 0
482 return d
485 def autolink(node1, node2, links):
486 link_made = False
488 for outp in node1.outputs:
489 for inp in node2.inputs:
490 if not inp.is_linked and inp.name == outp.name:
491 link_made = True
492 links.new(outp, inp)
493 return True
495 for outp in node1.outputs:
496 for inp in node2.inputs:
497 if not inp.is_linked and inp.type == outp.type:
498 link_made = True
499 links.new(outp, inp)
500 return True
502 # force some connection even if the type doesn't match
503 for outp in node1.outputs:
504 for inp in node2.inputs:
505 if not inp.is_linked:
506 link_made = True
507 links.new(outp, inp)
508 return True
510 # even if no sockets are open, force one of matching type
511 for outp in node1.outputs:
512 for inp in node2.inputs:
513 if inp.type == outp.type:
514 link_made = True
515 links.new(outp, inp)
516 return True
518 # do something!
519 for outp in node1.outputs:
520 for inp in node2.inputs:
521 link_made = True
522 links.new(outp, inp)
523 return True
525 print("Could not make a link from " + node1.name + " to " + node2.name)
526 return link_made
529 def node_at_pos(nodes, context, event):
530 nodes_near_mouse = []
531 nodes_under_mouse = []
532 target_node = None
534 store_mouse_cursor(context, event)
535 x, y = context.space_data.cursor_location
536 x = x
537 y = y
539 # Make a list of each corner (and middle of border) for each node.
540 # Will be sorted to find nearest point and thus nearest node
541 node_points_with_dist = []
542 for node in nodes:
543 locx = node.location.x
544 locy = node.location.y
545 dimx = node.dimensions.x/dpifac()
546 dimy = node.dimensions.y/dpifac()
547 node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - locy) ** 2)]) # Top Left
548 node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - locy) ** 2)]) # Top Right
549 node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - (locy-dimy)) ** 2)]) # Bottom Left
550 node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - (locy-dimy)) ** 2)]) # Bottom Right
552 node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - locy) ** 2)]) # Mid Top
553 node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - (locy-dimy)) ** 2)]) # Mid Bottom
554 node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Mid Left
555 node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Mid Right
557 #node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Center
559 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
561 for node in nodes:
562 locx = node.location.x
563 locy = node.location.y
564 dimx = node.dimensions.x/dpifac()
565 dimy = node.dimensions.y/dpifac()
566 if (locx <= x <= locx + dimx) and \
567 (locy - dimy <= y <= locy):
568 nodes_under_mouse.append(node)
570 if len(nodes_under_mouse) == 1:
571 if nodes_under_mouse[0] != nearest_node:
572 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
573 else:
574 target_node = nearest_node # else use the nearest node
575 else:
576 target_node = nearest_node
577 return target_node
580 def store_mouse_cursor(context, event):
581 space = context.space_data
582 v2d = context.region.view2d
583 tree = space.edit_tree
585 # convert mouse position to the View2D for later node placement
586 if context.region.type == 'WINDOW':
587 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
588 else:
589 space.cursor_location = tree.view_center
592 def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]):
593 bgl.glEnable(bgl.GL_BLEND)
594 bgl.glLineWidth(size)
595 bgl.glShadeModel(bgl.GL_SMOOTH)
597 bgl.glBegin(bgl.GL_LINE_STRIP)
598 try:
599 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)
600 bgl.glVertex2f(x1, y1)
601 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
602 bgl.glVertex2f(x2, y2)
603 except:
604 pass
605 bgl.glEnd()
606 bgl.glShadeModel(bgl.GL_FLAT)
609 def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]):
610 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
611 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
612 radius = radius
613 sides = 32
614 for i in range(sides + 1):
615 cosine = radius * cos(i * 2 * pi / sides) + mx
616 sine = radius * sin(i * 2 * pi / sides) + my
617 bgl.glVertex2f(cosine, sine)
618 bgl.glEnd()
621 def draw_rounded_node_border(node, radius=8, colour=[1.0, 1.0, 1.0, 0.7]):
622 bgl.glEnable(bgl.GL_BLEND)
623 settings = bpy.context.user_preferences.addons[__name__].preferences
624 if settings.bgl_antialiasing:
625 bgl.glEnable(bgl.GL_LINE_SMOOTH)
626 sides = 16
627 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
629 nlocx = (node.location.x+1)*dpifac()
630 nlocy = (node.location.y+1)*dpifac()
631 ndimx = node.dimensions.x
632 ndimy = node.dimensions.y
634 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
635 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy)
636 bgl.glVertex2f(mx,my)
637 for i in range(sides+1):
638 if (4<=i<=8):
639 if mx != 12000 and my != 12000: # nodes that go over the view border give 12000 as coords
640 cosine = radius * cos(i * 2 * pi / sides) + mx
641 sine = radius * sin(i * 2 * pi / sides) + my
642 bgl.glVertex2f(cosine, sine)
643 bgl.glEnd()
645 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
646 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy)
647 bgl.glVertex2f(mx,my)
648 for i in range(sides+1):
649 if (0<=i<=4):
650 if mx != 12000 and my != 12000:
651 cosine = radius * cos(i * 2 * pi / sides) + mx
652 sine = radius * sin(i * 2 * pi / sides) + my
653 bgl.glVertex2f(cosine, sine)
655 bgl.glEnd()
656 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
657 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy)
658 bgl.glVertex2f(mx,my)
659 for i in range(sides+1):
660 if (8<=i<=12):
661 if mx != 12000 and my != 12000:
662 cosine = radius * cos(i * 2 * pi / sides) + mx
663 sine = radius * sin(i * 2 * pi / sides) + my
664 bgl.glVertex2f(cosine, sine)
665 bgl.glEnd()
667 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
668 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy)
669 bgl.glVertex2f(mx,my)
670 for i in range(sides+1):
671 if (12<=i<=16):
672 if mx != 12000 and my != 12000:
673 cosine = radius * cos(i * 2 * pi / sides) + mx
674 sine = radius * sin(i * 2 * pi / sides) + my
675 bgl.glVertex2f(cosine, sine)
676 bgl.glEnd()
679 bgl.glBegin(bgl.GL_QUADS)
680 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy)
681 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy)
682 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
683 bgl.glVertex2f(m2x-radius,m2y) # draw order is important, start with bottom left and go anti-clockwise
684 bgl.glVertex2f(m2x,m2y)
685 bgl.glVertex2f(m1x,m1y)
686 bgl.glVertex2f(m1x-radius,m1y)
687 bgl.glEnd()
689 bgl.glBegin(bgl.GL_QUADS)
690 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy)
691 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy)
692 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
693 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
694 bgl.glVertex2f(m2x,m2y)
695 bgl.glVertex2f(m2x,m1y+radius)
696 bgl.glVertex2f(m1x,m1y+radius)
697 bgl.glEnd()
699 bgl.glBegin(bgl.GL_QUADS)
700 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy)
701 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy)
702 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
703 bgl.glVertex2f(m2x,m2y) # draw order is important, start with bottom left and go anti-clockwise
704 bgl.glVertex2f(m2x+radius,m2y)
705 bgl.glVertex2f(m1x+radius,m1y)
706 bgl.glVertex2f(m1x,m1y)
707 bgl.glEnd()
709 bgl.glBegin(bgl.GL_QUADS)
710 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy)
711 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy)
712 if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000:
713 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
714 bgl.glVertex2f(m2x,m2y)
715 bgl.glVertex2f(m2x,m1y-radius)
716 bgl.glVertex2f(m1x,m1y-radius)
717 bgl.glEnd()
719 bgl.glDisable(bgl.GL_BLEND)
720 if settings.bgl_antialiasing:
721 bgl.glDisable(bgl.GL_LINE_SMOOTH)
724 def draw_callback_mixnodes(self, context, mode):
725 if self.mouse_path:
726 nodes = context.space_data.node_tree.nodes
727 settings = context.user_preferences.addons[__name__].preferences
728 if settings.bgl_antialiasing:
729 bgl.glEnable(bgl.GL_LINE_SMOOTH)
731 if mode == "LINK":
732 col_outer = [1.0, 0.2, 0.2, 0.4]
733 col_inner = [0.0, 0.0, 0.0, 0.5]
734 col_circle_inner = [0.3, 0.05, 0.05, 1.0]
735 if mode == "LINKMENU":
736 col_outer = [0.4, 0.6, 1.0, 0.4]
737 col_inner = [0.0, 0.0, 0.0, 0.5]
738 col_circle_inner = [0.08, 0.15, .3, 1.0]
739 elif mode == "MIX":
740 col_outer = [0.2, 1.0, 0.2, 0.4]
741 col_inner = [0.0, 0.0, 0.0, 0.5]
742 col_circle_inner = [0.05, 0.3, 0.05, 1.0]
744 m1x = self.mouse_path[0][0]
745 m1y = self.mouse_path[0][1]
746 m2x = self.mouse_path[-1][0]
747 m2y = self.mouse_path[-1][1]
749 n1 = nodes[context.scene.NWLazySource]
750 n2 = nodes[context.scene.NWLazyTarget]
752 draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
753 draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
754 draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
755 draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
757 draw_line(m1x, m1y, m2x, m2y, 4, col_outer) # line outline
758 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
760 # circle outline
761 draw_circle(m1x, m1y, 6, col_outer)
762 draw_circle(m2x, m2y, 6, col_outer)
764 # circle inner
765 draw_circle(m1x, m1y, 5, col_circle_inner)
766 draw_circle(m2x, m2y, 5, col_circle_inner)
768 # restore opengl defaults
769 bgl.glLineWidth(1)
770 bgl.glDisable(bgl.GL_BLEND)
771 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
773 if settings.bgl_antialiasing:
774 bgl.glDisable(bgl.GL_LINE_SMOOTH)
777 def get_nodes_links(context):
778 space = context.space_data
779 tree = space.node_tree
780 nodes = tree.nodes
781 links = tree.links
782 active = nodes.active
783 context_active = context.active_node
784 # check if we are working on regular node tree or node group is currently edited.
785 # if group is edited - active node of space_tree is the group
786 # if context.active_node != space active node - it means that the group is being edited.
787 # in such case we set "nodes" to be nodes of this group, "links" to be links of this group
788 # if context.active_node == space.active_node it means that we are not currently editing group
789 is_main_tree = True
790 if active:
791 is_main_tree = context_active == active
792 if not is_main_tree: # if group is currently edited
793 tree = active.node_tree
794 nodes = tree.nodes
795 links = tree.links
797 return nodes, links
800 # Addon prefs
801 class NWNodeWrangler(bpy.types.AddonPreferences):
802 bl_idname = __name__
804 merge_hide = EnumProperty(
805 name="Hide Mix nodes",
806 items=(
807 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
808 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
809 ("NEVER", "Never", "Never collapse the new merge nodes")
811 default='NON_SHADER',
812 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded")
813 merge_position = EnumProperty(
814 name="Mix Node Position",
815 items=(
816 ("CENTER", "Center", "Place the Mix node between the two nodes"),
817 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
819 default='CENTER',
820 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes")
821 bgl_antialiasing = BoolProperty(
822 name="Line Antialiasing",
823 default=False,
824 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"
827 show_hotkey_list = BoolProperty(
828 name="Show Hotkey List",
829 default=False,
830 description="Expand this box into a list of all the hotkeys for functions in this addon"
832 hotkey_list_filter = StringProperty(
833 name=" Filter by Name",
834 default="",
835 description="Show only hotkeys that have this text in their name"
838 def draw(self, context):
839 layout = self.layout
840 col = layout.column()
841 col.prop(self, "merge_position")
842 col.prop(self, "merge_hide")
843 col.prop(self, "bgl_antialiasing")
845 box = col.box()
846 col = box.column(align=True)
848 hotkey_button_name = "Show Hotkey List"
849 if self.show_hotkey_list:
850 hotkey_button_name = "Hide Hotkey List"
851 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
852 if self.show_hotkey_list:
853 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
854 col.separator()
855 for hotkey in kmi_defs:
856 if hotkey[6]:
857 hotkey_name = hotkey[6]
859 if self.hotkey_list_filter.lower() in hotkey_name.lower():
860 row = col.row(align=True)
861 row.label(hotkey_name)
862 keystr = nice_hotkey_name(hotkey[1])
863 if hotkey[3]:
864 keystr = "Shift " + keystr
865 if hotkey[4]:
866 keystr = "Alt " + keystr
867 if hotkey[2]:
868 keystr = "Ctrl " + keystr
869 row.label(keystr)
872 class NWBase:
873 @classmethod
874 def poll(cls, context):
875 space = context.space_data
876 return space.type == 'NODE_EDITOR' and space.node_tree is not None
879 # OPERATORS
880 class NWLazyMix(Operator, NWBase):
881 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
882 bl_idname = "node.nw_lazy_mix"
883 bl_label = "Mix Nodes"
884 bl_options = {'REGISTER', 'UNDO'}
886 def modal(self, context, event):
887 context.area.tag_redraw()
888 nodes, links = get_nodes_links(context)
889 cont = True
891 start_pos = [event.mouse_region_x, event.mouse_region_y]
893 node1 = None
894 if not context.scene.NWBusyDrawing:
895 node1 = node_at_pos(nodes, context, event)
896 if node1:
897 context.scene.NWBusyDrawing = node1.name
898 else:
899 if context.scene.NWBusyDrawing != 'STOP':
900 node1 = nodes[context.scene.NWBusyDrawing]
902 context.scene.NWLazySource = node1.name
903 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
905 if event.type == 'MOUSEMOVE':
906 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
908 elif event.type == 'RIGHTMOUSE':
909 end_pos = [event.mouse_region_x, event.mouse_region_y]
910 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
912 node2 = None
913 node2 = node_at_pos(nodes, context, event)
914 if node2:
915 context.scene.NWBusyDrawing = node2.name
917 if node1 == node2:
918 cont = False
920 if cont:
921 if node1 and node2:
922 for node in nodes:
923 node.select = False
924 node1.select = True
925 node2.select = True
927 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
929 context.scene.NWBusyDrawing = ""
930 return {'FINISHED'}
932 elif event.type == 'ESC':
933 print('cancelled')
934 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
935 return {'CANCELLED'}
937 return {'RUNNING_MODAL'}
939 def invoke(self, context, event):
940 if context.area.type == 'NODE_EDITOR':
941 # the arguments we pass the the callback
942 args = (self, context, 'MIX')
943 # Add the region OpenGL drawing callback
944 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
945 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
947 self.mouse_path = []
949 context.window_manager.modal_handler_add(self)
950 return {'RUNNING_MODAL'}
951 else:
952 self.report({'WARNING'}, "View3D not found, cannot run operator")
953 return {'CANCELLED'}
956 class NWLazyConnect(Operator, NWBase):
957 """Connect two nodes without clicking a specific socket (automatically determined"""
958 bl_idname = "node.nw_lazy_connect"
959 bl_label = "Lazy Connect"
960 bl_options = {'REGISTER', 'UNDO'}
961 with_menu = BoolProperty()
963 def modal(self, context, event):
964 context.area.tag_redraw()
965 nodes, links = get_nodes_links(context)
966 cont = True
968 start_pos = [event.mouse_region_x, event.mouse_region_y]
970 node1 = None
971 if not context.scene.NWBusyDrawing:
972 node1 = node_at_pos(nodes, context, event)
973 if node1:
974 context.scene.NWBusyDrawing = node1.name
975 else:
976 if context.scene.NWBusyDrawing != 'STOP':
977 node1 = nodes[context.scene.NWBusyDrawing]
979 context.scene.NWLazySource = node1.name
980 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
982 if event.type == 'MOUSEMOVE':
983 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
985 elif event.type == 'RIGHTMOUSE':
986 end_pos = [event.mouse_region_x, event.mouse_region_y]
987 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
989 node2 = None
990 node2 = node_at_pos(nodes, context, event)
991 if node2:
992 context.scene.NWBusyDrawing = node2.name
994 if node1 == node2:
995 cont = False
997 link_success = False
998 if cont:
999 if node1 and node2:
1000 original_sel = []
1001 original_unsel = []
1002 for node in nodes:
1003 if node.select == True:
1004 node.select = False
1005 original_sel.append(node)
1006 else:
1007 original_unsel.append(node)
1008 node1.select = True
1009 node2.select = True
1011 #link_success = autolink(node1, node2, links)
1012 if self.with_menu:
1013 if len(node1.outputs) > 1 and node2.inputs:
1014 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1015 elif len(node1.outputs) == 1:
1016 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1017 else:
1018 link_success = autolink(node1, node2, links)
1020 for node in original_sel:
1021 node.select = True
1022 for node in original_unsel:
1023 node.select = False
1025 if link_success:
1026 hack_force_update(context, nodes)
1027 context.scene.NWBusyDrawing = ""
1028 return {'FINISHED'}
1030 elif event.type == 'ESC':
1031 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1032 return {'CANCELLED'}
1034 return {'RUNNING_MODAL'}
1036 def invoke(self, context, event):
1037 if context.area.type == 'NODE_EDITOR':
1038 nodes, links = get_nodes_links(context)
1039 node = node_at_pos(nodes, context, event)
1040 if node:
1041 context.scene.NWBusyDrawing = node.name
1043 # the arguments we pass the the callback
1044 mode = "LINK"
1045 if self.with_menu:
1046 mode = "LINKMENU"
1047 args = (self, context, mode)
1048 # Add the region OpenGL drawing callback
1049 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1050 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
1052 self.mouse_path = []
1054 context.window_manager.modal_handler_add(self)
1055 return {'RUNNING_MODAL'}
1056 else:
1057 self.report({'WARNING'}, "View3D not found, cannot run operator")
1058 return {'CANCELLED'}
1061 class NWDeleteUnused(Operator, NWBase):
1062 """Delete all nodes whose output is not used"""
1063 bl_idname = 'node.nw_del_unused'
1064 bl_label = 'Delete Unused Nodes'
1065 bl_options = {'REGISTER', 'UNDO'}
1067 @classmethod
1068 def poll(cls, context):
1069 valid = False
1070 if context.space_data:
1071 if context.space_data.node_tree:
1072 if context.space_data.node_tree.nodes:
1073 valid = True
1074 return valid
1076 def execute(self, context):
1077 nodes, links = get_nodes_links(context)
1078 end_types = 'OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1079 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LAMP', \
1080 'OUTPUT_WORLD', 'GROUP', 'GROUP_INPUT', 'GROUP_OUTPUT'
1082 # Store selection
1083 selection = []
1084 for node in nodes:
1085 if node.select == True:
1086 selection.append(node.name)
1088 deleted_nodes = []
1089 temp_deleted_nodes = []
1090 del_unused_iterations = len(nodes)
1091 for it in range(0, del_unused_iterations):
1092 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1093 for node in nodes:
1094 node.select = False
1095 for node in nodes:
1096 if is_end_node(node) and not node.type in end_types and node.type != 'FRAME':
1097 node.select = True
1098 deleted_nodes.append(node.name)
1099 bpy.ops.node.delete()
1101 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1102 break
1103 # get unique list of deleted nodes (iterations would count the same node more than once)
1104 deleted_nodes = list(set(deleted_nodes))
1105 for n in deleted_nodes:
1106 self.report({'INFO'}, "Node " + n + " deleted")
1107 num_deleted = len(deleted_nodes)
1108 n = ' node'
1109 if num_deleted > 1:
1110 n += 's'
1111 if num_deleted:
1112 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1113 else:
1114 self.report({'INFO'}, "Nothing deleted")
1116 # Restore selection
1117 nodes, links = get_nodes_links(context)
1118 for node in nodes:
1119 if node.name in selection:
1120 node.select = True
1121 return {'FINISHED'}
1123 def invoke(self, context, event):
1124 return context.window_manager.invoke_confirm(self, event)
1127 class NWSwapOutputs(Operator, NWBase):
1128 """Swap the output connections of the two selected nodes"""
1129 bl_idname = 'node.nw_swap_outputs'
1130 bl_label = 'Swap Outputs'
1131 bl_options = {'REGISTER', 'UNDO'}
1133 @classmethod
1134 def poll(cls, context):
1135 snode = context.space_data
1136 if context.selected_nodes:
1137 return len(context.selected_nodes) == 2
1138 else:
1139 return False
1141 def execute(self, context):
1142 nodes, links = get_nodes_links(context)
1143 selected_nodes = context.selected_nodes
1144 n1 = selected_nodes[0]
1145 n2 = selected_nodes[1]
1146 n1_outputs = []
1147 n2_outputs = []
1149 out_index = 0
1150 for output in n1.outputs:
1151 if output.links:
1152 for link in output.links:
1153 n1_outputs.append([out_index, link.to_socket])
1154 links.remove(link)
1155 out_index += 1
1157 out_index = 0
1158 for output in n2.outputs:
1159 if output.links:
1160 for link in output.links:
1161 n2_outputs.append([out_index, link.to_socket])
1162 links.remove(link)
1163 out_index += 1
1165 for connection in n1_outputs:
1166 try:
1167 links.new(n2.outputs[connection[0]], connection[1])
1168 except:
1169 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1170 for connection in n2_outputs:
1171 try:
1172 links.new(n1.outputs[connection[0]], connection[1])
1173 except:
1174 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1176 hack_force_update(context, nodes)
1177 return {'FINISHED'}
1180 class NWResetBG(Operator, NWBase):
1181 """Reset the zoom and position of the background image"""
1182 bl_idname = 'node.nw_bg_reset'
1183 bl_label = 'Reset Backdrop'
1184 bl_options = {'REGISTER', 'UNDO'}
1186 @classmethod
1187 def poll(cls, context):
1188 snode = context.space_data
1189 return snode.tree_type == 'CompositorNodeTree'
1191 def execute(self, context):
1192 context.space_data.backdrop_zoom = 1
1193 context.space_data.backdrop_x = 0
1194 context.space_data.backdrop_y = 0
1195 return {'FINISHED'}
1198 class NWAddAttrNode(Operator, NWBase):
1199 """Add an Attribute node with this name"""
1200 bl_idname = 'node.nw_add_attr_node'
1201 bl_label = 'Add UV map'
1202 attr_name = StringProperty()
1203 bl_options = {'REGISTER', 'UNDO'}
1205 def execute(self, context):
1206 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1207 nodes, links = get_nodes_links(context)
1208 nodes.active.attribute_name = self.attr_name
1209 return {'FINISHED'}
1212 class NWEmissionViewer(Operator, NWBase):
1213 bl_idname = "node.nw_emission_viewer"
1214 bl_label = "Emission Viewer"
1215 bl_description = "Connect active node to Emission Shader for shadeless previews"
1216 bl_options = {'REGISTER', 'UNDO'}
1218 @classmethod
1219 def poll(cls, context):
1220 space = context.space_data
1221 valid = False
1222 if space.type == 'NODE_EDITOR':
1223 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"):
1224 valid = True
1225 return valid
1227 def invoke(self, context, event):
1228 shader_type = context.space_data.shader_type
1229 if shader_type == 'OBJECT':
1230 shader_output_type = "OUTPUT_MATERIAL"
1231 shader_output_ident = "ShaderNodeOutputMaterial"
1232 shader_viewer_ident = "ShaderNodeEmission"
1233 elif shader_type == 'WORLD':
1234 shader_output_type = "OUTPUT_WORLD"
1235 shader_output_ident = "ShaderNodeOutputWorld"
1236 shader_viewer_ident = "ShaderNodeBackground"
1237 shader_types = [x[1] for x in shaders_shader_nodes_props]
1238 mlocx = event.mouse_region_x
1239 mlocy = event.mouse_region_y
1240 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1241 if 'FINISHED' in select_node: # only run if mouse click is on a node
1242 nodes, links = get_nodes_links(context)
1243 in_group = context.active_node != context.space_data.node_tree.nodes.active
1244 active = nodes.active
1245 valid = False
1246 output_types = [x[1] for x in shaders_output_nodes_props]
1247 if active:
1248 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1249 if active.select:
1250 if active.type not in shader_types:
1251 valid = True
1252 if valid:
1253 # get material_output node
1254 materialout_exists = False
1255 materialout = None # placeholder node
1256 for node in nodes:
1257 if node.type == shader_output_type:
1258 materialout_exists = True
1259 materialout = node
1260 if not materialout:
1261 materialout = nodes.new(shader_output_ident)
1262 sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
1263 max_xloc_node = sorted_by_xloc[-1]
1264 if max_xloc_node.name == 'Emission Viewer':
1265 max_xloc_node = sorted_by_xloc[-2]
1266 materialout.location.x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1267 sum_yloc = 0
1268 for node in nodes:
1269 sum_yloc += node.location.y
1270 materialout.location.y = sum_yloc / len(nodes) # put material output at average y location
1271 materialout.select = False
1272 # get Emission Viewer node
1273 emission_exists = False
1274 emission_placeholder = nodes[0]
1275 for node in nodes:
1276 if "Emission Viewer" in node.name:
1277 emission_exists = True
1278 emission_placeholder = node
1280 position = 0
1281 for link in links: # check if Emission Viewer is already connected to active node
1282 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:
1283 num_outputs = len(link.from_node.outputs)
1284 index = 0
1285 for output in link.from_node.outputs:
1286 if link.from_socket == output:
1287 position = index
1288 index = index + 1
1289 position = position + 1
1290 if position >= num_outputs:
1291 position = 0
1293 # Store selection
1294 selection = []
1295 for node in nodes:
1296 if node.select == True:
1297 selection.append(node.name)
1299 locx = active.location.x
1300 locy = active.location.y
1301 dimx = active.dimensions.x
1302 dimy = active.dimensions.y
1303 if not emission_exists:
1304 emission = nodes.new(shader_viewer_ident)
1305 emission.hide = True
1306 emission.location = [materialout.location.x, (materialout.location.y + 40)]
1307 emission.label = "Viewer"
1308 emission.name = "Emission Viewer"
1309 emission.use_custom_color = True
1310 emission.color = (0.6, 0.5, 0.4)
1311 else:
1312 emission = emission_placeholder
1314 nodes.active = emission
1315 links.new(active.outputs[position], emission.inputs[0])
1316 bpy.ops.node.nw_link_out()
1318 # Restore selection
1319 emission.select = False
1320 nodes.active = active
1321 for node in nodes:
1322 if node.name in selection:
1323 node.select = True
1324 else: # if active node is a shader, connect to output
1325 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1326 bpy.ops.node.nw_link_out()
1328 # ----Delete Emission Viewer----
1329 if [x for x in nodes if x.name == 'Emission Viewer']:
1330 # Store selection
1331 selection = []
1332 for node in nodes:
1333 if node.select == True:
1334 selection.append(node.name)
1335 node.select = False
1336 # Delete it
1337 nodes['Emission Viewer'].select = True
1338 bpy.ops.node.delete()
1339 # Restore selection
1340 for node in nodes:
1341 if node.name in selection:
1342 node.select = True
1344 return {'FINISHED'}
1345 else:
1346 return {'CANCELLED'}
1349 class NWFrameSelected(Operator, NWBase):
1350 bl_idname = "node.nw_frame_selected"
1351 bl_label = "Frame Selected"
1352 bl_description = "Add a frame node and parent the selected nodes to it"
1353 bl_options = {'REGISTER', 'UNDO'}
1354 label_prop = StringProperty(name='Label', default=' ', description='The visual name of the frame node')
1355 color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6),
1356 min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3)
1358 @classmethod
1359 def poll(cls, context):
1360 space = context.space_data
1361 valid = False
1362 if space.type == 'NODE_EDITOR':
1363 if space.node_tree is not None:
1364 valid = True
1365 return valid
1367 def execute(self, context):
1368 nodes, links = get_nodes_links(context)
1369 selected = []
1370 for node in nodes:
1371 if node.select == True:
1372 selected.append(node)
1374 bpy.ops.node.add_node(type='NodeFrame')
1375 frm = nodes.active
1376 frm.label = self.label_prop
1377 frm.use_custom_color = True
1378 frm.color = self.color_prop
1380 for node in selected:
1381 node.parent = frm
1383 return {'FINISHED'}
1386 class NWReloadImages(Operator, NWBase):
1387 bl_idname = "node.nw_reload_images"
1388 bl_label = "Reload Images"
1389 bl_description = "Update all the image nodes to match their files on disk"
1391 @classmethod
1392 def poll(cls, context):
1393 space = context.space_data
1394 valid = False
1395 if space.type == 'NODE_EDITOR':
1396 if space.node_tree is not None:
1397 valid = True
1398 return valid
1400 def execute(self, context):
1401 nodes, links = get_nodes_links(context)
1402 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1403 num_reloaded = 0
1404 for node in nodes:
1405 if node.type in image_types:
1406 if node.type == "TEXTURE":
1407 if node.texture: # node has texture assigned
1408 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1409 if node.texture.image: # texture has image assigned
1410 node.texture.image.reload()
1411 num_reloaded += 1
1412 else:
1413 if node.image:
1414 node.image.reload()
1415 num_reloaded += 1
1417 if num_reloaded:
1418 self.report({'INFO'}, "Reloaded images")
1419 print("Reloaded " + str(num_reloaded) + " images")
1420 hack_force_update(context, nodes)
1421 return {'FINISHED'}
1422 else:
1423 self.report({'WARNING'}, "No images found to reload in this node tree")
1424 return {'CANCELLED'}
1427 class NWSwitchNodeType(Operator, NWBase):
1428 """Switch type of selected nodes """
1429 bl_idname = "node.nw_swtch_node_type"
1430 bl_label = "Switch Node Type"
1431 bl_options = {'REGISTER', 'UNDO'}
1433 to_type = EnumProperty(
1434 name="Switch to type",
1435 items=list(shaders_input_nodes_props) +
1436 list(shaders_output_nodes_props) +
1437 list(shaders_shader_nodes_props) +
1438 list(shaders_texture_nodes_props) +
1439 list(shaders_color_nodes_props) +
1440 list(shaders_vector_nodes_props) +
1441 list(shaders_converter_nodes_props) +
1442 list(shaders_layout_nodes_props) +
1443 list(compo_input_nodes_props) +
1444 list(compo_output_nodes_props) +
1445 list(compo_color_nodes_props) +
1446 list(compo_converter_nodes_props) +
1447 list(compo_filter_nodes_props) +
1448 list(compo_vector_nodes_props) +
1449 list(compo_matte_nodes_props) +
1450 list(compo_distort_nodes_props) +
1451 list(compo_layout_nodes_props),
1454 def execute(self, context):
1455 nodes, links = get_nodes_links(context)
1456 to_type = self.to_type
1457 # Those types of nodes will not swap.
1458 src_excludes = ('NodeFrame')
1459 # Those attributes of nodes will be copied if possible
1460 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1461 'show_options', 'show_preview', 'show_texture',
1462 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1464 selected = [n for n in nodes if n.select]
1465 reselect = []
1466 for node in [n for n in selected if
1467 n.rna_type.identifier not in src_excludes and
1468 n.rna_type.identifier != to_type]:
1469 new_node = nodes.new(to_type)
1470 for attr in attrs_to_pass:
1471 if hasattr(node, attr) and hasattr(new_node, attr):
1472 setattr(new_node, attr, getattr(node, attr))
1473 # set image datablock of dst to image of src
1474 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1475 if node.image:
1476 new_node.image = node.image
1477 # Special cases
1478 if new_node.type == 'SWITCH':
1479 new_node.hide = True
1480 # Dictionaries: src_sockets and dst_sockets:
1481 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1482 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1483 # in 'INPUTS' and 'OUTPUTS':
1484 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1485 # socket entry:
1486 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1487 src_sockets = {
1488 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1489 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1491 dst_sockets = {
1492 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1493 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1495 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1496 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1497 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1498 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1499 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1500 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1501 # enumerate in inputs, then in outputs
1502 # find name, default value and links of socket
1503 for i, socket in enumerate(in_out):
1504 the_name = socket.name
1505 dval = None
1506 # Not every socket, especially in outputs has "default_value"
1507 if hasattr(socket, 'default_value'):
1508 dval = socket.default_value
1509 socket_links = []
1510 for lnk in socket.links:
1511 socket_links.append(lnk)
1512 # check type of socket to fill proper keys.
1513 for the_type in types_order_one:
1514 if socket.type == the_type:
1515 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1516 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1517 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1518 # Check which of the types in inputs/outputs is considered to be "main".
1519 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1520 for type_check in types_order_one:
1521 if sockets[in_out_name][type_check]:
1522 sockets[in_out_name]['MAIN'] = type_check
1523 break
1525 matches = {
1526 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1527 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1530 for inout, soctype in (
1531 ('INPUTS', 'MAIN',),
1532 ('INPUTS', 'SHADER',),
1533 ('INPUTS', 'RGBA',),
1534 ('INPUTS', 'VECTOR',),
1535 ('INPUTS', 'VALUE',),
1536 ('OUTPUTS', 'MAIN',),
1537 ('OUTPUTS', 'SHADER',),
1538 ('OUTPUTS', 'RGBA',),
1539 ('OUTPUTS', 'VECTOR',),
1540 ('OUTPUTS', 'VALUE',),
1542 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1543 if soctype == 'MAIN':
1544 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1545 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1546 else:
1547 sc = src_sockets[inout][soctype]
1548 dt = dst_sockets[inout][soctype]
1549 # start with 'dt' to determine number of possibilities.
1550 for i, soc in enumerate(dt):
1551 # if src main has enough entries - match them with dst main sockets by indexes.
1552 if len(sc) > i:
1553 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1554 # add 'VALUE_NAME' criterion to inputs.
1555 if inout == 'INPUTS' and soctype == 'VALUE':
1556 for s in sc:
1557 if s[2] == soc[2]: # if names match
1558 # append src (index, dval), dst (index, dval)
1559 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1561 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1562 # This creates better links when relinking textures.
1563 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1564 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1566 # Pass default values and RELINK:
1567 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1568 # INPUTS: Base on matches in proper order.
1569 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1570 # pass dvals
1571 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1572 new_node.inputs[dst_i].default_value = src_dval
1573 # Special case: switch to math
1574 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1575 new_node.type == 'MATH' and\
1576 tp == 'MAIN':
1577 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1578 new_node.inputs[dst_i].default_value = new_dst_dval
1579 if node.type == 'MIX_RGB':
1580 if node.blend_type in [o[0] for o in operations]:
1581 new_node.operation = node.blend_type
1582 # Special case: switch from math to some types
1583 if node.type == 'MATH' and\
1584 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1585 tp == 'MAIN':
1586 for i in range(3):
1587 new_node.inputs[dst_i].default_value[i] = src_dval
1588 if new_node.type == 'MIX_RGB':
1589 if node.operation in [t[0] for t in blend_types]:
1590 new_node.blend_type = node.operation
1591 # Set Fac of MIX_RGB to 1.0
1592 new_node.inputs[0].default_value = 1.0
1593 # make link only when dst matching input is not linked already.
1594 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1595 in_src_link = node.inputs[src_i].links[0]
1596 in_dst_socket = new_node.inputs[dst_i]
1597 links.new(in_src_link.from_socket, in_dst_socket)
1598 links.remove(in_src_link)
1599 # OUTPUTS: Base on matches in proper order.
1600 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1601 for out_src_link in node.outputs[src_i].links:
1602 out_dst_socket = new_node.outputs[dst_i]
1603 links.new(out_dst_socket, out_src_link.to_socket)
1604 # relink rest inputs if possible, no criteria
1605 for src_inp in node.inputs:
1606 for dst_inp in new_node.inputs:
1607 if src_inp.links and not dst_inp.links:
1608 src_link = src_inp.links[0]
1609 links.new(src_link.from_socket, dst_inp)
1610 links.remove(src_link)
1611 # relink rest outputs if possible, base on node kind if any left.
1612 for src_o in node.outputs:
1613 for out_src_link in src_o.links:
1614 for dst_o in new_node.outputs:
1615 if src_o.type == dst_o.type:
1616 links.new(dst_o, out_src_link.to_socket)
1617 # relink rest outputs no criteria if any left. Link all from first output.
1618 for src_o in node.outputs:
1619 for out_src_link in src_o.links:
1620 if new_node.outputs:
1621 links.new(new_node.outputs[0], out_src_link.to_socket)
1622 nodes.remove(node)
1623 return {'FINISHED'}
1626 class NWMergeNodes(Operator, NWBase):
1627 bl_idname = "node.nw_merge_nodes"
1628 bl_label = "Merge Nodes"
1629 bl_description = "Merge Selected Nodes"
1630 bl_options = {'REGISTER', 'UNDO'}
1632 mode = EnumProperty(
1633 name="mode",
1634 description="All possible blend types and math operations",
1635 items=blend_types + [op for op in operations if op not in blend_types],
1637 merge_type = EnumProperty(
1638 name="merge type",
1639 description="Type of Merge to be used",
1640 items=(
1641 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1642 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1643 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1644 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1648 def execute(self, context):
1649 settings = context.user_preferences.addons[__name__].preferences
1650 merge_hide = settings.merge_hide
1651 merge_position = settings.merge_position # 'center' or 'bottom'
1653 do_hide = False
1654 do_hide_shader = False
1655 if merge_hide == 'ALWAYS':
1656 do_hide = True
1657 do_hide_shader = True
1658 elif merge_hide == 'NON_SHADER':
1659 do_hide = True
1661 tree_type = context.space_data.node_tree.type
1662 if tree_type == 'COMPOSITING':
1663 node_type = 'CompositorNode'
1664 elif tree_type == 'SHADER':
1665 node_type = 'ShaderNode'
1666 nodes, links = get_nodes_links(context)
1667 mode = self.mode
1668 merge_type = self.merge_type
1669 selected_mix = [] # entry = [index, loc]
1670 selected_shader = [] # entry = [index, loc]
1671 selected_math = [] # entry = [index, loc]
1673 for i, node in enumerate(nodes):
1674 if node.select and node.outputs:
1675 if merge_type == 'AUTO':
1676 for (type, types_list, dst) in (
1677 ('SHADER', ('MIX', 'ADD'), selected_shader),
1678 ('RGBA', [t[0] for t in blend_types], selected_mix),
1679 ('VALUE', [t[0] for t in operations], selected_math),
1681 output_type = node.outputs[0].type
1682 valid_mode = mode in types_list
1683 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1684 # Cheat that output type is 'RGBA',
1685 # and that 'MIX' exists in math operations list.
1686 # This way when selected_mix list is analyzed:
1687 # Node data will be appended even though it doesn't meet requirements.
1688 if output_type != 'SHADER' and mode == 'MIX':
1689 output_type = 'RGBA'
1690 valid_mode = True
1691 if output_type == type and valid_mode:
1692 dst.append([i, node.location.x, node.location.y])
1693 else:
1694 for (type, types_list, dst) in (
1695 ('SHADER', ('MIX', 'ADD'), selected_shader),
1696 ('MIX', [t[0] for t in blend_types], selected_mix),
1697 ('MATH', [t[0] for t in operations], selected_math),
1699 if merge_type == type and mode in types_list:
1700 dst.append([i, node.location.x, node.location.y])
1701 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1702 # use only 'Mix' nodes for merging.
1703 # For that we add selected_math list to selected_mix list and clear selected_math.
1704 if selected_mix and selected_math and merge_type == 'AUTO':
1705 selected_mix += selected_math
1706 selected_math = []
1708 for nodes_list in [selected_mix, selected_shader, selected_math]:
1709 if nodes_list:
1710 count_before = len(nodes)
1711 # sort list by loc_x - reversed
1712 nodes_list.sort(key=lambda k: k[1], reverse=True)
1713 # get maximum loc_x
1714 loc_x = nodes_list[0][1] + 250.0
1715 nodes_list.sort(key=lambda k: k[2], reverse=True)
1716 if merge_position == 'CENTER':
1717 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)
1718 else:
1719 loc_y = nodes_list[len(nodes_list) - 1][2]
1720 offset_y = 100
1721 if not do_hide:
1722 offset_y = 200
1723 if nodes_list == selected_shader and not do_hide_shader:
1724 offset_y = 150.0
1725 the_range = len(nodes_list) - 1
1726 if len(nodes_list) == 1:
1727 the_range = 1
1728 for i in range(the_range):
1729 if nodes_list == selected_mix:
1730 add_type = node_type + 'MixRGB'
1731 add = nodes.new(add_type)
1732 add.blend_type = mode
1733 add.show_preview = False
1734 add.hide = do_hide
1735 if do_hide:
1736 loc_y = loc_y - 50
1737 first = 1
1738 second = 2
1739 add.width_hidden = 100.0
1740 elif nodes_list == selected_math:
1741 add_type = node_type + 'Math'
1742 add = nodes.new(add_type)
1743 add.operation = mode
1744 add.hide = do_hide
1745 if do_hide:
1746 loc_y = loc_y - 50
1747 first = 0
1748 second = 1
1749 add.width_hidden = 100.0
1750 elif nodes_list == selected_shader:
1751 if mode == 'MIX':
1752 add_type = node_type + 'MixShader'
1753 add = nodes.new(add_type)
1754 add.hide = do_hide_shader
1755 if do_hide_shader:
1756 loc_y = loc_y - 50
1757 first = 1
1758 second = 2
1759 add.width_hidden = 100.0
1760 elif mode == 'ADD':
1761 add_type = node_type + 'AddShader'
1762 add = nodes.new(add_type)
1763 add.hide = do_hide_shader
1764 if do_hide_shader:
1765 loc_y = loc_y - 50
1766 first = 0
1767 second = 1
1768 add.width_hidden = 100.0
1769 add.location = loc_x, loc_y
1770 loc_y += offset_y
1771 add.select = True
1772 count_adds = i + 1
1773 count_after = len(nodes)
1774 index = count_after - 1
1775 first_selected = nodes[nodes_list[0][0]]
1776 # "last" node has been added as first, so its index is count_before.
1777 last_add = nodes[count_before]
1778 # add links from last_add to all links 'to_socket' of out links of first selected.
1779 for fs_link in first_selected.outputs[0].links:
1780 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
1781 # Create list of invalid indexes.
1782 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader)]
1783 # Link only if "to_node" index not in invalid indexes list.
1784 if fs_link.to_node not in [nodes[i] for i in invalid_i]:
1785 links.new(last_add.outputs[0], fs_link.to_socket)
1786 # add link from "first" selected and "first" add node
1787 links.new(first_selected.outputs[0], nodes[count_after - 1].inputs[first])
1788 # add links between added ADD nodes and between selected and ADD nodes
1789 for i in range(count_adds):
1790 if i < count_adds - 1:
1791 links.new(nodes[index - 1].inputs[first], nodes[index].outputs[0])
1792 if len(nodes_list) > 1:
1793 links.new(nodes[index].inputs[second], nodes[nodes_list[i + 1][0]].outputs[0])
1794 index -= 1
1795 # set "last" of added nodes as active
1796 nodes.active = last_add
1797 for i, x, y in nodes_list:
1798 nodes[i].select = False
1800 return {'FINISHED'}
1803 class NWBatchChangeNodes(Operator, NWBase):
1804 bl_idname = "node.nw_batch_change"
1805 bl_label = "Batch Change"
1806 bl_description = "Batch Change Blend Type and Math Operation"
1807 bl_options = {'REGISTER', 'UNDO'}
1809 blend_type = EnumProperty(
1810 name="Blend Type",
1811 items=blend_types + navs,
1813 operation = EnumProperty(
1814 name="Operation",
1815 items=operations + navs,
1818 def execute(self, context):
1820 nodes, links = get_nodes_links(context)
1821 blend_type = self.blend_type
1822 operation = self.operation
1823 for node in context.selected_nodes:
1824 if node.type == 'MIX_RGB':
1825 if not blend_type in [nav[0] for nav in navs]:
1826 node.blend_type = blend_type
1827 else:
1828 if blend_type == 'NEXT':
1829 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1830 #index = blend_types.index(node.blend_type)
1831 if index == len(blend_types) - 1:
1832 node.blend_type = blend_types[0][0]
1833 else:
1834 node.blend_type = blend_types[index + 1][0]
1836 if blend_type == 'PREV':
1837 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1838 if index == 0:
1839 node.blend_type = blend_types[len(blend_types) - 1][0]
1840 else:
1841 node.blend_type = blend_types[index - 1][0]
1843 if node.type == 'MATH':
1844 if not operation in [nav[0] for nav in navs]:
1845 node.operation = operation
1846 else:
1847 if operation == 'NEXT':
1848 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1849 #index = operations.index(node.operation)
1850 if index == len(operations) - 1:
1851 node.operation = operations[0][0]
1852 else:
1853 node.operation = operations[index + 1][0]
1855 if operation == 'PREV':
1856 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1857 #index = operations.index(node.operation)
1858 if index == 0:
1859 node.operation = operations[len(operations) - 1][0]
1860 else:
1861 node.operation = operations[index - 1][0]
1863 return {'FINISHED'}
1866 class NWChangeMixFactor(Operator, NWBase):
1867 bl_idname = "node.nw_factor"
1868 bl_label = "Change Factor"
1869 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
1870 bl_options = {'REGISTER', 'UNDO'}
1872 # option: Change factor.
1873 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1874 # Else - change factor by option value.
1875 option = FloatProperty()
1877 def execute(self, context):
1878 nodes, links = get_nodes_links(context)
1879 option = self.option
1880 selected = [] # entry = index
1881 for si, node in enumerate(nodes):
1882 if node.select:
1883 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
1884 selected.append(si)
1886 for si in selected:
1887 fac = nodes[si].inputs[0]
1888 nodes[si].hide = False
1889 if option in {0.0, 1.0}:
1890 fac.default_value = option
1891 else:
1892 fac.default_value += option
1894 return {'FINISHED'}
1897 class NWCopySettings(Operator, NWBase):
1898 bl_idname = "node.nw_copy_settings"
1899 bl_label = "Copy Settings"
1900 bl_description = "Copy Settings of Active Node to Selected Nodes"
1901 bl_options = {'REGISTER', 'UNDO'}
1903 @classmethod
1904 def poll(cls, context):
1905 space = context.space_data
1906 valid = False
1907 if (space.type == 'NODE_EDITOR' and
1908 space.node_tree is not None and
1909 context.active_node is not None and
1910 context.active_node.type is not 'FRAME'
1912 valid = True
1913 return valid
1915 def execute(self, context):
1916 nodes, links = get_nodes_links(context)
1917 selected = [n for n in nodes if n.select]
1918 reselect = [] # duplicated nodes will be selected after execution
1919 active = nodes.active
1920 if active.select:
1921 reselect.append(active)
1923 for node in selected:
1924 if node.type == active.type and node != active:
1925 # duplicate active, relink links as in 'node', append copy to 'reselect', delete node
1926 bpy.ops.node.select_all(action='DESELECT')
1927 nodes.active = active
1928 active.select = True
1929 bpy.ops.node.duplicate()
1930 copied = nodes.active
1931 # Copied active should however inherit some properties from 'node'
1932 attributes = (
1933 'hide', 'show_preview', 'mute', 'label',
1934 'use_custom_color', 'color', 'width', 'width_hidden',
1936 for attr in attributes:
1937 setattr(copied, attr, getattr(node, attr))
1938 # Handle scenario when 'node' is in frame. 'copied' is in same frame then.
1939 if copied.parent:
1940 bpy.ops.node.parent_clear()
1941 locx = node.location.x
1942 locy = node.location.y
1943 # get absolute node location
1944 parent = node.parent
1945 while parent:
1946 locx += parent.location.x
1947 locy += parent.location.y
1948 parent = parent.parent
1949 copied.location = [locx, locy]
1950 # reconnect links from node to copied
1951 for i, input in enumerate(node.inputs):
1952 if input.links:
1953 link = input.links[0]
1954 links.new(link.from_socket, copied.inputs[i])
1955 for out, output in enumerate(node.outputs):
1956 if output.links:
1957 out_links = output.links
1958 for link in out_links:
1959 links.new(copied.outputs[out], link.to_socket)
1960 bpy.ops.node.select_all(action='DESELECT')
1961 node.select = True
1962 bpy.ops.node.delete()
1963 reselect.append(copied)
1964 else: # If selected wasn't copied, need to reselect it afterwards.
1965 reselect.append(node)
1966 # clean up
1967 bpy.ops.node.select_all(action='DESELECT')
1968 for node in reselect:
1969 node.select = True
1970 nodes.active = active
1972 return {'FINISHED'}
1975 class NWCopyLabel(Operator, NWBase):
1976 bl_idname = "node.nw_copy_label"
1977 bl_label = "Copy Label"
1978 bl_options = {'REGISTER', 'UNDO'}
1980 option = EnumProperty(
1981 name="option",
1982 description="Source of name of label",
1983 items=(
1984 ('FROM_ACTIVE', 'from active', 'from active node',),
1985 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1986 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1990 def execute(self, context):
1991 nodes, links = get_nodes_links(context)
1992 option = self.option
1993 active = nodes.active
1994 if option == 'FROM_ACTIVE':
1995 if active:
1996 src_label = active.label
1997 for node in [n for n in nodes if n.select and nodes.active != n]:
1998 node.label = src_label
1999 elif option == 'FROM_NODE':
2000 selected = [n for n in nodes if n.select]
2001 for node in selected:
2002 for input in node.inputs:
2003 if input.links:
2004 src = input.links[0].from_node
2005 node.label = src.label
2006 break
2007 elif option == 'FROM_SOCKET':
2008 selected = [n for n in nodes if n.select]
2009 for node in selected:
2010 for input in node.inputs:
2011 if input.links:
2012 src = input.links[0].from_socket
2013 node.label = src.name
2014 break
2016 return {'FINISHED'}
2019 class NWClearLabel(Operator, NWBase):
2020 bl_idname = "node.nw_clear_label"
2021 bl_label = "Clear Label"
2022 bl_options = {'REGISTER', 'UNDO'}
2024 option = BoolProperty()
2026 def execute(self, context):
2027 nodes, links = get_nodes_links(context)
2028 for node in [n for n in nodes if n.select]:
2029 node.label = ''
2031 return {'FINISHED'}
2033 def invoke(self, context, event):
2034 if self.option:
2035 return self.execute(context)
2036 else:
2037 return context.window_manager.invoke_confirm(self, event)
2040 class NWModifyLabels(Operator, NWBase):
2041 """Modify Labels of all selected nodes."""
2042 bl_idname = "node.nw_modify_labels"
2043 bl_label = "Modify Labels"
2044 bl_options = {'REGISTER', 'UNDO'}
2046 prepend = StringProperty(
2047 name="Add to Beginning"
2049 append = StringProperty(
2050 name="Add to End"
2052 replace_from = StringProperty(
2053 name="Text to Replace"
2055 replace_to = StringProperty(
2056 name="Replace with"
2059 def execute(self, context):
2060 nodes, links = get_nodes_links(context)
2061 for node in [n for n in nodes if n.select]:
2062 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2064 return {'FINISHED'}
2066 def invoke(self, context, event):
2067 self.prepend = ""
2068 self.append = ""
2069 self.remove = ""
2070 return context.window_manager.invoke_props_dialog(self)
2073 class NWAddTextureSetup(Operator, NWBase):
2074 bl_idname = "node.nw_add_texture"
2075 bl_label = "Texture Setup"
2076 bl_description = "Add Texture Node Setup to Selected Shaders"
2077 bl_options = {'REGISTER', 'UNDO'}
2079 @classmethod
2080 def poll(cls, context):
2081 space = context.space_data
2082 valid = False
2083 if space.type == 'NODE_EDITOR':
2084 if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None:
2085 valid = True
2086 return valid
2088 def execute(self, context):
2089 nodes, links = get_nodes_links(context)
2090 active = nodes.active
2091 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2092 texture_types = [x[1] for x in shaders_texture_nodes_props]
2093 valid = False
2094 if active:
2095 if active.select:
2096 if active.type in shader_types or active.type in texture_types:
2097 if not active.inputs[0].is_linked:
2098 valid = True
2099 if valid:
2100 locx = active.location.x
2101 locy = active.location.y
2103 xoffset = [500.0, 700.0]
2104 isshader = True
2105 if active.type not in shader_types:
2106 xoffset = [290.0, 500.0]
2107 isshader = False
2109 coordout = 2
2110 image_type = 'ShaderNodeTexImage'
2112 if (active.type in texture_types and active.type != 'TEX_IMAGE') or (active.type == 'BACKGROUND'):
2113 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2114 if active.type == 'BACKGROUND':
2115 image_type = 'ShaderNodeTexEnvironment'
2117 if isshader:
2118 tex = nodes.new(image_type)
2119 tex.location = [locx - 200.0, locy + 28.0]
2121 map = nodes.new('ShaderNodeMapping')
2122 map.location = [locx - xoffset[0], locy + 80.0]
2123 map.width = 240
2124 coord = nodes.new('ShaderNodeTexCoord')
2125 coord.location = [locx - xoffset[1], locy + 40.0]
2126 active.select = False
2128 if isshader:
2129 nodes.active = tex
2130 links.new(tex.outputs[0], active.inputs[0])
2131 links.new(map.outputs[0], tex.inputs[0])
2132 links.new(coord.outputs[coordout], map.inputs[0])
2134 else:
2135 nodes.active = map
2136 links.new(map.outputs[0], active.inputs[0])
2137 links.new(coord.outputs[coordout], map.inputs[0])
2139 return {'FINISHED'}
2142 class NWAddReroutes(Operator, NWBase):
2143 """Add Reroute Nodes and link them to outputs of selected nodes"""
2144 bl_idname = "node.nw_add_reroutes"
2145 bl_label = "Add Reroutes"
2146 bl_description = "Add Reroutes to Outputs"
2147 bl_options = {'REGISTER', 'UNDO'}
2149 option = EnumProperty(
2150 name="option",
2151 items=[
2152 ('ALL', 'to all', 'Add to all outputs'),
2153 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2154 ('LINKED', 'to linked', 'Add only to linked outputs'),
2158 def execute(self, context):
2159 tree_type = context.space_data.node_tree.type
2160 option = self.option
2161 nodes, links = get_nodes_links(context)
2162 # output valid when option is 'all' or when 'loose' output has no links
2163 valid = False
2164 post_select = [] # nodes to be selected after execution
2165 # create reroutes and recreate links
2166 for node in [n for n in nodes if n.select]:
2167 if node.outputs:
2168 x = node.location.x
2169 y = node.location.y
2170 width = node.width
2171 # unhide 'REROUTE' nodes to avoid issues with location.y
2172 if node.type == 'REROUTE':
2173 node.hide = False
2174 # When node is hidden - width_hidden not usable.
2175 # Hack needed to calculate real width
2176 if node.hide:
2177 bpy.ops.node.select_all(action='DESELECT')
2178 helper = nodes.new('NodeReroute')
2179 helper.select = True
2180 node.select = True
2181 # resize node and helper to zero. Then check locations to calculate width
2182 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2183 width = 2.0 * (helper.location.x - node.location.x)
2184 # restore node location
2185 node.location = x, y
2186 # delete helper
2187 node.select = False
2188 # only helper is selected now
2189 bpy.ops.node.delete()
2190 x = node.location.x + width + 20.0
2191 if node.type != 'REROUTE':
2192 y -= 35.0
2193 y_offset = -22.0
2194 loc = x, y
2195 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2196 for out_i, output in enumerate(node.outputs):
2197 pass_used = False # initial value to be analyzed if 'R_LAYERS'
2198 # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
2199 if node.type != 'R_LAYERS':
2200 pass_used = True
2201 else: # if 'R_LAYERS' check if output represent used render pass
2202 node_scene = node.scene
2203 node_layer = node.layer
2204 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2205 if output.name == 'Alpha':
2206 pass_used = True
2207 else:
2208 # check entries in global 'rl_outputs' variable
2209 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
2210 if output.name == out_name:
2211 pass_used = getattr(node_scene.render.layers[node_layer], render_pass)
2212 break
2213 if pass_used:
2214 valid = ((option == 'ALL') or
2215 (option == 'LOOSE' and not output.links) or
2216 (option == 'LINKED' and output.links))
2217 # Add reroutes only if valid, but offset location in all cases.
2218 if valid:
2219 n = nodes.new('NodeReroute')
2220 nodes.active = n
2221 for link in output.links:
2222 links.new(n.outputs[0], link.to_socket)
2223 links.new(output, n.inputs[0])
2224 n.location = loc
2225 post_select.append(n)
2226 reroutes_count += 1
2227 y += y_offset
2228 loc = x, y
2229 # disselect the node so that after execution of script only newly created nodes are selected
2230 node.select = False
2231 # nicer reroutes distribution along y when node.hide
2232 if node.hide:
2233 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2234 for reroute in [r for r in nodes if r.select]:
2235 reroute.location.y -= y_translate
2236 for node in post_select:
2237 node.select = True
2239 return {'FINISHED'}
2242 class NWLinkActiveToSelected(Operator, NWBase):
2243 """Link active node to selected nodes basing on various criteria"""
2244 bl_idname = "node.nw_link_active_to_selected"
2245 bl_label = "Link Active Node to Selected"
2246 bl_options = {'REGISTER', 'UNDO'}
2248 replace = BoolProperty()
2249 use_node_name = BoolProperty()
2250 use_outputs_names = BoolProperty()
2252 @classmethod
2253 def poll(cls, context):
2254 space = context.space_data
2255 valid = False
2256 if space.type == 'NODE_EDITOR':
2257 if space.node_tree is not None and context.active_node is not None:
2258 if context.active_node.select:
2259 valid = True
2260 return valid
2262 def execute(self, context):
2263 nodes, links = get_nodes_links(context)
2264 replace = self.replace
2265 use_node_name = self.use_node_name
2266 use_outputs_names = self.use_outputs_names
2267 active = nodes.active
2268 selected = [node for node in nodes if node.select and node != active]
2269 outputs = [] # Only usable outputs of active nodes will be stored here.
2270 for out in active.outputs:
2271 if active.type != 'R_LAYERS':
2272 outputs.append(out)
2273 else:
2274 # 'R_LAYERS' node type needs special handling.
2275 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2276 # Only outputs that represent used passes should be taken into account
2277 # Check if pass represented by output is used.
2278 # global 'rl_outputs' list will be used for that
2279 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
2280 pass_used = False # initial value. Will be set to True if pass is used
2281 if out.name == 'Alpha':
2282 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2283 pass_used = True
2284 elif out.name == out_name:
2285 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2286 pass_used = getattr(active.scene.render.layers[active.layer], render_pass)
2287 break
2288 if pass_used:
2289 outputs.append(out)
2290 doit = True # Will be changed to False when links successfully added to previous output.
2291 for out in outputs:
2292 if doit:
2293 for node in selected:
2294 dst_name = node.name # Will be compared with src_name if needed.
2295 # When node has label - use it as dst_name
2296 if node.label:
2297 dst_name = node.label
2298 valid = True # Initial value. Will be changed to False if names don't match.
2299 src_name = dst_name # If names not used - this asignment will keep valid = True.
2300 if use_node_name:
2301 # Set src_name to source node name or label
2302 src_name = active.name
2303 if active.label:
2304 src_name = active.label
2305 elif use_outputs_names:
2306 src_name = (out.name, )
2307 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
2308 if out.name in {out_name, exr_name}:
2309 src_name = (out_name, exr_name)
2310 if dst_name not in src_name:
2311 valid = False
2312 if valid:
2313 for input in node.inputs:
2314 if input.type == out.type or node.type == 'REROUTE':
2315 if replace or not input.is_linked:
2316 links.new(out, input)
2317 if not use_node_name and not use_outputs_names:
2318 doit = False
2319 break
2321 return {'FINISHED'}
2324 class NWAlignNodes(Operator, NWBase):
2325 bl_idname = "node.nw_align_nodes"
2326 bl_label = "Align nodes"
2327 bl_options = {'REGISTER', 'UNDO'}
2329 # option: 'Vertically', 'Horizontally'
2330 option = EnumProperty(
2331 name="option",
2332 description="Direction",
2333 items=(
2334 ('AXIS_X', "Align Vertically", 'Align Vertically'),
2335 ('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'),
2339 def execute(self, context):
2340 nodes, links = get_nodes_links(context)
2341 selected = [] # entry = [index, loc.x, loc.y, width, height]
2342 frames_reselect = [] # entry = frame node. will be used to reselect all selected frames
2343 active = nodes.active
2344 for i, node in enumerate(nodes):
2345 total_w = 0.0 # total width of all nodes. Will be calculated later.
2346 total_h = 0.0 # total height of all nodes. Will be calculated later
2347 if node.select:
2348 if node.type == 'FRAME':
2349 node.select = False
2350 frames_reselect.append(i)
2351 else:
2352 locx = node.location.x
2353 locy = node.location.y
2354 width = node.dimensions[0]
2355 height = node.dimensions[1]
2356 total_w += width # add nodes[i] width to total width of all nodes
2357 total_h += height # add nodes[i] height to total height of all nodes
2358 # calculate relative locations
2359 parent = node.parent
2360 while parent is not None:
2361 locx += parent.location.x
2362 locy += parent.location.y
2363 parent = parent.parent
2364 selected.append([i, locx, locy, width, height])
2365 count = len(selected)
2366 if count > 1: # aligning makes sense only if at least 2 nodes are selected
2367 selected_sorted_x = sorted(selected, key=lambda k: (k[1], -k[2]))
2368 selected_sorted_y = sorted(selected, key=lambda k: (-k[2], k[1]))
2369 min_x = selected_sorted_x[0][1] # min loc.x
2370 min_x_loc_y = selected_sorted_x[0][2] # loc y of node with min loc x
2371 min_x_w = selected_sorted_x[0][3] # width of node with max loc x
2372 max_x = selected_sorted_x[count - 1][1] # max loc.x
2373 max_x_loc_y = selected_sorted_x[count - 1][2] # loc y of node with max loc.x
2374 max_x_w = selected_sorted_x[count - 1][3] # width of node with max loc.x
2375 min_y = selected_sorted_y[0][2] # min loc.y
2376 min_y_loc_x = selected_sorted_y[0][1] # loc.x of node with min loc.y
2377 min_y_h = selected_sorted_y[0][4] # height of node with min loc.y
2378 min_y_w = selected_sorted_y[0][3] # width of node with min loc.y
2379 max_y = selected_sorted_y[count - 1][2] # max loc.y
2380 max_y_loc_x = selected_sorted_y[count - 1][1] # loc x of node with max loc.y
2381 max_y_w = selected_sorted_y[count - 1][3] # width of node with max loc.y
2382 max_y_h = selected_sorted_y[count - 1][4] # height of node with max loc.y
2384 if self.option == 'AXIS_Y': # Horizontally. Equivelent of s -> x -> 0 with even spacing.
2385 loc_x = min_x
2386 #loc_y = (max_x_loc_y + min_x_loc_y) / 2.0
2387 loc_y = (max_y - max_y_h / 2.0 + min_y - min_y_h / 2.0) / 2.0
2388 offset_x = (max_x - min_x - total_w + max_x_w) / (count - 1)
2389 for i, x, y, w, h in selected_sorted_x:
2390 nodes[i].location.x = loc_x
2391 nodes[i].location.y = loc_y + h / 2.0
2392 parent = nodes[i].parent
2393 while parent is not None:
2394 nodes[i].location.x -= parent.location.x
2395 nodes[i].location.y -= parent.location.y
2396 parent = parent.parent
2397 loc_x += offset_x + w
2398 else: # if self.option == 'AXIS_Y'
2399 loc_x = (max_x + max_x_w / 2.0 + min_x + min_x_w / 2.0) / 2.0
2400 loc_y = min_y
2401 offset_y = (max_y - min_y + total_h - min_y_h) / (count - 1)
2402 for i, x, y, w, h in selected_sorted_y:
2403 nodes[i].location.x = loc_x - w / 2.0
2404 nodes[i].location.y = loc_y
2405 parent = nodes[i].parent
2406 while parent is not None:
2407 nodes[i].location.x -= parent.location.x
2408 nodes[i].location.y -= parent.location.y
2409 parent = parent.parent
2410 loc_y += offset_y - h
2412 # reselect selected frames
2413 for i in frames_reselect:
2414 nodes[i].select = True
2415 # restore active node
2416 nodes.active = active
2418 return {'FINISHED'}
2421 class NWSelectParentChildren(Operator, NWBase):
2422 bl_idname = "node.nw_select_parent_child"
2423 bl_label = "Select Parent or Children"
2424 bl_options = {'REGISTER', 'UNDO'}
2426 option = EnumProperty(
2427 name="option",
2428 items=(
2429 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2430 ('CHILD', 'Select Children', 'Select members of selected frame'),
2434 def execute(self, context):
2435 nodes, links = get_nodes_links(context)
2436 option = self.option
2437 selected = [node for node in nodes if node.select]
2438 if option == 'PARENT':
2439 for sel in selected:
2440 parent = sel.parent
2441 if parent:
2442 parent.select = True
2443 else: # option == 'CHILD'
2444 for sel in selected:
2445 children = [node for node in nodes if node.parent == sel]
2446 for kid in children:
2447 kid.select = True
2449 return {'FINISHED'}
2452 class NWDetachOutputs(Operator, NWBase):
2453 """Detach outputs of selected node leaving inluts liked"""
2454 bl_idname = "node.nw_detach_outputs"
2455 bl_label = "Detach Outputs"
2456 bl_options = {'REGISTER', 'UNDO'}
2458 def execute(self, context):
2459 nodes, links = get_nodes_links(context)
2460 selected = context.selected_nodes
2461 bpy.ops.node.duplicate_move_keep_inputs()
2462 new_nodes = context.selected_nodes
2463 bpy.ops.node.select_all(action="DESELECT")
2464 for node in selected:
2465 node.select = True
2466 bpy.ops.node.delete_reconnect()
2467 for new_node in new_nodes:
2468 new_node.select = True
2469 bpy.ops.transform.translate('INVOKE_DEFAULT')
2471 return {'FINISHED'}
2474 class NWLinkToOutputNode(Operator, NWBase):
2475 """Link to Composite node or Material Output node"""
2476 bl_idname = "node.nw_link_out"
2477 bl_label = "Connect to Output"
2478 bl_options = {'REGISTER', 'UNDO'}
2480 @classmethod
2481 def poll(cls, context):
2482 space = context.space_data
2483 return (space.type == 'NODE_EDITOR' and space.node_tree is not None and context.active_node is not None)
2485 def execute(self, context):
2486 nodes, links = get_nodes_links(context)
2487 active = nodes.active
2488 output_node = None
2489 tree_type = context.space_data.tree_type
2490 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
2491 output_types_compo = ['COMPOSITE']
2492 output_types = output_types_shaders + output_types_compo
2493 for node in nodes:
2494 if node.type in output_types:
2495 output_node = node
2496 break
2497 if not output_node:
2498 bpy.ops.node.select_all(action="DESELECT")
2499 if tree_type == 'ShaderNodeTree':
2500 output_node = nodes.new('ShaderNodeOutputMaterial')
2501 elif tree_type == 'CompositorNodeTree':
2502 output_node = nodes.new('CompositorNodeComposite')
2503 output_node.location.x = active.location.x + active.dimensions.x + 80
2504 output_node.location.y = active.location.y
2505 if (output_node and active.outputs):
2506 output_index = 0
2507 for i, output in enumerate(active.outputs):
2508 if output.type == output_node.inputs[0].type:
2509 output_index = i
2510 break
2512 out_input_index = 0
2513 if tree_type == 'ShaderNodeTree':
2514 if active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
2515 out_input_index = 2
2516 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
2518 hack_force_update(context, nodes) # viewport render does not update
2520 return {'FINISHED'}
2523 class NWMakeLink(Operator, NWBase):
2524 """Make a link from one socket to another"""
2525 bl_idname = 'node.nw_make_link'
2526 bl_label = 'Make Link'
2527 bl_options = {'REGISTER', 'UNDO'}
2528 from_socket = IntProperty()
2529 to_socket = IntProperty()
2531 @classmethod
2532 def poll(cls, context):
2533 snode = context.space_data
2534 return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None)
2536 def execute(self, context):
2537 nodes, links = get_nodes_links(context)
2539 n1 = nodes[context.scene.NWLazySource]
2540 n2 = nodes[context.scene.NWLazyTarget]
2542 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2544 hack_force_update(context, nodes)
2546 return {'FINISHED'}
2549 class NWCallInputsMenu(Operator, NWBase):
2550 """Link from this output"""
2551 bl_idname = 'node.nw_call_inputs_menu'
2552 bl_label = 'Make Link'
2553 bl_options = {'REGISTER', 'UNDO'}
2554 from_socket = IntProperty()
2556 @classmethod
2557 def poll(cls, context):
2558 snode = context.space_data
2559 return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None)
2561 def execute(self, context):
2562 nodes, links = get_nodes_links(context)
2564 context.scene.NWSourceSocket = self.from_socket
2566 n1 = nodes[context.scene.NWLazySource]
2567 n2 = nodes[context.scene.NWLazyTarget]
2568 if len(n2.inputs) > 1:
2569 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
2570 elif len(n2.inputs) == 1:
2571 links.new(n1.outputs[self.from_socket], n2.inputs[0])
2572 return {'FINISHED'}
2576 # P A N E L
2579 def drawlayout(context, layout, mode='non-panel'):
2580 tree_type = context.space_data.tree_type
2582 col = layout.column(align=True)
2583 col.menu(NWMergeNodesMenu.bl_idname)
2584 col.separator()
2586 col = layout.column(align=True)
2587 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
2588 col.separator()
2590 if tree_type == 'ShaderNodeTree':
2591 col = layout.column(align=True)
2592 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
2593 col.separator()
2595 col = layout.column(align=True)
2596 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
2597 col.operator(NWSwapOutputs.bl_idname)
2598 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
2599 col.separator()
2601 col = layout.column(align=True)
2602 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
2603 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
2604 col.separator()
2606 col = layout.column(align=True)
2607 if mode == 'panel':
2608 row = col.row(align=True)
2609 row.operator(NWClearLabel.bl_idname).option = True
2610 row.operator(NWModifyLabels.bl_idname)
2611 else:
2612 col.operator(NWClearLabel.bl_idname).option = True
2613 col.operator(NWModifyLabels.bl_idname)
2614 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
2615 col.separator()
2616 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
2617 col.separator()
2619 col = layout.column(align=True)
2620 if tree_type == 'CompositorNodeTree':
2621 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
2622 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
2623 col.separator()
2625 col = layout.column(align=True)
2626 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
2627 col.separator()
2629 col = layout.column(align=True)
2630 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
2631 col.separator()
2634 class NodeWranglerPanel(Panel, NWBase):
2635 bl_idname = "NODE_PT_nw_node_wrangler"
2636 bl_space_type = 'NODE_EDITOR'
2637 bl_region_type = 'UI'
2638 bl_label = "Node Wrangler"
2640 prepend = StringProperty(
2641 name='prepend',
2643 append = StringProperty()
2644 remove = StringProperty()
2646 def draw(self, context):
2647 self.layout.label(text="(Quick access: Ctrl+Space)")
2648 drawlayout(context, self.layout, mode='panel')
2652 # M E N U S
2654 class NodeWranglerMenu(Menu, NWBase):
2655 bl_idname = "NODE_MT_nw_node_wrangler_menu"
2656 bl_label = "Node Wrangler"
2658 def draw(self, context):
2659 drawlayout(context, self.layout)
2662 class NWMergeNodesMenu(Menu, NWBase):
2663 bl_idname = "NODE_MT_nw_merge_nodes_menu"
2664 bl_label = "Merge Selected Nodes"
2666 def draw(self, context):
2667 type = context.space_data.tree_type
2668 layout = self.layout
2669 if type == 'ShaderNodeTree':
2670 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
2671 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
2672 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
2675 class NWMergeShadersMenu(Menu, NWBase):
2676 bl_idname = "NODE_MT_nw_merge_shaders_menu"
2677 bl_label = "Merge Selected Nodes using Shaders"
2679 def draw(self, context):
2680 layout = self.layout
2681 for type in ('MIX', 'ADD'):
2682 props = layout.operator(NWMergeNodes.bl_idname, text=type)
2683 props.mode = type
2684 props.merge_type = 'SHADER'
2687 class NWMergeMixMenu(Menu, NWBase):
2688 bl_idname = "NODE_MT_nw_merge_mix_menu"
2689 bl_label = "Merge Selected Nodes using Mix"
2691 def draw(self, context):
2692 layout = self.layout
2693 for type, name, description in blend_types:
2694 props = layout.operator(NWMergeNodes.bl_idname, text=name)
2695 props.mode = type
2696 props.merge_type = 'MIX'
2699 class NWConnectionListOutputs(Menu, NWBase):
2700 bl_idname = "NODE_MT_nw_connection_list_out"
2701 bl_label = "From:"
2703 def draw(self, context):
2704 layout = self.layout
2705 nodes, links = get_nodes_links(context)
2707 n1 = nodes[context.scene.NWLazySource]
2709 if n1.type == "R_LAYERS":
2710 index=0
2711 for o in n1.outputs:
2712 if o.enabled: # Check which passes the render layer has enabled
2713 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
2714 index+=1
2715 else:
2716 index=0
2717 for o in n1.outputs:
2718 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
2719 index+=1
2722 class NWConnectionListInputs(Menu, NWBase):
2723 bl_idname = "NODE_MT_nw_connection_list_in"
2724 bl_label = "To:"
2726 def draw(self, context):
2727 layout = self.layout
2728 nodes, links = get_nodes_links(context)
2730 n2 = nodes[context.scene.NWLazyTarget]
2732 #print (self.from_socket)
2734 index = 0
2735 for i in n2.inputs:
2736 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
2737 op.from_socket = context.scene.NWSourceSocket
2738 op.to_socket = index
2739 index+=1
2742 class NWMergeMathMenu(Menu, NWBase):
2743 bl_idname = "NODE_MT_nw_merge_math_menu"
2744 bl_label = "Merge Selected Nodes using Math"
2746 def draw(self, context):
2747 layout = self.layout
2748 for type, name, description in operations:
2749 props = layout.operator(NWMergeNodes.bl_idname, text=name)
2750 props.mode = type
2751 props.merge_type = 'MATH'
2754 class NWBatchChangeNodesMenu(Menu, NWBase):
2755 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
2756 bl_label = "Batch Change Selected Nodes"
2758 def draw(self, context):
2759 layout = self.layout
2760 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
2761 layout.menu(NWBatchChangeOperationMenu.bl_idname)
2764 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
2765 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
2766 bl_label = "Batch Change Blend Type"
2768 def draw(self, context):
2769 layout = self.layout
2770 for type, name, description in blend_types:
2771 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
2772 props.blend_type = type
2773 props.operation = 'CURRENT'
2776 class NWBatchChangeOperationMenu(Menu, NWBase):
2777 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
2778 bl_label = "Batch Change Math Operation"
2780 def draw(self, context):
2781 layout = self.layout
2782 for type, name, description in operations:
2783 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
2784 props.blend_type = 'CURRENT'
2785 props.operation = type
2788 class NWCopyToSelectedMenu(Menu, NWBase):
2789 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
2790 bl_label = "Copy to Selected"
2792 def draw(self, context):
2793 layout = self.layout
2794 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
2795 layout.menu(NWCopyLabelMenu.bl_idname)
2798 class NWCopyLabelMenu(Menu, NWBase):
2799 bl_idname = "NODE_MT_nw_copy_label_menu"
2800 bl_label = "Copy Label"
2802 def draw(self, context):
2803 layout = self.layout
2804 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
2805 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
2806 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
2809 class NWAddReroutesMenu(Menu, NWBase):
2810 bl_idname = "NODE_MT_nw_add_reroutes_menu"
2811 bl_label = "Add Reroutes"
2812 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
2814 def draw(self, context):
2815 layout = self.layout
2816 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
2817 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
2818 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
2821 class NWLinkActiveToSelectedMenu(Menu, NWBase):
2822 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
2823 bl_label = "Link Active to Selected"
2825 def draw(self, context):
2826 layout = self.layout
2827 layout.menu(NWLinkStandardMenu.bl_idname)
2828 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
2829 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
2832 class NWLinkStandardMenu(Menu, NWBase):
2833 bl_idname = "NODE_MT_nw_link_standard_menu"
2834 bl_label = "To All Selected"
2836 def draw(self, context):
2837 layout = self.layout
2838 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
2839 props.replace = False
2840 props.use_node_name = False
2841 props.use_outputs_names = False
2842 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
2843 props.replace = True
2844 props.use_node_name = False
2845 props.use_outputs_names = False
2848 class NWLinkUseNodeNameMenu(Menu, NWBase):
2849 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
2850 bl_label = "Use Node Name/Label"
2852 def draw(self, context):
2853 layout = self.layout
2854 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
2855 props.replace = False
2856 props.use_node_name = True
2857 props.use_outputs_names = False
2858 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
2859 props.replace = True
2860 props.use_node_name = True
2861 props.use_outputs_names = False
2864 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
2865 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
2866 bl_label = "Use Outputs Names"
2868 def draw(self, context):
2869 layout = self.layout
2870 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
2871 props.replace = False
2872 props.use_node_name = False
2873 props.use_outputs_names = True
2874 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
2875 props.replace = True
2876 props.use_node_name = False
2877 props.use_outputs_names = True
2880 class NWNodeAlignMenu(Menu, NWBase):
2881 bl_idname = "NODE_MT_nw_node_align_menu"
2882 bl_label = "Align Nodes"
2884 def draw(self, context):
2885 layout = self.layout
2886 layout.operator(NWAlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X'
2887 layout.operator(NWAlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y'
2890 # TODO, add to toolbar panel
2891 class NWUVMenu(bpy.types.Menu):
2892 bl_idname = "NODE_MT_nw_node_uvs_menu"
2893 bl_label = "UV Maps"
2895 @classmethod
2896 def poll(cls, context):
2897 if context.area.spaces[0].node_tree:
2898 if context.area.spaces[0].node_tree.type == 'SHADER':
2899 return True
2900 else:
2901 return False
2902 else:
2903 return False
2905 def draw(self, context):
2906 l = self.layout
2907 nodes, links = get_nodes_links(context)
2908 mat = context.object.active_material
2910 objs = []
2911 for obj in bpy.data.objects:
2912 for slot in obj.material_slots:
2913 if slot.material == mat:
2914 objs.append(obj)
2915 uvs = []
2916 for obj in objs:
2917 if obj.data.uv_layers:
2918 for uv in obj.data.uv_layers:
2919 uvs.append(uv.name)
2920 uvs = list(set(uvs)) # get a unique list
2922 if uvs:
2923 for uv in uvs:
2924 l.operator(NWAddAttrNode.bl_idname, text=uv).attr_name = uv
2925 else:
2926 l.label("No UV layers on objects with this material")
2929 class NWVertColMenu(bpy.types.Menu):
2930 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
2931 bl_label = "Vertex Colors"
2933 @classmethod
2934 def poll(cls, context):
2935 if context.area.spaces[0].node_tree:
2936 if context.area.spaces[0].node_tree.type == 'SHADER':
2937 return True
2938 else:
2939 return False
2940 else:
2941 return False
2943 def draw(self, context):
2944 l = self.layout
2945 nodes, links = get_nodes_links(context)
2946 mat = context.object.active_material
2948 objs = []
2949 for obj in bpy.data.objects:
2950 for slot in obj.material_slots:
2951 if slot.material == mat:
2952 objs.append(obj)
2953 vcols = []
2954 for obj in objs:
2955 if obj.data.vertex_colors:
2956 for vcol in obj.data.vertex_colors:
2957 vcols.append(vcol.name)
2958 vcols = list(set(vcols)) # get a unique list
2960 if vcols:
2961 for vcol in vcols:
2962 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
2963 else:
2964 l.label("No Vertex Color layers on objects with this material")
2967 class NWSwitchNodeTypeMenu(Menu, NWBase):
2968 bl_idname = "NODE_MT_nw_switch_node_type_menu"
2969 bl_label = "Switch Type to..."
2971 def draw(self, context):
2972 layout = self.layout
2973 tree = context.space_data.node_tree
2974 if tree.type == 'SHADER':
2975 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
2976 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
2977 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
2978 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
2979 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
2980 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
2981 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
2982 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
2983 if tree.type == 'COMPOSITING':
2984 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
2985 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
2986 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
2987 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
2988 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
2989 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
2990 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
2991 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
2992 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
2995 class NWSwitchShadersInputSubmenu(Menu, NWBase):
2996 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
2997 bl_label = "Input"
2999 def draw(self, context):
3000 layout = self.layout
3001 for ident, type, rna_name in shaders_input_nodes_props:
3002 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3003 props.to_type = ident
3006 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
3007 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
3008 bl_label = "Output"
3010 def draw(self, context):
3011 layout = self.layout
3012 for ident, type, rna_name in shaders_output_nodes_props:
3013 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3014 props.to_type = ident
3017 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
3018 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
3019 bl_label = "Shader"
3021 def draw(self, context):
3022 layout = self.layout
3023 for ident, type, rna_name in shaders_shader_nodes_props:
3024 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3025 props.to_type = ident
3028 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
3029 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
3030 bl_label = "Texture"
3032 def draw(self, context):
3033 layout = self.layout
3034 for ident, type, rna_name in shaders_texture_nodes_props:
3035 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3036 props.to_type = ident
3039 class NWSwitchShadersColorSubmenu(Menu, NWBase):
3040 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
3041 bl_label = "Color"
3043 def draw(self, context):
3044 layout = self.layout
3045 for ident, type, rna_name in shaders_color_nodes_props:
3046 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3047 props.to_type = ident
3050 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
3051 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
3052 bl_label = "Vector"
3054 def draw(self, context):
3055 layout = self.layout
3056 for ident, type, rna_name in shaders_vector_nodes_props:
3057 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3058 props.to_type = ident
3061 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
3062 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
3063 bl_label = "Converter"
3065 def draw(self, context):
3066 layout = self.layout
3067 for ident, type, rna_name in shaders_converter_nodes_props:
3068 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3069 props.to_type = ident
3072 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
3073 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
3074 bl_label = "Layout"
3076 def draw(self, context):
3077 layout = self.layout
3078 for ident, type, rna_name in shaders_layout_nodes_props:
3079 if type != 'FRAME':
3080 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3081 props.to_type = ident
3084 class NWSwitchCompoInputSubmenu(Menu, NWBase):
3085 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
3086 bl_label = "Input"
3088 def draw(self, context):
3089 layout = self.layout
3090 for ident, type, rna_name in compo_input_nodes_props:
3091 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3092 props.to_type = ident
3095 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
3096 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
3097 bl_label = "Output"
3099 def draw(self, context):
3100 layout = self.layout
3101 for ident, type, rna_name in compo_output_nodes_props:
3102 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3103 props.to_type = ident
3106 class NWSwitchCompoColorSubmenu(Menu, NWBase):
3107 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
3108 bl_label = "Color"
3110 def draw(self, context):
3111 layout = self.layout
3112 for ident, type, rna_name in compo_color_nodes_props:
3113 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3114 props.to_type = ident
3117 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
3118 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
3119 bl_label = "Converter"
3121 def draw(self, context):
3122 layout = self.layout
3123 for ident, type, rna_name in compo_converter_nodes_props:
3124 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3125 props.to_type = ident
3128 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
3129 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
3130 bl_label = "Filter"
3132 def draw(self, context):
3133 layout = self.layout
3134 for ident, type, rna_name in compo_filter_nodes_props:
3135 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3136 props.to_type = ident
3139 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
3140 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
3141 bl_label = "Vector"
3143 def draw(self, context):
3144 layout = self.layout
3145 for ident, type, rna_name in compo_vector_nodes_props:
3146 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3147 props.to_type = ident
3150 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
3151 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
3152 bl_label = "Matte"
3154 def draw(self, context):
3155 layout = self.layout
3156 for ident, type, rna_name in compo_matte_nodes_props:
3157 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3158 props.to_type = ident
3161 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
3162 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
3163 bl_label = "Distort"
3165 def draw(self, context):
3166 layout = self.layout
3167 for ident, type, rna_name in compo_distort_nodes_props:
3168 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3169 props.to_type = ident
3172 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
3173 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
3174 bl_label = "Layout"
3176 def draw(self, context):
3177 layout = self.layout
3178 for ident, type, rna_name in compo_layout_nodes_props:
3179 if type != 'FRAME':
3180 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
3181 props.to_type = ident
3185 # APPENDAGES TO EXISTING UI
3189 def select_parent_children_buttons(self, context):
3190 layout = self.layout
3191 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
3192 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
3195 def attr_nodes_menu_func(self, context):
3196 col = self.layout.column(align=True)
3197 col.menu("NODE_MT_nw_node_uvs_menu")
3198 col.menu("NODE_MT_nw_node_vertex_color_menu")
3199 col.separator()
3202 def bgreset_menu_func(self, context):
3203 self.layout.operator(NWResetBG.bl_idname)
3207 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
3210 addon_keymaps = []
3211 # kmi_defs entry: (identifier, key, CTRL, SHIFT, ALT, props, nice name)
3212 # props entry: (property name, property value)
3213 kmi_defs = (
3214 # MERGE NODES
3215 # NWMergeNodes with Ctrl (AUTO).
3216 (NWMergeNodes.bl_idname, 'NUMPAD_0', True, False, False,
3217 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
3218 (NWMergeNodes.bl_idname, 'ZERO', True, False, False,
3219 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
3220 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, False,
3221 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
3222 (NWMergeNodes.bl_idname, 'EQUAL', True, False, False,
3223 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
3224 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, False,
3225 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
3226 (NWMergeNodes.bl_idname, 'EIGHT', True, False, False,
3227 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
3228 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, False,
3229 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
3230 (NWMergeNodes.bl_idname, 'MINUS', True, False, False,
3231 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
3232 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, False,
3233 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
3234 (NWMergeNodes.bl_idname, 'SLASH', True, False, False,
3235 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
3236 (NWMergeNodes.bl_idname, 'COMMA', True, False, False,
3237 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
3238 (NWMergeNodes.bl_idname, 'PERIOD', True, False, False,
3239 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
3240 # NWMergeNodes with Ctrl Alt (MIX)
3241 (NWMergeNodes.bl_idname, 'NUMPAD_0', True, False, True,
3242 (('mode', 'MIX'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Mix)"),
3243 (NWMergeNodes.bl_idname, 'ZERO', True, False, True,
3244 (('mode', 'MIX'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Mix)"),
3245 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, True,
3246 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
3247 (NWMergeNodes.bl_idname, 'EQUAL', True, False, True,
3248 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
3249 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, True,
3250 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
3251 (NWMergeNodes.bl_idname, 'EIGHT', True, False, True,
3252 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
3253 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, True,
3254 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
3255 (NWMergeNodes.bl_idname, 'MINUS', True, False, True,
3256 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
3257 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, True,
3258 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
3259 (NWMergeNodes.bl_idname, 'SLASH', True, False, True,
3260 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
3261 # NWMergeNodes with Ctrl Shift (MATH)
3262 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', True, True, False,
3263 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
3264 (NWMergeNodes.bl_idname, 'EQUAL', True, True, False,
3265 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
3266 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, True, False,
3267 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
3268 (NWMergeNodes.bl_idname, 'EIGHT', True, True, False,
3269 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
3270 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', True, True, False,
3271 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
3272 (NWMergeNodes.bl_idname, 'MINUS', True, True, False,
3273 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
3274 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', True, True, False,
3275 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
3276 (NWMergeNodes.bl_idname, 'SLASH', True, True, False,
3277 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
3278 (NWMergeNodes.bl_idname, 'COMMA', True, True, False,
3279 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
3280 (NWMergeNodes.bl_idname, 'PERIOD', True, True, False,
3281 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
3282 # BATCH CHANGE NODES
3283 # NWBatchChangeNodes with Alt
3284 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', False, False, True,
3285 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
3286 (NWBatchChangeNodes.bl_idname, 'ZERO', False, False, True,
3287 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
3288 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', False, False, True,
3289 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
3290 (NWBatchChangeNodes.bl_idname, 'EQUAL', False, False, True,
3291 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
3292 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', False, False, True,
3293 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
3294 (NWBatchChangeNodes.bl_idname, 'EIGHT', False, False, True,
3295 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
3296 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', False, False, True,
3297 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
3298 (NWBatchChangeNodes.bl_idname, 'MINUS', False, False, True,
3299 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
3300 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', False, False, True,
3301 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
3302 (NWBatchChangeNodes.bl_idname, 'SLASH', False, False, True,
3303 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
3304 (NWBatchChangeNodes.bl_idname, 'COMMA', False, False, True,
3305 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
3306 (NWBatchChangeNodes.bl_idname, 'PERIOD', False, False, True,
3307 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
3308 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', False, False, True,
3309 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
3310 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', False, False, True,
3311 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
3312 # LINK ACTIVE TO SELECTED
3313 # Don't use names, don't replace links (K)
3314 (NWLinkActiveToSelected.bl_idname, 'K', False, False, False,
3315 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
3316 # Don't use names, replace links (Shift K)
3317 (NWLinkActiveToSelected.bl_idname, 'K', False, True, False,
3318 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
3319 # Use node name, don't replace links (')
3320 (NWLinkActiveToSelected.bl_idname, 'QUOTE', False, False, False,
3321 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
3322 # Use node name, replace links (Shift ')
3323 (NWLinkActiveToSelected.bl_idname, 'QUOTE', False, True, False,
3324 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
3325 # Don't use names, don't replace links (;)
3326 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, False, False,
3327 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
3328 # Don't use names, replace links (')
3329 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, True, False,
3330 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
3331 # CHANGE MIX FACTOR
3332 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
3333 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
3334 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
3335 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
3336 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
3337 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
3338 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
3339 (NWChangeMixFactor.bl_idname, 'ZERO', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
3340 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
3341 (NWChangeMixFactor.bl_idname, 'ONE', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
3342 # CLEAR LABEL (Alt L)
3343 (NWClearLabel.bl_idname, 'L', False, False, True, (('option', False),), "Clear node labels"),
3344 # MODIFY LABEL (Alt Shift L)
3345 (NWModifyLabels.bl_idname, 'L', False, True, True, None, "Modify node labels"),
3346 # Copy Label from active to selected
3347 (NWCopyLabel.bl_idname, 'V', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
3348 # DETACH OUTPUTS (Alt Shift D)
3349 (NWDetachOutputs.bl_idname, 'D', False, True, True, None, "Detach outputs"),
3350 # LINK TO OUTPUT NODE (O)
3351 (NWLinkToOutputNode.bl_idname, 'O', False, False, False, None, "Link to output node"),
3352 # SELECT PARENT/CHILDREN
3353 # Select Children
3354 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', False, False, False, (('option', 'CHILD'),), "Select children"),
3355 # Select Parent
3356 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', False, False, False, (('option', 'PARENT'),), "Select Parent"),
3357 # Add Texture Setup
3358 (NWAddTextureSetup.bl_idname, 'T', True, False, False, None, "Add texture setup"),
3359 # Reset backdrop
3360 (NWResetBG.bl_idname, 'Z', False, False, False, None, "Reset backdrop image zoom"),
3361 # Delete unused
3362 (NWDeleteUnused.bl_idname, 'X', False, False, True, None, "Delete unused nodes"),
3363 # Frame Seleted
3364 (NWFrameSelected.bl_idname, 'P', False, True, False, None, "Frame selected nodes"),
3365 # Swap Outputs
3366 (NWSwapOutputs.bl_idname, 'S', False, False, True, None, "Swap Outputs"),
3367 # Emission Viewer
3368 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', True, True, False, None, "Connect to Cycles Viewer node"),
3369 # Reload Images
3370 (NWReloadImages.bl_idname, 'R', False, False, True, None, "Reload images"),
3371 # Lazy Mix
3372 (NWLazyMix.bl_idname, 'RIGHTMOUSE', False, False, True, None, "Lazy Mix"),
3373 # Lazy Connect
3374 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', True, False, False, None, "Lazy Connect"),
3375 # Lazy Connect with Menu
3376 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
3377 # MENUS
3378 ('wm.call_menu', 'SPACE', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"),
3379 ('wm.call_menu', 'SLASH', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
3380 ('wm.call_menu', 'NUMPAD_SLASH', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
3381 ('wm.call_menu', 'EQUAL', False, True, False, (('name', NWNodeAlignMenu.bl_idname),), "Node alignment menu"),
3382 ('wm.call_menu', 'BACK_SLASH', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
3383 ('wm.call_menu', 'C', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
3384 ('wm.call_menu', 'S', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
3388 def register():
3389 # props
3390 bpy.types.Scene.NWBusyDrawing = StringProperty(
3391 name="Busy Drawing!",
3392 default="",
3393 description="An internal property used to store only the first mouse position")
3394 bpy.types.Scene.NWLazySource = StringProperty(
3395 name="Lazy Source!",
3396 default="x",
3397 description="An internal property used to store the first node in a Lazy Connect operation")
3398 bpy.types.Scene.NWLazyTarget = StringProperty(
3399 name="Lazy Target!",
3400 default="x",
3401 description="An internal property used to store the last node in a Lazy Connect operation")
3402 bpy.types.Scene.NWSourceSocket = IntProperty(
3403 name="Source Socket!",
3404 default=0,
3405 description="An internal property used to store the source socket in a Lazy Connect operation")
3407 bpy.utils.register_module(__name__)
3409 # keymaps
3410 km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
3411 for (identifier, key, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
3412 kmi = km.keymap_items.new(identifier, key, 'PRESS', ctrl=CTRL, shift=SHIFT, alt=ALT)
3413 if props:
3414 for prop, value in props:
3415 setattr(kmi.properties, prop, value)
3416 addon_keymaps.append((km, kmi))
3418 # menu items
3419 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
3420 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
3421 bpy.types.NODE_PT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
3422 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
3425 def unregister():
3426 # props
3427 del bpy.types.Scene.NWBusyDrawing
3428 del bpy.types.Scene.NWLazySource
3429 del bpy.types.Scene.NWLazyTarget
3430 del bpy.types.Scene.NWSourceSocket
3432 bpy.utils.unregister_module(__name__)
3434 # keymaps
3435 for km, kmi in addon_keymaps:
3436 km.keymap_items.remove(kmi)
3437 addon_keymaps.clear()
3439 # menuitems
3440 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
3441 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
3442 bpy.types.NODE_PT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
3443 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
3445 if __name__ == "__main__":
3446 register()