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