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 #####
20 "name": "Node Wrangler",
21 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
23 "blender": (2, 93, 0),
24 "location": "Node Editor Toolbar or Shift-W",
25 "description": "Various tools to enhance and speed up node-based workflow",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
33 from bpy
.types
import Operator
, Panel
, Menu
34 from bpy
.props
import (
43 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
44 from gpu_extras
.batch
import batch_for_shader
45 from mathutils
import Vector
46 from nodeitems_utils
import node_categories_iter
47 from math
import cos
, sin
, pi
, hypot
51 from itertools
import chain
53 from collections
import namedtuple
57 # list of outputs of Input Render Layer
58 # with attributes determining 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'])
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),
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 # Keeping things in alphabetical order so we don't need to sort later.
95 shaders_input_nodes_props
= (
96 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
97 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
98 ('ShaderNodeBevel', 'BEVEL', 'Bevel'),
99 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
100 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
101 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
102 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
103 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
104 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
105 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
106 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
107 ('ShaderNodeRGB', 'RGB', 'RGB'),
108 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
109 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
110 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
111 ('ShaderNodeValue', 'VALUE', 'Value'),
112 ('ShaderNodeVertexColor', 'VERTEX_COLOR', 'Vertex Color'),
113 ('ShaderNodeVolumeInfo', 'VOLUME_INFO', 'Volume Info'),
114 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
117 # (rna_type.identifier, type, rna_type.name)
118 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
119 # Keeping things in alphabetical order so we don't need to sort later.
120 shaders_output_nodes_props
= (
121 ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'),
122 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
123 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
124 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
126 # (rna_type.identifier, type, rna_type.name)
127 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
128 # Keeping things in alphabetical order so we don't need to sort later.
129 shaders_shader_nodes_props
= (
130 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
131 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
132 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
133 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
134 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
135 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
136 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
137 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
138 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
139 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
140 ('ShaderNodeBsdfHairPrincipled', 'BSDF_HAIR_PRINCIPLED', 'Principled Hair BSDF'),
141 ('ShaderNodeVolumePrincipled', 'PRINCIPLED_VOLUME', 'Principled Volume'),
142 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
143 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
144 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
145 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
146 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
147 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
148 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
149 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
150 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
152 # (rna_type.identifier, type, rna_type.name)
153 # Keeping things in alphabetical order so we don't need to sort later.
154 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
155 shaders_texture_nodes_props
= (
156 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
157 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
158 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
159 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
160 ('ShaderNodeTexIES', 'TEX_IES', 'IES Texture'),
161 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
162 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
163 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
164 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
165 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
166 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
167 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
168 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
169 ('ShaderNodeTexWhiteNoise', 'TEX_WHITE_NOISE', 'White Noise'),
171 # (rna_type.identifier, type, rna_type.name)
172 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
173 # Keeping things in alphabetical order so we don't need to sort later.
174 shaders_color_nodes_props
= (
175 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
176 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
177 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
178 ('ShaderNodeInvert', 'INVERT', 'Invert'),
179 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
180 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
181 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
183 # (rna_type.identifier, type, rna_type.name)
184 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
185 # Keeping things in alphabetical order so we don't need to sort later.
186 shaders_vector_nodes_props
= (
187 ('ShaderNodeBump', 'BUMP', 'Bump'),
188 ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'),
189 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
190 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
191 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
192 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
193 ('ShaderNodeVectorDisplacement', 'VECTOR_DISPLACEMENT', 'Vector Displacement'),
194 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
196 # (rna_type.identifier, type, rna_type.name)
197 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
198 # Keeping things in alphabetical order so we don't need to sort later.
199 shaders_converter_nodes_props
= (
200 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
201 ('ShaderNodeClamp', 'CLAMP', 'Clamp'),
202 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
203 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
204 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
205 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
206 ('ShaderNodeMapRange', 'MAP_RANGE', 'Map Range'),
207 ('ShaderNodeMath', 'MATH', 'Math'),
208 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
209 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
210 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
211 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
212 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
213 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
215 # (rna_type.identifier, type, rna_type.name)
216 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
217 # Keeping things in alphabetical order so we don't need to sort later.
218 shaders_layout_nodes_props
= (
219 ('NodeFrame', 'FRAME', 'Frame'),
220 ('NodeReroute', 'REROUTE', 'Reroute'),
224 # (rna_type.identifier, type, rna_type.name)
225 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
226 # Keeping things in alphabetical order so we don't need to sort later.
227 compo_input_nodes_props
= (
228 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
229 ('CompositorNodeImage', 'IMAGE', 'Image'),
230 ('CompositorNodeMask', 'MASK', 'Mask'),
231 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
232 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
233 ('CompositorNodeRGB', 'RGB', 'RGB'),
234 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
235 ('CompositorNodeTime', 'TIME', 'Time'),
236 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
237 ('CompositorNodeValue', 'VALUE', 'Value'),
239 # (rna_type.identifier, type, rna_type.name)
240 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
241 # Keeping things in alphabetical order so we don't need to sort later.
242 compo_output_nodes_props
= (
243 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
244 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
245 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
246 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
247 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
249 # (rna_type.identifier, type, rna_type.name)
250 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
251 # Keeping things in alphabetical order so we don't need to sort later.
252 compo_color_nodes_props
= (
253 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
254 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
255 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
256 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
257 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
258 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
259 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
260 ('CompositorNodeInvert', 'INVERT', 'Invert'),
261 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
262 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
263 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
264 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
266 # (rna_type.identifier, type, rna_type.name)
267 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
268 # Keeping things in alphabetical order so we don't need to sort later.
269 compo_converter_nodes_props
= (
270 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
271 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
272 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
273 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
274 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
275 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
276 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
277 ('CompositorNodeMath', 'MATH', 'Math'),
278 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
279 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
280 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
281 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
282 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
283 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
284 ('CompositorNodeSwitchView', 'VIEWSWITCH', 'View Switch'),
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 # Keeping things in alphabetical order so we don't need to sort later.
289 compo_filter_nodes_props
= (
290 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
291 ('CompositorNodeBlur', 'BLUR', 'Blur'),
292 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
293 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
294 ('CompositorNodeDenoise', 'DENOISE', 'Denoise'),
295 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
296 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
297 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
298 ('CompositorNodeFilter', 'FILTER', 'Filter'),
299 ('CompositorNodeGlare', 'GLARE', 'Glare'),
300 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
301 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
302 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
303 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
305 # (rna_type.identifier, type, rna_type.name)
306 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
307 # Keeping things in alphabetical order so we don't need to sort later.
308 compo_vector_nodes_props
= (
309 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
310 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
311 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
312 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
313 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
315 # (rna_type.identifier, type, rna_type.name)
316 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
317 # Keeping things in alphabetical order so we don't need to sort later.
318 compo_matte_nodes_props
= (
319 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
320 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
321 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
322 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
323 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
324 ('CompositorNodeCryptomatte', 'CRYPTOMATTE', 'Cryptomatte'),
325 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
326 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
327 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
328 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
329 ('CompositorNodeKeying', 'KEYING', 'Keying'),
330 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
331 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
333 # (rna_type.identifier, type, rna_type.name)
334 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
335 # Keeping things in alphabetical order so we don't need to sort later.
336 compo_distort_nodes_props
= (
337 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
338 ('CompositorNodeCrop', 'CROP', 'Crop'),
339 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
340 ('CompositorNodeFlip', 'FLIP', 'Flip'),
341 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
342 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
343 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
344 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
345 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
346 ('CompositorNodeScale', 'SCALE', 'Scale'),
347 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
348 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
349 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
351 # (rna_type.identifier, type, rna_type.name)
352 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
353 # Keeping things in alphabetical order so we don't need to sort later.
354 compo_layout_nodes_props
= (
355 ('NodeFrame', 'FRAME', 'Frame'),
356 ('NodeReroute', 'REROUTE', 'Reroute'),
357 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
359 # Blender Render material nodes
360 # (rna_type.identifier, type, rna_type.name)
361 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
362 blender_mat_input_nodes_props
= (
363 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
364 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
365 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
366 ('ShaderNodeValue', 'VALUE', 'Value'),
367 ('ShaderNodeRGB', 'RGB', 'RGB'),
368 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
369 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
370 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
373 # (rna_type.identifier, type, rna_type.name)
374 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
375 blender_mat_output_nodes_props
= (
376 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
379 # (rna_type.identifier, type, rna_type.name)
380 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
381 blender_mat_color_nodes_props
= (
382 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
383 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
384 ('ShaderNodeInvert', 'INVERT', 'Invert'),
385 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
388 # (rna_type.identifier, type, rna_type.name)
389 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
390 blender_mat_vector_nodes_props
= (
391 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
392 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
393 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
396 # (rna_type.identifier, type, rna_type.name)
397 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
398 blender_mat_converter_nodes_props
= (
399 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
400 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
401 ('ShaderNodeMath', 'MATH', 'Math'),
402 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
403 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
404 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
405 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
406 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
407 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
410 # (rna_type.identifier, type, rna_type.name)
411 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
412 blender_mat_layout_nodes_props
= (
413 ('NodeReroute', 'REROUTE', 'Reroute'),
417 # (rna_type.identifier, type, rna_type.name)
418 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
419 texture_input_nodes_props
= (
420 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
421 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
422 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
423 ('TextureNodeImage', 'IMAGE', 'Image'),
426 # (rna_type.identifier, type, rna_type.name)
427 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
428 texture_output_nodes_props
= (
429 ('TextureNodeOutput', 'OUTPUT', 'Output'),
430 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
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_color_nodes_props
= (
436 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
437 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
438 ('TextureNodeInvert', 'INVERT', 'Invert'),
439 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
440 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
441 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
444 # (rna_type.identifier, type, rna_type.name)
445 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
446 texture_pattern_nodes_props
= (
447 ('TextureNodeChecker', 'CHECKER', 'Checker'),
448 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
451 # (rna_type.identifier, type, rna_type.name)
452 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
453 texture_textures_nodes_props
= (
454 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
455 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
456 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
457 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
458 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
459 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
460 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
461 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
462 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
463 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
466 # (rna_type.identifier, type, rna_type.name)
467 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
468 texture_converter_nodes_props
= (
469 ('TextureNodeMath', 'MATH', 'Math'),
470 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
471 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
472 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
473 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
476 # (rna_type.identifier, type, rna_type.name)
477 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
478 texture_distort_nodes_props
= (
479 ('TextureNodeScale', 'SCALE', 'Scale'),
480 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
481 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
482 ('TextureNodeAt', 'AT', 'At'),
485 # (rna_type.identifier, type, rna_type.name)
486 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
487 texture_layout_nodes_props
= (
488 ('NodeReroute', 'REROUTE', 'Reroute'),
491 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
492 # used list, not tuple for easy merging with other lists.
494 ('MIX', 'Mix', 'Mix Mode'),
495 ('ADD', 'Add', 'Add Mode'),
496 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
497 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
498 ('SCREEN', 'Screen', 'Screen Mode'),
499 ('DIVIDE', 'Divide', 'Divide Mode'),
500 ('DIFFERENCE', 'Difference', 'Difference Mode'),
501 ('DARKEN', 'Darken', 'Darken Mode'),
502 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
503 ('OVERLAY', 'Overlay', 'Overlay Mode'),
504 ('DODGE', 'Dodge', 'Dodge Mode'),
505 ('BURN', 'Burn', 'Burn Mode'),
506 ('HUE', 'Hue', 'Hue Mode'),
507 ('SATURATION', 'Saturation', 'Saturation Mode'),
508 ('VALUE', 'Value', 'Value Mode'),
509 ('COLOR', 'Color', 'Color Mode'),
510 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
511 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
514 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
515 # used list, not tuple for easy merging with other lists.
517 ('ADD', 'Add', 'Add Mode'),
518 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
519 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
520 ('DIVIDE', 'Divide', 'Divide Mode'),
521 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
522 ('SINE', 'Sine', 'Sine Mode'),
523 ('COSINE', 'Cosine', 'Cosine Mode'),
524 ('TANGENT', 'Tangent', 'Tangent Mode'),
525 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
526 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
527 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
528 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
529 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
530 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
531 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
532 ('POWER', 'Power', 'Power Mode'),
533 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
534 ('SQRT', 'Square Root', 'Square Root Mode'),
535 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
536 ('EXPONENT', 'Exponent', 'Exponent Mode'),
537 ('MINIMUM', 'Minimum', 'Minimum Mode'),
538 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
539 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
540 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
541 ('SIGN', 'Sign', 'Sign Mode'),
542 ('COMPARE', 'Compare', 'Compare Mode'),
543 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
544 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
545 ('FRACT', 'Fraction', 'Fraction Mode'),
546 ('MODULO', 'Modulo', 'Modulo Mode'),
547 ('SNAP', 'Snap', 'Snap Mode'),
548 ('WRAP', 'Wrap', 'Wrap Mode'),
549 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
550 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
551 ('ROUND', 'Round', 'Round Mode'),
552 ('FLOOR', 'Floor', 'Floor Mode'),
553 ('CEIL', 'Ceil', 'Ceil Mode'),
554 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
555 ('RADIANS', 'To Radians', 'To Radians Mode'),
556 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
559 # Operations used by the geometry boolean node and join geometry node
560 geo_combine_operations
= [
561 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
562 ('INTERSECT', 'Intersect', 'Intersect Mode'),
563 ('UNION', 'Union', 'Union Mode'),
564 ('DIFFERENCE', 'Difference', 'Difference Mode'),
567 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
568 # used list, not tuple for easy merging with other lists.
570 ('CURRENT', 'Current', 'Leave at current state'),
571 ('NEXT', 'Next', 'Next blend type/operation'),
572 ('PREV', 'Prev', 'Previous blend type/operation'),
577 (1.0, 1.0, 1.0, 0.7),
578 (1.0, 0.0, 0.0, 0.7),
582 (0.0, 0.0, 0.0, 1.0),
583 (0.38, 0.77, 0.38, 1.0),
584 (0.38, 0.77, 0.38, 1.0)
587 (0.0, 0.0, 0.0, 1.0),
588 (0.77, 0.77, 0.16, 1.0),
589 (0.77, 0.77, 0.16, 1.0)
592 (0.0, 0.0, 0.0, 1.0),
593 (0.38, 0.38, 0.77, 1.0),
594 (0.38, 0.38, 0.77, 1.0)
597 (0.0, 0.0, 0.0, 1.0),
598 (0.63, 0.63, 0.63, 1.0),
599 (0.63, 0.63, 0.63, 1.0)
602 (1.0, 1.0, 1.0, 0.7),
603 (0.0, 0.0, 0.0, 0.7),
608 viewer_socket_name
= "tmp_viewer"
610 def get_nodes_from_category(category_name
, context
):
611 for category
in node_categories_iter(context
):
612 if category
.name
== category_name
:
613 return sorted(category
.items(context
), key
=lambda node
: node
.label
)
615 def is_visible_socket(socket
):
616 return not socket
.hide
and socket
.enabled
and socket
.type != 'CUSTOM'
618 def nice_hotkey_name(punc
):
619 # convert the ugly string name into the actual character
622 'MIDDLEMOUSE': "MMB",
624 'WHEELUPMOUSE': "Wheel Up",
625 'WHEELDOWNMOUSE': "Wheel Down",
626 'WHEELINMOUSE': "Wheel In",
627 'WHEELOUTMOUSE': "Wheel Out",
640 'LINE_FEED': "Enter",
649 'NUMPAD_1': "Numpad 1",
650 'NUMPAD_2': "Numpad 2",
651 'NUMPAD_3': "Numpad 3",
652 'NUMPAD_4': "Numpad 4",
653 'NUMPAD_5': "Numpad 5",
654 'NUMPAD_6': "Numpad 6",
655 'NUMPAD_7': "Numpad 7",
656 'NUMPAD_8': "Numpad 8",
657 'NUMPAD_9': "Numpad 9",
658 'NUMPAD_0': "Numpad 0",
659 'NUMPAD_PERIOD': "Numpad .",
660 'NUMPAD_SLASH': "Numpad /",
661 'NUMPAD_ASTERIX': "Numpad *",
662 'NUMPAD_MINUS': "Numpad -",
663 'NUMPAD_ENTER': "Numpad Enter",
664 'NUMPAD_PLUS': "Numpad +",
667 return nice_name
[punc
]
669 return punc
.replace("_", " ").title()
672 def force_update(context
):
673 context
.space_data
.node_tree
.update_tag()
677 prefs
= bpy
.context
.preferences
.system
678 return prefs
.dpi
* prefs
.pixel_size
/ 72
681 def node_mid_pt(node
, axis
):
683 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
685 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
691 def autolink(node1
, node2
, links
):
693 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
694 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
695 for outp
in available_outputs
:
696 for inp
in available_inputs
:
697 if not inp
.is_linked
and inp
.name
== outp
.name
:
702 for outp
in available_outputs
:
703 for inp
in available_inputs
:
704 if not inp
.is_linked
and inp
.type == outp
.type:
709 # force some connection even if the type doesn't match
710 if available_outputs
:
711 for inp
in available_inputs
:
712 if not inp
.is_linked
:
714 links
.new(available_outputs
[0], inp
)
717 # even if no sockets are open, force one of matching type
718 for outp
in available_outputs
:
719 for inp
in available_inputs
:
720 if inp
.type == outp
.type:
726 for outp
in available_outputs
:
727 for inp
in available_inputs
:
732 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
736 def node_at_pos(nodes
, context
, event
):
737 nodes_near_mouse
= []
738 nodes_under_mouse
= []
741 store_mouse_cursor(context
, event
)
742 x
, y
= context
.space_data
.cursor_location
746 # Make a list of each corner (and middle of border) for each node.
747 # Will be sorted to find nearest point and thus nearest node
748 node_points_with_dist
= []
751 if node
.type != 'FRAME': # no point trying to link to a frame node
752 locx
= node
.location
.x
753 locy
= node
.location
.y
754 dimx
= node
.dimensions
.x
/dpifac()
755 dimy
= node
.dimensions
.y
/dpifac()
757 locx
+= node
.parent
.location
.x
758 locy
+= node
.parent
.location
.y
759 if node
.parent
.parent
:
760 locx
+= node
.parent
.parent
.location
.x
761 locy
+= node
.parent
.parent
.location
.y
762 if node
.parent
.parent
.parent
:
763 locx
+= node
.parent
.parent
.parent
.location
.x
764 locy
+= node
.parent
.parent
.parent
.location
.y
765 if node
.parent
.parent
.parent
.parent
:
766 # Support three levels or parenting
767 # There's got to be a better way to do this...
770 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
771 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
772 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
773 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
775 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
776 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
777 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
778 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
780 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
783 if node
.type != 'FRAME' and skipnode
== False:
784 locx
= node
.location
.x
785 locy
= node
.location
.y
786 dimx
= node
.dimensions
.x
/dpifac()
787 dimy
= node
.dimensions
.y
/dpifac()
789 locx
+= node
.parent
.location
.x
790 locy
+= node
.parent
.location
.y
791 if (locx
<= x
<= locx
+ dimx
) and \
792 (locy
- dimy
<= y
<= locy
):
793 nodes_under_mouse
.append(node
)
795 if len(nodes_under_mouse
) == 1:
796 if nodes_under_mouse
[0] != nearest_node
:
797 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
799 target_node
= nearest_node
# else use the nearest node
801 target_node
= nearest_node
805 def store_mouse_cursor(context
, event
):
806 space
= context
.space_data
807 v2d
= context
.region
.view2d
808 tree
= space
.edit_tree
810 # convert mouse position to the View2D for later node placement
811 if context
.region
.type == 'WINDOW':
812 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
814 space
.cursor_location
= tree
.view_center
816 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
817 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
819 vertices
= ((x1
, y1
), (x2
, y2
))
820 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
821 colour
[1]+(1.0-colour
[1])/4,
822 colour
[2]+(1.0-colour
[2])/4,
823 colour
[3]+(1.0-colour
[3])/4),
826 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
827 bgl
.glLineWidth(size
* dpifac())
833 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
834 radius
= radius
* dpifac()
836 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
837 radius
* sin(i
* 2 * pi
/ sides
) + my
)
838 for i
in range(sides
+ 1)]
840 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
842 shader
.uniform_float("color", colour
)
845 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
846 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
847 bottom_bar
= (16*dpifac()) + 1
849 radius
= radius
*dpifac()
851 nlocx
= (node
.location
.x
+1)*dpifac()
852 nlocy
= (node
.location
.y
+1)*dpifac()
853 ndimx
= node
.dimensions
.x
854 ndimy
= node
.dimensions
.y
855 # This is a stupid way to do this... TODO use while loop
857 nlocx
+= node
.parent
.location
.x
858 nlocy
+= node
.parent
.location
.y
859 if node
.parent
.parent
:
860 nlocx
+= node
.parent
.parent
.location
.x
861 nlocy
+= node
.parent
.parent
.location
.y
862 if node
.parent
.parent
.parent
:
863 nlocx
+= node
.parent
.parent
.parent
.location
.x
864 nlocy
+= node
.parent
.parent
.parent
.location
.y
869 if node
.type == 'REROUTE':
877 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
879 for i
in range(sides
+1):
881 if my
> bottom_bar
and mx
< area_width
:
882 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
883 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
884 vertices
.append((cosine
,sine
))
885 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
887 shader
.uniform_float("color", colour
)
891 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
893 for i
in range(sides
+1):
895 if my
> bottom_bar
and mx
< area_width
:
896 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
897 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
898 vertices
.append((cosine
,sine
))
899 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
901 shader
.uniform_float("color", colour
)
905 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
907 for i
in range(sides
+1):
909 if my
> bottom_bar
and mx
< area_width
:
910 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
911 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
912 vertices
.append((cosine
,sine
))
913 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
915 shader
.uniform_float("color", colour
)
918 # Bottom right corner
919 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
921 for i
in range(sides
+1):
923 if my
> bottom_bar
and mx
< area_width
:
924 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
925 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
926 vertices
.append((cosine
,sine
))
927 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
929 shader
.uniform_float("color", colour
)
932 # prepare drawing all edges in one batch
938 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
939 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
940 if m1x
< area_width
and m2x
< area_width
:
941 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
942 (m1x
,m1y
), (m1x
-radius
,m1y
)])
943 indices
.extend([(id_last
, id_last
+1, id_last
+3),
944 (id_last
+3, id_last
+1, id_last
+2)])
948 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
949 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
950 m1x
= min(m1x
, area_width
)
951 m2x
= min(m2x
, area_width
)
952 if m1y
> bottom_bar
and m2y
> bottom_bar
:
953 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
954 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
955 indices
.extend([(id_last
, id_last
+1, id_last
+3),
956 (id_last
+3, id_last
+1, id_last
+2)])
960 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
961 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
962 m1y
= max(m1y
, bottom_bar
)
963 m2y
= max(m2y
, bottom_bar
)
964 if m1x
< area_width
and m2x
< area_width
:
965 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
966 (m1x
+radius
,m1y
), (m1x
,m1y
)])
967 indices
.extend([(id_last
, id_last
+1, id_last
+3),
968 (id_last
+3, id_last
+1, id_last
+2)])
972 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
973 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
974 m1x
= min(m1x
, area_width
)
975 m2x
= min(m2x
, area_width
)
976 if m1y
> bottom_bar
and m2y
> bottom_bar
:
977 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
978 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
979 indices
.extend([(id_last
, id_last
+1, id_last
+3),
980 (id_last
+3, id_last
+1, id_last
+2)])
982 # now draw all edges in one batch
983 if len(vertices
) != 0:
984 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
986 shader
.uniform_float("color", colour
)
989 def draw_callback_nodeoutline(self
, context
, mode
):
993 bgl
.glEnable(bgl
.GL_BLEND
)
994 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
995 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
997 nodes
, links
= get_nodes_links(context
)
999 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
1002 col_outer
= (1.0, 0.2, 0.2, 0.4)
1003 col_inner
= (0.0, 0.0, 0.0, 0.5)
1004 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
1005 elif mode
== "LINKMENU":
1006 col_outer
= (0.4, 0.6, 1.0, 0.4)
1007 col_inner
= (0.0, 0.0, 0.0, 0.5)
1008 col_circle_inner
= (0.08, 0.15, .3, 1.0)
1010 col_outer
= (0.2, 1.0, 0.2, 0.4)
1011 col_inner
= (0.0, 0.0, 0.0, 0.5)
1012 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
1014 m1x
= self
.mouse_path
[0][0]
1015 m1y
= self
.mouse_path
[0][1]
1016 m2x
= self
.mouse_path
[-1][0]
1017 m2y
= self
.mouse_path
[-1][1]
1019 n1
= nodes
[context
.scene
.NWLazySource
]
1020 n2
= nodes
[context
.scene
.NWLazyTarget
]
1023 col_outer
= (0.4, 0.4, 0.4, 0.4)
1024 col_inner
= (0.0, 0.0, 0.0, 0.5)
1025 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
1027 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
1028 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
1029 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
1030 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
1032 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
1033 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
1036 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
1037 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
1040 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
1041 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
1043 bgl
.glDisable(bgl
.GL_BLEND
)
1044 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
1045 def get_active_tree(context
):
1046 tree
= context
.space_data
.node_tree
1048 # Get nodes from currently edited tree.
1049 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1050 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1051 # the same as context.active_node, the user is in a group.
1052 # Check recursively until we find the real active node_tree:
1053 if tree
.nodes
.active
:
1054 while tree
.nodes
.active
!= context
.active_node
:
1055 tree
= tree
.nodes
.active
.node_tree
1059 def get_nodes_links(context
):
1060 tree
, path
= get_active_tree(context
)
1061 return tree
.nodes
, tree
.links
1063 def is_viewer_socket(socket
):
1064 # checks if a internal socket is a valid viewer socket
1065 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
1067 def get_internal_socket(socket
):
1068 #get the internal socket from a socket inside or outside the group
1070 if node
.type == 'GROUP_OUTPUT':
1071 source_iterator
= node
.inputs
1072 iterator
= node
.id_data
.outputs
1073 elif node
.type == 'GROUP_INPUT':
1074 source_iterator
= node
.outputs
1075 iterator
= node
.id_data
.inputs
1076 elif hasattr(node
, "node_tree"):
1077 if socket
.is_output
:
1078 source_iterator
= node
.outputs
1079 iterator
= node
.node_tree
.outputs
1081 source_iterator
= node
.inputs
1082 iterator
= node
.node_tree
.inputs
1086 for i
, s
in enumerate(source_iterator
):
1091 def is_viewer_link(link
, output_node
):
1092 if "Emission Viewer" in link
.to_node
.name
or link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
1094 if link
.to_node
.type == 'GROUP_OUTPUT':
1095 socket
= get_internal_socket(link
.to_socket
)
1096 if is_viewer_socket(socket
):
1100 def get_group_output_node(tree
):
1101 for node
in tree
.nodes
:
1102 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
1105 def get_output_location(tree
):
1106 # get right-most location
1107 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
1108 max_xloc_node
= sorted_by_xloc
[-1]
1109 if max_xloc_node
.name
== 'Emission Viewer':
1110 max_xloc_node
= sorted_by_xloc
[-2]
1112 # get average y location
1114 for node
in tree
.nodes
:
1115 sum_yloc
+= node
.location
.y
1117 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1118 loc_y
= sum_yloc
/ len(tree
.nodes
)
1122 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
1123 base_color
: StringProperty(
1125 default
='diffuse diff albedo base col color',
1126 description
='Naming Components for Base Color maps')
1127 sss_color
: StringProperty(
1128 name
='Subsurface Color',
1129 default
='sss subsurface',
1130 description
='Naming Components for Subsurface Color maps')
1131 metallic
: StringProperty(
1133 default
='metallic metalness metal mtl',
1134 description
='Naming Components for metallness maps')
1135 specular
: StringProperty(
1137 default
='specularity specular spec spc',
1138 description
='Naming Components for Specular maps')
1139 normal
: StringProperty(
1141 default
='normal nor nrm nrml norm',
1142 description
='Naming Components for Normal maps')
1143 bump
: StringProperty(
1146 description
='Naming Components for bump maps')
1147 rough
: StringProperty(
1149 default
='roughness rough rgh',
1150 description
='Naming Components for roughness maps')
1151 gloss
: StringProperty(
1153 default
='gloss glossy glossiness',
1154 description
='Naming Components for glossy maps')
1155 displacement
: StringProperty(
1156 name
='Displacement',
1157 default
='displacement displace disp dsp height heightmap',
1158 description
='Naming Components for displacement maps')
1161 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1162 bl_idname
= __name__
1164 merge_hide
: EnumProperty(
1165 name
="Hide Mix nodes",
1167 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1168 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1169 ("NEVER", "Never", "Never collapse the new merge nodes")
1171 default
='NON_SHADER',
1172 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1173 merge_position
: EnumProperty(
1174 name
="Mix Node Position",
1176 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1177 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1180 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1182 show_hotkey_list
: BoolProperty(
1183 name
="Show Hotkey List",
1185 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1187 hotkey_list_filter
: StringProperty(
1188 name
=" Filter by Name",
1190 description
="Show only hotkeys that have this text in their name"
1192 show_principled_lists
: BoolProperty(
1193 name
="Show Principled naming tags",
1195 description
="Expand this box into a list of all naming tags for principled texture setup"
1197 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1199 def draw(self
, context
):
1200 layout
= self
.layout
1201 col
= layout
.column()
1202 col
.prop(self
, "merge_position")
1203 col
.prop(self
, "merge_hide")
1206 col
= box
.column(align
=True)
1207 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1208 if self
.show_principled_lists
:
1209 tags
= self
.principled_tags
1211 col
.prop(tags
, "base_color")
1212 col
.prop(tags
, "sss_color")
1213 col
.prop(tags
, "metallic")
1214 col
.prop(tags
, "specular")
1215 col
.prop(tags
, "rough")
1216 col
.prop(tags
, "gloss")
1217 col
.prop(tags
, "normal")
1218 col
.prop(tags
, "bump")
1219 col
.prop(tags
, "displacement")
1222 col
= box
.column(align
=True)
1223 hotkey_button_name
= "Show Hotkey List"
1224 if self
.show_hotkey_list
:
1225 hotkey_button_name
= "Hide Hotkey List"
1226 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1227 if self
.show_hotkey_list
:
1228 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1230 for hotkey
in kmi_defs
:
1232 hotkey_name
= hotkey
[7]
1234 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1235 row
= col
.row(align
=True)
1236 row
.label(text
=hotkey_name
)
1237 keystr
= nice_hotkey_name(hotkey
[1])
1239 keystr
= "Shift " + keystr
1241 keystr
= "Alt " + keystr
1243 keystr
= "Ctrl " + keystr
1244 row
.label(text
=keystr
)
1248 def nw_check(context
):
1249 space
= context
.space_data
1250 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1253 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1260 def poll(cls
, context
):
1261 return nw_check(context
)
1265 class NWLazyMix(Operator
, NWBase
):
1266 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1267 bl_idname
= "node.nw_lazy_mix"
1268 bl_label
= "Mix Nodes"
1269 bl_options
= {'REGISTER', 'UNDO'}
1271 def modal(self
, context
, event
):
1272 context
.area
.tag_redraw()
1273 nodes
, links
= get_nodes_links(context
)
1276 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1279 if not context
.scene
.NWBusyDrawing
:
1280 node1
= node_at_pos(nodes
, context
, event
)
1282 context
.scene
.NWBusyDrawing
= node1
.name
1284 if context
.scene
.NWBusyDrawing
!= 'STOP':
1285 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1287 context
.scene
.NWLazySource
= node1
.name
1288 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1290 if event
.type == 'MOUSEMOVE':
1291 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1293 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1294 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1295 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1298 node2
= node_at_pos(nodes
, context
, event
)
1300 context
.scene
.NWBusyDrawing
= node2
.name
1312 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1314 context
.scene
.NWBusyDrawing
= ""
1317 elif event
.type == 'ESC':
1319 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1320 return {'CANCELLED'}
1322 return {'RUNNING_MODAL'}
1324 def invoke(self
, context
, event
):
1325 if context
.area
.type == 'NODE_EDITOR':
1326 # the arguments we pass the the callback
1327 args
= (self
, context
, 'MIX')
1328 # Add the region OpenGL drawing callback
1329 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1330 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1332 self
.mouse_path
= []
1334 context
.window_manager
.modal_handler_add(self
)
1335 return {'RUNNING_MODAL'}
1337 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1338 return {'CANCELLED'}
1341 class NWLazyConnect(Operator
, NWBase
):
1342 """Connect two nodes without clicking a specific socket (automatically determined"""
1343 bl_idname
= "node.nw_lazy_connect"
1344 bl_label
= "Lazy Connect"
1345 bl_options
= {'REGISTER', 'UNDO'}
1346 with_menu
: BoolProperty()
1348 def modal(self
, context
, event
):
1349 context
.area
.tag_redraw()
1350 nodes
, links
= get_nodes_links(context
)
1353 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1356 if not context
.scene
.NWBusyDrawing
:
1357 node1
= node_at_pos(nodes
, context
, event
)
1359 context
.scene
.NWBusyDrawing
= node1
.name
1361 if context
.scene
.NWBusyDrawing
!= 'STOP':
1362 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1364 context
.scene
.NWLazySource
= node1
.name
1365 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1367 if event
.type == 'MOUSEMOVE':
1368 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1370 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1371 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1372 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1375 node2
= node_at_pos(nodes
, context
, event
)
1377 context
.scene
.NWBusyDrawing
= node2
.name
1382 link_success
= False
1388 if node
.select
== True:
1390 original_sel
.append(node
)
1392 original_unsel
.append(node
)
1396 #link_success = autolink(node1, node2, links)
1398 if len(node1
.outputs
) > 1 and node2
.inputs
:
1399 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1400 elif len(node1
.outputs
) == 1:
1401 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1403 link_success
= autolink(node1
, node2
, links
)
1405 for node
in original_sel
:
1407 for node
in original_unsel
:
1411 force_update(context
)
1412 context
.scene
.NWBusyDrawing
= ""
1415 elif event
.type == 'ESC':
1416 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1417 return {'CANCELLED'}
1419 return {'RUNNING_MODAL'}
1421 def invoke(self
, context
, event
):
1422 if context
.area
.type == 'NODE_EDITOR':
1423 nodes
, links
= get_nodes_links(context
)
1424 node
= node_at_pos(nodes
, context
, event
)
1426 context
.scene
.NWBusyDrawing
= node
.name
1428 # the arguments we pass the the callback
1432 args
= (self
, context
, mode
)
1433 # Add the region OpenGL drawing callback
1434 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1435 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1437 self
.mouse_path
= []
1439 context
.window_manager
.modal_handler_add(self
)
1440 return {'RUNNING_MODAL'}
1442 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1443 return {'CANCELLED'}
1446 class NWDeleteUnused(Operator
, NWBase
):
1447 """Delete all nodes whose output is not used"""
1448 bl_idname
= 'node.nw_del_unused'
1449 bl_label
= 'Delete Unused Nodes'
1450 bl_options
= {'REGISTER', 'UNDO'}
1452 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1453 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1455 def is_unused_node(self
, node
):
1456 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1457 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1458 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1459 if node
.type in end_types
:
1462 for output
in node
.outputs
:
1468 def poll(cls
, context
):
1470 if nw_check(context
):
1471 if context
.space_data
.node_tree
.nodes
:
1475 def execute(self
, context
):
1476 nodes
, links
= get_nodes_links(context
)
1481 if node
.select
== True:
1482 selection
.append(node
.name
)
1488 temp_deleted_nodes
= []
1489 del_unused_iterations
= len(nodes
)
1490 for it
in range(0, del_unused_iterations
):
1491 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1493 if self
.is_unused_node(node
):
1495 deleted_nodes
.append(node
.name
)
1496 bpy
.ops
.node
.delete()
1498 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1501 if self
.delete_frames
:
1509 frames_in_use
.append(node
.parent
)
1511 if node
.type == 'FRAME' and node
not in frames_in_use
:
1514 repeat
= True # repeat for nested frames
1516 if node
not in frames_in_use
:
1518 deleted_nodes
.append(node
.name
)
1519 bpy
.ops
.node
.delete()
1521 if self
.delete_muted
:
1525 deleted_nodes
.append(node
.name
)
1526 bpy
.ops
.node
.delete_reconnect()
1528 # get unique list of deleted nodes (iterations would count the same node more than once)
1529 deleted_nodes
= list(set(deleted_nodes
))
1530 for n
in deleted_nodes
:
1531 self
.report({'INFO'}, "Node " + n
+ " deleted")
1532 num_deleted
= len(deleted_nodes
)
1537 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1539 self
.report({'INFO'}, "Nothing deleted")
1542 nodes
, links
= get_nodes_links(context
)
1544 if node
.name
in selection
:
1548 def invoke(self
, context
, event
):
1549 return context
.window_manager
.invoke_confirm(self
, event
)
1552 class NWSwapLinks(Operator
, NWBase
):
1553 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1554 bl_idname
= 'node.nw_swap_links'
1555 bl_label
= 'Swap Links'
1556 bl_options
= {'REGISTER', 'UNDO'}
1559 def poll(cls
, context
):
1561 if nw_check(context
):
1562 if context
.selected_nodes
:
1563 valid
= len(context
.selected_nodes
) <= 2
1566 def execute(self
, context
):
1567 nodes
, links
= get_nodes_links(context
)
1568 selected_nodes
= context
.selected_nodes
1569 n1
= selected_nodes
[0]
1572 if len(selected_nodes
) == 2:
1573 n2
= selected_nodes
[1]
1574 if n1
.outputs
and n2
.outputs
:
1579 for output
in n1
.outputs
:
1581 for link
in output
.links
:
1582 n1_outputs
.append([out_index
, link
.to_socket
])
1587 for output
in n2
.outputs
:
1589 for link
in output
.links
:
1590 n2_outputs
.append([out_index
, link
.to_socket
])
1594 for connection
in n1_outputs
:
1596 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1598 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1599 for connection
in n2_outputs
:
1601 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1603 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1605 if n1
.outputs
or n2
.outputs
:
1606 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1608 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1611 elif len(selected_nodes
) == 1:
1612 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1613 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1618 for i1
in n1
.inputs
:
1619 if i1
.is_linked
and not i1
.is_multi_input
:
1621 for i2
in n1
.inputs
:
1622 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1624 types
.append ([i1
, similar_types
, i
])
1626 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1631 for i2
in n1
.inputs
:
1632 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1634 i1f
= pair
[0].links
[0].from_socket
1635 i1t
= pair
[0].links
[0].to_socket
1636 i2f
= pair
[1].links
[0].from_socket
1637 i2t
= pair
[1].links
[0].to_socket
1642 fs
= t
[0].links
[0].from_socket
1644 links
.remove(t
[0].links
[0])
1645 if i
+1 == len(n1
.inputs
):
1648 while n1
.inputs
[i
].is_linked
:
1650 links
.new(fs
, n1
.inputs
[i
])
1651 elif len(types
) == 2:
1652 i1f
= types
[0][0].links
[0].from_socket
1653 i1t
= types
[0][0].links
[0].to_socket
1654 i2f
= types
[1][0].links
[0].from_socket
1655 i2t
= types
[1][0].links
[0].to_socket
1660 self
.report({'WARNING'}, "This node has no input connections to swap!")
1662 self
.report({'WARNING'}, "This node has no inputs to swap!")
1664 force_update(context
)
1668 class NWResetBG(Operator
, NWBase
):
1669 """Reset the zoom and position of the background image"""
1670 bl_idname
= 'node.nw_bg_reset'
1671 bl_label
= 'Reset Backdrop'
1672 bl_options
= {'REGISTER', 'UNDO'}
1675 def poll(cls
, context
):
1677 if nw_check(context
):
1678 snode
= context
.space_data
1679 valid
= snode
.tree_type
== 'CompositorNodeTree'
1682 def execute(self
, context
):
1683 context
.space_data
.backdrop_zoom
= 1
1684 context
.space_data
.backdrop_offset
[0] = 0
1685 context
.space_data
.backdrop_offset
[1] = 0
1689 class NWAddAttrNode(Operator
, NWBase
):
1690 """Add an Attribute node with this name"""
1691 bl_idname
= 'node.nw_add_attr_node'
1692 bl_label
= 'Add UV map'
1693 bl_options
= {'REGISTER', 'UNDO'}
1695 attr_name
: StringProperty()
1697 def execute(self
, context
):
1698 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1699 nodes
, links
= get_nodes_links(context
)
1700 nodes
.active
.attribute_name
= self
.attr_name
1703 class NWPreviewNode(Operator
, NWBase
):
1704 bl_idname
= "node.nw_preview_node"
1705 bl_label
= "Preview Node"
1706 bl_description
= "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output"
1707 bl_options
= {'REGISTER', 'UNDO'}
1709 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1710 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1711 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1712 run_in_geometry_nodes
: BoolProperty(default
=True)
1715 self
.shader_output_type
= ""
1716 self
.shader_output_ident
= ""
1717 self
.shader_viewer_ident
= ""
1720 def poll(cls
, context
):
1721 if nw_check(context
):
1722 space
= context
.space_data
1723 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1724 if context
.active_node
:
1725 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1731 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1732 #check if a viewer output already exists in a node group otherwise create
1733 if hasattr(node
, "node_tree"):
1735 if len(node
.node_tree
.outputs
):
1737 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1738 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1739 #if viewer output is already used but leads to the same socket we can still use it
1740 is_used
= self
.is_socket_used_other_mats(socket
)
1742 if connect_socket
== None:
1744 groupout
= get_group_output_node(node
.node_tree
)
1745 groupout_input
= groupout
.inputs
[i
]
1746 links
= groupout_input
.links
1747 if connect_socket
not in [link
.from_socket
for link
in links
]:
1753 if not index
and free_socket
:
1757 #create viewer socket
1758 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1759 index
= len(node
.node_tree
.outputs
) - 1
1760 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1763 def init_shader_variables(self
, space
, shader_type
):
1764 if shader_type
== 'OBJECT':
1765 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1766 self
.shader_output_type
= "OUTPUT_MATERIAL"
1767 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1768 self
.shader_viewer_ident
= "ShaderNodeEmission"
1770 self
.shader_output_type
= "OUTPUT_LIGHT"
1771 self
.shader_output_ident
= "ShaderNodeOutputLight"
1772 self
.shader_viewer_ident
= "ShaderNodeEmission"
1774 elif shader_type
== 'WORLD':
1775 self
.shader_output_type
= "OUTPUT_WORLD"
1776 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1777 self
.shader_viewer_ident
= "ShaderNodeBackground"
1779 def get_shader_output_node(self
, tree
):
1780 for node
in tree
.nodes
:
1781 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1785 def ensure_group_output(cls
, tree
):
1786 #check if a group output node exists otherwise create
1787 groupout
= get_group_output_node(tree
)
1789 groupout
= tree
.nodes
.new('NodeGroupOutput')
1790 loc_x
, loc_y
= get_output_location(tree
)
1791 groupout
.location
.x
= loc_x
1792 groupout
.location
.y
= loc_y
1793 groupout
.select
= False
1794 # So that we don't keep on adding new group outputs
1795 groupout
.is_active_output
= True
1799 def search_sockets(cls
, node
, sockets
, index
=None):
1800 # recursively scan nodes for viewer sockets and store in list
1801 for i
, input_socket
in enumerate(node
.inputs
):
1802 if index
and i
!= index
:
1804 if len(input_socket
.links
):
1805 link
= input_socket
.links
[0]
1806 next_node
= link
.from_node
1807 external_socket
= link
.from_socket
1808 if hasattr(next_node
, "node_tree"):
1809 for socket_index
, s
in enumerate(next_node
.outputs
):
1810 if s
== external_socket
:
1812 socket
= next_node
.node_tree
.outputs
[socket_index
]
1813 if is_viewer_socket(socket
) and socket
not in sockets
:
1814 sockets
.append(socket
)
1815 #continue search inside of node group but restrict socket to where we came from
1816 groupout
= get_group_output_node(next_node
.node_tree
)
1817 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1820 def scan_nodes(cls
, tree
, sockets
):
1821 # get all viewer sockets in a material tree
1822 for node
in tree
.nodes
:
1823 if hasattr(node
, "node_tree"):
1824 for socket
in node
.node_tree
.outputs
:
1825 if is_viewer_socket(socket
) and (socket
not in sockets
):
1826 sockets
.append(socket
)
1827 cls
.scan_nodes(node
.node_tree
, sockets
)
1829 def link_leads_to_used_socket(self
, link
):
1830 #return True if link leads to a socket that is already used in this material
1831 socket
= get_internal_socket(link
.to_socket
)
1832 return (socket
and self
.is_socket_used_active_mat(socket
))
1834 def is_socket_used_active_mat(self
, socket
):
1835 #ensure used sockets in active material is calculated and check given socket
1836 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1837 self
.used_viewer_sockets_active_mat
= []
1838 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1840 emission
= self
.get_viewer_node(materialout
)
1841 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_active_mat
)
1842 return socket
in self
.used_viewer_sockets_active_mat
1844 def is_socket_used_other_mats(self
, socket
):
1845 #ensure used sockets in other materials are calculated and check given socket
1846 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1847 self
.used_viewer_sockets_other_mats
= []
1848 for mat
in bpy
.data
.materials
:
1849 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1852 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1854 emission
= self
.get_viewer_node(materialout
)
1855 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_other_mats
)
1856 return socket
in self
.used_viewer_sockets_other_mats
1859 def get_viewer_node(materialout
):
1860 input_socket
= materialout
.inputs
[0]
1861 if len(input_socket
.links
) > 0:
1862 node
= input_socket
.links
[0].from_node
1863 if node
.type == 'EMISSION' and node
.name
== "Emission Viewer":
1866 def invoke(self
, context
, event
):
1867 space
= context
.space_data
1868 # Ignore operator when running in wrong context.
1869 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1870 return {'PASS_THROUGH'}
1872 shader_type
= space
.shader_type
1873 self
.init_shader_variables(space
, shader_type
)
1874 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1875 mlocx
= event
.mouse_region_x
1876 mlocy
= event
.mouse_region_y
1877 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
1878 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1879 active_tree
, path_to_tree
= get_active_tree(context
)
1880 nodes
, links
= active_tree
.nodes
, active_tree
.links
1881 base_node_tree
= space
.node_tree
1882 active
= nodes
.active
1884 # For geometry node trees we just connect to the group output,
1885 # because there is no "viewer node" yet.
1886 if space
.tree_type
== "GeometryNodeTree":
1889 for out
in active
.outputs
:
1890 if is_visible_socket(out
):
1899 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1900 self
.scan_nodes(base_node_tree
, delete_sockets
)
1902 # Find (or create if needed) the output of this node tree
1903 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1905 # Analyze outputs, make links
1908 for i
, out
in enumerate(active
.outputs
):
1909 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1910 valid_outputs
.append(i
)
1912 out_i
= valid_outputs
[0] # Start index of node's outputs
1913 for i
, valid_i
in enumerate(valid_outputs
):
1914 for out_link
in active
.outputs
[valid_i
].links
:
1915 if is_viewer_link(out_link
, geometryoutput
):
1916 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1917 if i
< len(valid_outputs
) - 1:
1918 out_i
= valid_outputs
[i
+ 1]
1920 out_i
= valid_outputs
[0]
1922 make_links
= [] # store sockets for new links
1923 delete_nodes
= [] # store unused nodes to delete in the end
1925 # If there is no 'GEOMETRY' output type - We can't preview the node
1928 socket_type
= 'GEOMETRY'
1929 # Find an input socket of the output of type geometry
1930 geometryoutindex
= None
1931 for i
,inp
in enumerate(geometryoutput
.inputs
):
1932 if inp
.type == socket_type
:
1933 geometryoutindex
= i
1935 if geometryoutindex
is None:
1936 # Create geometry socket
1937 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1938 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1940 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1941 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1942 for li_from
, li_to
in make_links
:
1943 base_node_tree
.links
.new(li_from
, li_to
)
1944 tree
= base_node_tree
1945 link_end
= output_socket
1946 while tree
.nodes
.active
!= active
:
1947 node
= tree
.nodes
.active
1948 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1949 link_start
= node
.outputs
[index
]
1950 node_socket
= node
.node_tree
.outputs
[index
]
1951 if node_socket
in delete_sockets
:
1952 delete_sockets
.remove(node_socket
)
1953 tree
.links
.new(link_start
, link_end
)
1955 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1956 tree
= tree
.nodes
.active
.node_tree
1957 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1960 for socket
in delete_sockets
:
1961 tree
= socket
.id_data
1962 tree
.outputs
.remove(socket
)
1965 for tree
, node
in delete_nodes
:
1966 tree
.nodes
.remove(node
)
1968 nodes
.active
= active
1969 active
.select
= True
1970 force_update(context
)
1974 # What follows is code for the shader editor
1975 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1978 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
):
1979 for out
in active
.outputs
:
1980 if is_visible_socket(out
):
1984 # get material_output node
1985 materialout
= None # placeholder node
1988 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1989 self
.scan_nodes(base_node_tree
, delete_sockets
)
1991 materialout
= self
.get_shader_output_node(base_node_tree
)
1993 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1994 materialout
.location
= get_output_location(base_node_tree
)
1995 materialout
.select
= False
1996 # Analyze outputs, add "Emission Viewer" if needed, make links
1999 for i
, out
in enumerate(active
.outputs
):
2000 if is_visible_socket(out
):
2001 valid_outputs
.append(i
)
2003 out_i
= valid_outputs
[0] # Start index of node's outputs
2004 for i
, valid_i
in enumerate(valid_outputs
):
2005 for out_link
in active
.outputs
[valid_i
].links
:
2006 if is_viewer_link(out_link
, materialout
):
2007 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
2008 if i
< len(valid_outputs
) - 1:
2009 out_i
= valid_outputs
[i
+ 1]
2011 out_i
= valid_outputs
[0]
2013 make_links
= [] # store sockets for new links
2014 delete_nodes
= [] # store unused nodes to delete in the end
2016 # If output type not 'SHADER' - "Emission Viewer" needed
2017 if active
.outputs
[out_i
].type != 'SHADER':
2018 socket_type
= 'NodeSocketColor'
2019 # get Emission Viewer node
2020 emission_exists
= False
2021 emission_placeholder
= base_node_tree
.nodes
[0]
2022 for node
in base_node_tree
.nodes
:
2023 if "Emission Viewer" in node
.name
:
2024 emission_exists
= True
2025 emission_placeholder
= node
2026 if not emission_exists
:
2027 emission
= base_node_tree
.nodes
.new(self
.shader_viewer_ident
)
2028 emission
.hide
= True
2029 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
2030 emission
.label
= "Viewer"
2031 emission
.name
= "Emission Viewer"
2032 emission
.use_custom_color
= True
2033 emission
.color
= (0.6, 0.5, 0.4)
2034 emission
.select
= False
2036 emission
= emission_placeholder
2037 output_socket
= emission
.inputs
[0]
2039 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2040 if emission
.outputs
[0].links
.__len
__() > 0:
2041 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
2042 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2044 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2046 # Set brightness of viewer to compensate for Film and CM exposure
2047 if context
.scene
.render
.engine
== 'CYCLES' and hasattr(context
.scene
, 'cycles'):
2048 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
2052 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
2053 emission
.inputs
[1].default_value
= intensity
2056 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2057 socket_type
= 'NodeSocketShader'
2058 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
2059 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
2060 output_socket
= materialout
.inputs
[materialout_index
]
2061 for node
in base_node_tree
.nodes
:
2062 if node
.name
== 'Emission Viewer':
2063 delete_nodes
.append((base_node_tree
, node
))
2064 for li_from
, li_to
in make_links
:
2065 base_node_tree
.links
.new(li_from
, li_to
)
2067 # Crate links through node groups until we reach the active node
2068 tree
= base_node_tree
2069 link_end
= output_socket
2070 while tree
.nodes
.active
!= active
:
2071 node
= tree
.nodes
.active
2072 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
2073 link_start
= node
.outputs
[index
]
2074 node_socket
= node
.node_tree
.outputs
[index
]
2075 if node_socket
in delete_sockets
:
2076 delete_sockets
.remove(node_socket
)
2077 tree
.links
.new(link_start
, link_end
)
2079 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
2080 tree
= tree
.nodes
.active
.node_tree
2081 tree
.links
.new(active
.outputs
[out_i
], link_end
)
2084 for socket
in delete_sockets
:
2085 if not self
.is_socket_used_other_mats(socket
):
2086 tree
= socket
.id_data
2087 tree
.outputs
.remove(socket
)
2090 for tree
, node
in delete_nodes
:
2091 tree
.nodes
.remove(node
)
2093 nodes
.active
= active
2094 active
.select
= True
2096 force_update(context
)
2100 return {'CANCELLED'}
2103 class NWFrameSelected(Operator
, NWBase
):
2104 bl_idname
= "node.nw_frame_selected"
2105 bl_label
= "Frame Selected"
2106 bl_description
= "Add a frame node and parent the selected nodes to it"
2107 bl_options
= {'REGISTER', 'UNDO'}
2109 label_prop
: StringProperty(
2111 description
='The visual name of the frame node',
2114 color_prop
: FloatVectorProperty(
2116 description
="The color of the frame node",
2117 default
=(0.6, 0.6, 0.6),
2118 min=0, max=1, step
=1, precision
=3,
2119 subtype
='COLOR_GAMMA', size
=3
2122 def execute(self
, context
):
2123 nodes
, links
= get_nodes_links(context
)
2126 if node
.select
== True:
2127 selected
.append(node
)
2129 bpy
.ops
.node
.add_node(type='NodeFrame')
2131 frm
.label
= self
.label_prop
2132 frm
.use_custom_color
= True
2133 frm
.color
= self
.color_prop
2135 for node
in selected
:
2141 class NWReloadImages(Operator
):
2142 bl_idname
= "node.nw_reload_images"
2143 bl_label
= "Reload Images"
2144 bl_description
= "Update all the image nodes to match their files on disk"
2147 def poll(cls
, context
):
2149 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
2150 if context
.active_node
is not None:
2151 for out
in context
.active_node
.outputs
:
2152 if is_visible_socket(out
):
2157 def execute(self
, context
):
2158 nodes
, links
= get_nodes_links(context
)
2159 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2162 if node
.type in image_types
:
2163 if node
.type == "TEXTURE":
2164 if node
.texture
: # node has texture assigned
2165 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2166 if node
.texture
.image
: # texture has image assigned
2167 node
.texture
.image
.reload()
2175 self
.report({'INFO'}, "Reloaded images")
2176 print("Reloaded " + str(num_reloaded
) + " images")
2177 force_update(context
)
2180 self
.report({'WARNING'}, "No images found to reload in this node tree")
2181 return {'CANCELLED'}
2184 class NWSwitchNodeType(Operator
, NWBase
):
2185 """Switch type of selected nodes """
2186 bl_idname
= "node.nw_swtch_node_type"
2187 bl_label
= "Switch Node Type"
2188 bl_options
= {'REGISTER', 'UNDO'}
2190 to_type
: EnumProperty(
2191 name
="Switch to type",
2192 items
=list(shaders_input_nodes_props
) +
2193 list(shaders_output_nodes_props
) +
2194 list(shaders_shader_nodes_props
) +
2195 list(shaders_texture_nodes_props
) +
2196 list(shaders_color_nodes_props
) +
2197 list(shaders_vector_nodes_props
) +
2198 list(shaders_converter_nodes_props
) +
2199 list(shaders_layout_nodes_props
) +
2200 list(compo_input_nodes_props
) +
2201 list(compo_output_nodes_props
) +
2202 list(compo_color_nodes_props
) +
2203 list(compo_converter_nodes_props
) +
2204 list(compo_filter_nodes_props
) +
2205 list(compo_vector_nodes_props
) +
2206 list(compo_matte_nodes_props
) +
2207 list(compo_distort_nodes_props
) +
2208 list(compo_layout_nodes_props
) +
2209 list(blender_mat_input_nodes_props
) +
2210 list(blender_mat_output_nodes_props
) +
2211 list(blender_mat_color_nodes_props
) +
2212 list(blender_mat_vector_nodes_props
) +
2213 list(blender_mat_converter_nodes_props
) +
2214 list(blender_mat_layout_nodes_props
) +
2215 list(texture_input_nodes_props
) +
2216 list(texture_output_nodes_props
) +
2217 list(texture_color_nodes_props
) +
2218 list(texture_pattern_nodes_props
) +
2219 list(texture_textures_nodes_props
) +
2220 list(texture_converter_nodes_props
) +
2221 list(texture_distort_nodes_props
) +
2222 list(texture_layout_nodes_props
)
2225 geo_to_type
: StringProperty(
2226 name
="Switch to type",
2230 def execute(self
, context
):
2231 nodes
, links
= get_nodes_links(context
)
2232 to_type
= self
.to_type
2233 if self
.geo_to_type
!= '':
2234 to_type
= self
.geo_to_type
2235 # Those types of nodes will not swap.
2236 src_excludes
= ('NodeFrame')
2237 # Those attributes of nodes will be copied if possible
2238 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
2239 'show_options', 'show_preview', 'show_texture',
2240 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2242 selected
= [n
for n
in nodes
if n
.select
]
2244 for node
in [n
for n
in selected
if
2245 n
.rna_type
.identifier
not in src_excludes
and
2246 n
.rna_type
.identifier
!= to_type
]:
2247 new_node
= nodes
.new(to_type
)
2248 for attr
in attrs_to_pass
:
2249 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
2250 setattr(new_node
, attr
, getattr(node
, attr
))
2251 # set image datablock of dst to image of src
2252 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
2254 new_node
.image
= node
.image
2256 if new_node
.type == 'SWITCH':
2257 new_node
.hide
= True
2258 # Dictionaries: src_sockets and dst_sockets:
2259 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2260 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2261 # in 'INPUTS' and 'OUTPUTS':
2262 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2264 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2266 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2267 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2270 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2271 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2273 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2274 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2275 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2276 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
2277 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2278 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
2279 # enumerate in inputs, then in outputs
2280 # find name, default value and links of socket
2281 for i
, socket
in enumerate(in_out
):
2282 the_name
= socket
.name
2284 # Not every socket, especially in outputs has "default_value"
2285 if hasattr(socket
, 'default_value'):
2286 dval
= socket
.default_value
2288 for lnk
in socket
.links
:
2289 socket_links
.append(lnk
)
2290 # check type of socket to fill proper keys.
2291 for the_type
in types_order_one
:
2292 if socket
.type == the_type
:
2293 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2294 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2295 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
2296 # Check which of the types in inputs/outputs is considered to be "main".
2297 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2298 for type_check
in types_order_one
:
2299 if sockets
[in_out_name
][type_check
]:
2300 sockets
[in_out_name
]['MAIN'] = type_check
2304 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2305 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2308 for inout
, soctype
in (
2309 ('INPUTS', 'MAIN',),
2310 ('INPUTS', 'SHADER',),
2311 ('INPUTS', 'RGBA',),
2312 ('INPUTS', 'VECTOR',),
2313 ('INPUTS', 'VALUE',),
2314 ('OUTPUTS', 'MAIN',),
2315 ('OUTPUTS', 'SHADER',),
2316 ('OUTPUTS', 'RGBA',),
2317 ('OUTPUTS', 'VECTOR',),
2318 ('OUTPUTS', 'VALUE',),
2320 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
2321 if soctype
== 'MAIN':
2322 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
2323 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
2325 sc
= src_sockets
[inout
][soctype
]
2326 dt
= dst_sockets
[inout
][soctype
]
2327 # start with 'dt' to determine number of possibilities.
2328 for i
, soc
in enumerate(dt
):
2329 # if src main has enough entries - match them with dst main sockets by indexes.
2331 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
2332 # add 'VALUE_NAME' criterion to inputs.
2333 if inout
== 'INPUTS' and soctype
== 'VALUE':
2335 if s
[2] == soc
[2]: # if names match
2336 # append src (index, dval), dst (index, dval)
2337 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
2339 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2340 # This creates better links when relinking textures.
2341 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
2342 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
2344 # Pass default values and RELINK:
2345 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2346 # INPUTS: Base on matches in proper order.
2347 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
2349 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
2350 new_node
.inputs
[dst_i
].default_value
= src_dval
2351 # Special case: switch to math
2352 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2353 new_node
.type == 'MATH' and\
2355 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
2356 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
2357 if node
.type == 'MIX_RGB':
2358 if node
.blend_type
in [o
[0] for o
in operations
]:
2359 new_node
.operation
= node
.blend_type
2360 # Special case: switch from math to some types
2361 if node
.type == 'MATH' and\
2362 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2365 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
2366 if new_node
.type == 'MIX_RGB':
2367 if node
.operation
in [t
[0] for t
in blend_types
]:
2368 new_node
.blend_type
= node
.operation
2369 # Set Fac of MIX_RGB to 1.0
2370 new_node
.inputs
[0].default_value
= 1.0
2371 # make link only when dst matching input is not linked already.
2372 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
2373 in_src_link
= node
.inputs
[src_i
].links
[0]
2374 in_dst_socket
= new_node
.inputs
[dst_i
]
2375 links
.new(in_src_link
.from_socket
, in_dst_socket
)
2376 links
.remove(in_src_link
)
2377 # OUTPUTS: Base on matches in proper order.
2378 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
2379 for out_src_link
in node
.outputs
[src_i
].links
:
2380 out_dst_socket
= new_node
.outputs
[dst_i
]
2381 links
.new(out_dst_socket
, out_src_link
.to_socket
)
2382 # relink rest inputs if possible, no criteria
2383 for src_inp
in node
.inputs
:
2384 for dst_inp
in new_node
.inputs
:
2385 if src_inp
.links
and not dst_inp
.links
:
2386 src_link
= src_inp
.links
[0]
2387 links
.new(src_link
.from_socket
, dst_inp
)
2388 links
.remove(src_link
)
2389 # relink rest outputs if possible, base on node kind if any left.
2390 for src_o
in node
.outputs
:
2391 for out_src_link
in src_o
.links
:
2392 for dst_o
in new_node
.outputs
:
2393 if src_o
.type == dst_o
.type:
2394 links
.new(dst_o
, out_src_link
.to_socket
)
2395 # relink rest outputs no criteria if any left. Link all from first output.
2396 for src_o
in node
.outputs
:
2397 for out_src_link
in src_o
.links
:
2398 if new_node
.outputs
:
2399 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2401 force_update(context
)
2405 class NWMergeNodes(Operator
, NWBase
):
2406 bl_idname
= "node.nw_merge_nodes"
2407 bl_label
= "Merge Nodes"
2408 bl_description
= "Merge Selected Nodes"
2409 bl_options
= {'REGISTER', 'UNDO'}
2413 description
="All possible blend types, boolean operations and math operations",
2414 items
= blend_types
+ [op
for op
in geo_combine_operations
if op
not in blend_types
] + [op
for op
in operations
if op
not in blend_types
],
2416 merge_type
: EnumProperty(
2418 description
="Type of Merge to be used",
2420 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2421 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2422 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2423 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2424 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2425 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2426 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2430 # Check if the link connects to a node that is in selected_nodes
2431 # If not, then check recursively for each link in the nodes outputs.
2432 # If yes, return True. If the recursion stops without finding a node
2433 # in selected_nodes, it returns False. The depth is used to prevent
2434 # getting stuck in a loop because of an already present cycle.
2436 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
2438 # We're stuck in a cycle, but that cycle was already present,
2439 # so we return False.
2440 # NOTE: The number 255 is arbitrary, but seems to work well.
2443 if node
in selected_nodes
:
2445 if not node
.outputs
:
2447 for output
in node
.outputs
:
2448 if output
.is_linked
:
2449 for olink
in output
.links
:
2450 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
2452 # None of the outputs found a node in selected_nodes, so there is no cycle.
2455 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2456 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2457 # be connected. The last one is assumed to be a multi input socket.
2458 # For convenience the node is returned.
2460 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
2461 # The y-location of the last node
2462 loc_y
= nodes_list
[-1][2]
2463 if merge_position
== 'CENTER':
2464 # Average the y-location
2465 for i
in range(len(nodes_list
)-1):
2466 loc_y
+= nodes_list
[i
][2]
2467 loc_y
= loc_y
/len(nodes_list
)
2468 new_node
= nodes
.new(node_name
)
2469 new_node
.hide
= do_hide
2470 new_node
.location
.x
= loc_x
2471 new_node
.location
.y
= loc_y
2472 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
2474 outputs_for_multi_input
= []
2475 for i
,node
in enumerate(selected_nodes
):
2477 # Search for the first node which had output links that do not create
2478 # a cycle, which we can then reconnect afterwards.
2479 if prev_links
== [] and node
.outputs
[0].is_linked
:
2480 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
2481 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2482 # To get the placement to look right we need to reverse the order in which we connect the
2483 # outputs to the multi input socket.
2484 if i
< len(socket_indices
) - 1:
2485 ind
= socket_indices
[i
]
2486 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
2488 outputs_for_multi_input
.insert(0, node
.outputs
[0])
2489 if outputs_for_multi_input
!= []:
2490 ind
= socket_indices
[-1]
2491 for output
in outputs_for_multi_input
:
2492 links
.new(output
, new_node
.inputs
[ind
])
2493 if prev_links
!= []:
2494 for link
in prev_links
:
2495 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
2498 def execute(self
, context
):
2499 settings
= context
.preferences
.addons
[__name__
].preferences
2500 merge_hide
= settings
.merge_hide
2501 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2504 do_hide_shader
= False
2505 if merge_hide
== 'ALWAYS':
2507 do_hide_shader
= True
2508 elif merge_hide
== 'NON_SHADER':
2511 tree_type
= context
.space_data
.node_tree
.type
2512 if tree_type
== 'GEOMETRY':
2513 node_type
= 'GeometryNode'
2514 if tree_type
== 'COMPOSITING':
2515 node_type
= 'CompositorNode'
2516 elif tree_type
== 'SHADER':
2517 node_type
= 'ShaderNode'
2518 elif tree_type
== 'TEXTURE':
2519 node_type
= 'TextureNode'
2520 nodes
, links
= get_nodes_links(context
)
2522 merge_type
= self
.merge_type
2523 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2524 # 'ZCOMBINE' works only if mode == 'MIX'
2525 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2526 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2529 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2531 # The math nodes used for geometry nodes are of type 'ShaderNode'
2532 if merge_type
== 'MATH' and tree_type
== 'GEOMETRY':
2533 node_type
= 'ShaderNode'
2534 selected_mix
= [] # entry = [index, loc]
2535 selected_shader
= [] # entry = [index, loc]
2536 selected_geometry
= [] # entry = [index, loc]
2537 selected_math
= [] # entry = [index, loc]
2538 selected_vector
= [] # entry = [index, loc]
2539 selected_z
= [] # entry = [index, loc]
2540 selected_alphaover
= [] # entry = [index, loc]
2542 for i
, node
in enumerate(nodes
):
2543 if node
.select
and node
.outputs
:
2544 if merge_type
== 'AUTO':
2545 for (type, types_list
, dst
) in (
2546 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2547 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2548 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2549 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2550 ('VECTOR', [], selected_vector
),
2552 output_type
= node
.outputs
[0].type
2553 valid_mode
= mode
in types_list
2554 # When mode is 'MIX' we have to cheat since the mix node is not used in
2556 if tree_type
== 'GEOMETRY':
2558 if output_type
== 'VALUE' and type == 'VALUE':
2560 elif output_type
== 'VECTOR' and type == 'VECTOR':
2562 elif type == 'GEOMETRY':
2564 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2565 # Cheat that output type is 'RGBA',
2566 # and that 'MIX' exists in math operations list.
2567 # This way when selected_mix list is analyzed:
2568 # Node data will be appended even though it doesn't meet requirements.
2569 elif output_type
!= 'SHADER' and mode
== 'MIX':
2570 output_type
= 'RGBA'
2572 if output_type
== type and valid_mode
:
2573 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2575 for (type, types_list
, dst
) in (
2576 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2577 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2578 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2579 ('MATH', [t
[0] for t
in operations
], selected_math
),
2580 ('ZCOMBINE', ('MIX', ), selected_z
),
2581 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2583 if merge_type
== type and mode
in types_list
:
2584 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2585 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2586 # use only 'Mix' nodes for merging.
2587 # For that we add selected_math list to selected_mix list and clear selected_math.
2588 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2589 selected_mix
+= selected_math
2591 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2594 count_before
= len(nodes
)
2595 # sort list by loc_x - reversed
2596 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2598 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2599 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2601 # Change the node type for math nodes in a geometry node tree.
2602 if tree_type
== 'GEOMETRY':
2603 if nodes_list
is selected_math
or nodes_list
is selected_vector
:
2604 node_type
= 'ShaderNode'
2608 node_type
= 'GeometryNode'
2609 if merge_position
== 'CENTER':
2610 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)
2611 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2617 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2621 if nodes_list
== selected_shader
and not do_hide_shader
:
2623 the_range
= len(nodes_list
) - 1
2624 if len(nodes_list
) == 1:
2627 for i
in range(the_range
):
2628 if nodes_list
== selected_mix
:
2629 add_type
= node_type
+ 'MixRGB'
2630 add
= nodes
.new(add_type
)
2631 add
.blend_type
= mode
2633 add
.inputs
[0].default_value
= 1.0
2634 add
.show_preview
= False
2640 add
.width_hidden
= 100.0
2641 elif nodes_list
== selected_math
:
2642 add_type
= node_type
+ 'Math'
2643 add
= nodes
.new(add_type
)
2644 add
.operation
= mode
2650 add
.width_hidden
= 100.0
2651 elif nodes_list
== selected_shader
:
2653 add_type
= node_type
+ 'MixShader'
2654 add
= nodes
.new(add_type
)
2655 add
.hide
= do_hide_shader
2660 add
.width_hidden
= 100.0
2662 add_type
= node_type
+ 'AddShader'
2663 add
= nodes
.new(add_type
)
2664 add
.hide
= do_hide_shader
2669 add
.width_hidden
= 100.0
2670 elif nodes_list
== selected_geometry
:
2671 if mode
in ('JOIN', 'MIX'):
2672 add_type
= node_type
+ 'JoinGeometry'
2673 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2675 add_type
= node_type
+ 'Boolean'
2676 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2677 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2678 add
.operation
= mode
2681 elif nodes_list
== selected_vector
:
2682 add_type
= node_type
+ 'VectorMath'
2683 add
= nodes
.new(add_type
)
2684 add
.operation
= mode
2690 add
.width_hidden
= 100.0
2691 elif nodes_list
== selected_z
:
2692 add
= nodes
.new('CompositorNodeZcombine')
2693 add
.show_preview
= False
2699 add
.width_hidden
= 100.0
2700 elif nodes_list
== selected_alphaover
:
2701 add
= nodes
.new('CompositorNodeAlphaOver')
2702 add
.show_preview
= False
2708 add
.width_hidden
= 100.0
2709 add
.location
= loc_x
, loc_y
2713 # This has already been handled separately
2717 count_after
= len(nodes
)
2718 index
= count_after
- 1
2719 first_selected
= nodes
[nodes_list
[0][0]]
2720 # "last" node has been added as first, so its index is count_before.
2721 last_add
= nodes
[count_before
]
2722 # Create list of invalid indexes.
2723 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2726 # Two nodes were selected and first selected has no output links, second selected has output links.
2727 # Then add links from last add to all links 'to_socket' of out links of second selected.
2728 if len(nodes_list
) == 2:
2729 if not first_selected
.outputs
[0].links
:
2730 second_selected
= nodes
[nodes_list
[1][0]]
2731 for ss_link
in second_selected
.outputs
[0].links
:
2732 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2733 # Link only if "to_node" index not in invalid indexes list.
2734 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2735 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2736 # add links from last_add to all links 'to_socket' of out links of first selected.
2737 for fs_link
in first_selected
.outputs
[0].links
:
2738 # Link only if "to_node" index not in invalid indexes list.
2739 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2740 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2741 # add link from "first" selected and "first" add node
2742 node_to
= nodes
[count_after
- 1]
2743 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2744 if node_to
.type == 'ZCOMBINE':
2745 for fs_out
in first_selected
.outputs
:
2746 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2747 links
.new(fs_out
, node_to
.inputs
[1])
2749 # add links between added ADD nodes and between selected and ADD nodes
2750 for i
in range(count_adds
):
2751 if i
< count_adds
- 1:
2752 node_from
= nodes
[index
]
2753 node_to
= nodes
[index
- 1]
2754 node_to_input_i
= first
2755 node_to_z_i
= 1 # if z combine - link z to first z input
2756 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2757 if node_to
.type == 'ZCOMBINE':
2758 for from_out
in node_from
.outputs
:
2759 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2760 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2761 if len(nodes_list
) > 1:
2762 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2763 node_to
= nodes
[index
]
2764 node_to_input_i
= second
2765 node_to_z_i
= 3 # if z combine - link z to second z input
2766 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2767 if node_to
.type == 'ZCOMBINE':
2768 for from_out
in node_from
.outputs
:
2769 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2770 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2772 # set "last" of added nodes as active
2773 nodes
.active
= last_add
2774 for i
, x
, y
, dx
, h
in nodes_list
:
2775 nodes
[i
].select
= False
2780 class NWBatchChangeNodes(Operator
, NWBase
):
2781 bl_idname
= "node.nw_batch_change"
2782 bl_label
= "Batch Change"
2783 bl_description
= "Batch Change Blend Type and Math Operation"
2784 bl_options
= {'REGISTER', 'UNDO'}
2786 blend_type
: EnumProperty(
2788 items
=blend_types
+ navs
,
2790 operation
: EnumProperty(
2792 items
=operations
+ navs
,
2795 def execute(self
, context
):
2796 blend_type
= self
.blend_type
2797 operation
= self
.operation
2798 for node
in context
.selected_nodes
:
2799 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2800 if not blend_type
in [nav
[0] for nav
in navs
]:
2801 node
.blend_type
= blend_type
2803 if blend_type
== 'NEXT':
2804 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2805 #index = blend_types.index(node.blend_type)
2806 if index
== len(blend_types
) - 1:
2807 node
.blend_type
= blend_types
[0][0]
2809 node
.blend_type
= blend_types
[index
+ 1][0]
2811 if blend_type
== 'PREV':
2812 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2814 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2816 node
.blend_type
= blend_types
[index
- 1][0]
2818 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2819 if not operation
in [nav
[0] for nav
in navs
]:
2820 node
.operation
= operation
2822 if operation
== 'NEXT':
2823 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2824 #index = operations.index(node.operation)
2825 if index
== len(operations
) - 1:
2826 node
.operation
= operations
[0][0]
2828 node
.operation
= operations
[index
+ 1][0]
2830 if operation
== 'PREV':
2831 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2832 #index = operations.index(node.operation)
2834 node
.operation
= operations
[len(operations
) - 1][0]
2836 node
.operation
= operations
[index
- 1][0]
2841 class NWChangeMixFactor(Operator
, NWBase
):
2842 bl_idname
= "node.nw_factor"
2843 bl_label
= "Change Factor"
2844 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2845 bl_options
= {'REGISTER', 'UNDO'}
2847 # option: Change factor.
2848 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2849 # Else - change factor by option value.
2850 option
: FloatProperty()
2852 def execute(self
, context
):
2853 nodes
, links
= get_nodes_links(context
)
2854 option
= self
.option
2855 selected
= [] # entry = index
2856 for si
, node
in enumerate(nodes
):
2858 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2862 fac
= nodes
[si
].inputs
[0]
2863 nodes
[si
].hide
= False
2864 if option
in {0.0, 1.0}:
2865 fac
.default_value
= option
2867 fac
.default_value
+= option
2872 class NWCopySettings(Operator
, NWBase
):
2873 bl_idname
= "node.nw_copy_settings"
2874 bl_label
= "Copy Settings"
2875 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2876 bl_options
= {'REGISTER', 'UNDO'}
2879 def poll(cls
, context
):
2881 if nw_check(context
):
2883 context
.active_node
is not None and
2884 context
.active_node
.type != 'FRAME'
2889 def execute(self
, context
):
2890 node_active
= context
.active_node
2891 node_selected
= context
.selected_nodes
2894 if not (len(node_selected
) > 1):
2895 self
.report({'ERROR'}, "2 nodes must be selected at least")
2896 return {'CANCELLED'}
2898 # Check if active node is in the selection
2899 selected_node_names
= [n
.name
for n
in node_selected
]
2900 if node_active
.name
not in selected_node_names
:
2901 self
.report({'ERROR'}, "No active node")
2902 return {'CANCELLED'}
2904 # Get nodes in selection by type
2905 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2907 if not (len(valid_nodes
) > 1) and node_active
:
2908 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2909 return {'CANCELLED'}
2911 if len(valid_nodes
) != len(node_selected
):
2912 # Report nodes that are not valid
2913 valid_node_names
= [n
.name
for n
in valid_nodes
]
2914 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2915 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2917 # Reference original
2919 #node_selected_names = [n.name for n in node_selected]
2924 # Deselect all nodes
2925 for i
in node_selected
:
2928 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2929 # Run through all other nodes
2930 for node
in valid_nodes
[1:]:
2932 # Check for frame node
2933 parent
= node
.parent
if node
.parent
else None
2934 node_loc
= [node
.location
.x
, node
.location
.y
]
2936 # Select original to duplicate
2939 # Duplicate selected node
2940 bpy
.ops
.node
.duplicate()
2941 new_node
= context
.selected_nodes
[0]
2944 new_node
.select
= False
2946 # Properties to copy
2947 node_tree
= node
.id_data
2948 props_to_copy
= 'bl_idname name location height width'.split(' ')
2952 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2953 for i
in (i
for i
in mappings
if i
.is_linked
):
2955 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2958 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2959 props_to_copy
.pop(0)
2961 for prop
in props_to_copy
:
2962 setattr(new_node
, prop
, props
[prop
])
2964 # Get the node tree to remove the old node
2965 nodes
= node_tree
.nodes
2967 new_node
.name
= props
['name']
2970 new_node
.parent
= parent
2971 new_node
.location
= node_loc
2973 for str_from
, str_to
in reconnections
:
2974 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2976 success_names
.append(new_node
.name
)
2979 node_tree
.nodes
.active
= orig
2980 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2984 class NWCopyLabel(Operator
, NWBase
):
2985 bl_idname
= "node.nw_copy_label"
2986 bl_label
= "Copy Label"
2987 bl_options
= {'REGISTER', 'UNDO'}
2989 option
: EnumProperty(
2991 description
="Source of name of label",
2993 ('FROM_ACTIVE', 'from active', 'from active node',),
2994 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2995 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2999 def execute(self
, context
):
3000 nodes
, links
= get_nodes_links(context
)
3001 option
= self
.option
3002 active
= nodes
.active
3003 if option
== 'FROM_ACTIVE':
3005 src_label
= active
.label
3006 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
3007 node
.label
= src_label
3008 elif option
== 'FROM_NODE':
3009 selected
= [n
for n
in nodes
if n
.select
]
3010 for node
in selected
:
3011 for input in node
.inputs
:
3013 src
= input.links
[0].from_node
3014 node
.label
= src
.label
3016 elif option
== 'FROM_SOCKET':
3017 selected
= [n
for n
in nodes
if n
.select
]
3018 for node
in selected
:
3019 for input in node
.inputs
:
3021 src
= input.links
[0].from_socket
3022 node
.label
= src
.name
3028 class NWClearLabel(Operator
, NWBase
):
3029 bl_idname
= "node.nw_clear_label"
3030 bl_label
= "Clear Label"
3031 bl_options
= {'REGISTER', 'UNDO'}
3033 option
: BoolProperty()
3035 def execute(self
, context
):
3036 nodes
, links
= get_nodes_links(context
)
3037 for node
in [n
for n
in nodes
if n
.select
]:
3042 def invoke(self
, context
, event
):
3044 return self
.execute(context
)
3046 return context
.window_manager
.invoke_confirm(self
, event
)
3049 class NWModifyLabels(Operator
, NWBase
):
3050 """Modify Labels of all selected nodes"""
3051 bl_idname
= "node.nw_modify_labels"
3052 bl_label
= "Modify Labels"
3053 bl_options
= {'REGISTER', 'UNDO'}
3055 prepend
: StringProperty(
3056 name
="Add to Beginning"
3058 append
: StringProperty(
3061 replace_from
: StringProperty(
3062 name
="Text to Replace"
3064 replace_to
: StringProperty(
3068 def execute(self
, context
):
3069 nodes
, links
= get_nodes_links(context
)
3070 for node
in [n
for n
in nodes
if n
.select
]:
3071 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
3075 def invoke(self
, context
, event
):
3079 return context
.window_manager
.invoke_props_dialog(self
)
3082 class NWAddTextureSetup(Operator
, NWBase
):
3083 bl_idname
= "node.nw_add_texture"
3084 bl_label
= "Texture Setup"
3085 bl_description
= "Add Texture Node Setup to Selected Shaders"
3086 bl_options
= {'REGISTER', 'UNDO'}
3088 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
3091 def poll(cls
, context
):
3093 if nw_check(context
):
3094 space
= context
.space_data
3095 if space
.tree_type
== 'ShaderNodeTree':
3099 def execute(self
, context
):
3100 nodes
, links
= get_nodes_links(context
)
3101 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3102 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
3103 selected_nodes
= [n
for n
in nodes
if n
.select
]
3104 for t_node
in selected_nodes
:
3108 for index
, i
in enumerate(t_node
.inputs
):
3114 locx
= t_node
.location
.x
3115 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
3117 xoffset
= [500, 700]
3119 if t_node
.type in texture_types
+ ['MAPPING']:
3120 xoffset
= [290, 500]
3124 image_type
= 'ShaderNodeTexImage'
3126 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
3127 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
3128 if t_node
.type == 'BACKGROUND':
3129 image_type
= 'ShaderNodeTexEnvironment'
3132 tex
= nodes
.new(image_type
)
3133 tex
.location
= [locx
- 200, locy
+ 112]
3135 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
3137 t_node
.select
= False
3138 if self
.add_mapping
or is_texture
:
3139 if t_node
.type != 'MAPPING':
3140 m
= nodes
.new('ShaderNodeMapping')
3141 m
.location
= [locx
- xoffset
[0], locy
+ 141]
3145 coord
= nodes
.new('ShaderNodeTexCoord')
3146 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
3149 links
.new(m
.outputs
[0], tex
.inputs
[0])
3150 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3153 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
3154 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3156 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
3160 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
3161 bl_idname
= "node.nw_add_textures_for_principled"
3162 bl_label
= "Principled Texture Setup"
3163 bl_description
= "Add Texture Node Setup for Principled BSDF"
3164 bl_options
= {'REGISTER', 'UNDO'}
3166 directory
: StringProperty(
3170 description
='Folder to search in for image files'
3172 files
: CollectionProperty(
3173 type=bpy
.types
.OperatorFileListElement
,
3174 options
={'HIDDEN', 'SKIP_SAVE'}
3177 relative_path
: BoolProperty(
3178 name
='Relative Path',
3179 description
='Select the file relative to the blend file',
3188 def draw(self
, context
):
3189 layout
= self
.layout
3190 layout
.alignment
= 'LEFT'
3192 layout
.prop(self
, 'relative_path')
3195 def poll(cls
, context
):
3197 if nw_check(context
):
3198 space
= context
.space_data
3199 if space
.tree_type
== 'ShaderNodeTree':
3203 def execute(self
, context
):
3204 # Check if everything is ok
3205 if not self
.directory
:
3206 self
.report({'INFO'}, 'No Folder Selected')
3207 return {'CANCELLED'}
3208 if not self
.files
[:]:
3209 self
.report({'INFO'}, 'No Files Selected')
3210 return {'CANCELLED'}
3212 nodes
, links
= get_nodes_links(context
)
3213 active_node
= nodes
.active
3214 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
3215 self
.report({'INFO'}, 'Select Principled BSDF')
3216 return {'CANCELLED'}
3219 def split_into__components(fname
):
3220 # Split filename into components
3221 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3223 fname
= path
.splitext(fname
)[0]
3225 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
3226 # Separate CamelCase by space
3227 fname
= re
.sub("([a-z])([A-Z])","\g<1> \g<2>",fname
)
3228 # Replace common separators with SPACE
3229 seperators
= ['_', '.', '-', '__', '--', '#']
3230 for sep
in seperators
:
3231 fname
= fname
.replace(sep
, ' ')
3233 components
= fname
.split(' ')
3234 components
= [c
.lower() for c
in components
]
3237 # Filter textures names for texturetypes in filenames
3238 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3239 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
3240 normal_abbr
= tags
.normal
.split(' ')
3241 bump_abbr
= tags
.bump
.split(' ')
3242 gloss_abbr
= tags
.gloss
.split(' ')
3243 rough_abbr
= tags
.rough
.split(' ')
3245 ['Displacement', tags
.displacement
.split(' '), None],
3246 ['Base Color', tags
.base_color
.split(' '), None],
3247 ['Subsurface Color', tags
.sss_color
.split(' '), None],
3248 ['Metallic', tags
.metallic
.split(' '), None],
3249 ['Specular', tags
.specular
.split(' '), None],
3250 ['Roughness', rough_abbr
+ gloss_abbr
, None],
3251 ['Normal', normal_abbr
+ bump_abbr
, None],
3254 # Look through texture_types and set value as filename of first matched file
3255 def match_files_to_socket_names():
3256 for sname
in socketnames
:
3257 for file in self
.files
:
3259 filenamecomponents
= split_into__components(fname
)
3260 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
3261 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3266 match_files_to_socket_names()
3267 # Remove socketnames without found files
3268 socketnames
= [s
for s
in socketnames
if s
[2]
3269 and path
.exists(self
.directory
+s
[2])]
3271 self
.report({'INFO'}, 'No matching images found')
3272 print('No matching images found')
3273 return {'CANCELLED'}
3275 # Don't override path earlier as os.path is used to check the absolute path
3276 import_path
= self
.directory
3277 if self
.relative_path
:
3278 if bpy
.data
.filepath
:
3279 import_path
= bpy
.path
.relpath(self
.directory
)
3281 self
.report({'WARNING'}, 'Relative paths cannot be used with unsaved scenes!')
3282 print('Relative paths cannot be used with unsaved scenes!')
3285 print('\nMatched Textures:')
3289 roughness_node
= None
3290 for i
, sname
in enumerate(socketnames
):
3291 print(i
, sname
[0], sname
[2])
3293 # DISPLACEMENT NODES
3294 if sname
[0] == 'Displacement':
3295 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
3296 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3297 disp_texture
.image
= img
3298 disp_texture
.label
= 'Displacement'
3299 if disp_texture
.image
:
3300 disp_texture
.image
.colorspace_settings
.is_data
= True
3302 # Add displacement offset nodes
3303 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
3304 disp_node
.location
= active_node
.location
+ Vector((0, -560))
3305 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
3307 # TODO Turn on true displacement in the material
3308 # Too complicated for now
3311 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
3313 if not output_node
[0].inputs
[2].is_linked
:
3314 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
3318 if not active_node
.inputs
[sname
[0]].is_linked
:
3319 # No texture node connected -> add texture node with new image
3320 texture_node
= nodes
.new(type='ShaderNodeTexImage')
3321 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3322 texture_node
.image
= img
3325 if sname
[0] == 'Normal':
3326 # Test if new texture node is normal or bump map
3327 fname_components
= split_into__components(sname
[2])
3328 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
3329 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
3331 # If Normal add normal node in between
3332 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
3333 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
3335 # If Bump add bump node in between
3336 normal_node
= nodes
.new(type='ShaderNodeBump')
3337 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
3339 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
3340 normal_node_texture
= texture_node
3342 elif sname
[0] == 'Roughness':
3343 # Test if glossy or roughness map
3344 fname_components
= split_into__components(sname
[2])
3345 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
3346 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
3349 # If Roughness nothing to to
3350 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3353 # If Gloss Map add invert node
3354 invert_node
= nodes
.new(type='ShaderNodeInvert')
3355 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
3357 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
3358 roughness_node
= texture_node
3361 # This is a simple connection Texture --> Input slot
3362 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3364 # Use non-color for all but 'Base Color' Textures
3365 if not sname
[0] in ['Base Color'] and texture_node
.image
:
3366 texture_node
.image
.colorspace_settings
.is_data
= True
3369 # If already texture connected. add to node list for alignment
3370 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
3372 # This are all connected texture nodes
3373 texture_nodes
.append(texture_node
)
3374 texture_node
.label
= sname
[0]
3377 texture_nodes
.append(disp_texture
)
3380 for i
, texture_node
in enumerate(texture_nodes
):
3381 offset
= Vector((-550, (i
* -280) + 200))
3382 texture_node
.location
= active_node
.location
+ offset
3385 # Extra alignment if normal node was added
3386 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
3389 # Alignment of invert node if glossy map
3390 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
3392 # Add texture input + mapping
3393 mapping
= nodes
.new(type='ShaderNodeMapping')
3394 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
3395 if len(texture_nodes
) > 1:
3396 # If more than one texture add reroute node in between
3397 reroute
= nodes
.new(type='NodeReroute')
3398 texture_nodes
.append(reroute
)
3399 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
3400 reroute
.location
= tex_coords
+ Vector((-50, -120))
3401 for texture_node
in texture_nodes
:
3402 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
3403 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
3405 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
3407 # Connect texture_coordiantes to mapping node
3408 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
3409 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
3410 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
3412 # Create frame around tex coords and mapping
3413 frame
= nodes
.new(type='NodeFrame')
3414 frame
.label
= 'Mapping'
3415 mapping
.parent
= frame
3416 texture_input
.parent
= frame
3419 # Create frame around texture nodes
3420 frame
= nodes
.new(type='NodeFrame')
3421 frame
.label
= 'Textures'
3422 for tnode
in texture_nodes
:
3423 tnode
.parent
= frame
3427 active_node
.select
= False
3430 force_update(context
)
3434 class NWAddReroutes(Operator
, NWBase
):
3435 """Add Reroute Nodes and link them to outputs of selected nodes"""
3436 bl_idname
= "node.nw_add_reroutes"
3437 bl_label
= "Add Reroutes"
3438 bl_description
= "Add Reroutes to Outputs"
3439 bl_options
= {'REGISTER', 'UNDO'}
3441 option
: EnumProperty(
3444 ('ALL', 'to all', 'Add to all outputs'),
3445 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3446 ('LINKED', 'to linked', 'Add only to linked outputs'),
3450 def execute(self
, context
):
3451 tree_type
= context
.space_data
.node_tree
.type
3452 option
= self
.option
3453 nodes
, links
= get_nodes_links(context
)
3454 # output valid when option is 'all' or when 'loose' output has no links
3456 post_select
= [] # nodes to be selected after execution
3457 # create reroutes and recreate links
3458 for node
in [n
for n
in nodes
if n
.select
]:
3463 # unhide 'REROUTE' nodes to avoid issues with location.y
3464 if node
.type == 'REROUTE':
3466 # When node is hidden - width_hidden not usable.
3467 # Hack needed to calculate real width
3469 bpy
.ops
.node
.select_all(action
='DESELECT')
3470 helper
= nodes
.new('NodeReroute')
3471 helper
.select
= True
3473 # resize node and helper to zero. Then check locations to calculate width
3474 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
3475 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
3476 # restore node location
3477 node
.location
= x
, y
3480 # only helper is selected now
3481 bpy
.ops
.node
.delete()
3482 x
= node
.location
.x
+ width
+ 20.0
3483 if node
.type != 'REROUTE':
3487 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3488 for out_i
, output
in enumerate(node
.outputs
):
3489 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3490 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3491 if node
.type != 'R_LAYERS':
3493 else: # if 'R_LAYERS' check if output represent used render pass
3494 node_scene
= node
.scene
3495 node_layer
= node
.layer
3496 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3497 if output
.name
== 'Alpha':
3500 # check entries in global 'rl_outputs' variable
3501 for rlo
in rl_outputs
:
3502 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3503 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3506 valid
= ((option
== 'ALL') or
3507 (option
== 'LOOSE' and not output
.links
) or
3508 (option
== 'LINKED' and output
.links
))
3509 # Add reroutes only if valid, but offset location in all cases.
3511 n
= nodes
.new('NodeReroute')
3513 for link
in output
.links
:
3514 links
.new(n
.outputs
[0], link
.to_socket
)
3515 links
.new(output
, n
.inputs
[0])
3517 post_select
.append(n
)
3521 # disselect the node so that after execution of script only newly created nodes are selected
3523 # nicer reroutes distribution along y when node.hide
3525 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3526 for reroute
in [r
for r
in nodes
if r
.select
]:
3527 reroute
.location
.y
-= y_translate
3528 for node
in post_select
:
3534 class NWLinkActiveToSelected(Operator
, NWBase
):
3535 """Link active node to selected nodes basing on various criteria"""
3536 bl_idname
= "node.nw_link_active_to_selected"
3537 bl_label
= "Link Active Node to Selected"
3538 bl_options
= {'REGISTER', 'UNDO'}
3540 replace
: BoolProperty()
3541 use_node_name
: BoolProperty()
3542 use_outputs_names
: BoolProperty()
3545 def poll(cls
, context
):
3547 if nw_check(context
):
3548 if context
.active_node
is not None:
3549 if context
.active_node
.select
:
3553 def execute(self
, context
):
3554 nodes
, links
= get_nodes_links(context
)
3555 replace
= self
.replace
3556 use_node_name
= self
.use_node_name
3557 use_outputs_names
= self
.use_outputs_names
3558 active
= nodes
.active
3559 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3560 outputs
= [] # Only usable outputs of active nodes will be stored here.
3561 for out
in active
.outputs
:
3562 if active
.type != 'R_LAYERS':
3565 # 'R_LAYERS' node type needs special handling.
3566 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3567 # Only outputs that represent used passes should be taken into account
3568 # Check if pass represented by output is used.
3569 # global 'rl_outputs' list will be used for that
3570 for rlo
in rl_outputs
:
3571 pass_used
= False # initial value. Will be set to True if pass is used
3572 if out
.name
== 'Alpha':
3573 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3575 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3576 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3577 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3581 doit
= True # Will be changed to False when links successfully added to previous output.
3584 for node
in selected
:
3585 dst_name
= node
.name
# Will be compared with src_name if needed.
3586 # When node has label - use it as dst_name
3588 dst_name
= node
.label
3589 valid
= True # Initial value. Will be changed to False if names don't match.
3590 src_name
= dst_name
# If names not used - this asignment will keep valid = True.
3592 # Set src_name to source node name or label
3593 src_name
= active
.name
3595 src_name
= active
.label
3596 elif use_outputs_names
:
3597 src_name
= (out
.name
, )
3598 for rlo
in rl_outputs
:
3599 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3600 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3601 if dst_name
not in src_name
:
3604 for input in node
.inputs
:
3605 if input.type == out
.type or node
.type == 'REROUTE':
3606 if replace
or not input.is_linked
:
3607 links
.new(out
, input)
3608 if not use_node_name
and not use_outputs_names
:
3615 class NWAlignNodes(Operator
, NWBase
):
3616 '''Align the selected nodes neatly in a row/column'''
3617 bl_idname
= "node.nw_align_nodes"
3618 bl_label
= "Align Nodes"
3619 bl_options
= {'REGISTER', 'UNDO'}
3620 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3622 def execute(self
, context
):
3623 nodes
, links
= get_nodes_links(context
)
3624 margin
= self
.margin
3628 if node
.select
and node
.type != 'FRAME':
3629 selection
.append(node
)
3631 # If no nodes are selected, align all nodes
3635 elif nodes
.active
in selection
:
3636 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3638 # Check if nodes should be laid out horizontally or vertically
3639 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3640 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3641 x_range
= max(x_locs
) - min(x_locs
)
3642 y_range
= max(y_locs
) - min(y_locs
)
3643 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3644 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3645 horizontal
= x_range
> y_range
3647 # Sort selection by location of node mid-point
3649 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3651 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3655 for node
in selection
:
3656 current_margin
= margin
3657 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3660 node
.location
.x
= current_pos
3661 current_pos
+= current_margin
+ node
.dimensions
.x
3662 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3664 node
.location
.y
= current_pos
3665 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3666 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3668 # If active node is selected, center nodes around it
3669 if active_loc
is not None:
3670 active_loc_diff
= active_loc
- nodes
.active
.location
3671 for node
in selection
:
3672 node
.location
+= active_loc_diff
3673 else: # Position nodes centered around where they used to be
3674 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
])
3675 new_mid
= (max(locs
) + min(locs
)) / 2
3676 for node
in selection
:
3678 node
.location
.x
+= (mid_x
- new_mid
)
3680 node
.location
.y
+= (mid_y
- new_mid
)
3685 class NWSelectParentChildren(Operator
, NWBase
):
3686 bl_idname
= "node.nw_select_parent_child"
3687 bl_label
= "Select Parent or Children"
3688 bl_options
= {'REGISTER', 'UNDO'}
3690 option
: EnumProperty(
3693 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3694 ('CHILD', 'Select Children', 'Select members of selected frame'),
3698 def execute(self
, context
):
3699 nodes
, links
= get_nodes_links(context
)
3700 option
= self
.option
3701 selected
= [node
for node
in nodes
if node
.select
]
3702 if option
== 'PARENT':
3703 for sel
in selected
:
3706 parent
.select
= True
3707 else: # option == 'CHILD'
3708 for sel
in selected
:
3709 children
= [node
for node
in nodes
if node
.parent
== sel
]
3710 for kid
in children
:
3716 class NWDetachOutputs(Operator
, NWBase
):
3717 """Detach outputs of selected node leaving inputs linked"""
3718 bl_idname
= "node.nw_detach_outputs"
3719 bl_label
= "Detach Outputs"
3720 bl_options
= {'REGISTER', 'UNDO'}
3722 def execute(self
, context
):
3723 nodes
, links
= get_nodes_links(context
)
3724 selected
= context
.selected_nodes
3725 bpy
.ops
.node
.duplicate_move_keep_inputs()
3726 new_nodes
= context
.selected_nodes
3727 bpy
.ops
.node
.select_all(action
="DESELECT")
3728 for node
in selected
:
3730 bpy
.ops
.node
.delete_reconnect()
3731 for new_node
in new_nodes
:
3732 new_node
.select
= True
3733 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3738 class NWLinkToOutputNode(Operator
):
3739 """Link to Composite node or Material Output node"""
3740 bl_idname
= "node.nw_link_out"
3741 bl_label
= "Connect to Output"
3742 bl_options
= {'REGISTER', 'UNDO'}
3745 def poll(cls
, context
):
3747 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
3748 if context
.active_node
is not None:
3749 for out
in context
.active_node
.outputs
:
3750 if is_visible_socket(out
):
3755 def execute(self
, context
):
3756 nodes
, links
= get_nodes_links(context
)
3757 active
= nodes
.active
3760 tree_type
= context
.space_data
.tree_type
3761 output_types_shaders
= [x
[1] for x
in shaders_output_nodes_props
]
3762 output_types_compo
= ['COMPOSITE']
3763 output_types_blender_mat
= ['OUTPUT']
3764 output_types_textures
= ['OUTPUT']
3765 output_types
= output_types_shaders
+ output_types_compo
+ output_types_blender_mat
3767 if node
.type in output_types
:
3771 bpy
.ops
.node
.select_all(action
="DESELECT")
3772 if tree_type
== 'ShaderNodeTree':
3773 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3774 elif tree_type
== 'CompositorNodeTree':
3775 output_node
= nodes
.new('CompositorNodeComposite')
3776 elif tree_type
== 'TextureNodeTree':
3777 output_node
= nodes
.new('TextureNodeOutput')
3778 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3779 output_node
.location
.y
= active
.location
.y
3780 if (output_node
and active
.outputs
):
3781 for i
, output
in enumerate(active
.outputs
):
3782 if is_visible_socket(output
):
3785 for i
, output
in enumerate(active
.outputs
):
3786 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3791 if tree_type
== 'ShaderNodeTree':
3792 if active
.outputs
[output_index
].name
== 'Volume':
3794 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3796 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3798 force_update(context
) # viewport render does not update
3803 class NWMakeLink(Operator
, NWBase
):
3804 """Make a link from one socket to another"""
3805 bl_idname
= 'node.nw_make_link'
3806 bl_label
= 'Make Link'
3807 bl_options
= {'REGISTER', 'UNDO'}
3808 from_socket
: IntProperty()
3809 to_socket
: IntProperty()
3811 def execute(self
, context
):
3812 nodes
, links
= get_nodes_links(context
)
3814 n1
= nodes
[context
.scene
.NWLazySource
]
3815 n2
= nodes
[context
.scene
.NWLazyTarget
]
3817 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3819 force_update(context
)
3824 class NWCallInputsMenu(Operator
, NWBase
):
3825 """Link from this output"""
3826 bl_idname
= 'node.nw_call_inputs_menu'
3827 bl_label
= 'Make Link'
3828 bl_options
= {'REGISTER', 'UNDO'}
3829 from_socket
: IntProperty()
3831 def execute(self
, context
):
3832 nodes
, links
= get_nodes_links(context
)
3834 context
.scene
.NWSourceSocket
= self
.from_socket
3836 n1
= nodes
[context
.scene
.NWLazySource
]
3837 n2
= nodes
[context
.scene
.NWLazyTarget
]
3838 if len(n2
.inputs
) > 1:
3839 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3840 elif len(n2
.inputs
) == 1:
3841 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3845 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3846 """Add an Image Sequence"""
3847 bl_idname
= 'node.nw_add_sequence'
3848 bl_label
= 'Import Image Sequence'
3849 bl_options
= {'REGISTER', 'UNDO'}
3851 directory
: StringProperty(
3854 filename
: StringProperty(
3857 files
: CollectionProperty(
3858 type=bpy
.types
.OperatorFileListElement
,
3859 options
={'HIDDEN', 'SKIP_SAVE'}
3862 def execute(self
, context
):
3863 nodes
, links
= get_nodes_links(context
)
3864 directory
= self
.directory
3865 filename
= self
.filename
3867 tree
= context
.space_data
.node_tree
3870 # print ("\nDIR:", directory)
3871 # print ("FN:", filename)
3872 # print ("Fs:", list(f.name for f in files), '\n')
3874 if tree
.type == 'SHADER':
3875 node_type
= "ShaderNodeTexImage"
3876 elif tree
.type == 'COMPOSITING':
3877 node_type
= "CompositorNodeImage"
3879 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3880 return {'CANCELLED'}
3882 if not files
[0].name
and not filename
:
3883 self
.report({'ERROR'}, "No file chosen")
3884 return {'CANCELLED'}
3885 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3886 # User has selected multiple files without an active one, or the active one is non-existant
3887 filename
= files
[0].name
3889 if not path
.exists(directory
+filename
):
3890 self
.report({'ERROR'}, filename
+" does not exist!")
3891 return {'CANCELLED'}
3893 without_ext
= '.'.join(filename
.split('.')[:-1])
3895 # if last digit isn't a number, it's not a sequence
3896 if not without_ext
[-1].isdigit():
3897 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3898 return {'CANCELLED'}
3901 extension
= filename
.split('.')[-1]
3902 reverse
= without_ext
[::-1] # reverse string
3905 for char
in reverse
:
3911 without_num
= without_ext
[:count_numbers
*-1]
3913 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3915 num_frames
= len(files
)
3917 nodes_list
= [node
for node
in nodes
]
3919 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3920 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3924 yloc
+= node_mid_pt(node
, 'y')
3925 yloc
= yloc
/len(nodes
)
3930 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3932 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3934 node
.label
= name_with_hashes
3936 img
= bpy
.data
.images
.load(directory
+(without_ext
+'.'+extension
))
3937 img
.source
= 'SEQUENCE'
3938 img
.name
= name_with_hashes
3940 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3941 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
3942 image_user
.frame_duration
= num_frames
3947 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3948 """Add multiple images at once"""
3949 bl_idname
= 'node.nw_add_multiple_images'
3950 bl_label
= 'Open Selected Images'
3951 bl_options
= {'REGISTER', 'UNDO'}
3952 directory
: StringProperty(
3955 files
: CollectionProperty(
3956 type=bpy
.types
.OperatorFileListElement
,
3957 options
={'HIDDEN', 'SKIP_SAVE'}
3960 def execute(self
, context
):
3961 nodes
, links
= get_nodes_links(context
)
3963 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3965 if context
.space_data
.node_tree
.type == 'SHADER':
3966 node_type
= "ShaderNodeTexImage"
3967 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3968 node_type
= "CompositorNodeImage"
3970 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3971 return {'CANCELLED'}
3974 for f
in self
.files
:
3977 node
= nodes
.new(node_type
)
3978 new_nodes
.append(node
)
3981 node
.width_hidden
= 100
3982 node
.location
.x
= xloc
3983 node
.location
.y
= yloc
3986 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3989 # shift new nodes up to center of tree
3990 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3992 if node
in new_nodes
:
3994 node
.location
.y
+= (list_size
/2)
4000 class NWViewerFocus(bpy
.types
.Operator
):
4001 """Set the viewer tile center to the mouse position"""
4002 bl_idname
= "node.nw_viewer_focus"
4003 bl_label
= "Viewer Focus"
4005 x
: bpy
.props
.IntProperty()
4006 y
: bpy
.props
.IntProperty()
4009 def poll(cls
, context
):
4010 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
4012 def execute(self
, context
):
4015 def invoke(self
, context
, event
):
4016 render
= context
.scene
.render
4017 space
= context
.space_data
4018 percent
= render
.resolution_percentage
*0.01
4020 nodes
, links
= get_nodes_links(context
)
4021 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
4024 mlocx
= event
.mouse_region_x
4025 mlocy
= event
.mouse_region_y
4026 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
4028 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
4029 region_x
= context
.region
.width
4030 region_y
= context
.region
.height
4032 region_center_x
= context
.region
.width
/ 2
4033 region_center_y
= context
.region
.height
/ 2
4035 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
4036 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
4038 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
4039 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
4041 margin_x
= region_center_x
- backdrop_center_x
4042 margin_y
= region_center_y
- backdrop_center_y
4044 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
4045 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
4047 for node
in viewers
:
4048 node
.center_x
= abs_mouse_x
4049 node
.center_y
= abs_mouse_y
4051 return {'PASS_THROUGH'}
4053 return self
.execute(context
)
4056 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
4057 """Save the current viewer node to an image file"""
4058 bl_idname
= "node.nw_save_viewer"
4059 bl_label
= "Save This Image"
4060 filepath
: StringProperty(subtype
="FILE_PATH")
4061 filename_ext
: EnumProperty(
4063 description
="Choose the file format to save to",
4064 items
=(('.bmp', "BMP", ""),
4065 ('.rgb', 'IRIS', ""),
4066 ('.png', 'PNG', ""),
4067 ('.jpg', 'JPEG', ""),
4068 ('.jp2', 'JPEG2000', ""),
4069 ('.tga', 'TARGA', ""),
4070 ('.cin', 'CINEON', ""),
4071 ('.dpx', 'DPX', ""),
4072 ('.exr', 'OPEN_EXR', ""),
4073 ('.hdr', 'HDR', ""),
4074 ('.tif', 'TIFF', "")),
4079 def poll(cls
, context
):
4081 if nw_check(context
):
4082 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4083 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
4084 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
4088 def execute(self
, context
):
4105 basename
, ext
= path
.splitext(fp
)
4106 old_render_format
= context
.scene
.render
.image_settings
.file_format
4107 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
4108 context
.area
.type = "IMAGE_EDITOR"
4109 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
4110 context
.area
.spaces
[0].image
.save_render(fp
)
4111 context
.area
.type = "NODE_EDITOR"
4112 context
.scene
.render
.image_settings
.file_format
= old_render_format
4116 class NWResetNodes(bpy
.types
.Operator
):
4117 """Reset Nodes in Selection"""
4118 bl_idname
= "node.nw_reset_nodes"
4119 bl_label
= "Reset Nodes"
4120 bl_options
= {'REGISTER', 'UNDO'}
4123 def poll(cls
, context
):
4124 space
= context
.space_data
4125 return space
.type == 'NODE_EDITOR'
4127 def execute(self
, context
):
4128 node_active
= context
.active_node
4129 node_selected
= context
.selected_nodes
4130 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4132 # Check if one node is selected at least
4133 if not (len(node_selected
) > 0):
4134 self
.report({'ERROR'}, "1 node must be selected at least")
4135 return {'CANCELLED'}
4137 active_node_name
= node_active
.name
if node_active
.select
else None
4138 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
4140 # Create output lists
4141 selected_node_names
= [n
.name
for n
in node_selected
]
4144 # Reset all valid children in a frame
4145 node_active_is_frame
= False
4146 if len(node_selected
) == 1 and node_active
.type == "FRAME":
4147 node_tree
= node_active
.id_data
4148 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
4150 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
4151 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
4152 node_active_is_frame
= True
4154 # Check if valid nodes in selection
4155 if not (len(valid_nodes
) > 0):
4156 # Check for frames only
4157 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
4158 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
4159 self
.report({'ERROR'}, "Please select only 1 frame to reset")
4161 self
.report({'ERROR'}, "No valid node(s) in selection")
4162 return {'CANCELLED'}
4164 # Report nodes that are not valid
4165 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
4166 valid_node_names
= [n
.name
for n
in valid_nodes
]
4167 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
4168 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
4170 # Deselect all nodes
4171 for i
in node_selected
:
4174 # Run through all valid nodes
4175 for node
in valid_nodes
:
4177 parent
= node
.parent
if node
.parent
else None
4178 node_loc
= [node
.location
.x
, node
.location
.y
]
4180 node_tree
= node
.id_data
4181 props_to_copy
= 'bl_idname name location height width'.split(' ')
4184 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
4185 for i
in (i
for i
in mappings
if i
.is_linked
):
4187 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
4189 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
4191 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
4192 props_to_copy
.pop(0)
4194 for prop
in props_to_copy
:
4195 setattr(new_node
, prop
, props
[prop
])
4197 nodes
= node_tree
.nodes
4199 new_node
.name
= props
['name']
4202 new_node
.parent
= parent
4203 new_node
.location
= node_loc
4205 for str_from
, str_to
in reconnections
:
4206 node_tree
.links
.new(eval(str_from
), eval(str_to
))
4208 new_node
.select
= False
4209 success_names
.append(new_node
.name
)
4211 # Reselect all nodes
4212 if selected_node_names
and node_active_is_frame
is False:
4213 for i
in selected_node_names
:
4214 node_tree
.nodes
[i
].select
= True
4216 if active_node_name
is not None:
4217 node_tree
.nodes
[active_node_name
].select
= True
4218 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
4220 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
4228 def drawlayout(context
, layout
, mode
='non-panel'):
4229 tree_type
= context
.space_data
.tree_type
4231 col
= layout
.column(align
=True)
4232 col
.menu(NWMergeNodesMenu
.bl_idname
)
4235 col
= layout
.column(align
=True)
4236 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
4239 if tree_type
== 'ShaderNodeTree':
4240 col
= layout
.column(align
=True)
4241 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
4242 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
4245 col
= layout
.column(align
=True)
4246 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
4247 col
.operator(NWSwapLinks
.bl_idname
)
4248 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
4251 col
= layout
.column(align
=True)
4252 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
4253 if tree_type
!= 'GeometryNodeTree':
4254 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
4257 col
= layout
.column(align
=True)
4259 row
= col
.row(align
=True)
4260 row
.operator(NWClearLabel
.bl_idname
).option
= True
4261 row
.operator(NWModifyLabels
.bl_idname
)
4263 col
.operator(NWClearLabel
.bl_idname
).option
= True
4264 col
.operator(NWModifyLabels
.bl_idname
)
4265 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
4267 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
4270 col
= layout
.column(align
=True)
4271 if tree_type
== 'CompositorNodeTree':
4272 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
4273 if tree_type
!= 'GeometryNodeTree':
4274 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
4277 col
= layout
.column(align
=True)
4278 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
4281 col
= layout
.column(align
=True)
4282 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
4285 col
= layout
.column(align
=True)
4286 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
4290 class NodeWranglerPanel(Panel
, NWBase
):
4291 bl_idname
= "NODE_PT_nw_node_wrangler"
4292 bl_space_type
= 'NODE_EDITOR'
4293 bl_label
= "Node Wrangler"
4294 bl_region_type
= "UI"
4295 bl_category
= "Node Wrangler"
4297 prepend
: StringProperty(
4300 append
: StringProperty()
4301 remove
: StringProperty()
4303 def draw(self
, context
):
4304 self
.layout
.label(text
="(Quick access: Shift+W)")
4305 drawlayout(context
, self
.layout
, mode
='panel')
4311 class NodeWranglerMenu(Menu
, NWBase
):
4312 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
4313 bl_label
= "Node Wrangler"
4315 def draw(self
, context
):
4316 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
4317 drawlayout(context
, self
.layout
)
4320 class NWMergeNodesMenu(Menu
, NWBase
):
4321 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
4322 bl_label
= "Merge Selected Nodes"
4324 def draw(self
, context
):
4325 type = context
.space_data
.tree_type
4326 layout
= self
.layout
4327 if type == 'ShaderNodeTree':
4328 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
4329 if type == 'GeometryNodeTree':
4330 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
4331 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4333 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
4334 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4335 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
4337 props
.merge_type
= 'ZCOMBINE'
4338 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
4340 props
.merge_type
= 'ALPHAOVER'
4342 class NWMergeGeometryMenu(Menu
, NWBase
):
4343 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
4344 bl_label
= "Merge Selected Nodes using Geometry Nodes"
4345 def draw(self
, context
):
4346 layout
= self
.layout
4347 # The boolean node + Join Geometry node
4348 for type, name
, description
in geo_combine_operations
:
4349 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4351 props
.merge_type
= 'GEOMETRY'
4353 class NWMergeShadersMenu(Menu
, NWBase
):
4354 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
4355 bl_label
= "Merge Selected Nodes using Shaders"
4357 def draw(self
, context
):
4358 layout
= self
.layout
4359 for type in ('MIX', 'ADD'):
4360 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
4362 props
.merge_type
= 'SHADER'
4365 class NWMergeMixMenu(Menu
, NWBase
):
4366 bl_idname
= "NODE_MT_nw_merge_mix_menu"
4367 bl_label
= "Merge Selected Nodes using Mix"
4369 def draw(self
, context
):
4370 layout
= self
.layout
4371 for type, name
, description
in blend_types
:
4372 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4374 props
.merge_type
= 'MIX'
4377 class NWConnectionListOutputs(Menu
, NWBase
):
4378 bl_idname
= "NODE_MT_nw_connection_list_out"
4381 def draw(self
, context
):
4382 layout
= self
.layout
4383 nodes
, links
= get_nodes_links(context
)
4385 n1
= nodes
[context
.scene
.NWLazySource
]
4387 for o
in n1
.outputs
:
4388 # Only show sockets that are exposed.
4390 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
4394 class NWConnectionListInputs(Menu
, NWBase
):
4395 bl_idname
= "NODE_MT_nw_connection_list_in"
4398 def draw(self
, context
):
4399 layout
= self
.layout
4400 nodes
, links
= get_nodes_links(context
)
4402 n2
= nodes
[context
.scene
.NWLazyTarget
]
4406 # Only show sockets that are exposed.
4407 # This prevents, for example, the scale value socket
4408 # of the vector math node being added to the list when
4409 # the mode is not 'SCALE'.
4411 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
4412 op
.from_socket
= context
.scene
.NWSourceSocket
4413 op
.to_socket
= index
4417 class NWMergeMathMenu(Menu
, NWBase
):
4418 bl_idname
= "NODE_MT_nw_merge_math_menu"
4419 bl_label
= "Merge Selected Nodes using Math"
4421 def draw(self
, context
):
4422 layout
= self
.layout
4423 for type, name
, description
in operations
:
4424 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4426 props
.merge_type
= 'MATH'
4429 class NWBatchChangeNodesMenu(Menu
, NWBase
):
4430 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
4431 bl_label
= "Batch Change Selected Nodes"
4433 def draw(self
, context
):
4434 layout
= self
.layout
4435 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
4436 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
4439 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
4440 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
4441 bl_label
= "Batch Change Blend Type"
4443 def draw(self
, context
):
4444 layout
= self
.layout
4445 for type, name
, description
in blend_types
:
4446 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4447 props
.blend_type
= type
4448 props
.operation
= 'CURRENT'
4451 class NWBatchChangeOperationMenu(Menu
, NWBase
):
4452 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
4453 bl_label
= "Batch Change Math Operation"
4455 def draw(self
, context
):
4456 layout
= self
.layout
4457 for type, name
, description
in operations
:
4458 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4459 props
.blend_type
= 'CURRENT'
4460 props
.operation
= type
4463 class NWCopyToSelectedMenu(Menu
, NWBase
):
4464 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4465 bl_label
= "Copy to Selected"
4467 def draw(self
, context
):
4468 layout
= self
.layout
4469 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4470 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4473 class NWCopyLabelMenu(Menu
, NWBase
):
4474 bl_idname
= "NODE_MT_nw_copy_label_menu"
4475 bl_label
= "Copy Label"
4477 def draw(self
, context
):
4478 layout
= self
.layout
4479 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4480 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4481 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4484 class NWAddReroutesMenu(Menu
, NWBase
):
4485 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4486 bl_label
= "Add Reroutes"
4487 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4489 def draw(self
, context
):
4490 layout
= self
.layout
4491 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4492 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4493 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4496 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4497 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4498 bl_label
= "Link Active to Selected"
4500 def draw(self
, context
):
4501 layout
= self
.layout
4502 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4503 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4504 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4507 class NWLinkStandardMenu(Menu
, NWBase
):
4508 bl_idname
= "NODE_MT_nw_link_standard_menu"
4509 bl_label
= "To All Selected"
4511 def draw(self
, context
):
4512 layout
= self
.layout
4513 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4514 props
.replace
= False
4515 props
.use_node_name
= False
4516 props
.use_outputs_names
= False
4517 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4518 props
.replace
= True
4519 props
.use_node_name
= False
4520 props
.use_outputs_names
= False
4523 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4524 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4525 bl_label
= "Use Node Name/Label"
4527 def draw(self
, context
):
4528 layout
= self
.layout
4529 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4530 props
.replace
= False
4531 props
.use_node_name
= True
4532 props
.use_outputs_names
= False
4533 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4534 props
.replace
= True
4535 props
.use_node_name
= True
4536 props
.use_outputs_names
= False
4539 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4540 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4541 bl_label
= "Use Outputs Names"
4543 def draw(self
, context
):
4544 layout
= self
.layout
4545 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4546 props
.replace
= False
4547 props
.use_node_name
= False
4548 props
.use_outputs_names
= True
4549 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4550 props
.replace
= True
4551 props
.use_node_name
= False
4552 props
.use_outputs_names
= True
4555 class NWVertColMenu(bpy
.types
.Menu
):
4556 bl_idname
= "NODE_MT_nw_node_vertex_color_menu"
4557 bl_label
= "Vertex Colors"
4560 def poll(cls
, context
):
4562 if nw_check(context
):
4563 snode
= context
.space_data
4564 valid
= snode
.tree_type
== 'ShaderNodeTree'
4567 def draw(self
, context
):
4569 nodes
, links
= get_nodes_links(context
)
4570 mat
= context
.object.active_material
4573 for obj
in bpy
.data
.objects
:
4574 for slot
in obj
.material_slots
:
4575 if slot
.material
== mat
:
4579 if obj
.data
.vertex_colors
:
4580 for vcol
in obj
.data
.vertex_colors
:
4581 vcols
.append(vcol
.name
)
4582 vcols
= list(set(vcols
)) # get a unique list
4586 l
.operator(NWAddAttrNode
.bl_idname
, text
=vcol
).attr_name
= vcol
4588 l
.label(text
="No Vertex Color layers on objects with this material")
4591 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4592 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4593 bl_label
= "Switch Type to..."
4595 def draw(self
, context
):
4596 layout
= self
.layout
4597 tree
= context
.space_data
.node_tree
4598 if tree
.type == 'SHADER':
4599 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4600 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4601 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4602 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4603 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4604 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4605 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4606 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4607 if tree
.type == 'COMPOSITING':
4608 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4609 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4610 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4611 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4612 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4613 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4614 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4615 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4616 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4617 if tree
.type == 'TEXTURE':
4618 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4619 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4620 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4621 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4622 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4623 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4624 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4625 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4626 if tree
.type == 'GEOMETRY':
4627 categories
= [c
for c
in node_categories_iter(context
)
4628 if c
.name
not in ['Group', 'Script']]
4629 for cat
in categories
:
4630 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4631 if hasattr(bpy
.types
, idname
):
4634 layout
.label(text
="Unable to load altered node lists.")
4635 layout
.label(text
="Please re-enable Node Wrangler.")
4639 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4640 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4643 def draw(self
, context
):
4644 layout
= self
.layout
4645 for ident
, node_type
, rna_name
in shaders_input_nodes_props
:
4646 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4647 props
.to_type
= ident
4650 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4651 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4654 def draw(self
, context
):
4655 layout
= self
.layout
4656 for ident
, node_type
, rna_name
in shaders_output_nodes_props
:
4657 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4658 props
.to_type
= ident
4661 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4662 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4665 def draw(self
, context
):
4666 layout
= self
.layout
4667 for ident
, node_type
, rna_name
in shaders_shader_nodes_props
:
4668 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4669 props
.to_type
= ident
4672 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4673 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4674 bl_label
= "Texture"
4676 def draw(self
, context
):
4677 layout
= self
.layout
4678 for ident
, node_type
, rna_name
in shaders_texture_nodes_props
:
4679 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4680 props
.to_type
= ident
4683 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4684 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4687 def draw(self
, context
):
4688 layout
= self
.layout
4689 for ident
, node_type
, rna_name
in shaders_color_nodes_props
:
4690 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4691 props
.to_type
= ident
4694 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4695 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4698 def draw(self
, context
):
4699 layout
= self
.layout
4700 for ident
, node_type
, rna_name
in shaders_vector_nodes_props
:
4701 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4702 props
.to_type
= ident
4705 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4706 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4707 bl_label
= "Converter"
4709 def draw(self
, context
):
4710 layout
= self
.layout
4711 for ident
, node_type
, rna_name
in shaders_converter_nodes_props
:
4712 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4713 props
.to_type
= ident
4716 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4717 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4720 def draw(self
, context
):
4721 layout
= self
.layout
4722 for ident
, node_type
, rna_name
in shaders_layout_nodes_props
:
4723 if node_type
!= 'FRAME':
4724 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4725 props
.to_type
= ident
4728 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4729 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4732 def draw(self
, context
):
4733 layout
= self
.layout
4734 for ident
, node_type
, rna_name
in compo_input_nodes_props
:
4735 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4736 props
.to_type
= ident
4739 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4740 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4743 def draw(self
, context
):
4744 layout
= self
.layout
4745 for ident
, node_type
, rna_name
in compo_output_nodes_props
:
4746 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4747 props
.to_type
= ident
4750 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4751 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4754 def draw(self
, context
):
4755 layout
= self
.layout
4756 for ident
, node_type
, rna_name
in compo_color_nodes_props
:
4757 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4758 props
.to_type
= ident
4761 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4762 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4763 bl_label
= "Converter"
4765 def draw(self
, context
):
4766 layout
= self
.layout
4767 for ident
, node_type
, rna_name
in compo_converter_nodes_props
:
4768 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4769 props
.to_type
= ident
4772 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4773 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4776 def draw(self
, context
):
4777 layout
= self
.layout
4778 for ident
, node_type
, rna_name
in compo_filter_nodes_props
:
4779 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4780 props
.to_type
= ident
4783 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4784 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4787 def draw(self
, context
):
4788 layout
= self
.layout
4789 for ident
, node_type
, rna_name
in compo_vector_nodes_props
:
4790 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4791 props
.to_type
= ident
4794 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4795 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4798 def draw(self
, context
):
4799 layout
= self
.layout
4800 for ident
, node_type
, rna_name
in compo_matte_nodes_props
:
4801 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4802 props
.to_type
= ident
4805 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4806 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4807 bl_label
= "Distort"
4809 def draw(self
, context
):
4810 layout
= self
.layout
4811 for ident
, node_type
, rna_name
in compo_distort_nodes_props
:
4812 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4813 props
.to_type
= ident
4816 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4817 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4820 def draw(self
, context
):
4821 layout
= self
.layout
4822 for ident
, node_type
, rna_name
in compo_layout_nodes_props
:
4823 if node_type
!= 'FRAME':
4824 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4825 props
.to_type
= ident
4828 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4829 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4832 def draw(self
, context
):
4833 layout
= self
.layout
4834 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4835 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4836 props
.to_type
= ident
4839 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4840 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4843 def draw(self
, context
):
4844 layout
= self
.layout
4845 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4846 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4847 props
.to_type
= ident
4850 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4851 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4854 def draw(self
, context
):
4855 layout
= self
.layout
4856 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4857 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4858 props
.to_type
= ident
4861 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4862 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4865 def draw(self
, context
):
4866 layout
= self
.layout
4867 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4868 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4869 props
.to_type
= ident
4872 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4873 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4874 bl_label
= "Converter"
4876 def draw(self
, context
):
4877 layout
= self
.layout
4878 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4879 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4880 props
.to_type
= ident
4883 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4884 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4887 def draw(self
, context
):
4888 layout
= self
.layout
4889 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4890 if node_type
!= 'FRAME':
4891 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4892 props
.to_type
= ident
4895 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4896 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4899 def draw(self
, context
):
4900 layout
= self
.layout
4901 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4902 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4903 props
.to_type
= ident
4906 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4907 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4910 def draw(self
, context
):
4911 layout
= self
.layout
4912 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4913 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4914 props
.to_type
= ident
4917 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4918 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4921 def draw(self
, context
):
4922 layout
= self
.layout
4923 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4924 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4925 props
.to_type
= ident
4928 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4929 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4930 bl_label
= "Pattern"
4932 def draw(self
, context
):
4933 layout
= self
.layout
4934 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4935 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4936 props
.to_type
= ident
4939 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4940 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4941 bl_label
= "Textures"
4943 def draw(self
, context
):
4944 layout
= self
.layout
4945 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4946 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4947 props
.to_type
= ident
4950 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4951 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4952 bl_label
= "Converter"
4954 def draw(self
, context
):
4955 layout
= self
.layout
4956 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4957 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4958 props
.to_type
= ident
4961 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4962 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4963 bl_label
= "Distort"
4965 def draw(self
, context
):
4966 layout
= self
.layout
4967 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4968 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4969 props
.to_type
= ident
4972 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4973 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4976 def draw(self
, context
):
4977 layout
= self
.layout
4978 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
4979 if node_type
!= 'FRAME':
4980 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4981 props
.to_type
= ident
4983 def draw_switch_category_submenu(self
, context
):
4984 layout
= self
.layout
4985 if self
.category
.name
== 'Layout':
4986 for node
in self
.category
.items(context
):
4987 if node
.nodetype
!= 'NodeFrame':
4988 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4989 props
.to_type
= node
.nodetype
4991 for node
in self
.category
.items(context
):
4992 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4993 props
.geo_to_type
= node
.nodetype
4996 # APPENDAGES TO EXISTING UI
5000 def select_parent_children_buttons(self
, context
):
5001 layout
= self
.layout
5002 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
5003 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
5006 def attr_nodes_menu_func(self
, context
):
5007 col
= self
.layout
.column(align
=True)
5008 col
.menu("NODE_MT_nw_node_vertex_color_menu")
5012 def multipleimages_menu_func(self
, context
):
5013 col
= self
.layout
.column(align
=True)
5014 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
5015 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
5019 def bgreset_menu_func(self
, context
):
5020 self
.layout
.operator(NWResetBG
.bl_idname
)
5023 def save_viewer_menu_func(self
, context
):
5024 if nw_check(context
):
5025 if context
.space_data
.tree_type
== 'CompositorNodeTree':
5026 if context
.scene
.node_tree
.nodes
.active
:
5027 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
5028 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
5031 def reset_nodes_button(self
, context
):
5032 node_active
= context
.active_node
5033 node_selected
= context
.selected_nodes
5034 node_ignore
= ["FRAME","REROUTE", "GROUP"]
5036 # Check if active node is in the selection and respective type
5037 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
5038 row
= self
.layout
.row()
5039 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
5040 self
.layout
.separator()
5042 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
5043 row
= self
.layout
.row()
5044 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
5045 self
.layout
.separator()
5049 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5051 switch_category_menus
= []
5053 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5054 # props entry: (property name, property value)
5057 # NWMergeNodes with Ctrl (AUTO).
5058 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
5059 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5060 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
5061 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5062 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5063 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5064 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
5065 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5066 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5067 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5068 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
5069 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5070 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5071 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5072 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
5073 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5074 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5075 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5076 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
5077 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5078 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
5079 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5080 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
5081 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5082 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5083 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5084 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5085 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
5086 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5087 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
5088 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5089 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5090 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5091 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
5092 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5093 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5094 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5095 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
5096 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5097 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5098 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5099 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
5100 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5101 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5102 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5103 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
5104 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5105 # NWMergeNodes with Ctrl Shift (MATH)
5106 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5107 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5108 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
5109 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5110 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5111 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5112 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
5113 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5114 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5115 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5116 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
5117 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5118 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5119 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5120 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
5121 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5122 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
5123 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5124 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
5125 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5126 # BATCH CHANGE NODES
5127 # NWBatchChangeNodes with Alt
5128 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
5129 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5130 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
5131 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5132 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5133 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5134 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
5135 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5136 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5137 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5138 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
5139 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5140 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5141 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5142 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
5143 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5144 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5145 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5146 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
5147 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5148 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
5149 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5150 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
5151 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5152 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
5153 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5154 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
5155 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5156 # LINK ACTIVE TO SELECTED
5157 # Don't use names, don't replace links (K)
5158 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
5159 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5160 # Don't use names, replace links (Shift K)
5161 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
5162 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5163 # Use node name, don't replace links (')
5164 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
5165 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5166 # Use node name, replace links (Shift ')
5167 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
5168 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5169 # Don't use names, don't replace links (;)
5170 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
5171 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5172 # Don't use names, replace links (')
5173 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
5174 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5176 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5177 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5178 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5179 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5180 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5181 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5182 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5183 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5184 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5185 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5186 # CLEAR LABEL (Alt L)
5187 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5188 # MODIFY LABEL (Alt Shift L)
5189 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5190 # Copy Label from active to selected
5191 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5192 # DETACH OUTPUTS (Alt Shift D)
5193 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5194 # LINK TO OUTPUT NODE (O)
5195 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5196 # SELECT PARENT/CHILDREN
5198 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5200 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5202 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5203 # Add Principled BSDF Texture Setup
5204 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5206 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5208 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5210 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5212 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5214 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5215 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5217 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
5219 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5221 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5222 # Lazy Connect with Menu
5223 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5224 # Viewer Tile Center
5225 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5227 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5228 # Reset Nodes (Back Space)
5229 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5231 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
5232 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5233 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5234 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
5235 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
5236 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
5241 NWPrincipledPreferences
,
5261 NWAddPrincipledSetup
,
5263 NWLinkActiveToSelected
,
5265 NWSelectParentChildren
,
5271 NWAddMultipleImages
,
5279 NWMergeGeometryMenu
,
5281 NWConnectionListOutputs
,
5282 NWConnectionListInputs
,
5284 NWBatchChangeNodesMenu
,
5285 NWBatchChangeBlendTypeMenu
,
5286 NWBatchChangeOperationMenu
,
5287 NWCopyToSelectedMenu
,
5290 NWLinkActiveToSelectedMenu
,
5292 NWLinkUseNodeNameMenu
,
5293 NWLinkUseOutputsNamesMenu
,
5295 NWSwitchNodeTypeMenu
,
5296 NWSwitchShadersInputSubmenu
,
5297 NWSwitchShadersOutputSubmenu
,
5298 NWSwitchShadersShaderSubmenu
,
5299 NWSwitchShadersTextureSubmenu
,
5300 NWSwitchShadersColorSubmenu
,
5301 NWSwitchShadersVectorSubmenu
,
5302 NWSwitchShadersConverterSubmenu
,
5303 NWSwitchShadersLayoutSubmenu
,
5304 NWSwitchCompoInputSubmenu
,
5305 NWSwitchCompoOutputSubmenu
,
5306 NWSwitchCompoColorSubmenu
,
5307 NWSwitchCompoConverterSubmenu
,
5308 NWSwitchCompoFilterSubmenu
,
5309 NWSwitchCompoVectorSubmenu
,
5310 NWSwitchCompoMatteSubmenu
,
5311 NWSwitchCompoDistortSubmenu
,
5312 NWSwitchCompoLayoutSubmenu
,
5313 NWSwitchMatInputSubmenu
,
5314 NWSwitchMatOutputSubmenu
,
5315 NWSwitchMatColorSubmenu
,
5316 NWSwitchMatVectorSubmenu
,
5317 NWSwitchMatConverterSubmenu
,
5318 NWSwitchMatLayoutSubmenu
,
5319 NWSwitchTexInputSubmenu
,
5320 NWSwitchTexOutputSubmenu
,
5321 NWSwitchTexColorSubmenu
,
5322 NWSwitchTexPatternSubmenu
,
5323 NWSwitchTexTexturesSubmenu
,
5324 NWSwitchTexConverterSubmenu
,
5325 NWSwitchTexDistortSubmenu
,
5326 NWSwitchTexLayoutSubmenu
,
5330 from bpy
.utils
import register_class
5333 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
5334 name
="Busy Drawing!",
5336 description
="An internal property used to store only the first mouse position")
5337 bpy
.types
.Scene
.NWLazySource
= StringProperty(
5338 name
="Lazy Source!",
5340 description
="An internal property used to store the first node in a Lazy Connect operation")
5341 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
5342 name
="Lazy Target!",
5344 description
="An internal property used to store the last node in a Lazy Connect operation")
5345 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
5346 name
="Source Socket!",
5348 description
="An internal property used to store the source socket in a Lazy Connect operation")
5349 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
5352 description
="An internal property used to determine if a socket is generated by the addon"
5359 addon_keymaps
.clear()
5360 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
5362 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
5363 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
5364 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
5366 for prop
, value
in props
:
5367 setattr(kmi
.properties
, prop
, value
)
5368 addon_keymaps
.append((km
, kmi
))
5371 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
5372 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
5373 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
5374 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
5375 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
5376 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
5377 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
5378 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
5381 switch_category_menus
.clear()
5382 for cat
in node_categories_iter(None):
5383 if cat
.name
not in ['Group', 'Script'] and cat
.identifier
.startswith('GEO'):
5384 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
5385 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
5386 "bl_space_type": 'NODE_EDITOR',
5387 "bl_label": cat
.name
,
5390 "draw": draw_switch_category_submenu
,
5393 switch_category_menus
.append(switch_category_type
)
5395 bpy
.utils
.register_class(switch_category_type
)
5399 from bpy
.utils
import unregister_class
5402 del bpy
.types
.Scene
.NWBusyDrawing
5403 del bpy
.types
.Scene
.NWLazySource
5404 del bpy
.types
.Scene
.NWLazyTarget
5405 del bpy
.types
.Scene
.NWSourceSocket
5406 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
5408 for cat_types
in switch_category_menus
:
5409 bpy
.utils
.unregister_class(cat_types
)
5410 switch_category_menus
.clear()
5413 for km
, kmi
in addon_keymaps
:
5414 km
.keymap_items
.remove(kmi
)
5415 addon_keymaps
.clear()
5418 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
5419 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
5420 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
5421 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
5422 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
5423 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
5424 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
5425 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
5428 unregister_class(cls
)
5430 if __name__
== "__main__":