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
621 ('LEFTMOUSE', "LMB"),
622 ('MIDDLEMOUSE', "MMB"),
623 ('RIGHTMOUSE', "RMB"),
624 ('WHEELUPMOUSE', "Wheel Up"),
625 ('WHEELDOWNMOUSE', "Wheel Down"),
626 ('WHEELINMOUSE', "Wheel In"),
627 ('WHEELOUTMOUSE', "Wheel Out"),
640 ('LINE_FEED', "Enter"),
647 ('BACK_SLASH', "\\"),
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 for (ugly
, nice
) in pairs
:
672 nice_punc
= punc
.replace("_", " ").title()
676 def force_update(context
):
677 context
.space_data
.node_tree
.update_tag()
681 prefs
= bpy
.context
.preferences
.system
682 return prefs
.dpi
* prefs
.pixel_size
/ 72
685 def node_mid_pt(node
, axis
):
687 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
689 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
695 def autolink(node1
, node2
, links
):
697 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
698 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
699 for outp
in available_outputs
:
700 for inp
in available_inputs
:
701 if not inp
.is_linked
and inp
.name
== outp
.name
:
706 for outp
in available_outputs
:
707 for inp
in available_inputs
:
708 if not inp
.is_linked
and inp
.type == outp
.type:
713 # force some connection even if the type doesn't match
714 if available_outputs
:
715 for inp
in available_inputs
:
716 if not inp
.is_linked
:
718 links
.new(available_outputs
[0], inp
)
721 # even if no sockets are open, force one of matching type
722 for outp
in available_outputs
:
723 for inp
in available_inputs
:
724 if inp
.type == outp
.type:
730 for outp
in available_outputs
:
731 for inp
in available_inputs
:
736 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
740 def node_at_pos(nodes
, context
, event
):
741 nodes_near_mouse
= []
742 nodes_under_mouse
= []
745 store_mouse_cursor(context
, event
)
746 x
, y
= context
.space_data
.cursor_location
750 # Make a list of each corner (and middle of border) for each node.
751 # Will be sorted to find nearest point and thus nearest node
752 node_points_with_dist
= []
755 if node
.type != 'FRAME': # no point trying to link to a frame node
756 locx
= node
.location
.x
757 locy
= node
.location
.y
758 dimx
= node
.dimensions
.x
/dpifac()
759 dimy
= node
.dimensions
.y
/dpifac()
761 locx
+= node
.parent
.location
.x
762 locy
+= node
.parent
.location
.y
763 if node
.parent
.parent
:
764 locx
+= node
.parent
.parent
.location
.x
765 locy
+= node
.parent
.parent
.location
.y
766 if node
.parent
.parent
.parent
:
767 locx
+= node
.parent
.parent
.parent
.location
.x
768 locy
+= node
.parent
.parent
.parent
.location
.y
769 if node
.parent
.parent
.parent
.parent
:
770 # Support three levels or parenting
771 # There's got to be a better way to do this...
774 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
775 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
776 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
777 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
779 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
780 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
781 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
782 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
784 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
787 if node
.type != 'FRAME' and skipnode
== False:
788 locx
= node
.location
.x
789 locy
= node
.location
.y
790 dimx
= node
.dimensions
.x
/dpifac()
791 dimy
= node
.dimensions
.y
/dpifac()
793 locx
+= node
.parent
.location
.x
794 locy
+= node
.parent
.location
.y
795 if (locx
<= x
<= locx
+ dimx
) and \
796 (locy
- dimy
<= y
<= locy
):
797 nodes_under_mouse
.append(node
)
799 if len(nodes_under_mouse
) == 1:
800 if nodes_under_mouse
[0] != nearest_node
:
801 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
803 target_node
= nearest_node
# else use the nearest node
805 target_node
= nearest_node
809 def store_mouse_cursor(context
, event
):
810 space
= context
.space_data
811 v2d
= context
.region
.view2d
812 tree
= space
.edit_tree
814 # convert mouse position to the View2D for later node placement
815 if context
.region
.type == 'WINDOW':
816 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
818 space
.cursor_location
= tree
.view_center
820 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
821 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
823 vertices
= ((x1
, y1
), (x2
, y2
))
824 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
825 colour
[1]+(1.0-colour
[1])/4,
826 colour
[2]+(1.0-colour
[2])/4,
827 colour
[3]+(1.0-colour
[3])/4),
830 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
831 bgl
.glLineWidth(size
* dpifac())
837 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
838 radius
= radius
* dpifac()
840 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
841 radius
* sin(i
* 2 * pi
/ sides
) + my
)
842 for i
in range(sides
+ 1)]
844 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
846 shader
.uniform_float("color", colour
)
849 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
850 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
851 bottom_bar
= (16*dpifac()) + 1
853 radius
= radius
*dpifac()
855 nlocx
= (node
.location
.x
+1)*dpifac()
856 nlocy
= (node
.location
.y
+1)*dpifac()
857 ndimx
= node
.dimensions
.x
858 ndimy
= node
.dimensions
.y
859 # This is a stupid way to do this... TODO use while loop
861 nlocx
+= node
.parent
.location
.x
862 nlocy
+= node
.parent
.location
.y
863 if node
.parent
.parent
:
864 nlocx
+= node
.parent
.parent
.location
.x
865 nlocy
+= node
.parent
.parent
.location
.y
866 if node
.parent
.parent
.parent
:
867 nlocx
+= node
.parent
.parent
.parent
.location
.x
868 nlocy
+= node
.parent
.parent
.parent
.location
.y
873 if node
.type == 'REROUTE':
881 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
883 for i
in range(sides
+1):
885 if my
> bottom_bar
and mx
< area_width
:
886 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
887 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
888 vertices
.append((cosine
,sine
))
889 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
891 shader
.uniform_float("color", colour
)
895 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
897 for i
in range(sides
+1):
899 if my
> bottom_bar
and mx
< area_width
:
900 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
901 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
902 vertices
.append((cosine
,sine
))
903 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
905 shader
.uniform_float("color", colour
)
909 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
911 for i
in range(sides
+1):
913 if my
> bottom_bar
and mx
< area_width
:
914 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
915 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
916 vertices
.append((cosine
,sine
))
917 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
919 shader
.uniform_float("color", colour
)
922 # Bottom right corner
923 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
925 for i
in range(sides
+1):
927 if my
> bottom_bar
and mx
< area_width
:
928 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
929 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
930 vertices
.append((cosine
,sine
))
931 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
933 shader
.uniform_float("color", colour
)
936 # prepare drawing all edges in one batch
942 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
943 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
944 if m1x
< area_width
and m2x
< area_width
:
945 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
946 (m1x
,m1y
), (m1x
-radius
,m1y
)])
947 indices
.extend([(id_last
, id_last
+1, id_last
+3),
948 (id_last
+3, id_last
+1, id_last
+2)])
952 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
953 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
954 m1x
= min(m1x
, area_width
)
955 m2x
= min(m2x
, area_width
)
956 if m1y
> bottom_bar
and m2y
> bottom_bar
:
957 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
958 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
959 indices
.extend([(id_last
, id_last
+1, id_last
+3),
960 (id_last
+3, id_last
+1, id_last
+2)])
964 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
965 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
966 m1y
= max(m1y
, bottom_bar
)
967 m2y
= max(m2y
, bottom_bar
)
968 if m1x
< area_width
and m2x
< area_width
:
969 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
970 (m1x
+radius
,m1y
), (m1x
,m1y
)])
971 indices
.extend([(id_last
, id_last
+1, id_last
+3),
972 (id_last
+3, id_last
+1, id_last
+2)])
976 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
977 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
978 m1x
= min(m1x
, area_width
)
979 m2x
= min(m2x
, area_width
)
980 if m1y
> bottom_bar
and m2y
> bottom_bar
:
981 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
982 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
983 indices
.extend([(id_last
, id_last
+1, id_last
+3),
984 (id_last
+3, id_last
+1, id_last
+2)])
986 # now draw all edges in one batch
987 if len(vertices
) != 0:
988 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
990 shader
.uniform_float("color", colour
)
993 def draw_callback_nodeoutline(self
, context
, mode
):
997 bgl
.glEnable(bgl
.GL_BLEND
)
998 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
999 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
1001 nodes
, links
= get_nodes_links(context
)
1003 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
1006 col_outer
= (1.0, 0.2, 0.2, 0.4)
1007 col_inner
= (0.0, 0.0, 0.0, 0.5)
1008 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
1009 elif mode
== "LINKMENU":
1010 col_outer
= (0.4, 0.6, 1.0, 0.4)
1011 col_inner
= (0.0, 0.0, 0.0, 0.5)
1012 col_circle_inner
= (0.08, 0.15, .3, 1.0)
1014 col_outer
= (0.2, 1.0, 0.2, 0.4)
1015 col_inner
= (0.0, 0.0, 0.0, 0.5)
1016 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
1018 m1x
= self
.mouse_path
[0][0]
1019 m1y
= self
.mouse_path
[0][1]
1020 m2x
= self
.mouse_path
[-1][0]
1021 m2y
= self
.mouse_path
[-1][1]
1023 n1
= nodes
[context
.scene
.NWLazySource
]
1024 n2
= nodes
[context
.scene
.NWLazyTarget
]
1027 col_outer
= (0.4, 0.4, 0.4, 0.4)
1028 col_inner
= (0.0, 0.0, 0.0, 0.5)
1029 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
1031 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
1032 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
1033 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
1034 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
1036 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
1037 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
1040 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
1041 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
1044 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
1045 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
1047 bgl
.glDisable(bgl
.GL_BLEND
)
1048 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
1049 def get_active_tree(context
):
1050 tree
= context
.space_data
.node_tree
1052 # Get nodes from currently edited tree.
1053 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1054 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1055 # the same as context.active_node, the user is in a group.
1056 # Check recursively until we find the real active node_tree:
1057 if tree
.nodes
.active
:
1058 while tree
.nodes
.active
!= context
.active_node
:
1059 tree
= tree
.nodes
.active
.node_tree
1063 def get_nodes_links(context
):
1064 tree
, path
= get_active_tree(context
)
1065 return tree
.nodes
, tree
.links
1067 def is_viewer_socket(socket
):
1068 # checks if a internal socket is a valid viewer socket
1069 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
1071 def get_internal_socket(socket
):
1072 #get the internal socket from a socket inside or outside the group
1074 if node
.type == 'GROUP_OUTPUT':
1075 source_iterator
= node
.inputs
1076 iterator
= node
.id_data
.outputs
1077 elif node
.type == 'GROUP_INPUT':
1078 source_iterator
= node
.outputs
1079 iterator
= node
.id_data
.inputs
1080 elif hasattr(node
, "node_tree"):
1081 if socket
.is_output
:
1082 source_iterator
= node
.outputs
1083 iterator
= node
.node_tree
.outputs
1085 source_iterator
= node
.inputs
1086 iterator
= node
.node_tree
.inputs
1090 for i
, s
in enumerate(source_iterator
):
1095 def is_viewer_link(link
, output_node
):
1096 if "Emission Viewer" in link
.to_node
.name
or link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
1098 if link
.to_node
.type == 'GROUP_OUTPUT':
1099 socket
= get_internal_socket(link
.to_socket
)
1100 if is_viewer_socket(socket
):
1104 def get_group_output_node(tree
):
1105 for node
in tree
.nodes
:
1106 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
1109 def get_output_location(tree
):
1110 # get right-most location
1111 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
1112 max_xloc_node
= sorted_by_xloc
[-1]
1113 if max_xloc_node
.name
== 'Emission Viewer':
1114 max_xloc_node
= sorted_by_xloc
[-2]
1116 # get average y location
1118 for node
in tree
.nodes
:
1119 sum_yloc
+= node
.location
.y
1121 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1122 loc_y
= sum_yloc
/ len(tree
.nodes
)
1126 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
1127 base_color
: StringProperty(
1129 default
='diffuse diff albedo base col color',
1130 description
='Naming Components for Base Color maps')
1131 sss_color
: StringProperty(
1132 name
='Subsurface Color',
1133 default
='sss subsurface',
1134 description
='Naming Components for Subsurface Color maps')
1135 metallic
: StringProperty(
1137 default
='metallic metalness metal mtl',
1138 description
='Naming Components for metallness maps')
1139 specular
: StringProperty(
1141 default
='specularity specular spec spc',
1142 description
='Naming Components for Specular maps')
1143 normal
: StringProperty(
1145 default
='normal nor nrm nrml norm',
1146 description
='Naming Components for Normal maps')
1147 bump
: StringProperty(
1150 description
='Naming Components for bump maps')
1151 rough
: StringProperty(
1153 default
='roughness rough rgh',
1154 description
='Naming Components for roughness maps')
1155 gloss
: StringProperty(
1157 default
='gloss glossy glossiness',
1158 description
='Naming Components for glossy maps')
1159 displacement
: StringProperty(
1160 name
='Displacement',
1161 default
='displacement displace disp dsp height heightmap',
1162 description
='Naming Components for displacement maps')
1165 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1166 bl_idname
= __name__
1168 merge_hide
: EnumProperty(
1169 name
="Hide Mix nodes",
1171 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1172 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1173 ("NEVER", "Never", "Never collapse the new merge nodes")
1175 default
='NON_SHADER',
1176 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1177 merge_position
: EnumProperty(
1178 name
="Mix Node Position",
1180 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1181 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1184 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1186 show_hotkey_list
: BoolProperty(
1187 name
="Show Hotkey List",
1189 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1191 hotkey_list_filter
: StringProperty(
1192 name
=" Filter by Name",
1194 description
="Show only hotkeys that have this text in their name"
1196 show_principled_lists
: BoolProperty(
1197 name
="Show Principled naming tags",
1199 description
="Expand this box into a list of all naming tags for principled texture setup"
1201 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1203 def draw(self
, context
):
1204 layout
= self
.layout
1205 col
= layout
.column()
1206 col
.prop(self
, "merge_position")
1207 col
.prop(self
, "merge_hide")
1210 col
= box
.column(align
=True)
1211 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1212 if self
.show_principled_lists
:
1213 tags
= self
.principled_tags
1215 col
.prop(tags
, "base_color")
1216 col
.prop(tags
, "sss_color")
1217 col
.prop(tags
, "metallic")
1218 col
.prop(tags
, "specular")
1219 col
.prop(tags
, "rough")
1220 col
.prop(tags
, "gloss")
1221 col
.prop(tags
, "normal")
1222 col
.prop(tags
, "bump")
1223 col
.prop(tags
, "displacement")
1226 col
= box
.column(align
=True)
1227 hotkey_button_name
= "Show Hotkey List"
1228 if self
.show_hotkey_list
:
1229 hotkey_button_name
= "Hide Hotkey List"
1230 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1231 if self
.show_hotkey_list
:
1232 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1234 for hotkey
in kmi_defs
:
1236 hotkey_name
= hotkey
[7]
1238 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1239 row
= col
.row(align
=True)
1240 row
.label(text
=hotkey_name
)
1241 keystr
= nice_hotkey_name(hotkey
[1])
1243 keystr
= "Shift " + keystr
1245 keystr
= "Alt " + keystr
1247 keystr
= "Ctrl " + keystr
1248 row
.label(text
=keystr
)
1252 def nw_check(context
):
1253 space
= context
.space_data
1254 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1257 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1264 def poll(cls
, context
):
1265 return nw_check(context
)
1269 class NWLazyMix(Operator
, NWBase
):
1270 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1271 bl_idname
= "node.nw_lazy_mix"
1272 bl_label
= "Mix Nodes"
1273 bl_options
= {'REGISTER', 'UNDO'}
1275 def modal(self
, context
, event
):
1276 context
.area
.tag_redraw()
1277 nodes
, links
= get_nodes_links(context
)
1280 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1283 if not context
.scene
.NWBusyDrawing
:
1284 node1
= node_at_pos(nodes
, context
, event
)
1286 context
.scene
.NWBusyDrawing
= node1
.name
1288 if context
.scene
.NWBusyDrawing
!= 'STOP':
1289 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1291 context
.scene
.NWLazySource
= node1
.name
1292 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1294 if event
.type == 'MOUSEMOVE':
1295 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1297 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1298 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1299 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1302 node2
= node_at_pos(nodes
, context
, event
)
1304 context
.scene
.NWBusyDrawing
= node2
.name
1316 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1318 context
.scene
.NWBusyDrawing
= ""
1321 elif event
.type == 'ESC':
1323 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1324 return {'CANCELLED'}
1326 return {'RUNNING_MODAL'}
1328 def invoke(self
, context
, event
):
1329 if context
.area
.type == 'NODE_EDITOR':
1330 # the arguments we pass the the callback
1331 args
= (self
, context
, 'MIX')
1332 # Add the region OpenGL drawing callback
1333 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1334 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1336 self
.mouse_path
= []
1338 context
.window_manager
.modal_handler_add(self
)
1339 return {'RUNNING_MODAL'}
1341 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1342 return {'CANCELLED'}
1345 class NWLazyConnect(Operator
, NWBase
):
1346 """Connect two nodes without clicking a specific socket (automatically determined"""
1347 bl_idname
= "node.nw_lazy_connect"
1348 bl_label
= "Lazy Connect"
1349 bl_options
= {'REGISTER', 'UNDO'}
1350 with_menu
: BoolProperty()
1352 def modal(self
, context
, event
):
1353 context
.area
.tag_redraw()
1354 nodes
, links
= get_nodes_links(context
)
1357 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1360 if not context
.scene
.NWBusyDrawing
:
1361 node1
= node_at_pos(nodes
, context
, event
)
1363 context
.scene
.NWBusyDrawing
= node1
.name
1365 if context
.scene
.NWBusyDrawing
!= 'STOP':
1366 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1368 context
.scene
.NWLazySource
= node1
.name
1369 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1371 if event
.type == 'MOUSEMOVE':
1372 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1374 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1375 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1376 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1379 node2
= node_at_pos(nodes
, context
, event
)
1381 context
.scene
.NWBusyDrawing
= node2
.name
1386 link_success
= False
1392 if node
.select
== True:
1394 original_sel
.append(node
)
1396 original_unsel
.append(node
)
1400 #link_success = autolink(node1, node2, links)
1402 if len(node1
.outputs
) > 1 and node2
.inputs
:
1403 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1404 elif len(node1
.outputs
) == 1:
1405 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1407 link_success
= autolink(node1
, node2
, links
)
1409 for node
in original_sel
:
1411 for node
in original_unsel
:
1415 force_update(context
)
1416 context
.scene
.NWBusyDrawing
= ""
1419 elif event
.type == 'ESC':
1420 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1421 return {'CANCELLED'}
1423 return {'RUNNING_MODAL'}
1425 def invoke(self
, context
, event
):
1426 if context
.area
.type == 'NODE_EDITOR':
1427 nodes
, links
= get_nodes_links(context
)
1428 node
= node_at_pos(nodes
, context
, event
)
1430 context
.scene
.NWBusyDrawing
= node
.name
1432 # the arguments we pass the the callback
1436 args
= (self
, context
, mode
)
1437 # Add the region OpenGL drawing callback
1438 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1439 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1441 self
.mouse_path
= []
1443 context
.window_manager
.modal_handler_add(self
)
1444 return {'RUNNING_MODAL'}
1446 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1447 return {'CANCELLED'}
1450 class NWDeleteUnused(Operator
, NWBase
):
1451 """Delete all nodes whose output is not used"""
1452 bl_idname
= 'node.nw_del_unused'
1453 bl_label
= 'Delete Unused Nodes'
1454 bl_options
= {'REGISTER', 'UNDO'}
1456 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1457 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1459 def is_unused_node(self
, node
):
1460 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1461 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1462 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1463 if node
.type in end_types
:
1466 for output
in node
.outputs
:
1472 def poll(cls
, context
):
1474 if nw_check(context
):
1475 if context
.space_data
.node_tree
.nodes
:
1479 def execute(self
, context
):
1480 nodes
, links
= get_nodes_links(context
)
1485 if node
.select
== True:
1486 selection
.append(node
.name
)
1492 temp_deleted_nodes
= []
1493 del_unused_iterations
= len(nodes
)
1494 for it
in range(0, del_unused_iterations
):
1495 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1497 if self
.is_unused_node(node
):
1499 deleted_nodes
.append(node
.name
)
1500 bpy
.ops
.node
.delete()
1502 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1505 if self
.delete_frames
:
1513 frames_in_use
.append(node
.parent
)
1515 if node
.type == 'FRAME' and node
not in frames_in_use
:
1518 repeat
= True # repeat for nested frames
1520 if node
not in frames_in_use
:
1522 deleted_nodes
.append(node
.name
)
1523 bpy
.ops
.node
.delete()
1525 if self
.delete_muted
:
1529 deleted_nodes
.append(node
.name
)
1530 bpy
.ops
.node
.delete_reconnect()
1532 # get unique list of deleted nodes (iterations would count the same node more than once)
1533 deleted_nodes
= list(set(deleted_nodes
))
1534 for n
in deleted_nodes
:
1535 self
.report({'INFO'}, "Node " + n
+ " deleted")
1536 num_deleted
= len(deleted_nodes
)
1541 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1543 self
.report({'INFO'}, "Nothing deleted")
1546 nodes
, links
= get_nodes_links(context
)
1548 if node
.name
in selection
:
1552 def invoke(self
, context
, event
):
1553 return context
.window_manager
.invoke_confirm(self
, event
)
1556 class NWSwapLinks(Operator
, NWBase
):
1557 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1558 bl_idname
= 'node.nw_swap_links'
1559 bl_label
= 'Swap Links'
1560 bl_options
= {'REGISTER', 'UNDO'}
1563 def poll(cls
, context
):
1565 if nw_check(context
):
1566 if context
.selected_nodes
:
1567 valid
= len(context
.selected_nodes
) <= 2
1570 def execute(self
, context
):
1571 nodes
, links
= get_nodes_links(context
)
1572 selected_nodes
= context
.selected_nodes
1573 n1
= selected_nodes
[0]
1576 if len(selected_nodes
) == 2:
1577 n2
= selected_nodes
[1]
1578 if n1
.outputs
and n2
.outputs
:
1583 for output
in n1
.outputs
:
1585 for link
in output
.links
:
1586 n1_outputs
.append([out_index
, link
.to_socket
])
1591 for output
in n2
.outputs
:
1593 for link
in output
.links
:
1594 n2_outputs
.append([out_index
, link
.to_socket
])
1598 for connection
in n1_outputs
:
1600 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1602 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1603 for connection
in n2_outputs
:
1605 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1607 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1609 if n1
.outputs
or n2
.outputs
:
1610 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1612 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1615 elif len(selected_nodes
) == 1:
1616 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1617 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1622 for i1
in n1
.inputs
:
1623 if i1
.is_linked
and not i1
.is_multi_input
:
1625 for i2
in n1
.inputs
:
1626 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1628 types
.append ([i1
, similar_types
, i
])
1630 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1635 for i2
in n1
.inputs
:
1636 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1638 i1f
= pair
[0].links
[0].from_socket
1639 i1t
= pair
[0].links
[0].to_socket
1640 i2f
= pair
[1].links
[0].from_socket
1641 i2t
= pair
[1].links
[0].to_socket
1646 fs
= t
[0].links
[0].from_socket
1648 links
.remove(t
[0].links
[0])
1649 if i
+1 == len(n1
.inputs
):
1652 while n1
.inputs
[i
].is_linked
:
1654 links
.new(fs
, n1
.inputs
[i
])
1655 elif len(types
) == 2:
1656 i1f
= types
[0][0].links
[0].from_socket
1657 i1t
= types
[0][0].links
[0].to_socket
1658 i2f
= types
[1][0].links
[0].from_socket
1659 i2t
= types
[1][0].links
[0].to_socket
1664 self
.report({'WARNING'}, "This node has no input connections to swap!")
1666 self
.report({'WARNING'}, "This node has no inputs to swap!")
1668 force_update(context
)
1672 class NWResetBG(Operator
, NWBase
):
1673 """Reset the zoom and position of the background image"""
1674 bl_idname
= 'node.nw_bg_reset'
1675 bl_label
= 'Reset Backdrop'
1676 bl_options
= {'REGISTER', 'UNDO'}
1679 def poll(cls
, context
):
1681 if nw_check(context
):
1682 snode
= context
.space_data
1683 valid
= snode
.tree_type
== 'CompositorNodeTree'
1686 def execute(self
, context
):
1687 context
.space_data
.backdrop_zoom
= 1
1688 context
.space_data
.backdrop_offset
[0] = 0
1689 context
.space_data
.backdrop_offset
[1] = 0
1693 class NWAddAttrNode(Operator
, NWBase
):
1694 """Add an Attribute node with this name"""
1695 bl_idname
= 'node.nw_add_attr_node'
1696 bl_label
= 'Add UV map'
1697 bl_options
= {'REGISTER', 'UNDO'}
1699 attr_name
: StringProperty()
1701 def execute(self
, context
):
1702 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1703 nodes
, links
= get_nodes_links(context
)
1704 nodes
.active
.attribute_name
= self
.attr_name
1707 class NWPreviewNode(Operator
, NWBase
):
1708 bl_idname
= "node.nw_preview_node"
1709 bl_label
= "Preview Node"
1710 bl_description
= "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output"
1711 bl_options
= {'REGISTER', 'UNDO'}
1713 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1714 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1715 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1716 run_in_geometry_nodes
: BoolProperty(default
=True)
1719 self
.shader_output_type
= ""
1720 self
.shader_output_ident
= ""
1721 self
.shader_viewer_ident
= ""
1724 def poll(cls
, context
):
1725 if nw_check(context
):
1726 space
= context
.space_data
1727 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1728 if context
.active_node
:
1729 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1735 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1736 #check if a viewer output already exists in a node group otherwise create
1737 if hasattr(node
, "node_tree"):
1739 if len(node
.node_tree
.outputs
):
1741 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1742 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1743 #if viewer output is already used but leads to the same socket we can still use it
1744 is_used
= self
.is_socket_used_other_mats(socket
)
1746 if connect_socket
== None:
1748 groupout
= get_group_output_node(node
.node_tree
)
1749 groupout_input
= groupout
.inputs
[i
]
1750 links
= groupout_input
.links
1751 if connect_socket
not in [link
.from_socket
for link
in links
]:
1757 if not index
and free_socket
:
1761 #create viewer socket
1762 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1763 index
= len(node
.node_tree
.outputs
) - 1
1764 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1767 def init_shader_variables(self
, space
, shader_type
):
1768 if shader_type
== 'OBJECT':
1769 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1770 self
.shader_output_type
= "OUTPUT_MATERIAL"
1771 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1772 self
.shader_viewer_ident
= "ShaderNodeEmission"
1774 self
.shader_output_type
= "OUTPUT_LIGHT"
1775 self
.shader_output_ident
= "ShaderNodeOutputLight"
1776 self
.shader_viewer_ident
= "ShaderNodeEmission"
1778 elif shader_type
== 'WORLD':
1779 self
.shader_output_type
= "OUTPUT_WORLD"
1780 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1781 self
.shader_viewer_ident
= "ShaderNodeBackground"
1783 def get_shader_output_node(self
, tree
):
1784 for node
in tree
.nodes
:
1785 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1789 def ensure_group_output(cls
, tree
):
1790 #check if a group output node exists otherwise create
1791 groupout
= get_group_output_node(tree
)
1793 groupout
= tree
.nodes
.new('NodeGroupOutput')
1794 loc_x
, loc_y
= get_output_location(tree
)
1795 groupout
.location
.x
= loc_x
1796 groupout
.location
.y
= loc_y
1797 groupout
.select
= False
1798 # So that we don't keep on adding new group outputs
1799 groupout
.is_active_output
= True
1803 def search_sockets(cls
, node
, sockets
, index
=None):
1804 # recursively scan nodes for viewer sockets and store in list
1805 for i
, input_socket
in enumerate(node
.inputs
):
1806 if index
and i
!= index
:
1808 if len(input_socket
.links
):
1809 link
= input_socket
.links
[0]
1810 next_node
= link
.from_node
1811 external_socket
= link
.from_socket
1812 if hasattr(next_node
, "node_tree"):
1813 for socket_index
, s
in enumerate(next_node
.outputs
):
1814 if s
== external_socket
:
1816 socket
= next_node
.node_tree
.outputs
[socket_index
]
1817 if is_viewer_socket(socket
) and socket
not in sockets
:
1818 sockets
.append(socket
)
1819 #continue search inside of node group but restrict socket to where we came from
1820 groupout
= get_group_output_node(next_node
.node_tree
)
1821 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1824 def scan_nodes(cls
, tree
, sockets
):
1825 # get all viewer sockets in a material tree
1826 for node
in tree
.nodes
:
1827 if hasattr(node
, "node_tree"):
1828 for socket
in node
.node_tree
.outputs
:
1829 if is_viewer_socket(socket
) and (socket
not in sockets
):
1830 sockets
.append(socket
)
1831 cls
.scan_nodes(node
.node_tree
, sockets
)
1833 def link_leads_to_used_socket(self
, link
):
1834 #return True if link leads to a socket that is already used in this material
1835 socket
= get_internal_socket(link
.to_socket
)
1836 return (socket
and self
.is_socket_used_active_mat(socket
))
1838 def is_socket_used_active_mat(self
, socket
):
1839 #ensure used sockets in active material is calculated and check given socket
1840 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1841 self
.used_viewer_sockets_active_mat
= []
1842 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1844 emission
= self
.get_viewer_node(materialout
)
1845 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_active_mat
)
1846 return socket
in self
.used_viewer_sockets_active_mat
1848 def is_socket_used_other_mats(self
, socket
):
1849 #ensure used sockets in other materials are calculated and check given socket
1850 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1851 self
.used_viewer_sockets_other_mats
= []
1852 for mat
in bpy
.data
.materials
:
1853 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1856 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1858 emission
= self
.get_viewer_node(materialout
)
1859 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_other_mats
)
1860 return socket
in self
.used_viewer_sockets_other_mats
1863 def get_viewer_node(materialout
):
1864 input_socket
= materialout
.inputs
[0]
1865 if len(input_socket
.links
) > 0:
1866 node
= input_socket
.links
[0].from_node
1867 if node
.type == 'EMISSION' and node
.name
== "Emission Viewer":
1870 def invoke(self
, context
, event
):
1871 space
= context
.space_data
1872 # Ignore operator when running in wrong context.
1873 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1874 return {'PASS_THROUGH'}
1876 shader_type
= space
.shader_type
1877 self
.init_shader_variables(space
, shader_type
)
1878 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1879 mlocx
= event
.mouse_region_x
1880 mlocy
= event
.mouse_region_y
1881 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
1882 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1883 active_tree
, path_to_tree
= get_active_tree(context
)
1884 nodes
, links
= active_tree
.nodes
, active_tree
.links
1885 base_node_tree
= space
.node_tree
1886 active
= nodes
.active
1888 # For geometry node trees we just connect to the group output,
1889 # because there is no "viewer node" yet.
1890 if space
.tree_type
== "GeometryNodeTree":
1893 for out
in active
.outputs
:
1894 if is_visible_socket(out
):
1903 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1904 self
.scan_nodes(base_node_tree
, delete_sockets
)
1906 # Find (or create if needed) the output of this node tree
1907 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1909 # Analyze outputs, make links
1912 for i
, out
in enumerate(active
.outputs
):
1913 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1914 valid_outputs
.append(i
)
1916 out_i
= valid_outputs
[0] # Start index of node's outputs
1917 for i
, valid_i
in enumerate(valid_outputs
):
1918 for out_link
in active
.outputs
[valid_i
].links
:
1919 if is_viewer_link(out_link
, geometryoutput
):
1920 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1921 if i
< len(valid_outputs
) - 1:
1922 out_i
= valid_outputs
[i
+ 1]
1924 out_i
= valid_outputs
[0]
1926 make_links
= [] # store sockets for new links
1927 delete_nodes
= [] # store unused nodes to delete in the end
1929 # If there is no 'GEOMETRY' output type - We can't preview the node
1932 socket_type
= 'GEOMETRY'
1933 # Find an input socket of the output of type geometry
1934 geometryoutindex
= None
1935 for i
,inp
in enumerate(geometryoutput
.inputs
):
1936 if inp
.type == socket_type
:
1937 geometryoutindex
= i
1939 if geometryoutindex
is None:
1940 # Create geometry socket
1941 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1942 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1944 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1945 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1946 for li_from
, li_to
in make_links
:
1947 base_node_tree
.links
.new(li_from
, li_to
)
1948 tree
= base_node_tree
1949 link_end
= output_socket
1950 while tree
.nodes
.active
!= active
:
1951 node
= tree
.nodes
.active
1952 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1953 link_start
= node
.outputs
[index
]
1954 node_socket
= node
.node_tree
.outputs
[index
]
1955 if node_socket
in delete_sockets
:
1956 delete_sockets
.remove(node_socket
)
1957 tree
.links
.new(link_start
, link_end
)
1959 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1960 tree
= tree
.nodes
.active
.node_tree
1961 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1964 for socket
in delete_sockets
:
1965 tree
= socket
.id_data
1966 tree
.outputs
.remove(socket
)
1969 for tree
, node
in delete_nodes
:
1970 tree
.nodes
.remove(node
)
1972 nodes
.active
= active
1973 active
.select
= True
1974 force_update(context
)
1978 # What follows is code for the shader editor
1979 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1982 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
):
1983 for out
in active
.outputs
:
1984 if is_visible_socket(out
):
1988 # get material_output node
1989 materialout
= None # placeholder node
1992 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1993 self
.scan_nodes(base_node_tree
, delete_sockets
)
1995 materialout
= self
.get_shader_output_node(base_node_tree
)
1997 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1998 materialout
.location
= get_output_location(base_node_tree
)
1999 materialout
.select
= False
2000 # Analyze outputs, add "Emission Viewer" if needed, make links
2003 for i
, out
in enumerate(active
.outputs
):
2004 if is_visible_socket(out
):
2005 valid_outputs
.append(i
)
2007 out_i
= valid_outputs
[0] # Start index of node's outputs
2008 for i
, valid_i
in enumerate(valid_outputs
):
2009 for out_link
in active
.outputs
[valid_i
].links
:
2010 if is_viewer_link(out_link
, materialout
):
2011 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
2012 if i
< len(valid_outputs
) - 1:
2013 out_i
= valid_outputs
[i
+ 1]
2015 out_i
= valid_outputs
[0]
2017 make_links
= [] # store sockets for new links
2018 delete_nodes
= [] # store unused nodes to delete in the end
2020 # If output type not 'SHADER' - "Emission Viewer" needed
2021 if active
.outputs
[out_i
].type != 'SHADER':
2022 socket_type
= 'NodeSocketColor'
2023 # get Emission Viewer node
2024 emission_exists
= False
2025 emission_placeholder
= base_node_tree
.nodes
[0]
2026 for node
in base_node_tree
.nodes
:
2027 if "Emission Viewer" in node
.name
:
2028 emission_exists
= True
2029 emission_placeholder
= node
2030 if not emission_exists
:
2031 emission
= base_node_tree
.nodes
.new(self
.shader_viewer_ident
)
2032 emission
.hide
= True
2033 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
2034 emission
.label
= "Viewer"
2035 emission
.name
= "Emission Viewer"
2036 emission
.use_custom_color
= True
2037 emission
.color
= (0.6, 0.5, 0.4)
2038 emission
.select
= False
2040 emission
= emission_placeholder
2041 output_socket
= emission
.inputs
[0]
2043 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2044 if emission
.outputs
[0].links
.__len
__() > 0:
2045 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
2046 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2048 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2050 # Set brightness of viewer to compensate for Film and CM exposure
2051 if context
.scene
.render
.engine
== 'CYCLES' and hasattr(context
.scene
, 'cycles'):
2052 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
2056 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
2057 emission
.inputs
[1].default_value
= intensity
2060 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2061 socket_type
= 'NodeSocketShader'
2062 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
2063 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
2064 output_socket
= materialout
.inputs
[materialout_index
]
2065 for node
in base_node_tree
.nodes
:
2066 if node
.name
== 'Emission Viewer':
2067 delete_nodes
.append((base_node_tree
, node
))
2068 for li_from
, li_to
in make_links
:
2069 base_node_tree
.links
.new(li_from
, li_to
)
2071 # Crate links through node groups until we reach the active node
2072 tree
= base_node_tree
2073 link_end
= output_socket
2074 while tree
.nodes
.active
!= active
:
2075 node
= tree
.nodes
.active
2076 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
2077 link_start
= node
.outputs
[index
]
2078 node_socket
= node
.node_tree
.outputs
[index
]
2079 if node_socket
in delete_sockets
:
2080 delete_sockets
.remove(node_socket
)
2081 tree
.links
.new(link_start
, link_end
)
2083 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
2084 tree
= tree
.nodes
.active
.node_tree
2085 tree
.links
.new(active
.outputs
[out_i
], link_end
)
2088 for socket
in delete_sockets
:
2089 if not self
.is_socket_used_other_mats(socket
):
2090 tree
= socket
.id_data
2091 tree
.outputs
.remove(socket
)
2094 for tree
, node
in delete_nodes
:
2095 tree
.nodes
.remove(node
)
2097 nodes
.active
= active
2098 active
.select
= True
2100 force_update(context
)
2104 return {'CANCELLED'}
2107 class NWFrameSelected(Operator
, NWBase
):
2108 bl_idname
= "node.nw_frame_selected"
2109 bl_label
= "Frame Selected"
2110 bl_description
= "Add a frame node and parent the selected nodes to it"
2111 bl_options
= {'REGISTER', 'UNDO'}
2113 label_prop
: StringProperty(
2115 description
='The visual name of the frame node',
2118 color_prop
: FloatVectorProperty(
2120 description
="The color of the frame node",
2121 default
=(0.6, 0.6, 0.6),
2122 min=0, max=1, step
=1, precision
=3,
2123 subtype
='COLOR_GAMMA', size
=3
2126 def execute(self
, context
):
2127 nodes
, links
= get_nodes_links(context
)
2130 if node
.select
== True:
2131 selected
.append(node
)
2133 bpy
.ops
.node
.add_node(type='NodeFrame')
2135 frm
.label
= self
.label_prop
2136 frm
.use_custom_color
= True
2137 frm
.color
= self
.color_prop
2139 for node
in selected
:
2145 class NWReloadImages(Operator
):
2146 bl_idname
= "node.nw_reload_images"
2147 bl_label
= "Reload Images"
2148 bl_description
= "Update all the image nodes to match their files on disk"
2151 def poll(cls
, context
):
2153 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
2154 if context
.active_node
is not None:
2155 for out
in context
.active_node
.outputs
:
2156 if is_visible_socket(out
):
2161 def execute(self
, context
):
2162 nodes
, links
= get_nodes_links(context
)
2163 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2166 if node
.type in image_types
:
2167 if node
.type == "TEXTURE":
2168 if node
.texture
: # node has texture assigned
2169 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2170 if node
.texture
.image
: # texture has image assigned
2171 node
.texture
.image
.reload()
2179 self
.report({'INFO'}, "Reloaded images")
2180 print("Reloaded " + str(num_reloaded
) + " images")
2181 force_update(context
)
2184 self
.report({'WARNING'}, "No images found to reload in this node tree")
2185 return {'CANCELLED'}
2188 class NWSwitchNodeType(Operator
, NWBase
):
2189 """Switch type of selected nodes """
2190 bl_idname
= "node.nw_swtch_node_type"
2191 bl_label
= "Switch Node Type"
2192 bl_options
= {'REGISTER', 'UNDO'}
2194 to_type
: EnumProperty(
2195 name
="Switch to type",
2196 items
=list(shaders_input_nodes_props
) +
2197 list(shaders_output_nodes_props
) +
2198 list(shaders_shader_nodes_props
) +
2199 list(shaders_texture_nodes_props
) +
2200 list(shaders_color_nodes_props
) +
2201 list(shaders_vector_nodes_props
) +
2202 list(shaders_converter_nodes_props
) +
2203 list(shaders_layout_nodes_props
) +
2204 list(compo_input_nodes_props
) +
2205 list(compo_output_nodes_props
) +
2206 list(compo_color_nodes_props
) +
2207 list(compo_converter_nodes_props
) +
2208 list(compo_filter_nodes_props
) +
2209 list(compo_vector_nodes_props
) +
2210 list(compo_matte_nodes_props
) +
2211 list(compo_distort_nodes_props
) +
2212 list(compo_layout_nodes_props
) +
2213 list(blender_mat_input_nodes_props
) +
2214 list(blender_mat_output_nodes_props
) +
2215 list(blender_mat_color_nodes_props
) +
2216 list(blender_mat_vector_nodes_props
) +
2217 list(blender_mat_converter_nodes_props
) +
2218 list(blender_mat_layout_nodes_props
) +
2219 list(texture_input_nodes_props
) +
2220 list(texture_output_nodes_props
) +
2221 list(texture_color_nodes_props
) +
2222 list(texture_pattern_nodes_props
) +
2223 list(texture_textures_nodes_props
) +
2224 list(texture_converter_nodes_props
) +
2225 list(texture_distort_nodes_props
) +
2226 list(texture_layout_nodes_props
)
2229 geo_to_type
: StringProperty(
2230 name
="Switch to type",
2234 def execute(self
, context
):
2235 nodes
, links
= get_nodes_links(context
)
2236 to_type
= self
.to_type
2237 if self
.geo_to_type
!= '':
2238 to_type
= self
.geo_to_type
2239 # Those types of nodes will not swap.
2240 src_excludes
= ('NodeFrame')
2241 # Those attributes of nodes will be copied if possible
2242 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
2243 'show_options', 'show_preview', 'show_texture',
2244 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2246 selected
= [n
for n
in nodes
if n
.select
]
2248 for node
in [n
for n
in selected
if
2249 n
.rna_type
.identifier
not in src_excludes
and
2250 n
.rna_type
.identifier
!= to_type
]:
2251 new_node
= nodes
.new(to_type
)
2252 for attr
in attrs_to_pass
:
2253 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
2254 setattr(new_node
, attr
, getattr(node
, attr
))
2255 # set image datablock of dst to image of src
2256 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
2258 new_node
.image
= node
.image
2260 if new_node
.type == 'SWITCH':
2261 new_node
.hide
= True
2262 # Dictionaries: src_sockets and dst_sockets:
2263 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2264 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2265 # in 'INPUTS' and 'OUTPUTS':
2266 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2268 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2270 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2271 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2274 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2275 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2277 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2278 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2279 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2280 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
2281 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2282 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
2283 # enumerate in inputs, then in outputs
2284 # find name, default value and links of socket
2285 for i
, socket
in enumerate(in_out
):
2286 the_name
= socket
.name
2288 # Not every socket, especially in outputs has "default_value"
2289 if hasattr(socket
, 'default_value'):
2290 dval
= socket
.default_value
2292 for lnk
in socket
.links
:
2293 socket_links
.append(lnk
)
2294 # check type of socket to fill proper keys.
2295 for the_type
in types_order_one
:
2296 if socket
.type == the_type
:
2297 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2298 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2299 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
2300 # Check which of the types in inputs/outputs is considered to be "main".
2301 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2302 for type_check
in types_order_one
:
2303 if sockets
[in_out_name
][type_check
]:
2304 sockets
[in_out_name
]['MAIN'] = type_check
2308 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2309 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2312 for inout
, soctype
in (
2313 ('INPUTS', 'MAIN',),
2314 ('INPUTS', 'SHADER',),
2315 ('INPUTS', 'RGBA',),
2316 ('INPUTS', 'VECTOR',),
2317 ('INPUTS', 'VALUE',),
2318 ('OUTPUTS', 'MAIN',),
2319 ('OUTPUTS', 'SHADER',),
2320 ('OUTPUTS', 'RGBA',),
2321 ('OUTPUTS', 'VECTOR',),
2322 ('OUTPUTS', 'VALUE',),
2324 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
2325 if soctype
== 'MAIN':
2326 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
2327 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
2329 sc
= src_sockets
[inout
][soctype
]
2330 dt
= dst_sockets
[inout
][soctype
]
2331 # start with 'dt' to determine number of possibilities.
2332 for i
, soc
in enumerate(dt
):
2333 # if src main has enough entries - match them with dst main sockets by indexes.
2335 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
2336 # add 'VALUE_NAME' criterion to inputs.
2337 if inout
== 'INPUTS' and soctype
== 'VALUE':
2339 if s
[2] == soc
[2]: # if names match
2340 # append src (index, dval), dst (index, dval)
2341 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
2343 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2344 # This creates better links when relinking textures.
2345 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
2346 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
2348 # Pass default values and RELINK:
2349 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2350 # INPUTS: Base on matches in proper order.
2351 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
2353 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
2354 new_node
.inputs
[dst_i
].default_value
= src_dval
2355 # Special case: switch to math
2356 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2357 new_node
.type == 'MATH' and\
2359 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
2360 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
2361 if node
.type == 'MIX_RGB':
2362 if node
.blend_type
in [o
[0] for o
in operations
]:
2363 new_node
.operation
= node
.blend_type
2364 # Special case: switch from math to some types
2365 if node
.type == 'MATH' and\
2366 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2369 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
2370 if new_node
.type == 'MIX_RGB':
2371 if node
.operation
in [t
[0] for t
in blend_types
]:
2372 new_node
.blend_type
= node
.operation
2373 # Set Fac of MIX_RGB to 1.0
2374 new_node
.inputs
[0].default_value
= 1.0
2375 # make link only when dst matching input is not linked already.
2376 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
2377 in_src_link
= node
.inputs
[src_i
].links
[0]
2378 in_dst_socket
= new_node
.inputs
[dst_i
]
2379 links
.new(in_src_link
.from_socket
, in_dst_socket
)
2380 links
.remove(in_src_link
)
2381 # OUTPUTS: Base on matches in proper order.
2382 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
2383 for out_src_link
in node
.outputs
[src_i
].links
:
2384 out_dst_socket
= new_node
.outputs
[dst_i
]
2385 links
.new(out_dst_socket
, out_src_link
.to_socket
)
2386 # relink rest inputs if possible, no criteria
2387 for src_inp
in node
.inputs
:
2388 for dst_inp
in new_node
.inputs
:
2389 if src_inp
.links
and not dst_inp
.links
:
2390 src_link
= src_inp
.links
[0]
2391 links
.new(src_link
.from_socket
, dst_inp
)
2392 links
.remove(src_link
)
2393 # relink rest outputs if possible, base on node kind if any left.
2394 for src_o
in node
.outputs
:
2395 for out_src_link
in src_o
.links
:
2396 for dst_o
in new_node
.outputs
:
2397 if src_o
.type == dst_o
.type:
2398 links
.new(dst_o
, out_src_link
.to_socket
)
2399 # relink rest outputs no criteria if any left. Link all from first output.
2400 for src_o
in node
.outputs
:
2401 for out_src_link
in src_o
.links
:
2402 if new_node
.outputs
:
2403 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2405 force_update(context
)
2409 class NWMergeNodes(Operator
, NWBase
):
2410 bl_idname
= "node.nw_merge_nodes"
2411 bl_label
= "Merge Nodes"
2412 bl_description
= "Merge Selected Nodes"
2413 bl_options
= {'REGISTER', 'UNDO'}
2417 description
="All possible blend types, boolean operations and math operations",
2418 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
],
2420 merge_type
: EnumProperty(
2422 description
="Type of Merge to be used",
2424 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2425 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2426 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2427 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2428 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2429 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2430 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2434 # Check if the link connects to a node that is in selected_nodes
2435 # If not, then check recursively for each link in the nodes outputs.
2436 # If yes, return True. If the recursion stops without finding a node
2437 # in selected_nodes, it returns False. The depth is used to prevent
2438 # getting stuck in a loop because of an already present cycle.
2440 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
2442 # We're stuck in a cycle, but that cycle was already present,
2443 # so we return False.
2444 # NOTE: The number 255 is arbitrary, but seems to work well.
2447 if node
in selected_nodes
:
2449 if not node
.outputs
:
2451 for output
in node
.outputs
:
2452 if output
.is_linked
:
2453 for olink
in output
.links
:
2454 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
2456 # None of the outputs found a node in selected_nodes, so there is no cycle.
2459 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2460 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2461 # be connected. The last one is assumed to be a multi input socket.
2462 # For convenience the node is returned.
2464 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
2465 # The y-location of the last node
2466 loc_y
= nodes_list
[-1][2]
2467 if merge_position
== 'CENTER':
2468 # Average the y-location
2469 for i
in range(len(nodes_list
)-1):
2470 loc_y
+= nodes_list
[i
][2]
2471 loc_y
= loc_y
/len(nodes_list
)
2472 new_node
= nodes
.new(node_name
)
2473 new_node
.hide
= do_hide
2474 new_node
.location
.x
= loc_x
2475 new_node
.location
.y
= loc_y
2476 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
2478 outputs_for_multi_input
= []
2479 for i
,node
in enumerate(selected_nodes
):
2481 # Search for the first node which had output links that do not create
2482 # a cycle, which we can then reconnect afterwards.
2483 if prev_links
== [] and node
.outputs
[0].is_linked
:
2484 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
2485 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2486 # To get the placement to look right we need to reverse the order in which we connect the
2487 # outputs to the multi input socket.
2488 if i
< len(socket_indices
) - 1:
2489 ind
= socket_indices
[i
]
2490 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
2492 outputs_for_multi_input
.insert(0, node
.outputs
[0])
2493 if outputs_for_multi_input
!= []:
2494 ind
= socket_indices
[-1]
2495 for output
in outputs_for_multi_input
:
2496 links
.new(output
, new_node
.inputs
[ind
])
2497 if prev_links
!= []:
2498 for link
in prev_links
:
2499 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
2502 def execute(self
, context
):
2503 settings
= context
.preferences
.addons
[__name__
].preferences
2504 merge_hide
= settings
.merge_hide
2505 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2508 do_hide_shader
= False
2509 if merge_hide
== 'ALWAYS':
2511 do_hide_shader
= True
2512 elif merge_hide
== 'NON_SHADER':
2515 tree_type
= context
.space_data
.node_tree
.type
2516 if tree_type
== 'GEOMETRY':
2517 node_type
= 'GeometryNode'
2518 if tree_type
== 'COMPOSITING':
2519 node_type
= 'CompositorNode'
2520 elif tree_type
== 'SHADER':
2521 node_type
= 'ShaderNode'
2522 elif tree_type
== 'TEXTURE':
2523 node_type
= 'TextureNode'
2524 nodes
, links
= get_nodes_links(context
)
2526 merge_type
= self
.merge_type
2527 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2528 # 'ZCOMBINE' works only if mode == 'MIX'
2529 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2530 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2533 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2535 # The math nodes used for geometry nodes are of type 'ShaderNode'
2536 if merge_type
== 'MATH' and tree_type
== 'GEOMETRY':
2537 node_type
= 'ShaderNode'
2538 selected_mix
= [] # entry = [index, loc]
2539 selected_shader
= [] # entry = [index, loc]
2540 selected_geometry
= [] # entry = [index, loc]
2541 selected_math
= [] # entry = [index, loc]
2542 selected_vector
= [] # entry = [index, loc]
2543 selected_z
= [] # entry = [index, loc]
2544 selected_alphaover
= [] # entry = [index, loc]
2546 for i
, node
in enumerate(nodes
):
2547 if node
.select
and node
.outputs
:
2548 if merge_type
== 'AUTO':
2549 for (type, types_list
, dst
) in (
2550 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2551 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2552 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2553 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2554 ('VECTOR', [], selected_vector
),
2556 output_type
= node
.outputs
[0].type
2557 valid_mode
= mode
in types_list
2558 # When mode is 'MIX' we have to cheat since the mix node is not used in
2560 if tree_type
== 'GEOMETRY':
2562 if output_type
== 'VALUE' and type == 'VALUE':
2564 elif output_type
== 'VECTOR' and type == 'VECTOR':
2566 elif type == 'GEOMETRY':
2568 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2569 # Cheat that output type is 'RGBA',
2570 # and that 'MIX' exists in math operations list.
2571 # This way when selected_mix list is analyzed:
2572 # Node data will be appended even though it doesn't meet requirements.
2573 elif output_type
!= 'SHADER' and mode
== 'MIX':
2574 output_type
= 'RGBA'
2576 if output_type
== type and valid_mode
:
2577 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2579 for (type, types_list
, dst
) in (
2580 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2581 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2582 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2583 ('MATH', [t
[0] for t
in operations
], selected_math
),
2584 ('ZCOMBINE', ('MIX', ), selected_z
),
2585 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2587 if merge_type
== type and mode
in types_list
:
2588 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2589 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2590 # use only 'Mix' nodes for merging.
2591 # For that we add selected_math list to selected_mix list and clear selected_math.
2592 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2593 selected_mix
+= selected_math
2595 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2598 count_before
= len(nodes
)
2599 # sort list by loc_x - reversed
2600 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2602 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2603 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2605 # Change the node type for math nodes in a geometry node tree.
2606 if tree_type
== 'GEOMETRY':
2607 if nodes_list
is selected_math
or nodes_list
is selected_vector
:
2608 node_type
= 'ShaderNode'
2612 node_type
= 'GeometryNode'
2613 if merge_position
== 'CENTER':
2614 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)
2615 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2621 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2625 if nodes_list
== selected_shader
and not do_hide_shader
:
2627 the_range
= len(nodes_list
) - 1
2628 if len(nodes_list
) == 1:
2631 for i
in range(the_range
):
2632 if nodes_list
== selected_mix
:
2633 add_type
= node_type
+ 'MixRGB'
2634 add
= nodes
.new(add_type
)
2635 add
.blend_type
= mode
2637 add
.inputs
[0].default_value
= 1.0
2638 add
.show_preview
= False
2644 add
.width_hidden
= 100.0
2645 elif nodes_list
== selected_math
:
2646 add_type
= node_type
+ 'Math'
2647 add
= nodes
.new(add_type
)
2648 add
.operation
= mode
2654 add
.width_hidden
= 100.0
2655 elif nodes_list
== selected_shader
:
2657 add_type
= node_type
+ 'MixShader'
2658 add
= nodes
.new(add_type
)
2659 add
.hide
= do_hide_shader
2664 add
.width_hidden
= 100.0
2666 add_type
= node_type
+ 'AddShader'
2667 add
= nodes
.new(add_type
)
2668 add
.hide
= do_hide_shader
2673 add
.width_hidden
= 100.0
2674 elif nodes_list
== selected_geometry
:
2675 if mode
in ('JOIN', 'MIX'):
2676 add_type
= node_type
+ 'JoinGeometry'
2677 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2679 add_type
= node_type
+ 'Boolean'
2680 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2681 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2682 add
.operation
= mode
2685 elif nodes_list
== selected_vector
:
2686 add_type
= node_type
+ 'VectorMath'
2687 add
= nodes
.new(add_type
)
2688 add
.operation
= mode
2694 add
.width_hidden
= 100.0
2695 elif nodes_list
== selected_z
:
2696 add
= nodes
.new('CompositorNodeZcombine')
2697 add
.show_preview
= False
2703 add
.width_hidden
= 100.0
2704 elif nodes_list
== selected_alphaover
:
2705 add
= nodes
.new('CompositorNodeAlphaOver')
2706 add
.show_preview
= False
2712 add
.width_hidden
= 100.0
2713 add
.location
= loc_x
, loc_y
2717 # This has already been handled separately
2721 count_after
= len(nodes
)
2722 index
= count_after
- 1
2723 first_selected
= nodes
[nodes_list
[0][0]]
2724 # "last" node has been added as first, so its index is count_before.
2725 last_add
= nodes
[count_before
]
2726 # Create list of invalid indexes.
2727 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2730 # Two nodes were selected and first selected has no output links, second selected has output links.
2731 # Then add links from last add to all links 'to_socket' of out links of second selected.
2732 if len(nodes_list
) == 2:
2733 if not first_selected
.outputs
[0].links
:
2734 second_selected
= nodes
[nodes_list
[1][0]]
2735 for ss_link
in second_selected
.outputs
[0].links
:
2736 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2737 # Link only if "to_node" index not in invalid indexes list.
2738 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2739 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2740 # add links from last_add to all links 'to_socket' of out links of first selected.
2741 for fs_link
in first_selected
.outputs
[0].links
:
2742 # Link only if "to_node" index not in invalid indexes list.
2743 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2744 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2745 # add link from "first" selected and "first" add node
2746 node_to
= nodes
[count_after
- 1]
2747 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2748 if node_to
.type == 'ZCOMBINE':
2749 for fs_out
in first_selected
.outputs
:
2750 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2751 links
.new(fs_out
, node_to
.inputs
[1])
2753 # add links between added ADD nodes and between selected and ADD nodes
2754 for i
in range(count_adds
):
2755 if i
< count_adds
- 1:
2756 node_from
= nodes
[index
]
2757 node_to
= nodes
[index
- 1]
2758 node_to_input_i
= first
2759 node_to_z_i
= 1 # if z combine - link z to first z input
2760 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2761 if node_to
.type == 'ZCOMBINE':
2762 for from_out
in node_from
.outputs
:
2763 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2764 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2765 if len(nodes_list
) > 1:
2766 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2767 node_to
= nodes
[index
]
2768 node_to_input_i
= second
2769 node_to_z_i
= 3 # if z combine - link z to second z input
2770 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2771 if node_to
.type == 'ZCOMBINE':
2772 for from_out
in node_from
.outputs
:
2773 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2774 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2776 # set "last" of added nodes as active
2777 nodes
.active
= last_add
2778 for i
, x
, y
, dx
, h
in nodes_list
:
2779 nodes
[i
].select
= False
2784 class NWBatchChangeNodes(Operator
, NWBase
):
2785 bl_idname
= "node.nw_batch_change"
2786 bl_label
= "Batch Change"
2787 bl_description
= "Batch Change Blend Type and Math Operation"
2788 bl_options
= {'REGISTER', 'UNDO'}
2790 blend_type
: EnumProperty(
2792 items
=blend_types
+ navs
,
2794 operation
: EnumProperty(
2796 items
=operations
+ navs
,
2799 def execute(self
, context
):
2800 blend_type
= self
.blend_type
2801 operation
= self
.operation
2802 for node
in context
.selected_nodes
:
2803 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2804 if not blend_type
in [nav
[0] for nav
in navs
]:
2805 node
.blend_type
= blend_type
2807 if blend_type
== 'NEXT':
2808 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2809 #index = blend_types.index(node.blend_type)
2810 if index
== len(blend_types
) - 1:
2811 node
.blend_type
= blend_types
[0][0]
2813 node
.blend_type
= blend_types
[index
+ 1][0]
2815 if blend_type
== 'PREV':
2816 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2818 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2820 node
.blend_type
= blend_types
[index
- 1][0]
2822 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2823 if not operation
in [nav
[0] for nav
in navs
]:
2824 node
.operation
= operation
2826 if operation
== 'NEXT':
2827 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2828 #index = operations.index(node.operation)
2829 if index
== len(operations
) - 1:
2830 node
.operation
= operations
[0][0]
2832 node
.operation
= operations
[index
+ 1][0]
2834 if operation
== 'PREV':
2835 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2836 #index = operations.index(node.operation)
2838 node
.operation
= operations
[len(operations
) - 1][0]
2840 node
.operation
= operations
[index
- 1][0]
2845 class NWChangeMixFactor(Operator
, NWBase
):
2846 bl_idname
= "node.nw_factor"
2847 bl_label
= "Change Factor"
2848 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2849 bl_options
= {'REGISTER', 'UNDO'}
2851 # option: Change factor.
2852 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2853 # Else - change factor by option value.
2854 option
: FloatProperty()
2856 def execute(self
, context
):
2857 nodes
, links
= get_nodes_links(context
)
2858 option
= self
.option
2859 selected
= [] # entry = index
2860 for si
, node
in enumerate(nodes
):
2862 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2866 fac
= nodes
[si
].inputs
[0]
2867 nodes
[si
].hide
= False
2868 if option
in {0.0, 1.0}:
2869 fac
.default_value
= option
2871 fac
.default_value
+= option
2876 class NWCopySettings(Operator
, NWBase
):
2877 bl_idname
= "node.nw_copy_settings"
2878 bl_label
= "Copy Settings"
2879 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2880 bl_options
= {'REGISTER', 'UNDO'}
2883 def poll(cls
, context
):
2885 if nw_check(context
):
2887 context
.active_node
is not None and
2888 context
.active_node
.type != 'FRAME'
2893 def execute(self
, context
):
2894 node_active
= context
.active_node
2895 node_selected
= context
.selected_nodes
2898 if not (len(node_selected
) > 1):
2899 self
.report({'ERROR'}, "2 nodes must be selected at least")
2900 return {'CANCELLED'}
2902 # Check if active node is in the selection
2903 selected_node_names
= [n
.name
for n
in node_selected
]
2904 if node_active
.name
not in selected_node_names
:
2905 self
.report({'ERROR'}, "No active node")
2906 return {'CANCELLED'}
2908 # Get nodes in selection by type
2909 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2911 if not (len(valid_nodes
) > 1) and node_active
:
2912 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2913 return {'CANCELLED'}
2915 if len(valid_nodes
) != len(node_selected
):
2916 # Report nodes that are not valid
2917 valid_node_names
= [n
.name
for n
in valid_nodes
]
2918 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2919 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2921 # Reference original
2923 #node_selected_names = [n.name for n in node_selected]
2928 # Deselect all nodes
2929 for i
in node_selected
:
2932 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2933 # Run through all other nodes
2934 for node
in valid_nodes
[1:]:
2936 # Check for frame node
2937 parent
= node
.parent
if node
.parent
else None
2938 node_loc
= [node
.location
.x
, node
.location
.y
]
2940 # Select original to duplicate
2943 # Duplicate selected node
2944 bpy
.ops
.node
.duplicate()
2945 new_node
= context
.selected_nodes
[0]
2948 new_node
.select
= False
2950 # Properties to copy
2951 node_tree
= node
.id_data
2952 props_to_copy
= 'bl_idname name location height width'.split(' ')
2956 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2957 for i
in (i
for i
in mappings
if i
.is_linked
):
2959 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2962 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2963 props_to_copy
.pop(0)
2965 for prop
in props_to_copy
:
2966 setattr(new_node
, prop
, props
[prop
])
2968 # Get the node tree to remove the old node
2969 nodes
= node_tree
.nodes
2971 new_node
.name
= props
['name']
2974 new_node
.parent
= parent
2975 new_node
.location
= node_loc
2977 for str_from
, str_to
in reconnections
:
2978 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2980 success_names
.append(new_node
.name
)
2983 node_tree
.nodes
.active
= orig
2984 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2988 class NWCopyLabel(Operator
, NWBase
):
2989 bl_idname
= "node.nw_copy_label"
2990 bl_label
= "Copy Label"
2991 bl_options
= {'REGISTER', 'UNDO'}
2993 option
: EnumProperty(
2995 description
="Source of name of label",
2997 ('FROM_ACTIVE', 'from active', 'from active node',),
2998 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2999 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
3003 def execute(self
, context
):
3004 nodes
, links
= get_nodes_links(context
)
3005 option
= self
.option
3006 active
= nodes
.active
3007 if option
== 'FROM_ACTIVE':
3009 src_label
= active
.label
3010 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
3011 node
.label
= src_label
3012 elif option
== 'FROM_NODE':
3013 selected
= [n
for n
in nodes
if n
.select
]
3014 for node
in selected
:
3015 for input in node
.inputs
:
3017 src
= input.links
[0].from_node
3018 node
.label
= src
.label
3020 elif option
== 'FROM_SOCKET':
3021 selected
= [n
for n
in nodes
if n
.select
]
3022 for node
in selected
:
3023 for input in node
.inputs
:
3025 src
= input.links
[0].from_socket
3026 node
.label
= src
.name
3032 class NWClearLabel(Operator
, NWBase
):
3033 bl_idname
= "node.nw_clear_label"
3034 bl_label
= "Clear Label"
3035 bl_options
= {'REGISTER', 'UNDO'}
3037 option
: BoolProperty()
3039 def execute(self
, context
):
3040 nodes
, links
= get_nodes_links(context
)
3041 for node
in [n
for n
in nodes
if n
.select
]:
3046 def invoke(self
, context
, event
):
3048 return self
.execute(context
)
3050 return context
.window_manager
.invoke_confirm(self
, event
)
3053 class NWModifyLabels(Operator
, NWBase
):
3054 """Modify Labels of all selected nodes"""
3055 bl_idname
= "node.nw_modify_labels"
3056 bl_label
= "Modify Labels"
3057 bl_options
= {'REGISTER', 'UNDO'}
3059 prepend
: StringProperty(
3060 name
="Add to Beginning"
3062 append
: StringProperty(
3065 replace_from
: StringProperty(
3066 name
="Text to Replace"
3068 replace_to
: StringProperty(
3072 def execute(self
, context
):
3073 nodes
, links
= get_nodes_links(context
)
3074 for node
in [n
for n
in nodes
if n
.select
]:
3075 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
3079 def invoke(self
, context
, event
):
3083 return context
.window_manager
.invoke_props_dialog(self
)
3086 class NWAddTextureSetup(Operator
, NWBase
):
3087 bl_idname
= "node.nw_add_texture"
3088 bl_label
= "Texture Setup"
3089 bl_description
= "Add Texture Node Setup to Selected Shaders"
3090 bl_options
= {'REGISTER', 'UNDO'}
3092 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
3095 def poll(cls
, context
):
3097 if nw_check(context
):
3098 space
= context
.space_data
3099 if space
.tree_type
== 'ShaderNodeTree':
3103 def execute(self
, context
):
3104 nodes
, links
= get_nodes_links(context
)
3105 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3106 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
3107 selected_nodes
= [n
for n
in nodes
if n
.select
]
3108 for t_node
in selected_nodes
:
3112 for index
, i
in enumerate(t_node
.inputs
):
3118 locx
= t_node
.location
.x
3119 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
3121 xoffset
= [500, 700]
3123 if t_node
.type in texture_types
+ ['MAPPING']:
3124 xoffset
= [290, 500]
3128 image_type
= 'ShaderNodeTexImage'
3130 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
3131 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
3132 if t_node
.type == 'BACKGROUND':
3133 image_type
= 'ShaderNodeTexEnvironment'
3136 tex
= nodes
.new(image_type
)
3137 tex
.location
= [locx
- 200, locy
+ 112]
3139 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
3141 t_node
.select
= False
3142 if self
.add_mapping
or is_texture
:
3143 if t_node
.type != 'MAPPING':
3144 m
= nodes
.new('ShaderNodeMapping')
3145 m
.location
= [locx
- xoffset
[0], locy
+ 141]
3149 coord
= nodes
.new('ShaderNodeTexCoord')
3150 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
3153 links
.new(m
.outputs
[0], tex
.inputs
[0])
3154 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3157 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
3158 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3160 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
3164 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
3165 bl_idname
= "node.nw_add_textures_for_principled"
3166 bl_label
= "Principled Texture Setup"
3167 bl_description
= "Add Texture Node Setup for Principled BSDF"
3168 bl_options
= {'REGISTER', 'UNDO'}
3170 directory
: StringProperty(
3174 description
='Folder to search in for image files'
3176 files
: CollectionProperty(
3177 type=bpy
.types
.OperatorFileListElement
,
3178 options
={'HIDDEN', 'SKIP_SAVE'}
3181 relative_path
: BoolProperty(
3182 name
='Relative Path',
3183 description
='Select the file relative to the blend file',
3192 def draw(self
, context
):
3193 layout
= self
.layout
3194 layout
.alignment
= 'LEFT'
3196 layout
.prop(self
, 'relative_path')
3199 def poll(cls
, context
):
3201 if nw_check(context
):
3202 space
= context
.space_data
3203 if space
.tree_type
== 'ShaderNodeTree':
3207 def execute(self
, context
):
3208 # Check if everything is ok
3209 if not self
.directory
:
3210 self
.report({'INFO'}, 'No Folder Selected')
3211 return {'CANCELLED'}
3212 if not self
.files
[:]:
3213 self
.report({'INFO'}, 'No Files Selected')
3214 return {'CANCELLED'}
3216 nodes
, links
= get_nodes_links(context
)
3217 active_node
= nodes
.active
3218 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
3219 self
.report({'INFO'}, 'Select Principled BSDF')
3220 return {'CANCELLED'}
3223 def split_into__components(fname
):
3224 # Split filename into components
3225 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3227 fname
= path
.splitext(fname
)[0]
3229 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
3230 # Separate CamelCase by space
3231 fname
= re
.sub("([a-z])([A-Z])","\g<1> \g<2>",fname
)
3232 # Replace common separators with SPACE
3233 seperators
= ['_', '.', '-', '__', '--', '#']
3234 for sep
in seperators
:
3235 fname
= fname
.replace(sep
, ' ')
3237 components
= fname
.split(' ')
3238 components
= [c
.lower() for c
in components
]
3241 # Filter textures names for texturetypes in filenames
3242 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3243 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
3244 normal_abbr
= tags
.normal
.split(' ')
3245 bump_abbr
= tags
.bump
.split(' ')
3246 gloss_abbr
= tags
.gloss
.split(' ')
3247 rough_abbr
= tags
.rough
.split(' ')
3249 ['Displacement', tags
.displacement
.split(' '), None],
3250 ['Base Color', tags
.base_color
.split(' '), None],
3251 ['Subsurface Color', tags
.sss_color
.split(' '), None],
3252 ['Metallic', tags
.metallic
.split(' '), None],
3253 ['Specular', tags
.specular
.split(' '), None],
3254 ['Roughness', rough_abbr
+ gloss_abbr
, None],
3255 ['Normal', normal_abbr
+ bump_abbr
, None],
3258 # Look through texture_types and set value as filename of first matched file
3259 def match_files_to_socket_names():
3260 for sname
in socketnames
:
3261 for file in self
.files
:
3263 filenamecomponents
= split_into__components(fname
)
3264 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
3265 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3270 match_files_to_socket_names()
3271 # Remove socketnames without found files
3272 socketnames
= [s
for s
in socketnames
if s
[2]
3273 and path
.exists(self
.directory
+s
[2])]
3275 self
.report({'INFO'}, 'No matching images found')
3276 print('No matching images found')
3277 return {'CANCELLED'}
3279 # Don't override path earlier as os.path is used to check the absolute path
3280 import_path
= self
.directory
3281 if self
.relative_path
:
3282 if bpy
.data
.filepath
:
3283 import_path
= bpy
.path
.relpath(self
.directory
)
3285 self
.report({'WARNING'}, 'Relative paths cannot be used with unsaved scenes!')
3286 print('Relative paths cannot be used with unsaved scenes!')
3289 print('\nMatched Textures:')
3293 roughness_node
= None
3294 for i
, sname
in enumerate(socketnames
):
3295 print(i
, sname
[0], sname
[2])
3297 # DISPLACEMENT NODES
3298 if sname
[0] == 'Displacement':
3299 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
3300 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3301 disp_texture
.image
= img
3302 disp_texture
.label
= 'Displacement'
3303 if disp_texture
.image
:
3304 disp_texture
.image
.colorspace_settings
.is_data
= True
3306 # Add displacement offset nodes
3307 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
3308 disp_node
.location
= active_node
.location
+ Vector((0, -560))
3309 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
3311 # TODO Turn on true displacement in the material
3312 # Too complicated for now
3315 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
3317 if not output_node
[0].inputs
[2].is_linked
:
3318 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
3322 if not active_node
.inputs
[sname
[0]].is_linked
:
3323 # No texture node connected -> add texture node with new image
3324 texture_node
= nodes
.new(type='ShaderNodeTexImage')
3325 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3326 texture_node
.image
= img
3329 if sname
[0] == 'Normal':
3330 # Test if new texture node is normal or bump map
3331 fname_components
= split_into__components(sname
[2])
3332 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
3333 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
3335 # If Normal add normal node in between
3336 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
3337 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
3339 # If Bump add bump node in between
3340 normal_node
= nodes
.new(type='ShaderNodeBump')
3341 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
3343 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
3344 normal_node_texture
= texture_node
3346 elif sname
[0] == 'Roughness':
3347 # Test if glossy or roughness map
3348 fname_components
= split_into__components(sname
[2])
3349 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
3350 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
3353 # If Roughness nothing to to
3354 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3357 # If Gloss Map add invert node
3358 invert_node
= nodes
.new(type='ShaderNodeInvert')
3359 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
3361 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
3362 roughness_node
= texture_node
3365 # This is a simple connection Texture --> Input slot
3366 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3368 # Use non-color for all but 'Base Color' Textures
3369 if not sname
[0] in ['Base Color'] and texture_node
.image
:
3370 texture_node
.image
.colorspace_settings
.is_data
= True
3373 # If already texture connected. add to node list for alignment
3374 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
3376 # This are all connected texture nodes
3377 texture_nodes
.append(texture_node
)
3378 texture_node
.label
= sname
[0]
3381 texture_nodes
.append(disp_texture
)
3384 for i
, texture_node
in enumerate(texture_nodes
):
3385 offset
= Vector((-550, (i
* -280) + 200))
3386 texture_node
.location
= active_node
.location
+ offset
3389 # Extra alignment if normal node was added
3390 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
3393 # Alignment of invert node if glossy map
3394 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
3396 # Add texture input + mapping
3397 mapping
= nodes
.new(type='ShaderNodeMapping')
3398 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
3399 if len(texture_nodes
) > 1:
3400 # If more than one texture add reroute node in between
3401 reroute
= nodes
.new(type='NodeReroute')
3402 texture_nodes
.append(reroute
)
3403 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
3404 reroute
.location
= tex_coords
+ Vector((-50, -120))
3405 for texture_node
in texture_nodes
:
3406 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
3407 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
3409 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
3411 # Connect texture_coordiantes to mapping node
3412 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
3413 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
3414 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
3416 # Create frame around tex coords and mapping
3417 frame
= nodes
.new(type='NodeFrame')
3418 frame
.label
= 'Mapping'
3419 mapping
.parent
= frame
3420 texture_input
.parent
= frame
3423 # Create frame around texture nodes
3424 frame
= nodes
.new(type='NodeFrame')
3425 frame
.label
= 'Textures'
3426 for tnode
in texture_nodes
:
3427 tnode
.parent
= frame
3431 active_node
.select
= False
3434 force_update(context
)
3438 class NWAddReroutes(Operator
, NWBase
):
3439 """Add Reroute Nodes and link them to outputs of selected nodes"""
3440 bl_idname
= "node.nw_add_reroutes"
3441 bl_label
= "Add Reroutes"
3442 bl_description
= "Add Reroutes to Outputs"
3443 bl_options
= {'REGISTER', 'UNDO'}
3445 option
: EnumProperty(
3448 ('ALL', 'to all', 'Add to all outputs'),
3449 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3450 ('LINKED', 'to linked', 'Add only to linked outputs'),
3454 def execute(self
, context
):
3455 tree_type
= context
.space_data
.node_tree
.type
3456 option
= self
.option
3457 nodes
, links
= get_nodes_links(context
)
3458 # output valid when option is 'all' or when 'loose' output has no links
3460 post_select
= [] # nodes to be selected after execution
3461 # create reroutes and recreate links
3462 for node
in [n
for n
in nodes
if n
.select
]:
3467 # unhide 'REROUTE' nodes to avoid issues with location.y
3468 if node
.type == 'REROUTE':
3470 # When node is hidden - width_hidden not usable.
3471 # Hack needed to calculate real width
3473 bpy
.ops
.node
.select_all(action
='DESELECT')
3474 helper
= nodes
.new('NodeReroute')
3475 helper
.select
= True
3477 # resize node and helper to zero. Then check locations to calculate width
3478 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
3479 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
3480 # restore node location
3481 node
.location
= x
, y
3484 # only helper is selected now
3485 bpy
.ops
.node
.delete()
3486 x
= node
.location
.x
+ width
+ 20.0
3487 if node
.type != 'REROUTE':
3491 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3492 for out_i
, output
in enumerate(node
.outputs
):
3493 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3494 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3495 if node
.type != 'R_LAYERS':
3497 else: # if 'R_LAYERS' check if output represent used render pass
3498 node_scene
= node
.scene
3499 node_layer
= node
.layer
3500 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3501 if output
.name
== 'Alpha':
3504 # check entries in global 'rl_outputs' variable
3505 for rlo
in rl_outputs
:
3506 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3507 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3510 valid
= ((option
== 'ALL') or
3511 (option
== 'LOOSE' and not output
.links
) or
3512 (option
== 'LINKED' and output
.links
))
3513 # Add reroutes only if valid, but offset location in all cases.
3515 n
= nodes
.new('NodeReroute')
3517 for link
in output
.links
:
3518 links
.new(n
.outputs
[0], link
.to_socket
)
3519 links
.new(output
, n
.inputs
[0])
3521 post_select
.append(n
)
3525 # disselect the node so that after execution of script only newly created nodes are selected
3527 # nicer reroutes distribution along y when node.hide
3529 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3530 for reroute
in [r
for r
in nodes
if r
.select
]:
3531 reroute
.location
.y
-= y_translate
3532 for node
in post_select
:
3538 class NWLinkActiveToSelected(Operator
, NWBase
):
3539 """Link active node to selected nodes basing on various criteria"""
3540 bl_idname
= "node.nw_link_active_to_selected"
3541 bl_label
= "Link Active Node to Selected"
3542 bl_options
= {'REGISTER', 'UNDO'}
3544 replace
: BoolProperty()
3545 use_node_name
: BoolProperty()
3546 use_outputs_names
: BoolProperty()
3549 def poll(cls
, context
):
3551 if nw_check(context
):
3552 if context
.active_node
is not None:
3553 if context
.active_node
.select
:
3557 def execute(self
, context
):
3558 nodes
, links
= get_nodes_links(context
)
3559 replace
= self
.replace
3560 use_node_name
= self
.use_node_name
3561 use_outputs_names
= self
.use_outputs_names
3562 active
= nodes
.active
3563 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3564 outputs
= [] # Only usable outputs of active nodes will be stored here.
3565 for out
in active
.outputs
:
3566 if active
.type != 'R_LAYERS':
3569 # 'R_LAYERS' node type needs special handling.
3570 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3571 # Only outputs that represent used passes should be taken into account
3572 # Check if pass represented by output is used.
3573 # global 'rl_outputs' list will be used for that
3574 for rlo
in rl_outputs
:
3575 pass_used
= False # initial value. Will be set to True if pass is used
3576 if out
.name
== 'Alpha':
3577 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3579 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3580 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3581 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3585 doit
= True # Will be changed to False when links successfully added to previous output.
3588 for node
in selected
:
3589 dst_name
= node
.name
# Will be compared with src_name if needed.
3590 # When node has label - use it as dst_name
3592 dst_name
= node
.label
3593 valid
= True # Initial value. Will be changed to False if names don't match.
3594 src_name
= dst_name
# If names not used - this asignment will keep valid = True.
3596 # Set src_name to source node name or label
3597 src_name
= active
.name
3599 src_name
= active
.label
3600 elif use_outputs_names
:
3601 src_name
= (out
.name
, )
3602 for rlo
in rl_outputs
:
3603 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3604 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3605 if dst_name
not in src_name
:
3608 for input in node
.inputs
:
3609 if input.type == out
.type or node
.type == 'REROUTE':
3610 if replace
or not input.is_linked
:
3611 links
.new(out
, input)
3612 if not use_node_name
and not use_outputs_names
:
3619 class NWAlignNodes(Operator
, NWBase
):
3620 '''Align the selected nodes neatly in a row/column'''
3621 bl_idname
= "node.nw_align_nodes"
3622 bl_label
= "Align Nodes"
3623 bl_options
= {'REGISTER', 'UNDO'}
3624 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3626 def execute(self
, context
):
3627 nodes
, links
= get_nodes_links(context
)
3628 margin
= self
.margin
3632 if node
.select
and node
.type != 'FRAME':
3633 selection
.append(node
)
3635 # If no nodes are selected, align all nodes
3639 elif nodes
.active
in selection
:
3640 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3642 # Check if nodes should be laid out horizontally or vertically
3643 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3644 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3645 x_range
= max(x_locs
) - min(x_locs
)
3646 y_range
= max(y_locs
) - min(y_locs
)
3647 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3648 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3649 horizontal
= x_range
> y_range
3651 # Sort selection by location of node mid-point
3653 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3655 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3659 for node
in selection
:
3660 current_margin
= margin
3661 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3664 node
.location
.x
= current_pos
3665 current_pos
+= current_margin
+ node
.dimensions
.x
3666 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3668 node
.location
.y
= current_pos
3669 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3670 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3672 # If active node is selected, center nodes around it
3673 if active_loc
is not None:
3674 active_loc_diff
= active_loc
- nodes
.active
.location
3675 for node
in selection
:
3676 node
.location
+= active_loc_diff
3677 else: # Position nodes centered around where they used to be
3678 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
])
3679 new_mid
= (max(locs
) + min(locs
)) / 2
3680 for node
in selection
:
3682 node
.location
.x
+= (mid_x
- new_mid
)
3684 node
.location
.y
+= (mid_y
- new_mid
)
3689 class NWSelectParentChildren(Operator
, NWBase
):
3690 bl_idname
= "node.nw_select_parent_child"
3691 bl_label
= "Select Parent or Children"
3692 bl_options
= {'REGISTER', 'UNDO'}
3694 option
: EnumProperty(
3697 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3698 ('CHILD', 'Select Children', 'Select members of selected frame'),
3702 def execute(self
, context
):
3703 nodes
, links
= get_nodes_links(context
)
3704 option
= self
.option
3705 selected
= [node
for node
in nodes
if node
.select
]
3706 if option
== 'PARENT':
3707 for sel
in selected
:
3710 parent
.select
= True
3711 else: # option == 'CHILD'
3712 for sel
in selected
:
3713 children
= [node
for node
in nodes
if node
.parent
== sel
]
3714 for kid
in children
:
3720 class NWDetachOutputs(Operator
, NWBase
):
3721 """Detach outputs of selected node leaving inputs linked"""
3722 bl_idname
= "node.nw_detach_outputs"
3723 bl_label
= "Detach Outputs"
3724 bl_options
= {'REGISTER', 'UNDO'}
3726 def execute(self
, context
):
3727 nodes
, links
= get_nodes_links(context
)
3728 selected
= context
.selected_nodes
3729 bpy
.ops
.node
.duplicate_move_keep_inputs()
3730 new_nodes
= context
.selected_nodes
3731 bpy
.ops
.node
.select_all(action
="DESELECT")
3732 for node
in selected
:
3734 bpy
.ops
.node
.delete_reconnect()
3735 for new_node
in new_nodes
:
3736 new_node
.select
= True
3737 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3742 class NWLinkToOutputNode(Operator
):
3743 """Link to Composite node or Material Output node"""
3744 bl_idname
= "node.nw_link_out"
3745 bl_label
= "Connect to Output"
3746 bl_options
= {'REGISTER', 'UNDO'}
3749 def poll(cls
, context
):
3751 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
3752 if context
.active_node
is not None:
3753 for out
in context
.active_node
.outputs
:
3754 if is_visible_socket(out
):
3759 def execute(self
, context
):
3760 nodes
, links
= get_nodes_links(context
)
3761 active
= nodes
.active
3764 tree_type
= context
.space_data
.tree_type
3765 output_types_shaders
= [x
[1] for x
in shaders_output_nodes_props
]
3766 output_types_compo
= ['COMPOSITE']
3767 output_types_blender_mat
= ['OUTPUT']
3768 output_types_textures
= ['OUTPUT']
3769 output_types
= output_types_shaders
+ output_types_compo
+ output_types_blender_mat
3771 if node
.type in output_types
:
3775 bpy
.ops
.node
.select_all(action
="DESELECT")
3776 if tree_type
== 'ShaderNodeTree':
3777 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3778 elif tree_type
== 'CompositorNodeTree':
3779 output_node
= nodes
.new('CompositorNodeComposite')
3780 elif tree_type
== 'TextureNodeTree':
3781 output_node
= nodes
.new('TextureNodeOutput')
3782 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3783 output_node
.location
.y
= active
.location
.y
3784 if (output_node
and active
.outputs
):
3785 for i
, output
in enumerate(active
.outputs
):
3786 if is_visible_socket(output
):
3789 for i
, output
in enumerate(active
.outputs
):
3790 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3795 if tree_type
== 'ShaderNodeTree':
3796 if active
.outputs
[output_index
].name
== 'Volume':
3798 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3800 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3802 force_update(context
) # viewport render does not update
3807 class NWMakeLink(Operator
, NWBase
):
3808 """Make a link from one socket to another"""
3809 bl_idname
= 'node.nw_make_link'
3810 bl_label
= 'Make Link'
3811 bl_options
= {'REGISTER', 'UNDO'}
3812 from_socket
: IntProperty()
3813 to_socket
: IntProperty()
3815 def execute(self
, context
):
3816 nodes
, links
= get_nodes_links(context
)
3818 n1
= nodes
[context
.scene
.NWLazySource
]
3819 n2
= nodes
[context
.scene
.NWLazyTarget
]
3821 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3823 force_update(context
)
3828 class NWCallInputsMenu(Operator
, NWBase
):
3829 """Link from this output"""
3830 bl_idname
= 'node.nw_call_inputs_menu'
3831 bl_label
= 'Make Link'
3832 bl_options
= {'REGISTER', 'UNDO'}
3833 from_socket
: IntProperty()
3835 def execute(self
, context
):
3836 nodes
, links
= get_nodes_links(context
)
3838 context
.scene
.NWSourceSocket
= self
.from_socket
3840 n1
= nodes
[context
.scene
.NWLazySource
]
3841 n2
= nodes
[context
.scene
.NWLazyTarget
]
3842 if len(n2
.inputs
) > 1:
3843 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3844 elif len(n2
.inputs
) == 1:
3845 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3849 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3850 """Add an Image Sequence"""
3851 bl_idname
= 'node.nw_add_sequence'
3852 bl_label
= 'Import Image Sequence'
3853 bl_options
= {'REGISTER', 'UNDO'}
3855 directory
: StringProperty(
3858 filename
: StringProperty(
3861 files
: CollectionProperty(
3862 type=bpy
.types
.OperatorFileListElement
,
3863 options
={'HIDDEN', 'SKIP_SAVE'}
3866 def execute(self
, context
):
3867 nodes
, links
= get_nodes_links(context
)
3868 directory
= self
.directory
3869 filename
= self
.filename
3871 tree
= context
.space_data
.node_tree
3874 # print ("\nDIR:", directory)
3875 # print ("FN:", filename)
3876 # print ("Fs:", list(f.name for f in files), '\n')
3878 if tree
.type == 'SHADER':
3879 node_type
= "ShaderNodeTexImage"
3880 elif tree
.type == 'COMPOSITING':
3881 node_type
= "CompositorNodeImage"
3883 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3884 return {'CANCELLED'}
3886 if not files
[0].name
and not filename
:
3887 self
.report({'ERROR'}, "No file chosen")
3888 return {'CANCELLED'}
3889 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3890 # User has selected multiple files without an active one, or the active one is non-existant
3891 filename
= files
[0].name
3893 if not path
.exists(directory
+filename
):
3894 self
.report({'ERROR'}, filename
+" does not exist!")
3895 return {'CANCELLED'}
3897 without_ext
= '.'.join(filename
.split('.')[:-1])
3899 # if last digit isn't a number, it's not a sequence
3900 if not without_ext
[-1].isdigit():
3901 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3902 return {'CANCELLED'}
3905 extension
= filename
.split('.')[-1]
3906 reverse
= without_ext
[::-1] # reverse string
3909 for char
in reverse
:
3915 without_num
= without_ext
[:count_numbers
*-1]
3917 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3919 num_frames
= len(files
)
3921 nodes_list
= [node
for node
in nodes
]
3923 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3924 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3928 yloc
+= node_mid_pt(node
, 'y')
3929 yloc
= yloc
/len(nodes
)
3934 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3936 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3938 node
.label
= name_with_hashes
3940 img
= bpy
.data
.images
.load(directory
+(without_ext
+'.'+extension
))
3941 img
.source
= 'SEQUENCE'
3942 img
.name
= name_with_hashes
3944 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3945 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
3946 image_user
.frame_duration
= num_frames
3951 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3952 """Add multiple images at once"""
3953 bl_idname
= 'node.nw_add_multiple_images'
3954 bl_label
= 'Open Selected Images'
3955 bl_options
= {'REGISTER', 'UNDO'}
3956 directory
: StringProperty(
3959 files
: CollectionProperty(
3960 type=bpy
.types
.OperatorFileListElement
,
3961 options
={'HIDDEN', 'SKIP_SAVE'}
3964 def execute(self
, context
):
3965 nodes
, links
= get_nodes_links(context
)
3967 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3969 if context
.space_data
.node_tree
.type == 'SHADER':
3970 node_type
= "ShaderNodeTexImage"
3971 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3972 node_type
= "CompositorNodeImage"
3974 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3975 return {'CANCELLED'}
3978 for f
in self
.files
:
3981 node
= nodes
.new(node_type
)
3982 new_nodes
.append(node
)
3985 node
.width_hidden
= 100
3986 node
.location
.x
= xloc
3987 node
.location
.y
= yloc
3990 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3993 # shift new nodes up to center of tree
3994 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3996 if node
in new_nodes
:
3998 node
.location
.y
+= (list_size
/2)
4004 class NWViewerFocus(bpy
.types
.Operator
):
4005 """Set the viewer tile center to the mouse position"""
4006 bl_idname
= "node.nw_viewer_focus"
4007 bl_label
= "Viewer Focus"
4009 x
: bpy
.props
.IntProperty()
4010 y
: bpy
.props
.IntProperty()
4013 def poll(cls
, context
):
4014 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
4016 def execute(self
, context
):
4019 def invoke(self
, context
, event
):
4020 render
= context
.scene
.render
4021 space
= context
.space_data
4022 percent
= render
.resolution_percentage
*0.01
4024 nodes
, links
= get_nodes_links(context
)
4025 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
4028 mlocx
= event
.mouse_region_x
4029 mlocy
= event
.mouse_region_y
4030 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
4032 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
4033 region_x
= context
.region
.width
4034 region_y
= context
.region
.height
4036 region_center_x
= context
.region
.width
/ 2
4037 region_center_y
= context
.region
.height
/ 2
4039 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
4040 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
4042 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
4043 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
4045 margin_x
= region_center_x
- backdrop_center_x
4046 margin_y
= region_center_y
- backdrop_center_y
4048 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
4049 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
4051 for node
in viewers
:
4052 node
.center_x
= abs_mouse_x
4053 node
.center_y
= abs_mouse_y
4055 return {'PASS_THROUGH'}
4057 return self
.execute(context
)
4060 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
4061 """Save the current viewer node to an image file"""
4062 bl_idname
= "node.nw_save_viewer"
4063 bl_label
= "Save This Image"
4064 filepath
: StringProperty(subtype
="FILE_PATH")
4065 filename_ext
: EnumProperty(
4067 description
="Choose the file format to save to",
4068 items
=(('.bmp', "BMP", ""),
4069 ('.rgb', 'IRIS', ""),
4070 ('.png', 'PNG', ""),
4071 ('.jpg', 'JPEG', ""),
4072 ('.jp2', 'JPEG2000', ""),
4073 ('.tga', 'TARGA', ""),
4074 ('.cin', 'CINEON', ""),
4075 ('.dpx', 'DPX', ""),
4076 ('.exr', 'OPEN_EXR', ""),
4077 ('.hdr', 'HDR', ""),
4078 ('.tif', 'TIFF', "")),
4083 def poll(cls
, context
):
4085 if nw_check(context
):
4086 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4087 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
4088 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
4092 def execute(self
, context
):
4109 basename
, ext
= path
.splitext(fp
)
4110 old_render_format
= context
.scene
.render
.image_settings
.file_format
4111 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
4112 context
.area
.type = "IMAGE_EDITOR"
4113 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
4114 context
.area
.spaces
[0].image
.save_render(fp
)
4115 context
.area
.type = "NODE_EDITOR"
4116 context
.scene
.render
.image_settings
.file_format
= old_render_format
4120 class NWResetNodes(bpy
.types
.Operator
):
4121 """Reset Nodes in Selection"""
4122 bl_idname
= "node.nw_reset_nodes"
4123 bl_label
= "Reset Nodes"
4124 bl_options
= {'REGISTER', 'UNDO'}
4127 def poll(cls
, context
):
4128 space
= context
.space_data
4129 return space
.type == 'NODE_EDITOR'
4131 def execute(self
, context
):
4132 node_active
= context
.active_node
4133 node_selected
= context
.selected_nodes
4134 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4136 # Check if one node is selected at least
4137 if not (len(node_selected
) > 0):
4138 self
.report({'ERROR'}, "1 node must be selected at least")
4139 return {'CANCELLED'}
4141 active_node_name
= node_active
.name
if node_active
.select
else None
4142 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
4144 # Create output lists
4145 selected_node_names
= [n
.name
for n
in node_selected
]
4148 # Reset all valid children in a frame
4149 node_active_is_frame
= False
4150 if len(node_selected
) == 1 and node_active
.type == "FRAME":
4151 node_tree
= node_active
.id_data
4152 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
4154 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
4155 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
4156 node_active_is_frame
= True
4158 # Check if valid nodes in selection
4159 if not (len(valid_nodes
) > 0):
4160 # Check for frames only
4161 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
4162 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
4163 self
.report({'ERROR'}, "Please select only 1 frame to reset")
4165 self
.report({'ERROR'}, "No valid node(s) in selection")
4166 return {'CANCELLED'}
4168 # Report nodes that are not valid
4169 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
4170 valid_node_names
= [n
.name
for n
in valid_nodes
]
4171 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
4172 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
4174 # Deselect all nodes
4175 for i
in node_selected
:
4178 # Run through all valid nodes
4179 for node
in valid_nodes
:
4181 parent
= node
.parent
if node
.parent
else None
4182 node_loc
= [node
.location
.x
, node
.location
.y
]
4184 node_tree
= node
.id_data
4185 props_to_copy
= 'bl_idname name location height width'.split(' ')
4188 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
4189 for i
in (i
for i
in mappings
if i
.is_linked
):
4191 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
4193 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
4195 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
4196 props_to_copy
.pop(0)
4198 for prop
in props_to_copy
:
4199 setattr(new_node
, prop
, props
[prop
])
4201 nodes
= node_tree
.nodes
4203 new_node
.name
= props
['name']
4206 new_node
.parent
= parent
4207 new_node
.location
= node_loc
4209 for str_from
, str_to
in reconnections
:
4210 node_tree
.links
.new(eval(str_from
), eval(str_to
))
4212 new_node
.select
= False
4213 success_names
.append(new_node
.name
)
4215 # Reselect all nodes
4216 if selected_node_names
and node_active_is_frame
is False:
4217 for i
in selected_node_names
:
4218 node_tree
.nodes
[i
].select
= True
4220 if active_node_name
is not None:
4221 node_tree
.nodes
[active_node_name
].select
= True
4222 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
4224 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
4232 def drawlayout(context
, layout
, mode
='non-panel'):
4233 tree_type
= context
.space_data
.tree_type
4235 col
= layout
.column(align
=True)
4236 col
.menu(NWMergeNodesMenu
.bl_idname
)
4239 col
= layout
.column(align
=True)
4240 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
4243 if tree_type
== 'ShaderNodeTree':
4244 col
= layout
.column(align
=True)
4245 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
4246 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
4249 col
= layout
.column(align
=True)
4250 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
4251 col
.operator(NWSwapLinks
.bl_idname
)
4252 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
4255 col
= layout
.column(align
=True)
4256 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
4257 if tree_type
!= 'GeometryNodeTree':
4258 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
4261 col
= layout
.column(align
=True)
4263 row
= col
.row(align
=True)
4264 row
.operator(NWClearLabel
.bl_idname
).option
= True
4265 row
.operator(NWModifyLabels
.bl_idname
)
4267 col
.operator(NWClearLabel
.bl_idname
).option
= True
4268 col
.operator(NWModifyLabels
.bl_idname
)
4269 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
4271 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
4274 col
= layout
.column(align
=True)
4275 if tree_type
== 'CompositorNodeTree':
4276 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
4277 if tree_type
!= 'GeometryNodeTree':
4278 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
4281 col
= layout
.column(align
=True)
4282 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
4285 col
= layout
.column(align
=True)
4286 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
4289 col
= layout
.column(align
=True)
4290 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
4294 class NodeWranglerPanel(Panel
, NWBase
):
4295 bl_idname
= "NODE_PT_nw_node_wrangler"
4296 bl_space_type
= 'NODE_EDITOR'
4297 bl_label
= "Node Wrangler"
4298 bl_region_type
= "UI"
4299 bl_category
= "Node Wrangler"
4301 prepend
: StringProperty(
4304 append
: StringProperty()
4305 remove
: StringProperty()
4307 def draw(self
, context
):
4308 self
.layout
.label(text
="(Quick access: Shift+W)")
4309 drawlayout(context
, self
.layout
, mode
='panel')
4315 class NodeWranglerMenu(Menu
, NWBase
):
4316 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
4317 bl_label
= "Node Wrangler"
4319 def draw(self
, context
):
4320 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
4321 drawlayout(context
, self
.layout
)
4324 class NWMergeNodesMenu(Menu
, NWBase
):
4325 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
4326 bl_label
= "Merge Selected Nodes"
4328 def draw(self
, context
):
4329 type = context
.space_data
.tree_type
4330 layout
= self
.layout
4331 if type == 'ShaderNodeTree':
4332 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
4333 if type == 'GeometryNodeTree':
4334 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
4335 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4337 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
4338 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4339 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
4341 props
.merge_type
= 'ZCOMBINE'
4342 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
4344 props
.merge_type
= 'ALPHAOVER'
4346 class NWMergeGeometryMenu(Menu
, NWBase
):
4347 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
4348 bl_label
= "Merge Selected Nodes using Geometry Nodes"
4349 def draw(self
, context
):
4350 layout
= self
.layout
4351 # The boolean node + Join Geometry node
4352 for type, name
, description
in geo_combine_operations
:
4353 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4355 props
.merge_type
= 'GEOMETRY'
4357 class NWMergeShadersMenu(Menu
, NWBase
):
4358 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
4359 bl_label
= "Merge Selected Nodes using Shaders"
4361 def draw(self
, context
):
4362 layout
= self
.layout
4363 for type in ('MIX', 'ADD'):
4364 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
4366 props
.merge_type
= 'SHADER'
4369 class NWMergeMixMenu(Menu
, NWBase
):
4370 bl_idname
= "NODE_MT_nw_merge_mix_menu"
4371 bl_label
= "Merge Selected Nodes using Mix"
4373 def draw(self
, context
):
4374 layout
= self
.layout
4375 for type, name
, description
in blend_types
:
4376 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4378 props
.merge_type
= 'MIX'
4381 class NWConnectionListOutputs(Menu
, NWBase
):
4382 bl_idname
= "NODE_MT_nw_connection_list_out"
4385 def draw(self
, context
):
4386 layout
= self
.layout
4387 nodes
, links
= get_nodes_links(context
)
4389 n1
= nodes
[context
.scene
.NWLazySource
]
4391 for o
in n1
.outputs
:
4392 # Only show sockets that are exposed.
4394 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
4398 class NWConnectionListInputs(Menu
, NWBase
):
4399 bl_idname
= "NODE_MT_nw_connection_list_in"
4402 def draw(self
, context
):
4403 layout
= self
.layout
4404 nodes
, links
= get_nodes_links(context
)
4406 n2
= nodes
[context
.scene
.NWLazyTarget
]
4410 # Only show sockets that are exposed.
4411 # This prevents, for example, the scale value socket
4412 # of the vector math node being added to the list when
4413 # the mode is not 'SCALE'.
4415 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
4416 op
.from_socket
= context
.scene
.NWSourceSocket
4417 op
.to_socket
= index
4421 class NWMergeMathMenu(Menu
, NWBase
):
4422 bl_idname
= "NODE_MT_nw_merge_math_menu"
4423 bl_label
= "Merge Selected Nodes using Math"
4425 def draw(self
, context
):
4426 layout
= self
.layout
4427 for type, name
, description
in operations
:
4428 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4430 props
.merge_type
= 'MATH'
4433 class NWBatchChangeNodesMenu(Menu
, NWBase
):
4434 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
4435 bl_label
= "Batch Change Selected Nodes"
4437 def draw(self
, context
):
4438 layout
= self
.layout
4439 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
4440 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
4443 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
4444 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
4445 bl_label
= "Batch Change Blend Type"
4447 def draw(self
, context
):
4448 layout
= self
.layout
4449 for type, name
, description
in blend_types
:
4450 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4451 props
.blend_type
= type
4452 props
.operation
= 'CURRENT'
4455 class NWBatchChangeOperationMenu(Menu
, NWBase
):
4456 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
4457 bl_label
= "Batch Change Math Operation"
4459 def draw(self
, context
):
4460 layout
= self
.layout
4461 for type, name
, description
in operations
:
4462 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4463 props
.blend_type
= 'CURRENT'
4464 props
.operation
= type
4467 class NWCopyToSelectedMenu(Menu
, NWBase
):
4468 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4469 bl_label
= "Copy to Selected"
4471 def draw(self
, context
):
4472 layout
= self
.layout
4473 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4474 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4477 class NWCopyLabelMenu(Menu
, NWBase
):
4478 bl_idname
= "NODE_MT_nw_copy_label_menu"
4479 bl_label
= "Copy Label"
4481 def draw(self
, context
):
4482 layout
= self
.layout
4483 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4484 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4485 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4488 class NWAddReroutesMenu(Menu
, NWBase
):
4489 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4490 bl_label
= "Add Reroutes"
4491 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4493 def draw(self
, context
):
4494 layout
= self
.layout
4495 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4496 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4497 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4500 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4501 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4502 bl_label
= "Link Active to Selected"
4504 def draw(self
, context
):
4505 layout
= self
.layout
4506 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4507 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4508 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4511 class NWLinkStandardMenu(Menu
, NWBase
):
4512 bl_idname
= "NODE_MT_nw_link_standard_menu"
4513 bl_label
= "To All Selected"
4515 def draw(self
, context
):
4516 layout
= self
.layout
4517 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4518 props
.replace
= False
4519 props
.use_node_name
= False
4520 props
.use_outputs_names
= False
4521 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4522 props
.replace
= True
4523 props
.use_node_name
= False
4524 props
.use_outputs_names
= False
4527 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4528 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4529 bl_label
= "Use Node Name/Label"
4531 def draw(self
, context
):
4532 layout
= self
.layout
4533 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4534 props
.replace
= False
4535 props
.use_node_name
= True
4536 props
.use_outputs_names
= False
4537 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4538 props
.replace
= True
4539 props
.use_node_name
= True
4540 props
.use_outputs_names
= False
4543 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4544 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4545 bl_label
= "Use Outputs Names"
4547 def draw(self
, context
):
4548 layout
= self
.layout
4549 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4550 props
.replace
= False
4551 props
.use_node_name
= False
4552 props
.use_outputs_names
= True
4553 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4554 props
.replace
= True
4555 props
.use_node_name
= False
4556 props
.use_outputs_names
= True
4559 class NWVertColMenu(bpy
.types
.Menu
):
4560 bl_idname
= "NODE_MT_nw_node_vertex_color_menu"
4561 bl_label
= "Vertex Colors"
4564 def poll(cls
, context
):
4566 if nw_check(context
):
4567 snode
= context
.space_data
4568 valid
= snode
.tree_type
== 'ShaderNodeTree'
4571 def draw(self
, context
):
4573 nodes
, links
= get_nodes_links(context
)
4574 mat
= context
.object.active_material
4577 for obj
in bpy
.data
.objects
:
4578 for slot
in obj
.material_slots
:
4579 if slot
.material
== mat
:
4583 if obj
.data
.vertex_colors
:
4584 for vcol
in obj
.data
.vertex_colors
:
4585 vcols
.append(vcol
.name
)
4586 vcols
= list(set(vcols
)) # get a unique list
4590 l
.operator(NWAddAttrNode
.bl_idname
, text
=vcol
).attr_name
= vcol
4592 l
.label(text
="No Vertex Color layers on objects with this material")
4595 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4596 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4597 bl_label
= "Switch Type to..."
4599 def draw(self
, context
):
4600 layout
= self
.layout
4601 tree
= context
.space_data
.node_tree
4602 if tree
.type == 'SHADER':
4603 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4604 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4605 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4606 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4607 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4608 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4609 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4610 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4611 if tree
.type == 'COMPOSITING':
4612 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4613 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4614 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4615 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4616 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4617 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4618 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4619 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4620 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4621 if tree
.type == 'TEXTURE':
4622 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4623 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4624 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4625 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4626 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4627 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4628 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4629 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4630 if tree
.type == 'GEOMETRY':
4631 categories
= [c
for c
in node_categories_iter(context
)
4632 if c
.name
not in ['Group', 'Script']]
4633 for cat
in categories
:
4634 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4635 if hasattr(bpy
.types
, idname
):
4638 layout
.label(text
="Unable to load altered node lists.")
4639 layout
.label(text
="Please re-enable Node Wrangler.")
4643 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4644 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4647 def draw(self
, context
):
4648 layout
= self
.layout
4649 for ident
, node_type
, rna_name
in shaders_input_nodes_props
:
4650 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4651 props
.to_type
= ident
4654 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4655 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4658 def draw(self
, context
):
4659 layout
= self
.layout
4660 for ident
, node_type
, rna_name
in shaders_output_nodes_props
:
4661 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4662 props
.to_type
= ident
4665 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4666 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4669 def draw(self
, context
):
4670 layout
= self
.layout
4671 for ident
, node_type
, rna_name
in shaders_shader_nodes_props
:
4672 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4673 props
.to_type
= ident
4676 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4677 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4678 bl_label
= "Texture"
4680 def draw(self
, context
):
4681 layout
= self
.layout
4682 for ident
, node_type
, rna_name
in shaders_texture_nodes_props
:
4683 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4684 props
.to_type
= ident
4687 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4688 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4691 def draw(self
, context
):
4692 layout
= self
.layout
4693 for ident
, node_type
, rna_name
in shaders_color_nodes_props
:
4694 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4695 props
.to_type
= ident
4698 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4699 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4702 def draw(self
, context
):
4703 layout
= self
.layout
4704 for ident
, node_type
, rna_name
in shaders_vector_nodes_props
:
4705 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4706 props
.to_type
= ident
4709 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4710 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4711 bl_label
= "Converter"
4713 def draw(self
, context
):
4714 layout
= self
.layout
4715 for ident
, node_type
, rna_name
in shaders_converter_nodes_props
:
4716 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4717 props
.to_type
= ident
4720 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4721 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4724 def draw(self
, context
):
4725 layout
= self
.layout
4726 for ident
, node_type
, rna_name
in shaders_layout_nodes_props
:
4727 if node_type
!= 'FRAME':
4728 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4729 props
.to_type
= ident
4732 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4733 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4736 def draw(self
, context
):
4737 layout
= self
.layout
4738 for ident
, node_type
, rna_name
in compo_input_nodes_props
:
4739 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4740 props
.to_type
= ident
4743 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4744 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4747 def draw(self
, context
):
4748 layout
= self
.layout
4749 for ident
, node_type
, rna_name
in compo_output_nodes_props
:
4750 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4751 props
.to_type
= ident
4754 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4755 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4758 def draw(self
, context
):
4759 layout
= self
.layout
4760 for ident
, node_type
, rna_name
in compo_color_nodes_props
:
4761 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4762 props
.to_type
= ident
4765 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4766 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4767 bl_label
= "Converter"
4769 def draw(self
, context
):
4770 layout
= self
.layout
4771 for ident
, node_type
, rna_name
in compo_converter_nodes_props
:
4772 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4773 props
.to_type
= ident
4776 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4777 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4780 def draw(self
, context
):
4781 layout
= self
.layout
4782 for ident
, node_type
, rna_name
in compo_filter_nodes_props
:
4783 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4784 props
.to_type
= ident
4787 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4788 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4791 def draw(self
, context
):
4792 layout
= self
.layout
4793 for ident
, node_type
, rna_name
in compo_vector_nodes_props
:
4794 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4795 props
.to_type
= ident
4798 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4799 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4802 def draw(self
, context
):
4803 layout
= self
.layout
4804 for ident
, node_type
, rna_name
in compo_matte_nodes_props
:
4805 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4806 props
.to_type
= ident
4809 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4810 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4811 bl_label
= "Distort"
4813 def draw(self
, context
):
4814 layout
= self
.layout
4815 for ident
, node_type
, rna_name
in compo_distort_nodes_props
:
4816 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4817 props
.to_type
= ident
4820 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4821 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4824 def draw(self
, context
):
4825 layout
= self
.layout
4826 for ident
, node_type
, rna_name
in compo_layout_nodes_props
:
4827 if node_type
!= 'FRAME':
4828 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4829 props
.to_type
= ident
4832 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4833 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4836 def draw(self
, context
):
4837 layout
= self
.layout
4838 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4839 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4840 props
.to_type
= ident
4843 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4844 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4847 def draw(self
, context
):
4848 layout
= self
.layout
4849 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4850 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4851 props
.to_type
= ident
4854 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4855 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4858 def draw(self
, context
):
4859 layout
= self
.layout
4860 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4861 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4862 props
.to_type
= ident
4865 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4866 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4869 def draw(self
, context
):
4870 layout
= self
.layout
4871 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4872 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4873 props
.to_type
= ident
4876 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4877 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4878 bl_label
= "Converter"
4880 def draw(self
, context
):
4881 layout
= self
.layout
4882 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4883 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4884 props
.to_type
= ident
4887 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4888 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4891 def draw(self
, context
):
4892 layout
= self
.layout
4893 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4894 if node_type
!= 'FRAME':
4895 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4896 props
.to_type
= ident
4899 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4900 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4903 def draw(self
, context
):
4904 layout
= self
.layout
4905 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4906 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4907 props
.to_type
= ident
4910 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4911 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4914 def draw(self
, context
):
4915 layout
= self
.layout
4916 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4917 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4918 props
.to_type
= ident
4921 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4922 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4925 def draw(self
, context
):
4926 layout
= self
.layout
4927 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4928 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4929 props
.to_type
= ident
4932 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4933 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4934 bl_label
= "Pattern"
4936 def draw(self
, context
):
4937 layout
= self
.layout
4938 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4939 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4940 props
.to_type
= ident
4943 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4944 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4945 bl_label
= "Textures"
4947 def draw(self
, context
):
4948 layout
= self
.layout
4949 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4950 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4951 props
.to_type
= ident
4954 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4955 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4956 bl_label
= "Converter"
4958 def draw(self
, context
):
4959 layout
= self
.layout
4960 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4961 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4962 props
.to_type
= ident
4965 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4966 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4967 bl_label
= "Distort"
4969 def draw(self
, context
):
4970 layout
= self
.layout
4971 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4972 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4973 props
.to_type
= ident
4976 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4977 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4980 def draw(self
, context
):
4981 layout
= self
.layout
4982 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
4983 if node_type
!= 'FRAME':
4984 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4985 props
.to_type
= ident
4987 def draw_switch_category_submenu(self
, context
):
4988 layout
= self
.layout
4989 if self
.category
.name
== 'Layout':
4990 for node
in self
.category
.items(context
):
4991 if node
.nodetype
!= 'NodeFrame':
4992 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4993 props
.to_type
= node
.nodetype
4995 for node
in self
.category
.items(context
):
4996 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4997 props
.geo_to_type
= node
.nodetype
5000 # APPENDAGES TO EXISTING UI
5004 def select_parent_children_buttons(self
, context
):
5005 layout
= self
.layout
5006 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
5007 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
5010 def attr_nodes_menu_func(self
, context
):
5011 col
= self
.layout
.column(align
=True)
5012 col
.menu("NODE_MT_nw_node_vertex_color_menu")
5016 def multipleimages_menu_func(self
, context
):
5017 col
= self
.layout
.column(align
=True)
5018 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
5019 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
5023 def bgreset_menu_func(self
, context
):
5024 self
.layout
.operator(NWResetBG
.bl_idname
)
5027 def save_viewer_menu_func(self
, context
):
5028 if nw_check(context
):
5029 if context
.space_data
.tree_type
== 'CompositorNodeTree':
5030 if context
.scene
.node_tree
.nodes
.active
:
5031 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
5032 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
5035 def reset_nodes_button(self
, context
):
5036 node_active
= context
.active_node
5037 node_selected
= context
.selected_nodes
5038 node_ignore
= ["FRAME","REROUTE", "GROUP"]
5040 # Check if active node is in the selection and respective type
5041 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
5042 row
= self
.layout
.row()
5043 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
5044 self
.layout
.separator()
5046 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
5047 row
= self
.layout
.row()
5048 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
5049 self
.layout
.separator()
5053 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5055 switch_category_menus
= []
5057 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5058 # props entry: (property name, property value)
5061 # NWMergeNodes with Ctrl (AUTO).
5062 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
5063 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5064 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
5065 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5066 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5067 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5068 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
5069 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5070 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5071 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5072 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
5073 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5074 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5075 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5076 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
5077 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5078 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5079 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5080 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
5081 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5082 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
5083 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5084 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
5085 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5086 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5087 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5088 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5089 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
5090 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5091 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
5092 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5093 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5094 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5095 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
5096 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5097 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5098 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5099 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
5100 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5101 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5102 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5103 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
5104 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5105 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5106 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5107 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
5108 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5109 # NWMergeNodes with Ctrl Shift (MATH)
5110 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5111 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5112 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
5113 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5114 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5115 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5116 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
5117 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5118 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5119 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5120 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
5121 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5122 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5123 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5124 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
5125 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5126 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
5127 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5128 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
5129 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5130 # BATCH CHANGE NODES
5131 # NWBatchChangeNodes with Alt
5132 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
5133 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5134 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
5135 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5136 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5137 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5138 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
5139 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5140 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5141 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5142 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
5143 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5144 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5145 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5146 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
5147 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5148 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5149 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5150 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
5151 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5152 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
5153 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5154 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
5155 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5156 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
5157 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5158 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
5159 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5160 # LINK ACTIVE TO SELECTED
5161 # Don't use names, don't replace links (K)
5162 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
5163 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5164 # Don't use names, replace links (Shift K)
5165 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
5166 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5167 # Use node name, don't replace links (')
5168 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
5169 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5170 # Use node name, replace links (Shift ')
5171 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
5172 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5173 # Don't use names, don't replace links (;)
5174 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
5175 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5176 # Don't use names, replace links (')
5177 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
5178 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5180 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5181 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5182 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5183 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5184 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5185 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5186 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5187 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5188 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5189 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5190 # CLEAR LABEL (Alt L)
5191 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5192 # MODIFY LABEL (Alt Shift L)
5193 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5194 # Copy Label from active to selected
5195 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5196 # DETACH OUTPUTS (Alt Shift D)
5197 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5198 # LINK TO OUTPUT NODE (O)
5199 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5200 # SELECT PARENT/CHILDREN
5202 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5204 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5206 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5207 # Add Principled BSDF Texture Setup
5208 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5210 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5212 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5214 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5216 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5218 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5219 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5221 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
5223 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5225 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5226 # Lazy Connect with Menu
5227 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5228 # Viewer Tile Center
5229 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5231 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5232 # Reset Nodes (Back Space)
5233 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5235 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
5236 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5237 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5238 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
5239 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
5240 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
5245 NWPrincipledPreferences
,
5265 NWAddPrincipledSetup
,
5267 NWLinkActiveToSelected
,
5269 NWSelectParentChildren
,
5275 NWAddMultipleImages
,
5283 NWMergeGeometryMenu
,
5285 NWConnectionListOutputs
,
5286 NWConnectionListInputs
,
5288 NWBatchChangeNodesMenu
,
5289 NWBatchChangeBlendTypeMenu
,
5290 NWBatchChangeOperationMenu
,
5291 NWCopyToSelectedMenu
,
5294 NWLinkActiveToSelectedMenu
,
5296 NWLinkUseNodeNameMenu
,
5297 NWLinkUseOutputsNamesMenu
,
5299 NWSwitchNodeTypeMenu
,
5300 NWSwitchShadersInputSubmenu
,
5301 NWSwitchShadersOutputSubmenu
,
5302 NWSwitchShadersShaderSubmenu
,
5303 NWSwitchShadersTextureSubmenu
,
5304 NWSwitchShadersColorSubmenu
,
5305 NWSwitchShadersVectorSubmenu
,
5306 NWSwitchShadersConverterSubmenu
,
5307 NWSwitchShadersLayoutSubmenu
,
5308 NWSwitchCompoInputSubmenu
,
5309 NWSwitchCompoOutputSubmenu
,
5310 NWSwitchCompoColorSubmenu
,
5311 NWSwitchCompoConverterSubmenu
,
5312 NWSwitchCompoFilterSubmenu
,
5313 NWSwitchCompoVectorSubmenu
,
5314 NWSwitchCompoMatteSubmenu
,
5315 NWSwitchCompoDistortSubmenu
,
5316 NWSwitchCompoLayoutSubmenu
,
5317 NWSwitchMatInputSubmenu
,
5318 NWSwitchMatOutputSubmenu
,
5319 NWSwitchMatColorSubmenu
,
5320 NWSwitchMatVectorSubmenu
,
5321 NWSwitchMatConverterSubmenu
,
5322 NWSwitchMatLayoutSubmenu
,
5323 NWSwitchTexInputSubmenu
,
5324 NWSwitchTexOutputSubmenu
,
5325 NWSwitchTexColorSubmenu
,
5326 NWSwitchTexPatternSubmenu
,
5327 NWSwitchTexTexturesSubmenu
,
5328 NWSwitchTexConverterSubmenu
,
5329 NWSwitchTexDistortSubmenu
,
5330 NWSwitchTexLayoutSubmenu
,
5334 from bpy
.utils
import register_class
5337 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
5338 name
="Busy Drawing!",
5340 description
="An internal property used to store only the first mouse position")
5341 bpy
.types
.Scene
.NWLazySource
= StringProperty(
5342 name
="Lazy Source!",
5344 description
="An internal property used to store the first node in a Lazy Connect operation")
5345 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
5346 name
="Lazy Target!",
5348 description
="An internal property used to store the last node in a Lazy Connect operation")
5349 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
5350 name
="Source Socket!",
5352 description
="An internal property used to store the source socket in a Lazy Connect operation")
5353 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
5356 description
="An internal property used to determine if a socket is generated by the addon"
5363 addon_keymaps
.clear()
5364 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
5366 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
5367 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
5368 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
5370 for prop
, value
in props
:
5371 setattr(kmi
.properties
, prop
, value
)
5372 addon_keymaps
.append((km
, kmi
))
5375 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
5376 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
5377 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
5378 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
5379 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
5380 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
5381 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
5382 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
5385 switch_category_menus
.clear()
5386 for cat
in node_categories_iter(None):
5387 if cat
.name
not in ['Group', 'Script'] and cat
.identifier
.startswith('GEO'):
5388 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
5389 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
5390 "bl_space_type": 'NODE_EDITOR',
5391 "bl_label": cat
.name
,
5394 "draw": draw_switch_category_submenu
,
5397 switch_category_menus
.append(switch_category_type
)
5399 bpy
.utils
.register_class(switch_category_type
)
5403 from bpy
.utils
import unregister_class
5406 del bpy
.types
.Scene
.NWBusyDrawing
5407 del bpy
.types
.Scene
.NWLazySource
5408 del bpy
.types
.Scene
.NWLazyTarget
5409 del bpy
.types
.Scene
.NWSourceSocket
5410 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
5412 for cat_types
in switch_category_menus
:
5413 bpy
.utils
.unregister_class(cat_types
)
5414 switch_category_menus
.clear()
5417 for km
, kmi
in addon_keymaps
:
5418 km
.keymap_items
.remove(kmi
)
5419 addon_keymaps
.clear()
5422 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
5423 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
5424 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
5425 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
5426 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
5427 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
5428 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
5429 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
5432 unregister_class(cls
)
5434 if __name__
== "__main__":