1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
17 from bpy
.types
import Operator
, Panel
, Menu
18 from bpy
.props
import (
27 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
28 from gpu_extras
.batch
import batch_for_shader
29 from mathutils
import Vector
30 from nodeitems_utils
import node_categories_iter
, NodeItemCustom
31 from math
import cos
, sin
, pi
, hypot
35 from itertools
import chain
37 from collections
import namedtuple
41 # list of outputs of Input Render Layer
42 # with attributes determining if pass is used,
43 # and MultiLayer EXR outputs names and corresponding render engines
45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
46 RL_entry
= namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
48 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
49 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
50 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
51 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
52 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
53 RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
54 RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
55 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
56 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
57 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
58 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
59 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
60 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
61 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
62 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
63 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
64 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
65 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
66 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
67 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
68 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
69 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
70 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
71 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
72 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
76 # (rna_type.identifier, type, rna_type.name)
77 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
78 # Keeping things in alphabetical order so we don't need to sort later.
79 shaders_input_nodes_props
= (
80 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
81 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
82 ('ShaderNodeBevel', 'BEVEL', 'Bevel'),
83 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
84 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
85 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
86 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
87 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
88 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
89 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
90 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
91 ('ShaderNodeRGB', 'RGB', 'RGB'),
92 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
93 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
94 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
95 ('ShaderNodeValue', 'VALUE', 'Value'),
96 ('ShaderNodeVertexColor', 'VERTEX_COLOR', 'Vertex Color'),
97 ('ShaderNodeVolumeInfo', 'VOLUME_INFO', 'Volume Info'),
98 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
101 # (rna_type.identifier, type, rna_type.name)
102 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
103 # Keeping things in alphabetical order so we don't need to sort later.
104 shaders_output_nodes_props
= (
105 ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'),
106 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
107 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
108 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
110 # (rna_type.identifier, type, rna_type.name)
111 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
112 # Keeping things in alphabetical order so we don't need to sort later.
113 shaders_shader_nodes_props
= (
114 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
115 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
116 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
117 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
118 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
119 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
120 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
121 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
122 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
123 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
124 ('ShaderNodeBsdfHairPrincipled', 'BSDF_HAIR_PRINCIPLED', 'Principled Hair BSDF'),
125 ('ShaderNodeVolumePrincipled', 'PRINCIPLED_VOLUME', 'Principled Volume'),
126 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
127 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
128 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
129 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
130 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
131 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
132 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
133 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
134 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
136 # (rna_type.identifier, type, rna_type.name)
137 # Keeping things in alphabetical order so we don't need to sort later.
138 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
139 shaders_texture_nodes_props
= (
140 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
141 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
142 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
143 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
144 ('ShaderNodeTexIES', 'TEX_IES', 'IES Texture'),
145 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
146 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
147 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
148 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
149 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
150 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
151 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
152 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
153 ('ShaderNodeTexWhiteNoise', 'TEX_WHITE_NOISE', 'White Noise'),
155 # (rna_type.identifier, type, rna_type.name)
156 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
157 # Keeping things in alphabetical order so we don't need to sort later.
158 shaders_color_nodes_props
= (
159 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
160 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
161 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
162 ('ShaderNodeInvert', 'INVERT', 'Invert'),
163 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
164 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
165 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
167 # (rna_type.identifier, type, rna_type.name)
168 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
169 # Keeping things in alphabetical order so we don't need to sort later.
170 shaders_vector_nodes_props
= (
171 ('ShaderNodeBump', 'BUMP', 'Bump'),
172 ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'),
173 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
174 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
175 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
176 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
177 ('ShaderNodeVectorDisplacement', 'VECTOR_DISPLACEMENT', 'Vector Displacement'),
178 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
180 # (rna_type.identifier, type, rna_type.name)
181 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
182 # Keeping things in alphabetical order so we don't need to sort later.
183 shaders_converter_nodes_props
= (
184 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
185 ('ShaderNodeClamp', 'CLAMP', 'Clamp'),
186 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
187 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
188 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
189 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
190 ('ShaderNodeMapRange', 'MAP_RANGE', 'Map Range'),
191 ('ShaderNodeMath', 'MATH', 'Math'),
192 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
193 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
194 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
195 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
196 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
197 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
199 # (rna_type.identifier, type, rna_type.name)
200 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
201 # Keeping things in alphabetical order so we don't need to sort later.
202 shaders_layout_nodes_props
= (
203 ('NodeFrame', 'FRAME', 'Frame'),
204 ('NodeReroute', 'REROUTE', 'Reroute'),
208 # (rna_type.identifier, type, rna_type.name)
209 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
210 # Keeping things in alphabetical order so we don't need to sort later.
211 compo_input_nodes_props
= (
212 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
213 ('CompositorNodeImage', 'IMAGE', 'Image'),
214 ('CompositorNodeMask', 'MASK', 'Mask'),
215 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
216 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
217 ('CompositorNodeRGB', 'RGB', 'RGB'),
218 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
219 ('CompositorNodeTime', 'TIME', 'Time'),
220 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
221 ('CompositorNodeValue', 'VALUE', 'Value'),
223 # (rna_type.identifier, type, rna_type.name)
224 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
225 # Keeping things in alphabetical order so we don't need to sort later.
226 compo_output_nodes_props
= (
227 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
228 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
229 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
230 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
231 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
233 # (rna_type.identifier, type, rna_type.name)
234 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
235 # Keeping things in alphabetical order so we don't need to sort later.
236 compo_color_nodes_props
= (
237 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
238 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
239 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
240 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
241 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
242 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
243 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
244 ('CompositorNodeInvert', 'INVERT', 'Invert'),
245 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
246 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
247 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
248 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
250 # (rna_type.identifier, type, rna_type.name)
251 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
252 # Keeping things in alphabetical order so we don't need to sort later.
253 compo_converter_nodes_props
= (
254 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
255 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
256 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
257 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
258 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
259 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
260 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
261 ('CompositorNodeMath', 'MATH', 'Math'),
262 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
263 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
264 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
265 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
266 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
267 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
268 ('CompositorNodeSwitchView', 'VIEWSWITCH', 'View Switch'),
270 # (rna_type.identifier, type, rna_type.name)
271 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
272 # Keeping things in alphabetical order so we don't need to sort later.
273 compo_filter_nodes_props
= (
274 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
275 ('CompositorNodeBlur', 'BLUR', 'Blur'),
276 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
277 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
278 ('CompositorNodeDenoise', 'DENOISE', 'Denoise'),
279 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
280 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
281 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
282 ('CompositorNodeFilter', 'FILTER', 'Filter'),
283 ('CompositorNodeGlare', 'GLARE', 'Glare'),
284 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
285 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
286 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
287 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
289 # (rna_type.identifier, type, rna_type.name)
290 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
291 # Keeping things in alphabetical order so we don't need to sort later.
292 compo_vector_nodes_props
= (
293 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
294 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
295 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
296 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
297 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
299 # (rna_type.identifier, type, rna_type.name)
300 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
301 # Keeping things in alphabetical order so we don't need to sort later.
302 compo_matte_nodes_props
= (
303 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
304 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
305 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
306 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
307 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
308 ('CompositorNodeCryptomatte', 'CRYPTOMATTE', 'Cryptomatte'),
309 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
310 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
311 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
312 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
313 ('CompositorNodeKeying', 'KEYING', 'Keying'),
314 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
315 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
317 # (rna_type.identifier, type, rna_type.name)
318 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
319 # Keeping things in alphabetical order so we don't need to sort later.
320 compo_distort_nodes_props
= (
321 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
322 ('CompositorNodeCrop', 'CROP', 'Crop'),
323 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
324 ('CompositorNodeFlip', 'FLIP', 'Flip'),
325 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
326 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
327 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
328 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
329 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
330 ('CompositorNodeScale', 'SCALE', 'Scale'),
331 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
332 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
333 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
335 # (rna_type.identifier, type, rna_type.name)
336 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
337 # Keeping things in alphabetical order so we don't need to sort later.
338 compo_layout_nodes_props
= (
339 ('NodeFrame', 'FRAME', 'Frame'),
340 ('NodeReroute', 'REROUTE', 'Reroute'),
341 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
343 # Blender Render material nodes
344 # (rna_type.identifier, type, rna_type.name)
345 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
346 blender_mat_input_nodes_props
= (
347 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
348 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
349 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
350 ('ShaderNodeValue', 'VALUE', 'Value'),
351 ('ShaderNodeRGB', 'RGB', 'RGB'),
352 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
353 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
354 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
357 # (rna_type.identifier, type, rna_type.name)
358 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
359 blender_mat_output_nodes_props
= (
360 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
363 # (rna_type.identifier, type, rna_type.name)
364 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
365 blender_mat_color_nodes_props
= (
366 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
367 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
368 ('ShaderNodeInvert', 'INVERT', 'Invert'),
369 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
372 # (rna_type.identifier, type, rna_type.name)
373 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
374 blender_mat_vector_nodes_props
= (
375 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
376 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
377 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
380 # (rna_type.identifier, type, rna_type.name)
381 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
382 blender_mat_converter_nodes_props
= (
383 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
384 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
385 ('ShaderNodeMath', 'MATH', 'Math'),
386 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
387 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
388 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
389 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
390 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
391 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
394 # (rna_type.identifier, type, rna_type.name)
395 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
396 blender_mat_layout_nodes_props
= (
397 ('NodeReroute', 'REROUTE', 'Reroute'),
401 # (rna_type.identifier, type, rna_type.name)
402 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
403 texture_input_nodes_props
= (
404 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
405 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
406 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
407 ('TextureNodeImage', 'IMAGE', 'Image'),
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 texture_output_nodes_props
= (
413 ('TextureNodeOutput', 'OUTPUT', 'Output'),
414 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
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_color_nodes_props
= (
420 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
421 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
422 ('TextureNodeInvert', 'INVERT', 'Invert'),
423 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
424 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
425 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
428 # (rna_type.identifier, type, rna_type.name)
429 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
430 texture_pattern_nodes_props
= (
431 ('TextureNodeChecker', 'CHECKER', 'Checker'),
432 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
435 # (rna_type.identifier, type, rna_type.name)
436 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
437 texture_textures_nodes_props
= (
438 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
439 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
440 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
441 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
442 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
443 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
444 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
445 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
446 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
447 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
450 # (rna_type.identifier, type, rna_type.name)
451 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
452 texture_converter_nodes_props
= (
453 ('TextureNodeMath', 'MATH', 'Math'),
454 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
455 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
456 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
457 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
460 # (rna_type.identifier, type, rna_type.name)
461 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
462 texture_distort_nodes_props
= (
463 ('TextureNodeScale', 'SCALE', 'Scale'),
464 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
465 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
466 ('TextureNodeAt', 'AT', 'At'),
469 # (rna_type.identifier, type, rna_type.name)
470 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
471 texture_layout_nodes_props
= (
472 ('NodeReroute', 'REROUTE', 'Reroute'),
475 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
476 # used list, not tuple for easy merging with other lists.
478 ('MIX', 'Mix', 'Mix Mode'),
479 ('ADD', 'Add', 'Add Mode'),
480 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
481 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
482 ('SCREEN', 'Screen', 'Screen Mode'),
483 ('DIVIDE', 'Divide', 'Divide Mode'),
484 ('DIFFERENCE', 'Difference', 'Difference Mode'),
485 ('DARKEN', 'Darken', 'Darken Mode'),
486 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
487 ('OVERLAY', 'Overlay', 'Overlay Mode'),
488 ('DODGE', 'Dodge', 'Dodge Mode'),
489 ('BURN', 'Burn', 'Burn Mode'),
490 ('HUE', 'Hue', 'Hue Mode'),
491 ('SATURATION', 'Saturation', 'Saturation Mode'),
492 ('VALUE', 'Value', 'Value Mode'),
493 ('COLOR', 'Color', 'Color Mode'),
494 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
495 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
498 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
499 # used list, not tuple for easy merging with other lists.
501 ('ADD', 'Add', 'Add Mode'),
502 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
503 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
504 ('DIVIDE', 'Divide', 'Divide Mode'),
505 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
506 ('SINE', 'Sine', 'Sine Mode'),
507 ('COSINE', 'Cosine', 'Cosine Mode'),
508 ('TANGENT', 'Tangent', 'Tangent Mode'),
509 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
510 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
511 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
512 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
513 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
514 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
515 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
516 ('POWER', 'Power', 'Power Mode'),
517 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
518 ('SQRT', 'Square Root', 'Square Root Mode'),
519 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
520 ('EXPONENT', 'Exponent', 'Exponent Mode'),
521 ('MINIMUM', 'Minimum', 'Minimum Mode'),
522 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
523 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
524 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
525 ('SIGN', 'Sign', 'Sign Mode'),
526 ('COMPARE', 'Compare', 'Compare Mode'),
527 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
528 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
529 ('FRACT', 'Fraction', 'Fraction Mode'),
530 ('MODULO', 'Modulo', 'Modulo Mode'),
531 ('SNAP', 'Snap', 'Snap Mode'),
532 ('WRAP', 'Wrap', 'Wrap Mode'),
533 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
534 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
535 ('ROUND', 'Round', 'Round Mode'),
536 ('FLOOR', 'Floor', 'Floor Mode'),
537 ('CEIL', 'Ceil', 'Ceil Mode'),
538 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
539 ('RADIANS', 'To Radians', 'To Radians Mode'),
540 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
543 # Operations used by the geometry boolean node and join geometry node
544 geo_combine_operations
= [
545 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
546 ('INTERSECT', 'Intersect', 'Intersect Mode'),
547 ('UNION', 'Union', 'Union Mode'),
548 ('DIFFERENCE', 'Difference', 'Difference Mode'),
551 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
552 # used list, not tuple for easy merging with other lists.
554 ('CURRENT', 'Current', 'Leave at current state'),
555 ('NEXT', 'Next', 'Next blend type/operation'),
556 ('PREV', 'Prev', 'Previous blend type/operation'),
561 (1.0, 1.0, 1.0, 0.7),
562 (1.0, 0.0, 0.0, 0.7),
566 (0.0, 0.0, 0.0, 1.0),
567 (0.38, 0.77, 0.38, 1.0),
568 (0.38, 0.77, 0.38, 1.0)
571 (0.0, 0.0, 0.0, 1.0),
572 (0.77, 0.77, 0.16, 1.0),
573 (0.77, 0.77, 0.16, 1.0)
576 (0.0, 0.0, 0.0, 1.0),
577 (0.38, 0.38, 0.77, 1.0),
578 (0.38, 0.38, 0.77, 1.0)
581 (0.0, 0.0, 0.0, 1.0),
582 (0.63, 0.63, 0.63, 1.0),
583 (0.63, 0.63, 0.63, 1.0)
586 (1.0, 1.0, 1.0, 0.7),
587 (0.0, 0.0, 0.0, 0.7),
592 viewer_socket_name
= "tmp_viewer"
594 def get_nodes_from_category(category_name
, context
):
595 for category
in node_categories_iter(context
):
596 if category
.name
== category_name
:
597 return sorted(category
.items(context
), key
=lambda node
: node
.label
)
599 def is_visible_socket(socket
):
600 return not socket
.hide
and socket
.enabled
and socket
.type != 'CUSTOM'
602 def nice_hotkey_name(punc
):
603 # convert the ugly string name into the actual character
606 'MIDDLEMOUSE': "MMB",
608 'WHEELUPMOUSE': "Wheel Up",
609 'WHEELDOWNMOUSE': "Wheel Down",
610 'WHEELINMOUSE': "Wheel In",
611 'WHEELOUTMOUSE': "Wheel Out",
624 'LINE_FEED': "Enter",
633 'NUMPAD_1': "Numpad 1",
634 'NUMPAD_2': "Numpad 2",
635 'NUMPAD_3': "Numpad 3",
636 'NUMPAD_4': "Numpad 4",
637 'NUMPAD_5': "Numpad 5",
638 'NUMPAD_6': "Numpad 6",
639 'NUMPAD_7': "Numpad 7",
640 'NUMPAD_8': "Numpad 8",
641 'NUMPAD_9': "Numpad 9",
642 'NUMPAD_0': "Numpad 0",
643 'NUMPAD_PERIOD': "Numpad .",
644 'NUMPAD_SLASH': "Numpad /",
645 'NUMPAD_ASTERIX': "Numpad *",
646 'NUMPAD_MINUS': "Numpad -",
647 'NUMPAD_ENTER': "Numpad Enter",
648 'NUMPAD_PLUS': "Numpad +",
651 return nice_name
[punc
]
653 return punc
.replace("_", " ").title()
656 def force_update(context
):
657 context
.space_data
.node_tree
.update_tag()
661 prefs
= bpy
.context
.preferences
.system
662 return prefs
.dpi
* prefs
.pixel_size
/ 72
665 def node_mid_pt(node
, axis
):
667 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
669 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
675 def autolink(node1
, node2
, links
):
677 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
678 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
679 for outp
in available_outputs
:
680 for inp
in available_inputs
:
681 if not inp
.is_linked
and inp
.name
== outp
.name
:
686 for outp
in available_outputs
:
687 for inp
in available_inputs
:
688 if not inp
.is_linked
and inp
.type == outp
.type:
693 # force some connection even if the type doesn't match
694 if available_outputs
:
695 for inp
in available_inputs
:
696 if not inp
.is_linked
:
698 links
.new(available_outputs
[0], inp
)
701 # even if no sockets are open, force one of matching type
702 for outp
in available_outputs
:
703 for inp
in available_inputs
:
704 if inp
.type == outp
.type:
710 for outp
in available_outputs
:
711 for inp
in available_inputs
:
716 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
719 def abs_node_location(node
):
720 abs_location
= node
.location
721 if node
.parent
is None:
723 return abs_location
+ abs_node_location(node
.parent
)
725 def node_at_pos(nodes
, context
, event
):
726 nodes_under_mouse
= []
729 store_mouse_cursor(context
, event
)
730 x
, y
= context
.space_data
.cursor_location
732 # Make a list of each corner (and middle of border) for each node.
733 # Will be sorted to find nearest point and thus nearest node
734 node_points_with_dist
= []
737 if node
.type != 'FRAME': # no point trying to link to a frame node
738 dimx
= node
.dimensions
.x
/dpifac()
739 dimy
= node
.dimensions
.y
/dpifac()
740 locx
, locy
= abs_node_location(node
)
743 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
744 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
745 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
746 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
748 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
749 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
750 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
751 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
753 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
756 if node
.type != 'FRAME' and skipnode
== False:
757 locx
, locy
= abs_node_location(node
)
758 dimx
= node
.dimensions
.x
/dpifac()
759 dimy
= node
.dimensions
.y
/dpifac()
760 if (locx
<= x
<= locx
+ dimx
) and \
761 (locy
- dimy
<= y
<= locy
):
762 nodes_under_mouse
.append(node
)
764 if len(nodes_under_mouse
) == 1:
765 if nodes_under_mouse
[0] != nearest_node
:
766 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
768 target_node
= nearest_node
# else use the nearest node
770 target_node
= nearest_node
774 def store_mouse_cursor(context
, event
):
775 space
= context
.space_data
776 v2d
= context
.region
.view2d
777 tree
= space
.edit_tree
779 # convert mouse position to the View2D for later node placement
780 if context
.region
.type == 'WINDOW':
781 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
783 space
.cursor_location
= tree
.view_center
785 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
786 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
788 vertices
= ((x1
, y1
), (x2
, y2
))
789 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
790 colour
[1]+(1.0-colour
[1])/4,
791 colour
[2]+(1.0-colour
[2])/4,
792 colour
[3]+(1.0-colour
[3])/4),
795 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
796 bgl
.glLineWidth(size
* dpifac())
802 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
803 radius
= radius
* dpifac()
805 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
806 radius
* sin(i
* 2 * pi
/ sides
) + my
)
807 for i
in range(sides
+ 1)]
809 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
811 shader
.uniform_float("color", colour
)
815 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
816 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
817 bottom_bar
= (16*dpifac()) + 1
819 radius
= radius
*dpifac()
821 nlocx
, nlocy
= abs_node_location(node
)
823 nlocx
= (nlocx
+1)*dpifac()
824 nlocy
= (nlocy
+1)*dpifac()
825 ndimx
= node
.dimensions
.x
826 ndimy
= node
.dimensions
.y
831 if node
.type == 'REROUTE':
839 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
841 for i
in range(sides
+1):
843 if my
> bottom_bar
and mx
< area_width
:
844 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
845 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
846 vertices
.append((cosine
,sine
))
847 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
849 shader
.uniform_float("color", colour
)
853 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
855 for i
in range(sides
+1):
857 if my
> bottom_bar
and mx
< area_width
:
858 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
859 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
860 vertices
.append((cosine
,sine
))
861 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
863 shader
.uniform_float("color", colour
)
867 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
869 for i
in range(sides
+1):
871 if my
> bottom_bar
and mx
< area_width
:
872 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
873 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
874 vertices
.append((cosine
,sine
))
875 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
877 shader
.uniform_float("color", colour
)
880 # Bottom right corner
881 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, 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
)
894 # prepare drawing all edges in one batch
900 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
901 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
902 if m1x
< area_width
and m2x
< area_width
:
903 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
904 (m1x
,m1y
), (m1x
-radius
,m1y
)])
905 indices
.extend([(id_last
, id_last
+1, id_last
+3),
906 (id_last
+3, id_last
+1, id_last
+2)])
910 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
911 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
912 m1x
= min(m1x
, area_width
)
913 m2x
= min(m2x
, area_width
)
914 if m1y
> bottom_bar
and m2y
> bottom_bar
:
915 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
916 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
917 indices
.extend([(id_last
, id_last
+1, id_last
+3),
918 (id_last
+3, id_last
+1, id_last
+2)])
922 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
923 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
924 m1y
= max(m1y
, bottom_bar
)
925 m2y
= max(m2y
, bottom_bar
)
926 if m1x
< area_width
and m2x
< area_width
:
927 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
928 (m1x
+radius
,m1y
), (m1x
,m1y
)])
929 indices
.extend([(id_last
, id_last
+1, id_last
+3),
930 (id_last
+3, id_last
+1, id_last
+2)])
934 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
935 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
936 m1x
= min(m1x
, area_width
)
937 m2x
= min(m2x
, area_width
)
938 if m1y
> bottom_bar
and m2y
> bottom_bar
:
939 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
940 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
941 indices
.extend([(id_last
, id_last
+1, id_last
+3),
942 (id_last
+3, id_last
+1, id_last
+2)])
944 # now draw all edges in one batch
945 if len(vertices
) != 0:
946 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
948 shader
.uniform_float("color", colour
)
951 def draw_callback_nodeoutline(self
, context
, mode
):
955 bgl
.glEnable(bgl
.GL_BLEND
)
956 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
957 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
959 nodes
, links
= get_nodes_links(context
)
961 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
964 col_outer
= (1.0, 0.2, 0.2, 0.4)
965 col_inner
= (0.0, 0.0, 0.0, 0.5)
966 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
967 elif mode
== "LINKMENU":
968 col_outer
= (0.4, 0.6, 1.0, 0.4)
969 col_inner
= (0.0, 0.0, 0.0, 0.5)
970 col_circle_inner
= (0.08, 0.15, .3, 1.0)
972 col_outer
= (0.2, 1.0, 0.2, 0.4)
973 col_inner
= (0.0, 0.0, 0.0, 0.5)
974 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
976 m1x
= self
.mouse_path
[0][0]
977 m1y
= self
.mouse_path
[0][1]
978 m2x
= self
.mouse_path
[-1][0]
979 m2y
= self
.mouse_path
[-1][1]
981 n1
= nodes
[context
.scene
.NWLazySource
]
982 n2
= nodes
[context
.scene
.NWLazyTarget
]
985 col_outer
= (0.4, 0.4, 0.4, 0.4)
986 col_inner
= (0.0, 0.0, 0.0, 0.5)
987 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
989 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
990 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
991 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
992 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
994 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
995 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
998 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
999 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
1002 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
1003 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
1005 bgl
.glDisable(bgl
.GL_BLEND
)
1006 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
1007 def get_active_tree(context
):
1008 tree
= context
.space_data
.node_tree
1010 # Get nodes from currently edited tree.
1011 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1012 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1013 # the same as context.active_node, the user is in a group.
1014 # Check recursively until we find the real active node_tree:
1015 if tree
.nodes
.active
:
1016 while tree
.nodes
.active
!= context
.active_node
:
1017 tree
= tree
.nodes
.active
.node_tree
1021 def get_nodes_links(context
):
1022 tree
, path
= get_active_tree(context
)
1023 return tree
.nodes
, tree
.links
1025 def is_viewer_socket(socket
):
1026 # checks if a internal socket is a valid viewer socket
1027 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
1029 def get_internal_socket(socket
):
1030 #get the internal socket from a socket inside or outside the group
1032 if node
.type == 'GROUP_OUTPUT':
1033 source_iterator
= node
.inputs
1034 iterator
= node
.id_data
.outputs
1035 elif node
.type == 'GROUP_INPUT':
1036 source_iterator
= node
.outputs
1037 iterator
= node
.id_data
.inputs
1038 elif hasattr(node
, "node_tree"):
1039 if socket
.is_output
:
1040 source_iterator
= node
.outputs
1041 iterator
= node
.node_tree
.outputs
1043 source_iterator
= node
.inputs
1044 iterator
= node
.node_tree
.inputs
1048 for i
, s
in enumerate(source_iterator
):
1053 def is_viewer_link(link
, output_node
):
1054 if "Emission Viewer" in link
.to_node
.name
or link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
1056 if link
.to_node
.type == 'GROUP_OUTPUT':
1057 socket
= get_internal_socket(link
.to_socket
)
1058 if is_viewer_socket(socket
):
1062 def get_group_output_node(tree
):
1063 for node
in tree
.nodes
:
1064 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
1067 def get_output_location(tree
):
1068 # get right-most location
1069 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
1070 max_xloc_node
= sorted_by_xloc
[-1]
1071 if max_xloc_node
.name
== 'Emission Viewer':
1072 max_xloc_node
= sorted_by_xloc
[-2]
1074 # get average y location
1076 for node
in tree
.nodes
:
1077 sum_yloc
+= node
.location
.y
1079 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1080 loc_y
= sum_yloc
/ len(tree
.nodes
)
1084 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
1085 base_color
: StringProperty(
1087 default
='diffuse diff albedo base col color',
1088 description
='Naming Components for Base Color maps')
1089 sss_color
: StringProperty(
1090 name
='Subsurface Color',
1091 default
='sss subsurface',
1092 description
='Naming Components for Subsurface Color maps')
1093 metallic
: StringProperty(
1095 default
='metallic metalness metal mtl',
1096 description
='Naming Components for metallness maps')
1097 specular
: StringProperty(
1099 default
='specularity specular spec spc',
1100 description
='Naming Components for Specular maps')
1101 normal
: StringProperty(
1103 default
='normal nor nrm nrml norm',
1104 description
='Naming Components for Normal maps')
1105 bump
: StringProperty(
1108 description
='Naming Components for bump maps')
1109 rough
: StringProperty(
1111 default
='roughness rough rgh',
1112 description
='Naming Components for roughness maps')
1113 gloss
: StringProperty(
1115 default
='gloss glossy glossiness',
1116 description
='Naming Components for glossy maps')
1117 displacement
: StringProperty(
1118 name
='Displacement',
1119 default
='displacement displace disp dsp height heightmap',
1120 description
='Naming Components for displacement maps')
1121 transmission
: StringProperty(
1122 name
='Transmission',
1123 default
='transmission transparency',
1124 description
='Naming Components for transmission maps')
1125 emission
: StringProperty(
1127 default
='emission emissive emit',
1128 description
='Naming Components for emission maps')
1129 alpha
: StringProperty(
1131 default
='alpha opacity',
1132 description
='Naming Components for alpha maps')
1133 ambient_occlusion
: StringProperty(
1134 name
='Ambient Occlusion',
1135 default
='ao ambient occlusion',
1136 description
='Naming Components for AO maps')
1139 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1140 bl_idname
= __name__
1142 merge_hide
: EnumProperty(
1143 name
="Hide Mix nodes",
1145 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1146 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1147 ("NEVER", "Never", "Never collapse the new merge nodes")
1149 default
='NON_SHADER',
1150 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1151 merge_position
: EnumProperty(
1152 name
="Mix Node Position",
1154 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1155 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1158 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1160 show_hotkey_list
: BoolProperty(
1161 name
="Show Hotkey List",
1163 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1165 hotkey_list_filter
: StringProperty(
1166 name
=" Filter by Name",
1168 description
="Show only hotkeys that have this text in their name"
1170 show_principled_lists
: BoolProperty(
1171 name
="Show Principled naming tags",
1173 description
="Expand this box into a list of all naming tags for principled texture setup"
1175 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1177 def draw(self
, context
):
1178 layout
= self
.layout
1179 col
= layout
.column()
1180 col
.prop(self
, "merge_position")
1181 col
.prop(self
, "merge_hide")
1184 col
= box
.column(align
=True)
1185 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1186 if self
.show_principled_lists
:
1187 tags
= self
.principled_tags
1189 col
.prop(tags
, "base_color")
1190 col
.prop(tags
, "sss_color")
1191 col
.prop(tags
, "metallic")
1192 col
.prop(tags
, "specular")
1193 col
.prop(tags
, "rough")
1194 col
.prop(tags
, "gloss")
1195 col
.prop(tags
, "normal")
1196 col
.prop(tags
, "bump")
1197 col
.prop(tags
, "displacement")
1198 col
.prop(tags
, "transmission")
1199 col
.prop(tags
, "emission")
1200 col
.prop(tags
, "alpha")
1201 col
.prop(tags
, "ambient_occlusion")
1204 col
= box
.column(align
=True)
1205 hotkey_button_name
= "Show Hotkey List"
1206 if self
.show_hotkey_list
:
1207 hotkey_button_name
= "Hide Hotkey List"
1208 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1209 if self
.show_hotkey_list
:
1210 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1212 for hotkey
in kmi_defs
:
1214 hotkey_name
= hotkey
[7]
1216 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1217 row
= col
.row(align
=True)
1218 row
.label(text
=hotkey_name
)
1219 keystr
= nice_hotkey_name(hotkey
[1])
1221 keystr
= "Shift " + keystr
1223 keystr
= "Alt " + keystr
1225 keystr
= "Ctrl " + keystr
1226 row
.label(text
=keystr
)
1230 def nw_check(context
):
1231 space
= context
.space_data
1232 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1235 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1242 def poll(cls
, context
):
1243 return nw_check(context
)
1247 class NWLazyMix(Operator
, NWBase
):
1248 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1249 bl_idname
= "node.nw_lazy_mix"
1250 bl_label
= "Mix Nodes"
1251 bl_options
= {'REGISTER', 'UNDO'}
1253 def modal(self
, context
, event
):
1254 context
.area
.tag_redraw()
1255 nodes
, links
= get_nodes_links(context
)
1258 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1261 if not context
.scene
.NWBusyDrawing
:
1262 node1
= node_at_pos(nodes
, context
, event
)
1264 context
.scene
.NWBusyDrawing
= node1
.name
1266 if context
.scene
.NWBusyDrawing
!= 'STOP':
1267 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1269 context
.scene
.NWLazySource
= node1
.name
1270 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1272 if event
.type == 'MOUSEMOVE':
1273 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1275 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1276 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1277 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1280 node2
= node_at_pos(nodes
, context
, event
)
1282 context
.scene
.NWBusyDrawing
= node2
.name
1294 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1296 context
.scene
.NWBusyDrawing
= ""
1299 elif event
.type == 'ESC':
1301 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1302 return {'CANCELLED'}
1304 return {'RUNNING_MODAL'}
1306 def invoke(self
, context
, event
):
1307 if context
.area
.type == 'NODE_EDITOR':
1308 # the arguments we pass the the callback
1309 args
= (self
, context
, 'MIX')
1310 # Add the region OpenGL drawing callback
1311 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1312 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1314 self
.mouse_path
= []
1316 context
.window_manager
.modal_handler_add(self
)
1317 return {'RUNNING_MODAL'}
1319 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1320 return {'CANCELLED'}
1323 class NWLazyConnect(Operator
, NWBase
):
1324 """Connect two nodes without clicking a specific socket (automatically determined"""
1325 bl_idname
= "node.nw_lazy_connect"
1326 bl_label
= "Lazy Connect"
1327 bl_options
= {'REGISTER', 'UNDO'}
1328 with_menu
: BoolProperty()
1330 def modal(self
, context
, event
):
1331 context
.area
.tag_redraw()
1332 nodes
, links
= get_nodes_links(context
)
1335 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1338 if not context
.scene
.NWBusyDrawing
:
1339 node1
= node_at_pos(nodes
, context
, event
)
1341 context
.scene
.NWBusyDrawing
= node1
.name
1343 if context
.scene
.NWBusyDrawing
!= 'STOP':
1344 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1346 context
.scene
.NWLazySource
= node1
.name
1347 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1349 if event
.type == 'MOUSEMOVE':
1350 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1352 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1353 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1354 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1357 node2
= node_at_pos(nodes
, context
, event
)
1359 context
.scene
.NWBusyDrawing
= node2
.name
1364 link_success
= False
1370 if node
.select
== True:
1372 original_sel
.append(node
)
1374 original_unsel
.append(node
)
1378 #link_success = autolink(node1, node2, links)
1380 if len(node1
.outputs
) > 1 and node2
.inputs
:
1381 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1382 elif len(node1
.outputs
) == 1:
1383 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1385 link_success
= autolink(node1
, node2
, links
)
1387 for node
in original_sel
:
1389 for node
in original_unsel
:
1393 force_update(context
)
1394 context
.scene
.NWBusyDrawing
= ""
1397 elif event
.type == 'ESC':
1398 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1399 return {'CANCELLED'}
1401 return {'RUNNING_MODAL'}
1403 def invoke(self
, context
, event
):
1404 if context
.area
.type == 'NODE_EDITOR':
1405 nodes
, links
= get_nodes_links(context
)
1406 node
= node_at_pos(nodes
, context
, event
)
1408 context
.scene
.NWBusyDrawing
= node
.name
1410 # the arguments we pass the the callback
1414 args
= (self
, context
, mode
)
1415 # Add the region OpenGL drawing callback
1416 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1417 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1419 self
.mouse_path
= []
1421 context
.window_manager
.modal_handler_add(self
)
1422 return {'RUNNING_MODAL'}
1424 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1425 return {'CANCELLED'}
1428 class NWDeleteUnused(Operator
, NWBase
):
1429 """Delete all nodes whose output is not used"""
1430 bl_idname
= 'node.nw_del_unused'
1431 bl_label
= 'Delete Unused Nodes'
1432 bl_options
= {'REGISTER', 'UNDO'}
1434 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1435 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1437 def is_unused_node(self
, node
):
1438 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1439 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1440 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1441 if node
.type in end_types
:
1444 for output
in node
.outputs
:
1450 def poll(cls
, context
):
1452 if nw_check(context
):
1453 if context
.space_data
.node_tree
.nodes
:
1457 def execute(self
, context
):
1458 nodes
, links
= get_nodes_links(context
)
1463 if node
.select
== True:
1464 selection
.append(node
.name
)
1470 temp_deleted_nodes
= []
1471 del_unused_iterations
= len(nodes
)
1472 for it
in range(0, del_unused_iterations
):
1473 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1475 if self
.is_unused_node(node
):
1477 deleted_nodes
.append(node
.name
)
1478 bpy
.ops
.node
.delete()
1480 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1483 if self
.delete_frames
:
1491 frames_in_use
.append(node
.parent
)
1493 if node
.type == 'FRAME' and node
not in frames_in_use
:
1496 repeat
= True # repeat for nested frames
1498 if node
not in frames_in_use
:
1500 deleted_nodes
.append(node
.name
)
1501 bpy
.ops
.node
.delete()
1503 if self
.delete_muted
:
1507 deleted_nodes
.append(node
.name
)
1508 bpy
.ops
.node
.delete_reconnect()
1510 # get unique list of deleted nodes (iterations would count the same node more than once)
1511 deleted_nodes
= list(set(deleted_nodes
))
1512 for n
in deleted_nodes
:
1513 self
.report({'INFO'}, "Node " + n
+ " deleted")
1514 num_deleted
= len(deleted_nodes
)
1519 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1521 self
.report({'INFO'}, "Nothing deleted")
1524 nodes
, links
= get_nodes_links(context
)
1526 if node
.name
in selection
:
1530 def invoke(self
, context
, event
):
1531 return context
.window_manager
.invoke_confirm(self
, event
)
1534 class NWSwapLinks(Operator
, NWBase
):
1535 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1536 bl_idname
= 'node.nw_swap_links'
1537 bl_label
= 'Swap Links'
1538 bl_options
= {'REGISTER', 'UNDO'}
1541 def poll(cls
, context
):
1543 if nw_check(context
):
1544 if context
.selected_nodes
:
1545 valid
= len(context
.selected_nodes
) <= 2
1548 def execute(self
, context
):
1549 nodes
, links
= get_nodes_links(context
)
1550 selected_nodes
= context
.selected_nodes
1551 n1
= selected_nodes
[0]
1554 if len(selected_nodes
) == 2:
1555 n2
= selected_nodes
[1]
1556 if n1
.outputs
and n2
.outputs
:
1561 for output
in n1
.outputs
:
1563 for link
in output
.links
:
1564 n1_outputs
.append([out_index
, link
.to_socket
])
1569 for output
in n2
.outputs
:
1571 for link
in output
.links
:
1572 n2_outputs
.append([out_index
, link
.to_socket
])
1576 for connection
in n1_outputs
:
1578 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1580 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1581 for connection
in n2_outputs
:
1583 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1585 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1587 if n1
.outputs
or n2
.outputs
:
1588 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1590 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1593 elif len(selected_nodes
) == 1:
1594 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1595 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1600 for i1
in n1
.inputs
:
1601 if i1
.is_linked
and not i1
.is_multi_input
:
1603 for i2
in n1
.inputs
:
1604 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1606 types
.append ([i1
, similar_types
, i
])
1608 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1613 for i2
in n1
.inputs
:
1614 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1616 i1f
= pair
[0].links
[0].from_socket
1617 i1t
= pair
[0].links
[0].to_socket
1618 i2f
= pair
[1].links
[0].from_socket
1619 i2t
= pair
[1].links
[0].to_socket
1624 fs
= t
[0].links
[0].from_socket
1626 links
.remove(t
[0].links
[0])
1627 if i
+1 == len(n1
.inputs
):
1630 while n1
.inputs
[i
].is_linked
:
1632 links
.new(fs
, n1
.inputs
[i
])
1633 elif len(types
) == 2:
1634 i1f
= types
[0][0].links
[0].from_socket
1635 i1t
= types
[0][0].links
[0].to_socket
1636 i2f
= types
[1][0].links
[0].from_socket
1637 i2t
= types
[1][0].links
[0].to_socket
1642 self
.report({'WARNING'}, "This node has no input connections to swap!")
1644 self
.report({'WARNING'}, "This node has no inputs to swap!")
1646 force_update(context
)
1650 class NWResetBG(Operator
, NWBase
):
1651 """Reset the zoom and position of the background image"""
1652 bl_idname
= 'node.nw_bg_reset'
1653 bl_label
= 'Reset Backdrop'
1654 bl_options
= {'REGISTER', 'UNDO'}
1657 def poll(cls
, context
):
1659 if nw_check(context
):
1660 snode
= context
.space_data
1661 valid
= snode
.tree_type
== 'CompositorNodeTree'
1664 def execute(self
, context
):
1665 context
.space_data
.backdrop_zoom
= 1
1666 context
.space_data
.backdrop_offset
[0] = 0
1667 context
.space_data
.backdrop_offset
[1] = 0
1671 class NWAddAttrNode(Operator
, NWBase
):
1672 """Add an Attribute node with this name"""
1673 bl_idname
= 'node.nw_add_attr_node'
1674 bl_label
= 'Add UV map'
1675 bl_options
= {'REGISTER', 'UNDO'}
1677 attr_name
: StringProperty()
1679 def execute(self
, context
):
1680 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1681 nodes
, links
= get_nodes_links(context
)
1682 nodes
.active
.attribute_name
= self
.attr_name
1685 class NWPreviewNode(Operator
, NWBase
):
1686 bl_idname
= "node.nw_preview_node"
1687 bl_label
= "Preview Node"
1688 bl_description
= "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output"
1689 bl_options
= {'REGISTER', 'UNDO'}
1691 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1692 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1693 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1694 run_in_geometry_nodes
: BoolProperty(default
=True)
1697 self
.shader_output_type
= ""
1698 self
.shader_output_ident
= ""
1699 self
.shader_viewer_ident
= ""
1702 def poll(cls
, context
):
1703 if nw_check(context
):
1704 space
= context
.space_data
1705 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1706 if context
.active_node
:
1707 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1713 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1714 #check if a viewer output already exists in a node group otherwise create
1715 if hasattr(node
, "node_tree"):
1717 if len(node
.node_tree
.outputs
):
1719 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1720 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1721 #if viewer output is already used but leads to the same socket we can still use it
1722 is_used
= self
.is_socket_used_other_mats(socket
)
1724 if connect_socket
== None:
1726 groupout
= get_group_output_node(node
.node_tree
)
1727 groupout_input
= groupout
.inputs
[i
]
1728 links
= groupout_input
.links
1729 if connect_socket
not in [link
.from_socket
for link
in links
]:
1735 if not index
and free_socket
:
1739 #create viewer socket
1740 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1741 index
= len(node
.node_tree
.outputs
) - 1
1742 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1745 def init_shader_variables(self
, space
, shader_type
):
1746 if shader_type
== 'OBJECT':
1747 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1748 self
.shader_output_type
= "OUTPUT_MATERIAL"
1749 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1750 self
.shader_viewer_ident
= "ShaderNodeEmission"
1752 self
.shader_output_type
= "OUTPUT_LIGHT"
1753 self
.shader_output_ident
= "ShaderNodeOutputLight"
1754 self
.shader_viewer_ident
= "ShaderNodeEmission"
1756 elif shader_type
== 'WORLD':
1757 self
.shader_output_type
= "OUTPUT_WORLD"
1758 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1759 self
.shader_viewer_ident
= "ShaderNodeBackground"
1761 def get_shader_output_node(self
, tree
):
1762 for node
in tree
.nodes
:
1763 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1767 def ensure_group_output(cls
, tree
):
1768 #check if a group output node exists otherwise create
1769 groupout
= get_group_output_node(tree
)
1771 groupout
= tree
.nodes
.new('NodeGroupOutput')
1772 loc_x
, loc_y
= get_output_location(tree
)
1773 groupout
.location
.x
= loc_x
1774 groupout
.location
.y
= loc_y
1775 groupout
.select
= False
1776 # So that we don't keep on adding new group outputs
1777 groupout
.is_active_output
= True
1781 def search_sockets(cls
, node
, sockets
, index
=None):
1782 # recursively scan nodes for viewer sockets and store in list
1783 for i
, input_socket
in enumerate(node
.inputs
):
1784 if index
and i
!= index
:
1786 if len(input_socket
.links
):
1787 link
= input_socket
.links
[0]
1788 next_node
= link
.from_node
1789 external_socket
= link
.from_socket
1790 if hasattr(next_node
, "node_tree"):
1791 for socket_index
, s
in enumerate(next_node
.outputs
):
1792 if s
== external_socket
:
1794 socket
= next_node
.node_tree
.outputs
[socket_index
]
1795 if is_viewer_socket(socket
) and socket
not in sockets
:
1796 sockets
.append(socket
)
1797 #continue search inside of node group but restrict socket to where we came from
1798 groupout
= get_group_output_node(next_node
.node_tree
)
1799 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1802 def scan_nodes(cls
, tree
, sockets
):
1803 # get all viewer sockets in a material tree
1804 for node
in tree
.nodes
:
1805 if hasattr(node
, "node_tree"):
1806 for socket
in node
.node_tree
.outputs
:
1807 if is_viewer_socket(socket
) and (socket
not in sockets
):
1808 sockets
.append(socket
)
1809 cls
.scan_nodes(node
.node_tree
, sockets
)
1811 def link_leads_to_used_socket(self
, link
):
1812 #return True if link leads to a socket that is already used in this material
1813 socket
= get_internal_socket(link
.to_socket
)
1814 return (socket
and self
.is_socket_used_active_mat(socket
))
1816 def is_socket_used_active_mat(self
, socket
):
1817 #ensure used sockets in active material is calculated and check given socket
1818 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1819 self
.used_viewer_sockets_active_mat
= []
1820 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1822 emission
= self
.get_viewer_node(materialout
)
1823 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_active_mat
)
1824 return socket
in self
.used_viewer_sockets_active_mat
1826 def is_socket_used_other_mats(self
, socket
):
1827 #ensure used sockets in other materials are calculated and check given socket
1828 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1829 self
.used_viewer_sockets_other_mats
= []
1830 for mat
in bpy
.data
.materials
:
1831 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1834 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1836 emission
= self
.get_viewer_node(materialout
)
1837 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_other_mats
)
1838 return socket
in self
.used_viewer_sockets_other_mats
1841 def get_viewer_node(materialout
):
1842 input_socket
= materialout
.inputs
[0]
1843 if len(input_socket
.links
) > 0:
1844 node
= input_socket
.links
[0].from_node
1845 if node
.type == 'EMISSION' and node
.name
== "Emission Viewer":
1848 def invoke(self
, context
, event
):
1849 space
= context
.space_data
1850 # Ignore operator when running in wrong context.
1851 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1852 return {'PASS_THROUGH'}
1854 shader_type
= space
.shader_type
1855 self
.init_shader_variables(space
, shader_type
)
1856 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1857 mlocx
= event
.mouse_region_x
1858 mlocy
= event
.mouse_region_y
1859 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
1860 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1861 active_tree
, path_to_tree
= get_active_tree(context
)
1862 nodes
, links
= active_tree
.nodes
, active_tree
.links
1863 base_node_tree
= space
.node_tree
1864 active
= nodes
.active
1866 # For geometry node trees we just connect to the group output,
1867 # because there is no "viewer node" yet.
1868 if space
.tree_type
== "GeometryNodeTree":
1871 for out
in active
.outputs
:
1872 if is_visible_socket(out
):
1881 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1882 self
.scan_nodes(base_node_tree
, delete_sockets
)
1884 # Find (or create if needed) the output of this node tree
1885 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1887 # Analyze outputs, make links
1890 for i
, out
in enumerate(active
.outputs
):
1891 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1892 valid_outputs
.append(i
)
1894 out_i
= valid_outputs
[0] # Start index of node's outputs
1895 for i
, valid_i
in enumerate(valid_outputs
):
1896 for out_link
in active
.outputs
[valid_i
].links
:
1897 if is_viewer_link(out_link
, geometryoutput
):
1898 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1899 if i
< len(valid_outputs
) - 1:
1900 out_i
= valid_outputs
[i
+ 1]
1902 out_i
= valid_outputs
[0]
1904 make_links
= [] # store sockets for new links
1905 delete_nodes
= [] # store unused nodes to delete in the end
1907 # If there is no 'GEOMETRY' output type - We can't preview the node
1910 socket_type
= 'GEOMETRY'
1911 # Find an input socket of the output of type geometry
1912 geometryoutindex
= None
1913 for i
,inp
in enumerate(geometryoutput
.inputs
):
1914 if inp
.type == socket_type
:
1915 geometryoutindex
= i
1917 if geometryoutindex
is None:
1918 # Create geometry socket
1919 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1920 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1922 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1923 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1924 for li_from
, li_to
in make_links
:
1925 base_node_tree
.links
.new(li_from
, li_to
)
1926 tree
= base_node_tree
1927 link_end
= output_socket
1928 while tree
.nodes
.active
!= active
:
1929 node
= tree
.nodes
.active
1930 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1931 link_start
= node
.outputs
[index
]
1932 node_socket
= node
.node_tree
.outputs
[index
]
1933 if node_socket
in delete_sockets
:
1934 delete_sockets
.remove(node_socket
)
1935 tree
.links
.new(link_start
, link_end
)
1937 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1938 tree
= tree
.nodes
.active
.node_tree
1939 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1942 for socket
in delete_sockets
:
1943 tree
= socket
.id_data
1944 tree
.outputs
.remove(socket
)
1947 for tree
, node
in delete_nodes
:
1948 tree
.nodes
.remove(node
)
1950 nodes
.active
= active
1951 active
.select
= True
1952 force_update(context
)
1956 # What follows is code for the shader editor
1957 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1960 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
):
1961 for out
in active
.outputs
:
1962 if is_visible_socket(out
):
1966 # get material_output node
1967 materialout
= None # placeholder node
1970 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1971 self
.scan_nodes(base_node_tree
, delete_sockets
)
1973 materialout
= self
.get_shader_output_node(base_node_tree
)
1975 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1976 materialout
.location
= get_output_location(base_node_tree
)
1977 materialout
.select
= False
1978 # Analyze outputs, add "Emission Viewer" if needed, make links
1981 for i
, out
in enumerate(active
.outputs
):
1982 if is_visible_socket(out
):
1983 valid_outputs
.append(i
)
1985 out_i
= valid_outputs
[0] # Start index of node's outputs
1986 for i
, valid_i
in enumerate(valid_outputs
):
1987 for out_link
in active
.outputs
[valid_i
].links
:
1988 if is_viewer_link(out_link
, materialout
):
1989 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1990 if i
< len(valid_outputs
) - 1:
1991 out_i
= valid_outputs
[i
+ 1]
1993 out_i
= valid_outputs
[0]
1995 make_links
= [] # store sockets for new links
1996 delete_nodes
= [] # store unused nodes to delete in the end
1998 # If output type not 'SHADER' - "Emission Viewer" needed
1999 if active
.outputs
[out_i
].type != 'SHADER':
2000 socket_type
= 'NodeSocketColor'
2001 # get Emission Viewer node
2002 emission_exists
= False
2003 emission_placeholder
= base_node_tree
.nodes
[0]
2004 for node
in base_node_tree
.nodes
:
2005 if "Emission Viewer" in node
.name
:
2006 emission_exists
= True
2007 emission_placeholder
= node
2008 if not emission_exists
:
2009 emission
= base_node_tree
.nodes
.new(self
.shader_viewer_ident
)
2010 emission
.hide
= True
2011 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
2012 emission
.label
= "Viewer"
2013 emission
.name
= "Emission Viewer"
2014 emission
.use_custom_color
= True
2015 emission
.color
= (0.6, 0.5, 0.4)
2016 emission
.select
= False
2018 emission
= emission_placeholder
2019 output_socket
= emission
.inputs
[0]
2021 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2022 if emission
.outputs
[0].links
.__len
__() > 0:
2023 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
2024 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2026 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2028 # Set brightness of viewer to compensate for Film and CM exposure
2029 if context
.scene
.render
.engine
== 'CYCLES' and hasattr(context
.scene
, 'cycles'):
2030 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
2034 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
2035 emission
.inputs
[1].default_value
= intensity
2038 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2039 socket_type
= 'NodeSocketShader'
2040 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
2041 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
2042 output_socket
= materialout
.inputs
[materialout_index
]
2043 for node
in base_node_tree
.nodes
:
2044 if node
.name
== 'Emission Viewer':
2045 delete_nodes
.append((base_node_tree
, node
))
2046 for li_from
, li_to
in make_links
:
2047 base_node_tree
.links
.new(li_from
, li_to
)
2049 # Create links through node groups until we reach the active node
2050 tree
= base_node_tree
2051 link_end
= output_socket
2052 while tree
.nodes
.active
!= active
:
2053 node
= tree
.nodes
.active
2054 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
2055 link_start
= node
.outputs
[index
]
2056 node_socket
= node
.node_tree
.outputs
[index
]
2057 if node_socket
in delete_sockets
:
2058 delete_sockets
.remove(node_socket
)
2059 tree
.links
.new(link_start
, link_end
)
2061 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
2062 tree
= tree
.nodes
.active
.node_tree
2063 tree
.links
.new(active
.outputs
[out_i
], link_end
)
2066 for socket
in delete_sockets
:
2067 if not self
.is_socket_used_other_mats(socket
):
2068 tree
= socket
.id_data
2069 tree
.outputs
.remove(socket
)
2072 for tree
, node
in delete_nodes
:
2073 tree
.nodes
.remove(node
)
2075 nodes
.active
= active
2076 active
.select
= True
2078 force_update(context
)
2082 return {'CANCELLED'}
2085 class NWFrameSelected(Operator
, NWBase
):
2086 bl_idname
= "node.nw_frame_selected"
2087 bl_label
= "Frame Selected"
2088 bl_description
= "Add a frame node and parent the selected nodes to it"
2089 bl_options
= {'REGISTER', 'UNDO'}
2091 label_prop
: StringProperty(
2093 description
='The visual name of the frame node',
2096 color_prop
: FloatVectorProperty(
2098 description
="The color of the frame node",
2099 default
=(0.6, 0.6, 0.6),
2100 min=0, max=1, step
=1, precision
=3,
2101 subtype
='COLOR_GAMMA', size
=3
2104 def execute(self
, context
):
2105 nodes
, links
= get_nodes_links(context
)
2108 if node
.select
== True:
2109 selected
.append(node
)
2111 bpy
.ops
.node
.add_node(type='NodeFrame')
2113 frm
.label
= self
.label_prop
2114 frm
.use_custom_color
= True
2115 frm
.color
= self
.color_prop
2117 for node
in selected
:
2123 class NWReloadImages(Operator
):
2124 bl_idname
= "node.nw_reload_images"
2125 bl_label
= "Reload Images"
2126 bl_description
= "Update all the image nodes to match their files on disk"
2129 def poll(cls
, context
):
2131 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
2132 if context
.active_node
is not None:
2133 for out
in context
.active_node
.outputs
:
2134 if is_visible_socket(out
):
2139 def execute(self
, context
):
2140 nodes
, links
= get_nodes_links(context
)
2141 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2144 if node
.type in image_types
:
2145 if node
.type == "TEXTURE":
2146 if node
.texture
: # node has texture assigned
2147 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2148 if node
.texture
.image
: # texture has image assigned
2149 node
.texture
.image
.reload()
2157 self
.report({'INFO'}, "Reloaded images")
2158 print("Reloaded " + str(num_reloaded
) + " images")
2159 force_update(context
)
2162 self
.report({'WARNING'}, "No images found to reload in this node tree")
2163 return {'CANCELLED'}
2166 class NWSwitchNodeType(Operator
, NWBase
):
2167 """Switch type of selected nodes """
2168 bl_idname
= "node.nw_swtch_node_type"
2169 bl_label
= "Switch Node Type"
2170 bl_options
= {'REGISTER', 'UNDO'}
2172 to_type
: EnumProperty(
2173 name
="Switch to type",
2174 items
=list(shaders_input_nodes_props
) +
2175 list(shaders_output_nodes_props
) +
2176 list(shaders_shader_nodes_props
) +
2177 list(shaders_texture_nodes_props
) +
2178 list(shaders_color_nodes_props
) +
2179 list(shaders_vector_nodes_props
) +
2180 list(shaders_converter_nodes_props
) +
2181 list(shaders_layout_nodes_props
) +
2182 list(compo_input_nodes_props
) +
2183 list(compo_output_nodes_props
) +
2184 list(compo_color_nodes_props
) +
2185 list(compo_converter_nodes_props
) +
2186 list(compo_filter_nodes_props
) +
2187 list(compo_vector_nodes_props
) +
2188 list(compo_matte_nodes_props
) +
2189 list(compo_distort_nodes_props
) +
2190 list(compo_layout_nodes_props
) +
2191 list(blender_mat_input_nodes_props
) +
2192 list(blender_mat_output_nodes_props
) +
2193 list(blender_mat_color_nodes_props
) +
2194 list(blender_mat_vector_nodes_props
) +
2195 list(blender_mat_converter_nodes_props
) +
2196 list(blender_mat_layout_nodes_props
) +
2197 list(texture_input_nodes_props
) +
2198 list(texture_output_nodes_props
) +
2199 list(texture_color_nodes_props
) +
2200 list(texture_pattern_nodes_props
) +
2201 list(texture_textures_nodes_props
) +
2202 list(texture_converter_nodes_props
) +
2203 list(texture_distort_nodes_props
) +
2204 list(texture_layout_nodes_props
)
2207 geo_to_type
: StringProperty(
2208 name
="Switch to type",
2212 def execute(self
, context
):
2213 nodes
, links
= get_nodes_links(context
)
2214 to_type
= self
.to_type
2215 if self
.geo_to_type
!= '':
2216 to_type
= self
.geo_to_type
2217 # Those types of nodes will not swap.
2218 src_excludes
= ('NodeFrame')
2219 # Those attributes of nodes will be copied if possible
2220 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
2221 'show_options', 'show_preview', 'show_texture',
2222 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2224 selected
= [n
for n
in nodes
if n
.select
]
2226 for node
in [n
for n
in selected
if
2227 n
.rna_type
.identifier
not in src_excludes
and
2228 n
.rna_type
.identifier
!= to_type
]:
2229 new_node
= nodes
.new(to_type
)
2230 for attr
in attrs_to_pass
:
2231 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
2232 setattr(new_node
, attr
, getattr(node
, attr
))
2233 # set image datablock of dst to image of src
2234 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
2236 new_node
.image
= node
.image
2238 if new_node
.type == 'SWITCH':
2239 new_node
.hide
= True
2240 # Dictionaries: src_sockets and dst_sockets:
2241 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2242 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2243 # in 'INPUTS' and 'OUTPUTS':
2244 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2246 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2248 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2249 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2252 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2253 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2255 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2256 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2257 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2258 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
2259 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2260 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
2261 # enumerate in inputs, then in outputs
2262 # find name, default value and links of socket
2263 for i
, socket
in enumerate(in_out
):
2264 the_name
= socket
.name
2266 # Not every socket, especially in outputs has "default_value"
2267 if hasattr(socket
, 'default_value'):
2268 dval
= socket
.default_value
2270 for lnk
in socket
.links
:
2271 socket_links
.append(lnk
)
2272 # check type of socket to fill proper keys.
2273 for the_type
in types_order_one
:
2274 if socket
.type == the_type
:
2275 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2276 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2277 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
2278 # Check which of the types in inputs/outputs is considered to be "main".
2279 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2280 for type_check
in types_order_one
:
2281 if sockets
[in_out_name
][type_check
]:
2282 sockets
[in_out_name
]['MAIN'] = type_check
2286 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2287 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2290 for inout
, soctype
in (
2291 ('INPUTS', 'MAIN',),
2292 ('INPUTS', 'SHADER',),
2293 ('INPUTS', 'RGBA',),
2294 ('INPUTS', 'VECTOR',),
2295 ('INPUTS', 'VALUE',),
2296 ('OUTPUTS', 'MAIN',),
2297 ('OUTPUTS', 'SHADER',),
2298 ('OUTPUTS', 'RGBA',),
2299 ('OUTPUTS', 'VECTOR',),
2300 ('OUTPUTS', 'VALUE',),
2302 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
2303 if soctype
== 'MAIN':
2304 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
2305 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
2307 sc
= src_sockets
[inout
][soctype
]
2308 dt
= dst_sockets
[inout
][soctype
]
2309 # start with 'dt' to determine number of possibilities.
2310 for i
, soc
in enumerate(dt
):
2311 # if src main has enough entries - match them with dst main sockets by indexes.
2313 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
2314 # add 'VALUE_NAME' criterion to inputs.
2315 if inout
== 'INPUTS' and soctype
== 'VALUE':
2317 if s
[2] == soc
[2]: # if names match
2318 # append src (index, dval), dst (index, dval)
2319 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
2321 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2322 # This creates better links when relinking textures.
2323 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
2324 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
2326 # Pass default values and RELINK:
2327 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2328 # INPUTS: Base on matches in proper order.
2329 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
2331 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
2332 new_node
.inputs
[dst_i
].default_value
= src_dval
2333 # Special case: switch to math
2334 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2335 new_node
.type == 'MATH' and\
2337 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
2338 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
2339 if node
.type == 'MIX_RGB':
2340 if node
.blend_type
in [o
[0] for o
in operations
]:
2341 new_node
.operation
= node
.blend_type
2342 # Special case: switch from math to some types
2343 if node
.type == 'MATH' and\
2344 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2347 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
2348 if new_node
.type == 'MIX_RGB':
2349 if node
.operation
in [t
[0] for t
in blend_types
]:
2350 new_node
.blend_type
= node
.operation
2351 # Set Fac of MIX_RGB to 1.0
2352 new_node
.inputs
[0].default_value
= 1.0
2353 # make link only when dst matching input is not linked already.
2354 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
2355 in_src_link
= node
.inputs
[src_i
].links
[0]
2356 in_dst_socket
= new_node
.inputs
[dst_i
]
2357 links
.new(in_src_link
.from_socket
, in_dst_socket
)
2358 links
.remove(in_src_link
)
2359 # OUTPUTS: Base on matches in proper order.
2360 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
2361 for out_src_link
in node
.outputs
[src_i
].links
:
2362 out_dst_socket
= new_node
.outputs
[dst_i
]
2363 links
.new(out_dst_socket
, out_src_link
.to_socket
)
2364 # relink rest inputs if possible, no criteria
2365 for src_inp
in node
.inputs
:
2366 for dst_inp
in new_node
.inputs
:
2367 if src_inp
.links
and not dst_inp
.links
:
2368 src_link
= src_inp
.links
[0]
2369 links
.new(src_link
.from_socket
, dst_inp
)
2370 links
.remove(src_link
)
2371 # relink rest outputs if possible, base on node kind if any left.
2372 for src_o
in node
.outputs
:
2373 for out_src_link
in src_o
.links
:
2374 for dst_o
in new_node
.outputs
:
2375 if src_o
.type == dst_o
.type:
2376 links
.new(dst_o
, out_src_link
.to_socket
)
2377 # relink rest outputs no criteria if any left. Link all from first output.
2378 for src_o
in node
.outputs
:
2379 for out_src_link
in src_o
.links
:
2380 if new_node
.outputs
:
2381 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2383 force_update(context
)
2387 class NWMergeNodes(Operator
, NWBase
):
2388 bl_idname
= "node.nw_merge_nodes"
2389 bl_label
= "Merge Nodes"
2390 bl_description
= "Merge Selected Nodes"
2391 bl_options
= {'REGISTER', 'UNDO'}
2395 description
="All possible blend types, boolean operations and math operations",
2396 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
],
2398 merge_type
: EnumProperty(
2400 description
="Type of Merge to be used",
2402 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2403 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2404 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2405 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2406 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2407 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2408 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2412 # Check if the link connects to a node that is in selected_nodes
2413 # If not, then check recursively for each link in the nodes outputs.
2414 # If yes, return True. If the recursion stops without finding a node
2415 # in selected_nodes, it returns False. The depth is used to prevent
2416 # getting stuck in a loop because of an already present cycle.
2418 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
2420 # We're stuck in a cycle, but that cycle was already present,
2421 # so we return False.
2422 # NOTE: The number 255 is arbitrary, but seems to work well.
2425 if node
in selected_nodes
:
2427 if not node
.outputs
:
2429 for output
in node
.outputs
:
2430 if output
.is_linked
:
2431 for olink
in output
.links
:
2432 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
2434 # None of the outputs found a node in selected_nodes, so there is no cycle.
2437 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2438 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2439 # be connected. The last one is assumed to be a multi input socket.
2440 # For convenience the node is returned.
2442 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
2443 # The y-location of the last node
2444 loc_y
= nodes_list
[-1][2]
2445 if merge_position
== 'CENTER':
2446 # Average the y-location
2447 for i
in range(len(nodes_list
)-1):
2448 loc_y
+= nodes_list
[i
][2]
2449 loc_y
= loc_y
/len(nodes_list
)
2450 new_node
= nodes
.new(node_name
)
2451 new_node
.hide
= do_hide
2452 new_node
.location
.x
= loc_x
2453 new_node
.location
.y
= loc_y
2454 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
2456 outputs_for_multi_input
= []
2457 for i
,node
in enumerate(selected_nodes
):
2459 # Search for the first node which had output links that do not create
2460 # a cycle, which we can then reconnect afterwards.
2461 if prev_links
== [] and node
.outputs
[0].is_linked
:
2462 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
2463 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2464 # To get the placement to look right we need to reverse the order in which we connect the
2465 # outputs to the multi input socket.
2466 if i
< len(socket_indices
) - 1:
2467 ind
= socket_indices
[i
]
2468 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
2470 outputs_for_multi_input
.insert(0, node
.outputs
[0])
2471 if outputs_for_multi_input
!= []:
2472 ind
= socket_indices
[-1]
2473 for output
in outputs_for_multi_input
:
2474 links
.new(output
, new_node
.inputs
[ind
])
2475 if prev_links
!= []:
2476 for link
in prev_links
:
2477 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
2480 def execute(self
, context
):
2481 settings
= context
.preferences
.addons
[__name__
].preferences
2482 merge_hide
= settings
.merge_hide
2483 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2486 do_hide_shader
= False
2487 if merge_hide
== 'ALWAYS':
2489 do_hide_shader
= True
2490 elif merge_hide
== 'NON_SHADER':
2493 tree_type
= context
.space_data
.node_tree
.type
2494 if tree_type
== 'GEOMETRY':
2495 node_type
= 'GeometryNode'
2496 if tree_type
== 'COMPOSITING':
2497 node_type
= 'CompositorNode'
2498 elif tree_type
== 'SHADER':
2499 node_type
= 'ShaderNode'
2500 elif tree_type
== 'TEXTURE':
2501 node_type
= 'TextureNode'
2502 nodes
, links
= get_nodes_links(context
)
2504 merge_type
= self
.merge_type
2505 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2506 # 'ZCOMBINE' works only if mode == 'MIX'
2507 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2508 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2511 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2513 # The math nodes used for geometry nodes are of type 'ShaderNode'
2514 if merge_type
== 'MATH' and tree_type
== 'GEOMETRY':
2515 node_type
= 'ShaderNode'
2516 selected_mix
= [] # entry = [index, loc]
2517 selected_shader
= [] # entry = [index, loc]
2518 selected_geometry
= [] # entry = [index, loc]
2519 selected_math
= [] # entry = [index, loc]
2520 selected_vector
= [] # entry = [index, loc]
2521 selected_z
= [] # entry = [index, loc]
2522 selected_alphaover
= [] # entry = [index, loc]
2524 for i
, node
in enumerate(nodes
):
2525 if node
.select
and node
.outputs
:
2526 if merge_type
== 'AUTO':
2527 for (type, types_list
, dst
) in (
2528 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2529 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2530 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2531 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2532 ('VECTOR', [], selected_vector
),
2534 output_type
= node
.outputs
[0].type
2535 valid_mode
= mode
in types_list
2536 # When mode is 'MIX' we have to cheat since the mix node is not used in
2538 if tree_type
== 'GEOMETRY':
2540 if output_type
== 'VALUE' and type == 'VALUE':
2542 elif output_type
== 'VECTOR' and type == 'VECTOR':
2544 elif type == 'GEOMETRY':
2546 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2547 # Cheat that output type is 'RGBA',
2548 # and that 'MIX' exists in math operations list.
2549 # This way when selected_mix list is analyzed:
2550 # Node data will be appended even though it doesn't meet requirements.
2551 elif output_type
!= 'SHADER' and mode
== 'MIX':
2552 output_type
= 'RGBA'
2554 if output_type
== type and valid_mode
:
2555 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2557 for (type, types_list
, dst
) in (
2558 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2559 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2560 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2561 ('MATH', [t
[0] for t
in operations
], selected_math
),
2562 ('ZCOMBINE', ('MIX', ), selected_z
),
2563 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2565 if merge_type
== type and mode
in types_list
:
2566 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2567 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2568 # use only 'Mix' nodes for merging.
2569 # For that we add selected_math list to selected_mix list and clear selected_math.
2570 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2571 selected_mix
+= selected_math
2573 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2576 count_before
= len(nodes
)
2577 # sort list by loc_x - reversed
2578 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2580 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2581 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2583 # Change the node type for math nodes in a geometry node tree.
2584 if tree_type
== 'GEOMETRY':
2585 if nodes_list
is selected_math
or nodes_list
is selected_vector
:
2586 node_type
= 'ShaderNode'
2590 node_type
= 'GeometryNode'
2591 if merge_position
== 'CENTER':
2592 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)
2593 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2599 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2603 if nodes_list
== selected_shader
and not do_hide_shader
:
2605 the_range
= len(nodes_list
) - 1
2606 if len(nodes_list
) == 1:
2609 for i
in range(the_range
):
2610 if nodes_list
== selected_mix
:
2611 add_type
= node_type
+ 'MixRGB'
2612 add
= nodes
.new(add_type
)
2613 add
.blend_type
= mode
2615 add
.inputs
[0].default_value
= 1.0
2616 add
.show_preview
= False
2622 add
.width_hidden
= 100.0
2623 elif nodes_list
== selected_math
:
2624 add_type
= node_type
+ 'Math'
2625 add
= nodes
.new(add_type
)
2626 add
.operation
= mode
2632 add
.width_hidden
= 100.0
2633 elif nodes_list
== selected_shader
:
2635 add_type
= node_type
+ 'MixShader'
2636 add
= nodes
.new(add_type
)
2637 add
.hide
= do_hide_shader
2642 add
.width_hidden
= 100.0
2644 add_type
= node_type
+ 'AddShader'
2645 add
= nodes
.new(add_type
)
2646 add
.hide
= do_hide_shader
2651 add
.width_hidden
= 100.0
2652 elif nodes_list
== selected_geometry
:
2653 if mode
in ('JOIN', 'MIX'):
2654 add_type
= node_type
+ 'JoinGeometry'
2655 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2657 add_type
= node_type
+ 'Boolean'
2658 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2659 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2660 add
.operation
= mode
2663 elif nodes_list
== selected_vector
:
2664 add_type
= node_type
+ 'VectorMath'
2665 add
= nodes
.new(add_type
)
2666 add
.operation
= mode
2672 add
.width_hidden
= 100.0
2673 elif nodes_list
== selected_z
:
2674 add
= nodes
.new('CompositorNodeZcombine')
2675 add
.show_preview
= False
2681 add
.width_hidden
= 100.0
2682 elif nodes_list
== selected_alphaover
:
2683 add
= nodes
.new('CompositorNodeAlphaOver')
2684 add
.show_preview
= False
2690 add
.width_hidden
= 100.0
2691 add
.location
= loc_x
, loc_y
2695 # This has already been handled separately
2699 count_after
= len(nodes
)
2700 index
= count_after
- 1
2701 first_selected
= nodes
[nodes_list
[0][0]]
2702 # "last" node has been added as first, so its index is count_before.
2703 last_add
= nodes
[count_before
]
2704 # Create list of invalid indexes.
2705 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2708 # Two nodes were selected and first selected has no output links, second selected has output links.
2709 # Then add links from last add to all links 'to_socket' of out links of second selected.
2710 if len(nodes_list
) == 2:
2711 if not first_selected
.outputs
[0].links
:
2712 second_selected
= nodes
[nodes_list
[1][0]]
2713 for ss_link
in second_selected
.outputs
[0].links
:
2714 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2715 # Link only if "to_node" index not in invalid indexes list.
2716 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2717 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2718 # add links from last_add to all links 'to_socket' of out links of first selected.
2719 for fs_link
in first_selected
.outputs
[0].links
:
2720 # Link only if "to_node" index not in invalid indexes list.
2721 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2722 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2723 # add link from "first" selected and "first" add node
2724 node_to
= nodes
[count_after
- 1]
2725 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2726 if node_to
.type == 'ZCOMBINE':
2727 for fs_out
in first_selected
.outputs
:
2728 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2729 links
.new(fs_out
, node_to
.inputs
[1])
2731 # add links between added ADD nodes and between selected and ADD nodes
2732 for i
in range(count_adds
):
2733 if i
< count_adds
- 1:
2734 node_from
= nodes
[index
]
2735 node_to
= nodes
[index
- 1]
2736 node_to_input_i
= first
2737 node_to_z_i
= 1 # if z combine - link z to first z input
2738 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2739 if node_to
.type == 'ZCOMBINE':
2740 for from_out
in node_from
.outputs
:
2741 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2742 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2743 if len(nodes_list
) > 1:
2744 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2745 node_to
= nodes
[index
]
2746 node_to_input_i
= second
2747 node_to_z_i
= 3 # if z combine - link z to second z input
2748 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2749 if node_to
.type == 'ZCOMBINE':
2750 for from_out
in node_from
.outputs
:
2751 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2752 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2754 # set "last" of added nodes as active
2755 nodes
.active
= last_add
2756 for i
, x
, y
, dx
, h
in nodes_list
:
2757 nodes
[i
].select
= False
2762 class NWBatchChangeNodes(Operator
, NWBase
):
2763 bl_idname
= "node.nw_batch_change"
2764 bl_label
= "Batch Change"
2765 bl_description
= "Batch Change Blend Type and Math Operation"
2766 bl_options
= {'REGISTER', 'UNDO'}
2768 blend_type
: EnumProperty(
2770 items
=blend_types
+ navs
,
2772 operation
: EnumProperty(
2774 items
=operations
+ navs
,
2777 def execute(self
, context
):
2778 blend_type
= self
.blend_type
2779 operation
= self
.operation
2780 for node
in context
.selected_nodes
:
2781 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2782 if not blend_type
in [nav
[0] for nav
in navs
]:
2783 node
.blend_type
= blend_type
2785 if blend_type
== 'NEXT':
2786 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2787 #index = blend_types.index(node.blend_type)
2788 if index
== len(blend_types
) - 1:
2789 node
.blend_type
= blend_types
[0][0]
2791 node
.blend_type
= blend_types
[index
+ 1][0]
2793 if blend_type
== 'PREV':
2794 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2796 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2798 node
.blend_type
= blend_types
[index
- 1][0]
2800 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2801 if not operation
in [nav
[0] for nav
in navs
]:
2802 node
.operation
= operation
2804 if operation
== 'NEXT':
2805 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2806 #index = operations.index(node.operation)
2807 if index
== len(operations
) - 1:
2808 node
.operation
= operations
[0][0]
2810 node
.operation
= operations
[index
+ 1][0]
2812 if operation
== 'PREV':
2813 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2814 #index = operations.index(node.operation)
2816 node
.operation
= operations
[len(operations
) - 1][0]
2818 node
.operation
= operations
[index
- 1][0]
2823 class NWChangeMixFactor(Operator
, NWBase
):
2824 bl_idname
= "node.nw_factor"
2825 bl_label
= "Change Factor"
2826 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2827 bl_options
= {'REGISTER', 'UNDO'}
2829 # option: Change factor.
2830 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2831 # Else - change factor by option value.
2832 option
: FloatProperty()
2834 def execute(self
, context
):
2835 nodes
, links
= get_nodes_links(context
)
2836 option
= self
.option
2837 selected
= [] # entry = index
2838 for si
, node
in enumerate(nodes
):
2840 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2844 fac
= nodes
[si
].inputs
[0]
2845 nodes
[si
].hide
= False
2846 if option
in {0.0, 1.0}:
2847 fac
.default_value
= option
2849 fac
.default_value
+= option
2854 class NWCopySettings(Operator
, NWBase
):
2855 bl_idname
= "node.nw_copy_settings"
2856 bl_label
= "Copy Settings"
2857 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2858 bl_options
= {'REGISTER', 'UNDO'}
2861 def poll(cls
, context
):
2863 if nw_check(context
):
2865 context
.active_node
is not None and
2866 context
.active_node
.type != 'FRAME'
2871 def execute(self
, context
):
2872 node_active
= context
.active_node
2873 node_selected
= context
.selected_nodes
2876 if not (len(node_selected
) > 1):
2877 self
.report({'ERROR'}, "2 nodes must be selected at least")
2878 return {'CANCELLED'}
2880 # Check if active node is in the selection
2881 selected_node_names
= [n
.name
for n
in node_selected
]
2882 if node_active
.name
not in selected_node_names
:
2883 self
.report({'ERROR'}, "No active node")
2884 return {'CANCELLED'}
2886 # Get nodes in selection by type
2887 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2889 if not (len(valid_nodes
) > 1) and node_active
:
2890 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2891 return {'CANCELLED'}
2893 if len(valid_nodes
) != len(node_selected
):
2894 # Report nodes that are not valid
2895 valid_node_names
= [n
.name
for n
in valid_nodes
]
2896 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2897 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2899 # Reference original
2901 #node_selected_names = [n.name for n in node_selected]
2906 # Deselect all nodes
2907 for i
in node_selected
:
2910 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2911 # Run through all other nodes
2912 for node
in valid_nodes
[1:]:
2914 # Check for frame node
2915 parent
= node
.parent
if node
.parent
else None
2916 node_loc
= [node
.location
.x
, node
.location
.y
]
2918 # Select original to duplicate
2921 # Duplicate selected node
2922 bpy
.ops
.node
.duplicate()
2923 new_node
= context
.selected_nodes
[0]
2926 new_node
.select
= False
2928 # Properties to copy
2929 node_tree
= node
.id_data
2930 props_to_copy
= 'bl_idname name location height width'.split(' ')
2934 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2935 for i
in (i
for i
in mappings
if i
.is_linked
):
2937 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2940 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2941 props_to_copy
.pop(0)
2943 for prop
in props_to_copy
:
2944 setattr(new_node
, prop
, props
[prop
])
2946 # Get the node tree to remove the old node
2947 nodes
= node_tree
.nodes
2949 new_node
.name
= props
['name']
2952 new_node
.parent
= parent
2953 new_node
.location
= node_loc
2955 for str_from
, str_to
in reconnections
:
2956 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2958 success_names
.append(new_node
.name
)
2961 node_tree
.nodes
.active
= orig
2962 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2966 class NWCopyLabel(Operator
, NWBase
):
2967 bl_idname
= "node.nw_copy_label"
2968 bl_label
= "Copy Label"
2969 bl_options
= {'REGISTER', 'UNDO'}
2971 option
: EnumProperty(
2973 description
="Source of name of label",
2975 ('FROM_ACTIVE', 'from active', 'from active node',),
2976 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2977 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2981 def execute(self
, context
):
2982 nodes
, links
= get_nodes_links(context
)
2983 option
= self
.option
2984 active
= nodes
.active
2985 if option
== 'FROM_ACTIVE':
2987 src_label
= active
.label
2988 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2989 node
.label
= src_label
2990 elif option
== 'FROM_NODE':
2991 selected
= [n
for n
in nodes
if n
.select
]
2992 for node
in selected
:
2993 for input in node
.inputs
:
2995 src
= input.links
[0].from_node
2996 node
.label
= src
.label
2998 elif option
== 'FROM_SOCKET':
2999 selected
= [n
for n
in nodes
if n
.select
]
3000 for node
in selected
:
3001 for input in node
.inputs
:
3003 src
= input.links
[0].from_socket
3004 node
.label
= src
.name
3010 class NWClearLabel(Operator
, NWBase
):
3011 bl_idname
= "node.nw_clear_label"
3012 bl_label
= "Clear Label"
3013 bl_options
= {'REGISTER', 'UNDO'}
3015 option
: BoolProperty()
3017 def execute(self
, context
):
3018 nodes
, links
= get_nodes_links(context
)
3019 for node
in [n
for n
in nodes
if n
.select
]:
3024 def invoke(self
, context
, event
):
3026 return self
.execute(context
)
3028 return context
.window_manager
.invoke_confirm(self
, event
)
3031 class NWModifyLabels(Operator
, NWBase
):
3032 """Modify Labels of all selected nodes"""
3033 bl_idname
= "node.nw_modify_labels"
3034 bl_label
= "Modify Labels"
3035 bl_options
= {'REGISTER', 'UNDO'}
3037 prepend
: StringProperty(
3038 name
="Add to Beginning"
3040 append
: StringProperty(
3043 replace_from
: StringProperty(
3044 name
="Text to Replace"
3046 replace_to
: StringProperty(
3050 def execute(self
, context
):
3051 nodes
, links
= get_nodes_links(context
)
3052 for node
in [n
for n
in nodes
if n
.select
]:
3053 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
3057 def invoke(self
, context
, event
):
3061 return context
.window_manager
.invoke_props_dialog(self
)
3064 class NWAddTextureSetup(Operator
, NWBase
):
3065 bl_idname
= "node.nw_add_texture"
3066 bl_label
= "Texture Setup"
3067 bl_description
= "Add Texture Node Setup to Selected Shaders"
3068 bl_options
= {'REGISTER', 'UNDO'}
3070 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
3073 def poll(cls
, context
):
3075 if nw_check(context
):
3076 space
= context
.space_data
3077 if space
.tree_type
== 'ShaderNodeTree':
3081 def execute(self
, context
):
3082 nodes
, links
= get_nodes_links(context
)
3083 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3084 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
3085 selected_nodes
= [n
for n
in nodes
if n
.select
]
3086 for t_node
in selected_nodes
:
3090 for index
, i
in enumerate(t_node
.inputs
):
3096 locx
= t_node
.location
.x
3097 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
3099 xoffset
= [500, 700]
3101 if t_node
.type in texture_types
+ ['MAPPING']:
3102 xoffset
= [290, 500]
3106 image_type
= 'ShaderNodeTexImage'
3108 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
3109 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
3110 if t_node
.type == 'BACKGROUND':
3111 image_type
= 'ShaderNodeTexEnvironment'
3114 tex
= nodes
.new(image_type
)
3115 tex
.location
= [locx
- 200, locy
+ 112]
3117 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
3119 t_node
.select
= False
3120 if self
.add_mapping
or is_texture
:
3121 if t_node
.type != 'MAPPING':
3122 m
= nodes
.new('ShaderNodeMapping')
3123 m
.location
= [locx
- xoffset
[0], locy
+ 141]
3127 coord
= nodes
.new('ShaderNodeTexCoord')
3128 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
3131 links
.new(m
.outputs
[0], tex
.inputs
[0])
3132 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3135 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
3136 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3138 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
3142 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
3143 bl_idname
= "node.nw_add_textures_for_principled"
3144 bl_label
= "Principled Texture Setup"
3145 bl_description
= "Add Texture Node Setup for Principled BSDF"
3146 bl_options
= {'REGISTER', 'UNDO'}
3148 directory
: StringProperty(
3152 description
='Folder to search in for image files'
3154 files
: CollectionProperty(
3155 type=bpy
.types
.OperatorFileListElement
,
3156 options
={'HIDDEN', 'SKIP_SAVE'}
3159 relative_path
: BoolProperty(
3160 name
='Relative Path',
3161 description
='Set the file path relative to the blend file, when possible',
3170 def draw(self
, context
):
3171 layout
= self
.layout
3172 layout
.alignment
= 'LEFT'
3174 layout
.prop(self
, 'relative_path')
3177 def poll(cls
, context
):
3179 if nw_check(context
):
3180 space
= context
.space_data
3181 if space
.tree_type
== 'ShaderNodeTree':
3185 def execute(self
, context
):
3186 # Check if everything is ok
3187 if not self
.directory
:
3188 self
.report({'INFO'}, 'No Folder Selected')
3189 return {'CANCELLED'}
3190 if not self
.files
[:]:
3191 self
.report({'INFO'}, 'No Files Selected')
3192 return {'CANCELLED'}
3194 nodes
, links
= get_nodes_links(context
)
3195 active_node
= nodes
.active
3196 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
3197 self
.report({'INFO'}, 'Select Principled BSDF')
3198 return {'CANCELLED'}
3201 def split_into__components(fname
):
3202 # Split filename into components
3203 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3205 fname
= path
.splitext(fname
)[0]
3207 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
3208 # Separate CamelCase by space
3209 fname
= re
.sub(r
"([a-z])([A-Z])", r
"\g<1> \g<2>",fname
)
3210 # Replace common separators with SPACE
3211 separators
= ['_', '.', '-', '__', '--', '#']
3212 for sep
in separators
:
3213 fname
= fname
.replace(sep
, ' ')
3215 components
= fname
.split(' ')
3216 components
= [c
.lower() for c
in components
]
3219 # Filter textures names for texturetypes in filenames
3220 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3221 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
3222 normal_abbr
= tags
.normal
.split(' ')
3223 bump_abbr
= tags
.bump
.split(' ')
3224 gloss_abbr
= tags
.gloss
.split(' ')
3225 rough_abbr
= tags
.rough
.split(' ')
3227 ['Displacement', tags
.displacement
.split(' '), None],
3228 ['Base Color', tags
.base_color
.split(' '), None],
3229 ['Subsurface Color', tags
.sss_color
.split(' '), None],
3230 ['Metallic', tags
.metallic
.split(' '), None],
3231 ['Specular', tags
.specular
.split(' '), None],
3232 ['Roughness', rough_abbr
+ gloss_abbr
, None],
3233 ['Normal', normal_abbr
+ bump_abbr
, None],
3234 ['Transmission', tags
.transmission
.split(' '), None],
3235 ['Emission', tags
.emission
.split(' '), None],
3236 ['Alpha', tags
.alpha
.split(' '), None],
3237 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
3240 # Look through texture_types and set value as filename of first matched file
3241 def match_files_to_socket_names():
3242 for sname
in socketnames
:
3243 for file in self
.files
:
3245 filenamecomponents
= split_into__components(fname
)
3246 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
3247 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3252 match_files_to_socket_names()
3253 # Remove socketnames without found files
3254 socketnames
= [s
for s
in socketnames
if s
[2]
3255 and path
.exists(self
.directory
+s
[2])]
3257 self
.report({'INFO'}, 'No matching images found')
3258 print('No matching images found')
3259 return {'CANCELLED'}
3261 # Don't override path earlier as os.path is used to check the absolute path
3262 import_path
= self
.directory
3263 if self
.relative_path
:
3264 if bpy
.data
.filepath
:
3266 import_path
= bpy
.path
.relpath(self
.directory
)
3271 print('\nMatched Textures:')
3276 roughness_node
= None
3277 for i
, sname
in enumerate(socketnames
):
3278 print(i
, sname
[0], sname
[2])
3280 # DISPLACEMENT NODES
3281 if sname
[0] == 'Displacement':
3282 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
3283 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3284 disp_texture
.image
= img
3285 disp_texture
.label
= 'Displacement'
3286 if disp_texture
.image
:
3287 disp_texture
.image
.colorspace_settings
.is_data
= True
3289 # Add displacement offset nodes
3290 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
3291 # Align the Displacement node under the active Principled BSDF node
3292 disp_node
.location
= active_node
.location
+ Vector((100, -700))
3293 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
3295 # TODO Turn on true displacement in the material
3296 # Too complicated for now
3299 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
3301 if not output_node
[0].inputs
[2].is_linked
:
3302 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
3306 # AMBIENT OCCLUSION TEXTURE
3307 if sname
[0] == 'Ambient Occlusion':
3308 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
3309 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3310 ao_texture
.image
= img
3311 ao_texture
.label
= sname
[0]
3312 if ao_texture
.image
:
3313 ao_texture
.image
.colorspace_settings
.is_data
= True
3317 if not active_node
.inputs
[sname
[0]].is_linked
:
3318 # No texture node connected -> add texture node with new image
3319 texture_node
= nodes
.new(type='ShaderNodeTexImage')
3320 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3321 texture_node
.image
= img
3324 if sname
[0] == 'Normal':
3325 # Test if new texture node is normal or bump map
3326 fname_components
= split_into__components(sname
[2])
3327 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
3328 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
3330 # If Normal add normal node in between
3331 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
3332 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
3334 # If Bump add bump node in between
3335 normal_node
= nodes
.new(type='ShaderNodeBump')
3336 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
3338 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
3339 normal_node_texture
= texture_node
3341 elif sname
[0] == 'Roughness':
3342 # Test if glossy or roughness map
3343 fname_components
= split_into__components(sname
[2])
3344 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
3345 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
3348 # If Roughness nothing to to
3349 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3352 # If Gloss Map add invert node
3353 invert_node
= nodes
.new(type='ShaderNodeInvert')
3354 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
3356 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
3357 roughness_node
= texture_node
3360 # This is a simple connection Texture --> Input slot
3361 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3363 # Use non-color for all but 'Base Color' Textures
3364 if not sname
[0] in ['Base Color', 'Emission'] and texture_node
.image
:
3365 texture_node
.image
.colorspace_settings
.is_data
= True
3368 # If already texture connected. add to node list for alignment
3369 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
3371 # This are all connected texture nodes
3372 texture_nodes
.append(texture_node
)
3373 texture_node
.label
= sname
[0]
3376 texture_nodes
.append(disp_texture
)
3379 # We want the ambient occlusion texture to be the top most texture node
3380 texture_nodes
.insert(0, ao_texture
)
3383 for i
, texture_node
in enumerate(texture_nodes
):
3384 offset
= Vector((-550, (i
* -280) + 200))
3385 texture_node
.location
= active_node
.location
+ offset
3388 # Extra alignment if normal node was added
3389 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
3392 # Alignment of invert node if glossy map
3393 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
3395 # Add texture input + mapping
3396 mapping
= nodes
.new(type='ShaderNodeMapping')
3397 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
3398 if len(texture_nodes
) > 1:
3399 # If more than one texture add reroute node in between
3400 reroute
= nodes
.new(type='NodeReroute')
3401 texture_nodes
.append(reroute
)
3402 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
3403 reroute
.location
= tex_coords
+ Vector((-50, -120))
3404 for texture_node
in texture_nodes
:
3405 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
3406 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
3408 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
3410 # Connect texture_coordiantes to mapping node
3411 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
3412 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
3413 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
3415 # Create frame around tex coords and mapping
3416 frame
= nodes
.new(type='NodeFrame')
3417 frame
.label
= 'Mapping'
3418 mapping
.parent
= frame
3419 texture_input
.parent
= frame
3422 # Create frame around texture nodes
3423 frame
= nodes
.new(type='NodeFrame')
3424 frame
.label
= 'Textures'
3425 for tnode
in texture_nodes
:
3426 tnode
.parent
= frame
3430 active_node
.select
= False
3433 force_update(context
)
3437 class NWAddReroutes(Operator
, NWBase
):
3438 """Add Reroute Nodes and link them to outputs of selected nodes"""
3439 bl_idname
= "node.nw_add_reroutes"
3440 bl_label
= "Add Reroutes"
3441 bl_description
= "Add Reroutes to Outputs"
3442 bl_options
= {'REGISTER', 'UNDO'}
3444 option
: EnumProperty(
3447 ('ALL', 'to all', 'Add to all outputs'),
3448 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3449 ('LINKED', 'to linked', 'Add only to linked outputs'),
3453 def execute(self
, context
):
3454 tree_type
= context
.space_data
.node_tree
.type
3455 option
= self
.option
3456 nodes
, links
= get_nodes_links(context
)
3457 # output valid when option is 'all' or when 'loose' output has no links
3459 post_select
= [] # nodes to be selected after execution
3460 # create reroutes and recreate links
3461 for node
in [n
for n
in nodes
if n
.select
]:
3466 # unhide 'REROUTE' nodes to avoid issues with location.y
3467 if node
.type == 'REROUTE':
3469 # When node is hidden - width_hidden not usable.
3470 # Hack needed to calculate real width
3472 bpy
.ops
.node
.select_all(action
='DESELECT')
3473 helper
= nodes
.new('NodeReroute')
3474 helper
.select
= True
3476 # resize node and helper to zero. Then check locations to calculate width
3477 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
3478 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
3479 # restore node location
3480 node
.location
= x
, y
3483 # only helper is selected now
3484 bpy
.ops
.node
.delete()
3485 x
= node
.location
.x
+ width
+ 20.0
3486 if node
.type != 'REROUTE':
3490 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3491 for out_i
, output
in enumerate(node
.outputs
):
3492 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3493 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3494 if node
.type != 'R_LAYERS':
3496 else: # if 'R_LAYERS' check if output represent used render pass
3497 node_scene
= node
.scene
3498 node_layer
= node
.layer
3499 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3500 if output
.name
== 'Alpha':
3503 # check entries in global 'rl_outputs' variable
3504 for rlo
in rl_outputs
:
3505 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3506 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3509 valid
= ((option
== 'ALL') or
3510 (option
== 'LOOSE' and not output
.links
) or
3511 (option
== 'LINKED' and output
.links
))
3512 # Add reroutes only if valid, but offset location in all cases.
3514 n
= nodes
.new('NodeReroute')
3516 for link
in output
.links
:
3517 links
.new(n
.outputs
[0], link
.to_socket
)
3518 links
.new(output
, n
.inputs
[0])
3520 post_select
.append(n
)
3524 # disselect the node so that after execution of script only newly created nodes are selected
3526 # nicer reroutes distribution along y when node.hide
3528 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3529 for reroute
in [r
for r
in nodes
if r
.select
]:
3530 reroute
.location
.y
-= y_translate
3531 for node
in post_select
:
3537 class NWLinkActiveToSelected(Operator
, NWBase
):
3538 """Link active node to selected nodes basing on various criteria"""
3539 bl_idname
= "node.nw_link_active_to_selected"
3540 bl_label
= "Link Active Node to Selected"
3541 bl_options
= {'REGISTER', 'UNDO'}
3543 replace
: BoolProperty()
3544 use_node_name
: BoolProperty()
3545 use_outputs_names
: BoolProperty()
3548 def poll(cls
, context
):
3550 if nw_check(context
):
3551 if context
.active_node
is not None:
3552 if context
.active_node
.select
:
3556 def execute(self
, context
):
3557 nodes
, links
= get_nodes_links(context
)
3558 replace
= self
.replace
3559 use_node_name
= self
.use_node_name
3560 use_outputs_names
= self
.use_outputs_names
3561 active
= nodes
.active
3562 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3563 outputs
= [] # Only usable outputs of active nodes will be stored here.
3564 for out
in active
.outputs
:
3565 if active
.type != 'R_LAYERS':
3568 # 'R_LAYERS' node type needs special handling.
3569 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3570 # Only outputs that represent used passes should be taken into account
3571 # Check if pass represented by output is used.
3572 # global 'rl_outputs' list will be used for that
3573 for rlo
in rl_outputs
:
3574 pass_used
= False # initial value. Will be set to True if pass is used
3575 if out
.name
== 'Alpha':
3576 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3578 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3579 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3580 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3584 doit
= True # Will be changed to False when links successfully added to previous output.
3587 for node
in selected
:
3588 dst_name
= node
.name
# Will be compared with src_name if needed.
3589 # When node has label - use it as dst_name
3591 dst_name
= node
.label
3592 valid
= True # Initial value. Will be changed to False if names don't match.
3593 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
3595 # Set src_name to source node name or label
3596 src_name
= active
.name
3598 src_name
= active
.label
3599 elif use_outputs_names
:
3600 src_name
= (out
.name
, )
3601 for rlo
in rl_outputs
:
3602 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3603 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3604 if dst_name
not in src_name
:
3607 for input in node
.inputs
:
3608 if input.type == out
.type or node
.type == 'REROUTE':
3609 if replace
or not input.is_linked
:
3610 links
.new(out
, input)
3611 if not use_node_name
and not use_outputs_names
:
3618 class NWAlignNodes(Operator
, NWBase
):
3619 '''Align the selected nodes neatly in a row/column'''
3620 bl_idname
= "node.nw_align_nodes"
3621 bl_label
= "Align Nodes"
3622 bl_options
= {'REGISTER', 'UNDO'}
3623 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3625 def execute(self
, context
):
3626 nodes
, links
= get_nodes_links(context
)
3627 margin
= self
.margin
3631 if node
.select
and node
.type != 'FRAME':
3632 selection
.append(node
)
3634 # If no nodes are selected, align all nodes
3638 elif nodes
.active
in selection
:
3639 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3641 # Check if nodes should be laid out horizontally or vertically
3642 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3643 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3644 x_range
= max(x_locs
) - min(x_locs
)
3645 y_range
= max(y_locs
) - min(y_locs
)
3646 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3647 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3648 horizontal
= x_range
> y_range
3650 # Sort selection by location of node mid-point
3652 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3654 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3658 for node
in selection
:
3659 current_margin
= margin
3660 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3663 node
.location
.x
= current_pos
3664 current_pos
+= current_margin
+ node
.dimensions
.x
3665 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3667 node
.location
.y
= current_pos
3668 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3669 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3671 # If active node is selected, center nodes around it
3672 if active_loc
is not None:
3673 active_loc_diff
= active_loc
- nodes
.active
.location
3674 for node
in selection
:
3675 node
.location
+= active_loc_diff
3676 else: # Position nodes centered around where they used to be
3677 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
])
3678 new_mid
= (max(locs
) + min(locs
)) / 2
3679 for node
in selection
:
3681 node
.location
.x
+= (mid_x
- new_mid
)
3683 node
.location
.y
+= (mid_y
- new_mid
)
3688 class NWSelectParentChildren(Operator
, NWBase
):
3689 bl_idname
= "node.nw_select_parent_child"
3690 bl_label
= "Select Parent or Children"
3691 bl_options
= {'REGISTER', 'UNDO'}
3693 option
: EnumProperty(
3696 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3697 ('CHILD', 'Select Children', 'Select members of selected frame'),
3701 def execute(self
, context
):
3702 nodes
, links
= get_nodes_links(context
)
3703 option
= self
.option
3704 selected
= [node
for node
in nodes
if node
.select
]
3705 if option
== 'PARENT':
3706 for sel
in selected
:
3709 parent
.select
= True
3710 else: # option == 'CHILD'
3711 for sel
in selected
:
3712 children
= [node
for node
in nodes
if node
.parent
== sel
]
3713 for kid
in children
:
3719 class NWDetachOutputs(Operator
, NWBase
):
3720 """Detach outputs of selected node leaving inputs linked"""
3721 bl_idname
= "node.nw_detach_outputs"
3722 bl_label
= "Detach Outputs"
3723 bl_options
= {'REGISTER', 'UNDO'}
3725 def execute(self
, context
):
3726 nodes
, links
= get_nodes_links(context
)
3727 selected
= context
.selected_nodes
3728 bpy
.ops
.node
.duplicate_move_keep_inputs()
3729 new_nodes
= context
.selected_nodes
3730 bpy
.ops
.node
.select_all(action
="DESELECT")
3731 for node
in selected
:
3733 bpy
.ops
.node
.delete_reconnect()
3734 for new_node
in new_nodes
:
3735 new_node
.select
= True
3736 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3741 class NWLinkToOutputNode(Operator
):
3742 """Link to Composite node or Material Output node"""
3743 bl_idname
= "node.nw_link_out"
3744 bl_label
= "Connect to Output"
3745 bl_options
= {'REGISTER', 'UNDO'}
3748 def poll(cls
, context
):
3750 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
3751 if context
.active_node
is not None:
3752 for out
in context
.active_node
.outputs
:
3753 if is_visible_socket(out
):
3758 def execute(self
, context
):
3759 nodes
, links
= get_nodes_links(context
)
3760 active
= nodes
.active
3763 tree_type
= context
.space_data
.tree_type
3764 output_types_shaders
= [x
[1] for x
in shaders_output_nodes_props
]
3765 output_types_compo
= ['COMPOSITE']
3766 output_types_blender_mat
= ['OUTPUT']
3767 output_types_textures
= ['OUTPUT']
3768 output_types
= output_types_shaders
+ output_types_compo
+ output_types_blender_mat
3770 if node
.type in output_types
:
3774 bpy
.ops
.node
.select_all(action
="DESELECT")
3775 if tree_type
== 'ShaderNodeTree':
3776 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3777 elif tree_type
== 'CompositorNodeTree':
3778 output_node
= nodes
.new('CompositorNodeComposite')
3779 elif tree_type
== 'TextureNodeTree':
3780 output_node
= nodes
.new('TextureNodeOutput')
3781 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3782 output_node
.location
.y
= active
.location
.y
3783 if (output_node
and active
.outputs
):
3784 for i
, output
in enumerate(active
.outputs
):
3785 if is_visible_socket(output
):
3788 for i
, output
in enumerate(active
.outputs
):
3789 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3794 if tree_type
== 'ShaderNodeTree':
3795 if active
.outputs
[output_index
].name
== 'Volume':
3797 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3799 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3801 force_update(context
) # viewport render does not update
3806 class NWMakeLink(Operator
, NWBase
):
3807 """Make a link from one socket to another"""
3808 bl_idname
= 'node.nw_make_link'
3809 bl_label
= 'Make Link'
3810 bl_options
= {'REGISTER', 'UNDO'}
3811 from_socket
: IntProperty()
3812 to_socket
: IntProperty()
3814 def execute(self
, context
):
3815 nodes
, links
= get_nodes_links(context
)
3817 n1
= nodes
[context
.scene
.NWLazySource
]
3818 n2
= nodes
[context
.scene
.NWLazyTarget
]
3820 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3822 force_update(context
)
3827 class NWCallInputsMenu(Operator
, NWBase
):
3828 """Link from this output"""
3829 bl_idname
= 'node.nw_call_inputs_menu'
3830 bl_label
= 'Make Link'
3831 bl_options
= {'REGISTER', 'UNDO'}
3832 from_socket
: IntProperty()
3834 def execute(self
, context
):
3835 nodes
, links
= get_nodes_links(context
)
3837 context
.scene
.NWSourceSocket
= self
.from_socket
3839 n1
= nodes
[context
.scene
.NWLazySource
]
3840 n2
= nodes
[context
.scene
.NWLazyTarget
]
3841 if len(n2
.inputs
) > 1:
3842 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3843 elif len(n2
.inputs
) == 1:
3844 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3848 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3849 """Add an Image Sequence"""
3850 bl_idname
= 'node.nw_add_sequence'
3851 bl_label
= 'Import Image Sequence'
3852 bl_options
= {'REGISTER', 'UNDO'}
3854 directory
: StringProperty(
3857 filename
: StringProperty(
3860 files
: CollectionProperty(
3861 type=bpy
.types
.OperatorFileListElement
,
3862 options
={'HIDDEN', 'SKIP_SAVE'}
3865 def execute(self
, context
):
3866 nodes
, links
= get_nodes_links(context
)
3867 directory
= self
.directory
3868 filename
= self
.filename
3870 tree
= context
.space_data
.node_tree
3873 # print ("\nDIR:", directory)
3874 # print ("FN:", filename)
3875 # print ("Fs:", list(f.name for f in files), '\n')
3877 if tree
.type == 'SHADER':
3878 node_type
= "ShaderNodeTexImage"
3879 elif tree
.type == 'COMPOSITING':
3880 node_type
= "CompositorNodeImage"
3882 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3883 return {'CANCELLED'}
3885 if not files
[0].name
and not filename
:
3886 self
.report({'ERROR'}, "No file chosen")
3887 return {'CANCELLED'}
3888 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3889 # User has selected multiple files without an active one, or the active one is non-existant
3890 filename
= files
[0].name
3892 if not path
.exists(directory
+filename
):
3893 self
.report({'ERROR'}, filename
+" does not exist!")
3894 return {'CANCELLED'}
3896 without_ext
= '.'.join(filename
.split('.')[:-1])
3898 # if last digit isn't a number, it's not a sequence
3899 if not without_ext
[-1].isdigit():
3900 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3901 return {'CANCELLED'}
3904 extension
= filename
.split('.')[-1]
3905 reverse
= without_ext
[::-1] # reverse string
3908 for char
in reverse
:
3914 without_num
= without_ext
[:count_numbers
*-1]
3916 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3918 num_frames
= len(files
)
3920 nodes_list
= [node
for node
in nodes
]
3922 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3923 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3927 yloc
+= node_mid_pt(node
, 'y')
3928 yloc
= yloc
/len(nodes
)
3933 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3935 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3937 node
.label
= name_with_hashes
3939 img
= bpy
.data
.images
.load(directory
+(without_ext
+'.'+extension
))
3940 img
.source
= 'SEQUENCE'
3941 img
.name
= name_with_hashes
3943 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3944 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
3945 image_user
.frame_duration
= num_frames
3950 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3951 """Add multiple images at once"""
3952 bl_idname
= 'node.nw_add_multiple_images'
3953 bl_label
= 'Open Selected Images'
3954 bl_options
= {'REGISTER', 'UNDO'}
3955 directory
: StringProperty(
3958 files
: CollectionProperty(
3959 type=bpy
.types
.OperatorFileListElement
,
3960 options
={'HIDDEN', 'SKIP_SAVE'}
3963 def execute(self
, context
):
3964 nodes
, links
= get_nodes_links(context
)
3966 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3968 if context
.space_data
.node_tree
.type == 'SHADER':
3969 node_type
= "ShaderNodeTexImage"
3970 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3971 node_type
= "CompositorNodeImage"
3973 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3974 return {'CANCELLED'}
3977 for f
in self
.files
:
3980 node
= nodes
.new(node_type
)
3981 new_nodes
.append(node
)
3984 node
.width_hidden
= 100
3985 node
.location
.x
= xloc
3986 node
.location
.y
= yloc
3989 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3992 # shift new nodes up to center of tree
3993 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3995 if node
in new_nodes
:
3997 node
.location
.y
+= (list_size
/2)
4003 class NWViewerFocus(bpy
.types
.Operator
):
4004 """Set the viewer tile center to the mouse position"""
4005 bl_idname
= "node.nw_viewer_focus"
4006 bl_label
= "Viewer Focus"
4008 x
: bpy
.props
.IntProperty()
4009 y
: bpy
.props
.IntProperty()
4012 def poll(cls
, context
):
4013 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
4015 def execute(self
, context
):
4018 def invoke(self
, context
, event
):
4019 render
= context
.scene
.render
4020 space
= context
.space_data
4021 percent
= render
.resolution_percentage
*0.01
4023 nodes
, links
= get_nodes_links(context
)
4024 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
4027 mlocx
= event
.mouse_region_x
4028 mlocy
= event
.mouse_region_y
4029 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
4031 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
4032 region_x
= context
.region
.width
4033 region_y
= context
.region
.height
4035 region_center_x
= context
.region
.width
/ 2
4036 region_center_y
= context
.region
.height
/ 2
4038 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
4039 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
4041 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
4042 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
4044 margin_x
= region_center_x
- backdrop_center_x
4045 margin_y
= region_center_y
- backdrop_center_y
4047 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
4048 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
4050 for node
in viewers
:
4051 node
.center_x
= abs_mouse_x
4052 node
.center_y
= abs_mouse_y
4054 return {'PASS_THROUGH'}
4056 return self
.execute(context
)
4059 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
4060 """Save the current viewer node to an image file"""
4061 bl_idname
= "node.nw_save_viewer"
4062 bl_label
= "Save This Image"
4063 filepath
: StringProperty(subtype
="FILE_PATH")
4064 filename_ext
: EnumProperty(
4066 description
="Choose the file format to save to",
4067 items
=(('.bmp', "BMP", ""),
4068 ('.rgb', 'IRIS', ""),
4069 ('.png', 'PNG', ""),
4070 ('.jpg', 'JPEG', ""),
4071 ('.jp2', 'JPEG2000', ""),
4072 ('.tga', 'TARGA', ""),
4073 ('.cin', 'CINEON', ""),
4074 ('.dpx', 'DPX', ""),
4075 ('.exr', 'OPEN_EXR', ""),
4076 ('.hdr', 'HDR', ""),
4077 ('.tif', 'TIFF', "")),
4082 def poll(cls
, context
):
4084 if nw_check(context
):
4085 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4086 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
4087 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
4091 def execute(self
, context
):
4108 basename
, ext
= path
.splitext(fp
)
4109 old_render_format
= context
.scene
.render
.image_settings
.file_format
4110 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
4111 context
.area
.type = "IMAGE_EDITOR"
4112 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
4113 context
.area
.spaces
[0].image
.save_render(fp
)
4114 context
.area
.type = "NODE_EDITOR"
4115 context
.scene
.render
.image_settings
.file_format
= old_render_format
4119 class NWResetNodes(bpy
.types
.Operator
):
4120 """Reset Nodes in Selection"""
4121 bl_idname
= "node.nw_reset_nodes"
4122 bl_label
= "Reset Nodes"
4123 bl_options
= {'REGISTER', 'UNDO'}
4126 def poll(cls
, context
):
4127 space
= context
.space_data
4128 return space
.type == 'NODE_EDITOR'
4130 def execute(self
, context
):
4131 node_active
= context
.active_node
4132 node_selected
= context
.selected_nodes
4133 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4135 # Check if one node is selected at least
4136 if not (len(node_selected
) > 0):
4137 self
.report({'ERROR'}, "1 node must be selected at least")
4138 return {'CANCELLED'}
4140 active_node_name
= node_active
.name
if node_active
.select
else None
4141 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
4143 # Create output lists
4144 selected_node_names
= [n
.name
for n
in node_selected
]
4147 # Reset all valid children in a frame
4148 node_active_is_frame
= False
4149 if len(node_selected
) == 1 and node_active
.type == "FRAME":
4150 node_tree
= node_active
.id_data
4151 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
4153 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
4154 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
4155 node_active_is_frame
= True
4157 # Check if valid nodes in selection
4158 if not (len(valid_nodes
) > 0):
4159 # Check for frames only
4160 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
4161 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
4162 self
.report({'ERROR'}, "Please select only 1 frame to reset")
4164 self
.report({'ERROR'}, "No valid node(s) in selection")
4165 return {'CANCELLED'}
4167 # Report nodes that are not valid
4168 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
4169 valid_node_names
= [n
.name
for n
in valid_nodes
]
4170 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
4171 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
4173 # Deselect all nodes
4174 for i
in node_selected
:
4177 # Run through all valid nodes
4178 for node
in valid_nodes
:
4180 parent
= node
.parent
if node
.parent
else None
4181 node_loc
= [node
.location
.x
, node
.location
.y
]
4183 node_tree
= node
.id_data
4184 props_to_copy
= 'bl_idname name location height width'.split(' ')
4187 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
4188 for i
in (i
for i
in mappings
if i
.is_linked
):
4190 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
4192 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
4194 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
4195 props_to_copy
.pop(0)
4197 for prop
in props_to_copy
:
4198 setattr(new_node
, prop
, props
[prop
])
4200 nodes
= node_tree
.nodes
4202 new_node
.name
= props
['name']
4205 new_node
.parent
= parent
4206 new_node
.location
= node_loc
4208 for str_from
, str_to
in reconnections
:
4209 node_tree
.links
.new(eval(str_from
), eval(str_to
))
4211 new_node
.select
= False
4212 success_names
.append(new_node
.name
)
4214 # Reselect all nodes
4215 if selected_node_names
and node_active_is_frame
is False:
4216 for i
in selected_node_names
:
4217 node_tree
.nodes
[i
].select
= True
4219 if active_node_name
is not None:
4220 node_tree
.nodes
[active_node_name
].select
= True
4221 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
4223 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
4231 def drawlayout(context
, layout
, mode
='non-panel'):
4232 tree_type
= context
.space_data
.tree_type
4234 col
= layout
.column(align
=True)
4235 col
.menu(NWMergeNodesMenu
.bl_idname
)
4238 col
= layout
.column(align
=True)
4239 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
4242 if tree_type
== 'ShaderNodeTree':
4243 col
= layout
.column(align
=True)
4244 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
4245 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
4248 col
= layout
.column(align
=True)
4249 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
4250 col
.operator(NWSwapLinks
.bl_idname
)
4251 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
4254 col
= layout
.column(align
=True)
4255 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
4256 if tree_type
!= 'GeometryNodeTree':
4257 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
4260 col
= layout
.column(align
=True)
4262 row
= col
.row(align
=True)
4263 row
.operator(NWClearLabel
.bl_idname
).option
= True
4264 row
.operator(NWModifyLabels
.bl_idname
)
4266 col
.operator(NWClearLabel
.bl_idname
).option
= True
4267 col
.operator(NWModifyLabels
.bl_idname
)
4268 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
4270 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
4273 col
= layout
.column(align
=True)
4274 if tree_type
== 'CompositorNodeTree':
4275 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
4276 if tree_type
!= 'GeometryNodeTree':
4277 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
4280 col
= layout
.column(align
=True)
4281 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
4284 col
= layout
.column(align
=True)
4285 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
4288 col
= layout
.column(align
=True)
4289 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
4293 class NodeWranglerPanel(Panel
, NWBase
):
4294 bl_idname
= "NODE_PT_nw_node_wrangler"
4295 bl_space_type
= 'NODE_EDITOR'
4296 bl_label
= "Node Wrangler"
4297 bl_region_type
= "UI"
4298 bl_category
= "Node Wrangler"
4300 prepend
: StringProperty(
4303 append
: StringProperty()
4304 remove
: StringProperty()
4306 def draw(self
, context
):
4307 self
.layout
.label(text
="(Quick access: Shift+W)")
4308 drawlayout(context
, self
.layout
, mode
='panel')
4314 class NodeWranglerMenu(Menu
, NWBase
):
4315 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
4316 bl_label
= "Node Wrangler"
4318 def draw(self
, context
):
4319 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
4320 drawlayout(context
, self
.layout
)
4323 class NWMergeNodesMenu(Menu
, NWBase
):
4324 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
4325 bl_label
= "Merge Selected Nodes"
4327 def draw(self
, context
):
4328 type = context
.space_data
.tree_type
4329 layout
= self
.layout
4330 if type == 'ShaderNodeTree':
4331 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
4332 if type == 'GeometryNodeTree':
4333 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
4334 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4336 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
4337 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4338 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
4340 props
.merge_type
= 'ZCOMBINE'
4341 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
4343 props
.merge_type
= 'ALPHAOVER'
4345 class NWMergeGeometryMenu(Menu
, NWBase
):
4346 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
4347 bl_label
= "Merge Selected Nodes using Geometry Nodes"
4348 def draw(self
, context
):
4349 layout
= self
.layout
4350 # The boolean node + Join Geometry node
4351 for type, name
, description
in geo_combine_operations
:
4352 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4354 props
.merge_type
= 'GEOMETRY'
4356 class NWMergeShadersMenu(Menu
, NWBase
):
4357 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
4358 bl_label
= "Merge Selected Nodes using Shaders"
4360 def draw(self
, context
):
4361 layout
= self
.layout
4362 for type in ('MIX', 'ADD'):
4363 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
4365 props
.merge_type
= 'SHADER'
4368 class NWMergeMixMenu(Menu
, NWBase
):
4369 bl_idname
= "NODE_MT_nw_merge_mix_menu"
4370 bl_label
= "Merge Selected Nodes using Mix"
4372 def draw(self
, context
):
4373 layout
= self
.layout
4374 for type, name
, description
in blend_types
:
4375 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4377 props
.merge_type
= 'MIX'
4380 class NWConnectionListOutputs(Menu
, NWBase
):
4381 bl_idname
= "NODE_MT_nw_connection_list_out"
4384 def draw(self
, context
):
4385 layout
= self
.layout
4386 nodes
, links
= get_nodes_links(context
)
4388 n1
= nodes
[context
.scene
.NWLazySource
]
4390 for o
in n1
.outputs
:
4391 # Only show sockets that are exposed.
4393 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
4397 class NWConnectionListInputs(Menu
, NWBase
):
4398 bl_idname
= "NODE_MT_nw_connection_list_in"
4401 def draw(self
, context
):
4402 layout
= self
.layout
4403 nodes
, links
= get_nodes_links(context
)
4405 n2
= nodes
[context
.scene
.NWLazyTarget
]
4409 # Only show sockets that are exposed.
4410 # This prevents, for example, the scale value socket
4411 # of the vector math node being added to the list when
4412 # the mode is not 'SCALE'.
4414 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
4415 op
.from_socket
= context
.scene
.NWSourceSocket
4416 op
.to_socket
= index
4420 class NWMergeMathMenu(Menu
, NWBase
):
4421 bl_idname
= "NODE_MT_nw_merge_math_menu"
4422 bl_label
= "Merge Selected Nodes using Math"
4424 def draw(self
, context
):
4425 layout
= self
.layout
4426 for type, name
, description
in operations
:
4427 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4429 props
.merge_type
= 'MATH'
4432 class NWBatchChangeNodesMenu(Menu
, NWBase
):
4433 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
4434 bl_label
= "Batch Change Selected Nodes"
4436 def draw(self
, context
):
4437 layout
= self
.layout
4438 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
4439 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
4442 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
4443 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
4444 bl_label
= "Batch Change Blend Type"
4446 def draw(self
, context
):
4447 layout
= self
.layout
4448 for type, name
, description
in blend_types
:
4449 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4450 props
.blend_type
= type
4451 props
.operation
= 'CURRENT'
4454 class NWBatchChangeOperationMenu(Menu
, NWBase
):
4455 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
4456 bl_label
= "Batch Change Math Operation"
4458 def draw(self
, context
):
4459 layout
= self
.layout
4460 for type, name
, description
in operations
:
4461 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4462 props
.blend_type
= 'CURRENT'
4463 props
.operation
= type
4466 class NWCopyToSelectedMenu(Menu
, NWBase
):
4467 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4468 bl_label
= "Copy to Selected"
4470 def draw(self
, context
):
4471 layout
= self
.layout
4472 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4473 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4476 class NWCopyLabelMenu(Menu
, NWBase
):
4477 bl_idname
= "NODE_MT_nw_copy_label_menu"
4478 bl_label
= "Copy Label"
4480 def draw(self
, context
):
4481 layout
= self
.layout
4482 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4483 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4484 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4487 class NWAddReroutesMenu(Menu
, NWBase
):
4488 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4489 bl_label
= "Add Reroutes"
4490 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4492 def draw(self
, context
):
4493 layout
= self
.layout
4494 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4495 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4496 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4499 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4500 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4501 bl_label
= "Link Active to Selected"
4503 def draw(self
, context
):
4504 layout
= self
.layout
4505 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4506 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4507 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4510 class NWLinkStandardMenu(Menu
, NWBase
):
4511 bl_idname
= "NODE_MT_nw_link_standard_menu"
4512 bl_label
= "To All Selected"
4514 def draw(self
, context
):
4515 layout
= self
.layout
4516 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4517 props
.replace
= False
4518 props
.use_node_name
= False
4519 props
.use_outputs_names
= False
4520 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4521 props
.replace
= True
4522 props
.use_node_name
= False
4523 props
.use_outputs_names
= False
4526 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4527 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4528 bl_label
= "Use Node Name/Label"
4530 def draw(self
, context
):
4531 layout
= self
.layout
4532 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4533 props
.replace
= False
4534 props
.use_node_name
= True
4535 props
.use_outputs_names
= False
4536 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4537 props
.replace
= True
4538 props
.use_node_name
= True
4539 props
.use_outputs_names
= False
4542 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4543 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4544 bl_label
= "Use Outputs Names"
4546 def draw(self
, context
):
4547 layout
= self
.layout
4548 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4549 props
.replace
= False
4550 props
.use_node_name
= False
4551 props
.use_outputs_names
= True
4552 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4553 props
.replace
= True
4554 props
.use_node_name
= False
4555 props
.use_outputs_names
= True
4558 class NWAttributeMenu(bpy
.types
.Menu
):
4559 bl_idname
= "NODE_MT_nw_node_attribute_menu"
4560 bl_label
= "Attributes"
4563 def poll(cls
, context
):
4565 if nw_check(context
):
4566 snode
= context
.space_data
4567 valid
= snode
.tree_type
== 'ShaderNodeTree'
4570 def draw(self
, context
):
4572 nodes
, links
= get_nodes_links(context
)
4573 mat
= context
.object.active_material
4576 for obj
in bpy
.data
.objects
:
4577 for slot
in obj
.material_slots
:
4578 if slot
.material
== mat
:
4582 if obj
.data
.attributes
:
4583 for attr
in obj
.data
.attributes
:
4584 attrs
.append(attr
.name
)
4585 attrs
= list(set(attrs
)) # get a unique list
4589 l
.operator(NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
4591 l
.label(text
="No attributes on objects with this material")
4594 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4595 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4596 bl_label
= "Switch Type to..."
4598 def draw(self
, context
):
4599 layout
= self
.layout
4600 tree
= context
.space_data
.node_tree
4601 if tree
.type == 'SHADER':
4602 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4603 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4604 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4605 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4606 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4607 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4608 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4609 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4610 if tree
.type == 'COMPOSITING':
4611 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4612 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4613 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4614 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4615 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4616 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4617 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4618 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4619 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4620 if tree
.type == 'TEXTURE':
4621 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4622 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4623 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4624 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4625 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4626 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4627 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4628 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4629 if tree
.type == 'GEOMETRY':
4630 categories
= [c
for c
in node_categories_iter(context
)
4631 if c
.name
not in ['Group', 'Script']]
4632 for cat
in categories
:
4633 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4634 if hasattr(bpy
.types
, idname
):
4637 layout
.label(text
="Unable to load altered node lists.")
4638 layout
.label(text
="Please re-enable Node Wrangler.")
4642 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4643 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4646 def draw(self
, context
):
4647 layout
= self
.layout
4648 for ident
, node_type
, rna_name
in shaders_input_nodes_props
:
4649 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4650 props
.to_type
= ident
4653 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4654 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4657 def draw(self
, context
):
4658 layout
= self
.layout
4659 for ident
, node_type
, rna_name
in shaders_output_nodes_props
:
4660 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4661 props
.to_type
= ident
4664 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4665 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4668 def draw(self
, context
):
4669 layout
= self
.layout
4670 for ident
, node_type
, rna_name
in shaders_shader_nodes_props
:
4671 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4672 props
.to_type
= ident
4675 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4676 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4677 bl_label
= "Texture"
4679 def draw(self
, context
):
4680 layout
= self
.layout
4681 for ident
, node_type
, rna_name
in shaders_texture_nodes_props
:
4682 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4683 props
.to_type
= ident
4686 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4687 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4690 def draw(self
, context
):
4691 layout
= self
.layout
4692 for ident
, node_type
, rna_name
in shaders_color_nodes_props
:
4693 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4694 props
.to_type
= ident
4697 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4698 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4701 def draw(self
, context
):
4702 layout
= self
.layout
4703 for ident
, node_type
, rna_name
in shaders_vector_nodes_props
:
4704 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4705 props
.to_type
= ident
4708 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4709 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4710 bl_label
= "Converter"
4712 def draw(self
, context
):
4713 layout
= self
.layout
4714 for ident
, node_type
, rna_name
in shaders_converter_nodes_props
:
4715 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4716 props
.to_type
= ident
4719 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4720 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4723 def draw(self
, context
):
4724 layout
= self
.layout
4725 for ident
, node_type
, rna_name
in shaders_layout_nodes_props
:
4726 if node_type
!= 'FRAME':
4727 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4728 props
.to_type
= ident
4731 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4732 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4735 def draw(self
, context
):
4736 layout
= self
.layout
4737 for ident
, node_type
, rna_name
in compo_input_nodes_props
:
4738 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4739 props
.to_type
= ident
4742 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4743 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4746 def draw(self
, context
):
4747 layout
= self
.layout
4748 for ident
, node_type
, rna_name
in compo_output_nodes_props
:
4749 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4750 props
.to_type
= ident
4753 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4754 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4757 def draw(self
, context
):
4758 layout
= self
.layout
4759 for ident
, node_type
, rna_name
in compo_color_nodes_props
:
4760 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4761 props
.to_type
= ident
4764 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4765 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4766 bl_label
= "Converter"
4768 def draw(self
, context
):
4769 layout
= self
.layout
4770 for ident
, node_type
, rna_name
in compo_converter_nodes_props
:
4771 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4772 props
.to_type
= ident
4775 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4776 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4779 def draw(self
, context
):
4780 layout
= self
.layout
4781 for ident
, node_type
, rna_name
in compo_filter_nodes_props
:
4782 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4783 props
.to_type
= ident
4786 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4787 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4790 def draw(self
, context
):
4791 layout
= self
.layout
4792 for ident
, node_type
, rna_name
in compo_vector_nodes_props
:
4793 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4794 props
.to_type
= ident
4797 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4798 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4801 def draw(self
, context
):
4802 layout
= self
.layout
4803 for ident
, node_type
, rna_name
in compo_matte_nodes_props
:
4804 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4805 props
.to_type
= ident
4808 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4809 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4810 bl_label
= "Distort"
4812 def draw(self
, context
):
4813 layout
= self
.layout
4814 for ident
, node_type
, rna_name
in compo_distort_nodes_props
:
4815 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4816 props
.to_type
= ident
4819 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4820 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4823 def draw(self
, context
):
4824 layout
= self
.layout
4825 for ident
, node_type
, rna_name
in compo_layout_nodes_props
:
4826 if node_type
!= 'FRAME':
4827 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4828 props
.to_type
= ident
4831 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4832 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4835 def draw(self
, context
):
4836 layout
= self
.layout
4837 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4838 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4839 props
.to_type
= ident
4842 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4843 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4846 def draw(self
, context
):
4847 layout
= self
.layout
4848 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4849 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4850 props
.to_type
= ident
4853 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4854 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4857 def draw(self
, context
):
4858 layout
= self
.layout
4859 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4860 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4861 props
.to_type
= ident
4864 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4865 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4868 def draw(self
, context
):
4869 layout
= self
.layout
4870 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4871 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4872 props
.to_type
= ident
4875 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4876 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4877 bl_label
= "Converter"
4879 def draw(self
, context
):
4880 layout
= self
.layout
4881 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4882 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4883 props
.to_type
= ident
4886 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4887 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4890 def draw(self
, context
):
4891 layout
= self
.layout
4892 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4893 if node_type
!= 'FRAME':
4894 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4895 props
.to_type
= ident
4898 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4899 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4902 def draw(self
, context
):
4903 layout
= self
.layout
4904 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4905 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4906 props
.to_type
= ident
4909 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4910 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4913 def draw(self
, context
):
4914 layout
= self
.layout
4915 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4916 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4917 props
.to_type
= ident
4920 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4921 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4924 def draw(self
, context
):
4925 layout
= self
.layout
4926 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4927 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4928 props
.to_type
= ident
4931 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4932 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4933 bl_label
= "Pattern"
4935 def draw(self
, context
):
4936 layout
= self
.layout
4937 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4938 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4939 props
.to_type
= ident
4942 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4943 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4944 bl_label
= "Textures"
4946 def draw(self
, context
):
4947 layout
= self
.layout
4948 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4949 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4950 props
.to_type
= ident
4953 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4954 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4955 bl_label
= "Converter"
4957 def draw(self
, context
):
4958 layout
= self
.layout
4959 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4960 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4961 props
.to_type
= ident
4964 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4965 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4966 bl_label
= "Distort"
4968 def draw(self
, context
):
4969 layout
= self
.layout
4970 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4971 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4972 props
.to_type
= ident
4975 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4976 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4979 def draw(self
, context
):
4980 layout
= self
.layout
4981 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
4982 if node_type
!= 'FRAME':
4983 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4984 props
.to_type
= ident
4986 def draw_switch_category_submenu(self
, context
):
4987 layout
= self
.layout
4988 if self
.category
.name
== 'Layout':
4989 for node
in self
.category
.items(context
):
4990 if node
.nodetype
!= 'NodeFrame':
4991 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4992 props
.to_type
= node
.nodetype
4994 for node
in self
.category
.items(context
):
4995 if isinstance(node
, NodeItemCustom
):
4996 node
.draw(self
, layout
, context
)
4998 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4999 props
.geo_to_type
= node
.nodetype
5002 # APPENDAGES TO EXISTING UI
5006 def select_parent_children_buttons(self
, context
):
5007 layout
= self
.layout
5008 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
5009 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
5012 def attr_nodes_menu_func(self
, context
):
5013 col
= self
.layout
.column(align
=True)
5014 col
.menu("NODE_MT_nw_node_attribute_menu")
5018 def multipleimages_menu_func(self
, context
):
5019 col
= self
.layout
.column(align
=True)
5020 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
5021 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
5025 def bgreset_menu_func(self
, context
):
5026 self
.layout
.operator(NWResetBG
.bl_idname
)
5029 def save_viewer_menu_func(self
, context
):
5030 if nw_check(context
):
5031 if context
.space_data
.tree_type
== 'CompositorNodeTree':
5032 if context
.scene
.node_tree
.nodes
.active
:
5033 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
5034 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
5037 def reset_nodes_button(self
, context
):
5038 node_active
= context
.active_node
5039 node_selected
= context
.selected_nodes
5040 node_ignore
= ["FRAME","REROUTE", "GROUP"]
5042 # Check if active node is in the selection and respective type
5043 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
5044 row
= self
.layout
.row()
5045 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
5046 self
.layout
.separator()
5048 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
5049 row
= self
.layout
.row()
5050 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
5051 self
.layout
.separator()
5055 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5057 switch_category_menus
= []
5059 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5060 # props entry: (property name, property value)
5063 # NWMergeNodes with Ctrl (AUTO).
5064 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
5065 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5066 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
5067 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5068 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5069 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5070 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
5071 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5072 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5073 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5074 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
5075 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5076 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5077 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5078 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
5079 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5080 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5081 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5082 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
5083 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5084 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
5085 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5086 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
5087 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5088 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5089 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5090 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5091 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
5092 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5093 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
5094 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5095 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5096 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5097 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
5098 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5099 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5100 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5101 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
5102 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5103 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5104 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5105 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
5106 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5107 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5108 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5109 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
5110 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5111 # NWMergeNodes with Ctrl Shift (MATH)
5112 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5113 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5114 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
5115 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5116 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5117 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5118 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
5119 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5120 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5121 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5122 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
5123 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5124 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5125 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5126 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
5127 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5128 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
5129 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5130 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
5131 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5132 # BATCH CHANGE NODES
5133 # NWBatchChangeNodes with Alt
5134 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
5135 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5136 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
5137 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5138 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5139 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5140 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
5141 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5142 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5143 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5144 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
5145 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5146 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5147 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5148 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
5149 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5150 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5151 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5152 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
5153 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5154 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
5155 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5156 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
5157 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5158 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
5159 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5160 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
5161 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5162 # LINK ACTIVE TO SELECTED
5163 # Don't use names, don't replace links (K)
5164 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
5165 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5166 # Don't use names, replace links (Shift K)
5167 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
5168 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5169 # Use node name, don't replace links (')
5170 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
5171 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5172 # Use node name, replace links (Shift ')
5173 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
5174 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5175 # Don't use names, don't replace links (;)
5176 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
5177 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5178 # Don't use names, replace links (')
5179 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
5180 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5182 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5183 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5184 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5185 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5186 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5187 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5188 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5189 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5190 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5191 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5192 # CLEAR LABEL (Alt L)
5193 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5194 # MODIFY LABEL (Alt Shift L)
5195 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5196 # Copy Label from active to selected
5197 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5198 # DETACH OUTPUTS (Alt Shift D)
5199 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5200 # LINK TO OUTPUT NODE (O)
5201 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5202 # SELECT PARENT/CHILDREN
5204 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5206 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5208 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5209 # Add Principled BSDF Texture Setup
5210 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5212 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5214 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5216 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5218 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5220 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5221 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5223 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
5225 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5227 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5228 # Lazy Connect with Menu
5229 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5230 # Viewer Tile Center
5231 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5233 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5234 # Reset Nodes (Back Space)
5235 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5237 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
5238 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5239 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5240 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
5241 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
5242 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
5247 NWPrincipledPreferences
,
5267 NWAddPrincipledSetup
,
5269 NWLinkActiveToSelected
,
5271 NWSelectParentChildren
,
5277 NWAddMultipleImages
,
5285 NWMergeGeometryMenu
,
5287 NWConnectionListOutputs
,
5288 NWConnectionListInputs
,
5290 NWBatchChangeNodesMenu
,
5291 NWBatchChangeBlendTypeMenu
,
5292 NWBatchChangeOperationMenu
,
5293 NWCopyToSelectedMenu
,
5296 NWLinkActiveToSelectedMenu
,
5298 NWLinkUseNodeNameMenu
,
5299 NWLinkUseOutputsNamesMenu
,
5301 NWSwitchNodeTypeMenu
,
5302 NWSwitchShadersInputSubmenu
,
5303 NWSwitchShadersOutputSubmenu
,
5304 NWSwitchShadersShaderSubmenu
,
5305 NWSwitchShadersTextureSubmenu
,
5306 NWSwitchShadersColorSubmenu
,
5307 NWSwitchShadersVectorSubmenu
,
5308 NWSwitchShadersConverterSubmenu
,
5309 NWSwitchShadersLayoutSubmenu
,
5310 NWSwitchCompoInputSubmenu
,
5311 NWSwitchCompoOutputSubmenu
,
5312 NWSwitchCompoColorSubmenu
,
5313 NWSwitchCompoConverterSubmenu
,
5314 NWSwitchCompoFilterSubmenu
,
5315 NWSwitchCompoVectorSubmenu
,
5316 NWSwitchCompoMatteSubmenu
,
5317 NWSwitchCompoDistortSubmenu
,
5318 NWSwitchCompoLayoutSubmenu
,
5319 NWSwitchMatInputSubmenu
,
5320 NWSwitchMatOutputSubmenu
,
5321 NWSwitchMatColorSubmenu
,
5322 NWSwitchMatVectorSubmenu
,
5323 NWSwitchMatConverterSubmenu
,
5324 NWSwitchMatLayoutSubmenu
,
5325 NWSwitchTexInputSubmenu
,
5326 NWSwitchTexOutputSubmenu
,
5327 NWSwitchTexColorSubmenu
,
5328 NWSwitchTexPatternSubmenu
,
5329 NWSwitchTexTexturesSubmenu
,
5330 NWSwitchTexConverterSubmenu
,
5331 NWSwitchTexDistortSubmenu
,
5332 NWSwitchTexLayoutSubmenu
,
5336 from bpy
.utils
import register_class
5339 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
5340 name
="Busy Drawing!",
5342 description
="An internal property used to store only the first mouse position")
5343 bpy
.types
.Scene
.NWLazySource
= StringProperty(
5344 name
="Lazy Source!",
5346 description
="An internal property used to store the first node in a Lazy Connect operation")
5347 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
5348 name
="Lazy Target!",
5350 description
="An internal property used to store the last node in a Lazy Connect operation")
5351 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
5352 name
="Source Socket!",
5354 description
="An internal property used to store the source socket in a Lazy Connect operation")
5355 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
5358 description
="An internal property used to determine if a socket is generated by the addon"
5365 addon_keymaps
.clear()
5366 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
5368 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
5369 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
5370 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
5372 for prop
, value
in props
:
5373 setattr(kmi
.properties
, prop
, value
)
5374 addon_keymaps
.append((km
, kmi
))
5377 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
5378 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
5379 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
5380 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
5381 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
5382 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
5383 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
5384 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
5387 switch_category_menus
.clear()
5388 for cat
in node_categories_iter(None):
5389 if cat
.name
not in ['Group', 'Script'] and cat
.identifier
.startswith('GEO'):
5390 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
5391 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
5392 "bl_space_type": 'NODE_EDITOR',
5393 "bl_label": cat
.name
,
5396 "draw": draw_switch_category_submenu
,
5399 switch_category_menus
.append(switch_category_type
)
5401 bpy
.utils
.register_class(switch_category_type
)
5405 from bpy
.utils
import unregister_class
5408 del bpy
.types
.Scene
.NWBusyDrawing
5409 del bpy
.types
.Scene
.NWLazySource
5410 del bpy
.types
.Scene
.NWLazyTarget
5411 del bpy
.types
.Scene
.NWSourceSocket
5412 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
5414 for cat_types
in switch_category_menus
:
5415 bpy
.utils
.unregister_class(cat_types
)
5416 switch_category_menus
.clear()
5419 for km
, kmi
in addon_keymaps
:
5420 km
.keymap_items
.remove(kmi
)
5421 addon_keymaps
.clear()
5424 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
5425 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
5426 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
5427 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
5428 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
5429 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
5430 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
5431 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
5434 unregister_class(cls
)
5436 if __name__
== "__main__":