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 Value'),
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 Value'),
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 Value'),
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",
1169 options
={'TEXTEDIT_UPDATE'}
1171 show_principled_lists
: BoolProperty(
1172 name
="Show Principled naming tags",
1174 description
="Expand this box into a list of all naming tags for principled texture setup"
1176 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1178 def draw(self
, context
):
1179 layout
= self
.layout
1180 col
= layout
.column()
1181 col
.prop(self
, "merge_position")
1182 col
.prop(self
, "merge_hide")
1185 col
= box
.column(align
=True)
1186 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1187 if self
.show_principled_lists
:
1188 tags
= self
.principled_tags
1190 col
.prop(tags
, "base_color")
1191 col
.prop(tags
, "sss_color")
1192 col
.prop(tags
, "metallic")
1193 col
.prop(tags
, "specular")
1194 col
.prop(tags
, "rough")
1195 col
.prop(tags
, "gloss")
1196 col
.prop(tags
, "normal")
1197 col
.prop(tags
, "bump")
1198 col
.prop(tags
, "displacement")
1199 col
.prop(tags
, "transmission")
1200 col
.prop(tags
, "emission")
1201 col
.prop(tags
, "alpha")
1202 col
.prop(tags
, "ambient_occlusion")
1205 col
= box
.column(align
=True)
1206 hotkey_button_name
= "Show Hotkey List"
1207 if self
.show_hotkey_list
:
1208 hotkey_button_name
= "Hide Hotkey List"
1209 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1210 if self
.show_hotkey_list
:
1211 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1213 for hotkey
in kmi_defs
:
1215 hotkey_name
= hotkey
[7]
1217 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1218 row
= col
.row(align
=True)
1219 row
.label(text
=hotkey_name
)
1220 keystr
= nice_hotkey_name(hotkey
[1])
1222 keystr
= "Shift " + keystr
1224 keystr
= "Alt " + keystr
1226 keystr
= "Ctrl " + keystr
1227 row
.label(text
=keystr
)
1231 def nw_check(context
):
1232 space
= context
.space_data
1233 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1236 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1243 def poll(cls
, context
):
1244 return nw_check(context
)
1248 class NWLazyMix(Operator
, NWBase
):
1249 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1250 bl_idname
= "node.nw_lazy_mix"
1251 bl_label
= "Mix Nodes"
1252 bl_options
= {'REGISTER', 'UNDO'}
1254 def modal(self
, context
, event
):
1255 context
.area
.tag_redraw()
1256 nodes
, links
= get_nodes_links(context
)
1259 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1262 if not context
.scene
.NWBusyDrawing
:
1263 node1
= node_at_pos(nodes
, context
, event
)
1265 context
.scene
.NWBusyDrawing
= node1
.name
1267 if context
.scene
.NWBusyDrawing
!= 'STOP':
1268 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1270 context
.scene
.NWLazySource
= node1
.name
1271 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1273 if event
.type == 'MOUSEMOVE':
1274 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1276 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1277 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1278 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1281 node2
= node_at_pos(nodes
, context
, event
)
1283 context
.scene
.NWBusyDrawing
= node2
.name
1295 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1297 context
.scene
.NWBusyDrawing
= ""
1300 elif event
.type == 'ESC':
1302 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1303 return {'CANCELLED'}
1305 return {'RUNNING_MODAL'}
1307 def invoke(self
, context
, event
):
1308 if context
.area
.type == 'NODE_EDITOR':
1309 # the arguments we pass the the callback
1310 args
= (self
, context
, 'MIX')
1311 # Add the region OpenGL drawing callback
1312 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1313 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1315 self
.mouse_path
= []
1317 context
.window_manager
.modal_handler_add(self
)
1318 return {'RUNNING_MODAL'}
1320 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1321 return {'CANCELLED'}
1324 class NWLazyConnect(Operator
, NWBase
):
1325 """Connect two nodes without clicking a specific socket (automatically determined"""
1326 bl_idname
= "node.nw_lazy_connect"
1327 bl_label
= "Lazy Connect"
1328 bl_options
= {'REGISTER', 'UNDO'}
1329 with_menu
: BoolProperty()
1331 def modal(self
, context
, event
):
1332 context
.area
.tag_redraw()
1333 nodes
, links
= get_nodes_links(context
)
1336 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1339 if not context
.scene
.NWBusyDrawing
:
1340 node1
= node_at_pos(nodes
, context
, event
)
1342 context
.scene
.NWBusyDrawing
= node1
.name
1344 if context
.scene
.NWBusyDrawing
!= 'STOP':
1345 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1347 context
.scene
.NWLazySource
= node1
.name
1348 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1350 if event
.type == 'MOUSEMOVE':
1351 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1353 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1354 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1355 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1358 node2
= node_at_pos(nodes
, context
, event
)
1360 context
.scene
.NWBusyDrawing
= node2
.name
1365 link_success
= False
1371 if node
.select
== True:
1373 original_sel
.append(node
)
1375 original_unsel
.append(node
)
1379 #link_success = autolink(node1, node2, links)
1381 if len(node1
.outputs
) > 1 and node2
.inputs
:
1382 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1383 elif len(node1
.outputs
) == 1:
1384 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1386 link_success
= autolink(node1
, node2
, links
)
1388 for node
in original_sel
:
1390 for node
in original_unsel
:
1394 force_update(context
)
1395 context
.scene
.NWBusyDrawing
= ""
1398 elif event
.type == 'ESC':
1399 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1400 return {'CANCELLED'}
1402 return {'RUNNING_MODAL'}
1404 def invoke(self
, context
, event
):
1405 if context
.area
.type == 'NODE_EDITOR':
1406 nodes
, links
= get_nodes_links(context
)
1407 node
= node_at_pos(nodes
, context
, event
)
1409 context
.scene
.NWBusyDrawing
= node
.name
1411 # the arguments we pass the the callback
1415 args
= (self
, context
, mode
)
1416 # Add the region OpenGL drawing callback
1417 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1418 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1420 self
.mouse_path
= []
1422 context
.window_manager
.modal_handler_add(self
)
1423 return {'RUNNING_MODAL'}
1425 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1426 return {'CANCELLED'}
1429 class NWDeleteUnused(Operator
, NWBase
):
1430 """Delete all nodes whose output is not used"""
1431 bl_idname
= 'node.nw_del_unused'
1432 bl_label
= 'Delete Unused Nodes'
1433 bl_options
= {'REGISTER', 'UNDO'}
1435 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1436 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1438 def is_unused_node(self
, node
):
1439 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1440 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1441 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1442 if node
.type in end_types
:
1445 for output
in node
.outputs
:
1451 def poll(cls
, context
):
1453 if nw_check(context
):
1454 if context
.space_data
.node_tree
.nodes
:
1458 def execute(self
, context
):
1459 nodes
, links
= get_nodes_links(context
)
1464 if node
.select
== True:
1465 selection
.append(node
.name
)
1471 temp_deleted_nodes
= []
1472 del_unused_iterations
= len(nodes
)
1473 for it
in range(0, del_unused_iterations
):
1474 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1476 if self
.is_unused_node(node
):
1478 deleted_nodes
.append(node
.name
)
1479 bpy
.ops
.node
.delete()
1481 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1484 if self
.delete_frames
:
1492 frames_in_use
.append(node
.parent
)
1494 if node
.type == 'FRAME' and node
not in frames_in_use
:
1497 repeat
= True # repeat for nested frames
1499 if node
not in frames_in_use
:
1501 deleted_nodes
.append(node
.name
)
1502 bpy
.ops
.node
.delete()
1504 if self
.delete_muted
:
1508 deleted_nodes
.append(node
.name
)
1509 bpy
.ops
.node
.delete_reconnect()
1511 # get unique list of deleted nodes (iterations would count the same node more than once)
1512 deleted_nodes
= list(set(deleted_nodes
))
1513 for n
in deleted_nodes
:
1514 self
.report({'INFO'}, "Node " + n
+ " deleted")
1515 num_deleted
= len(deleted_nodes
)
1520 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1522 self
.report({'INFO'}, "Nothing deleted")
1525 nodes
, links
= get_nodes_links(context
)
1527 if node
.name
in selection
:
1531 def invoke(self
, context
, event
):
1532 return context
.window_manager
.invoke_confirm(self
, event
)
1535 class NWSwapLinks(Operator
, NWBase
):
1536 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1537 bl_idname
= 'node.nw_swap_links'
1538 bl_label
= 'Swap Links'
1539 bl_options
= {'REGISTER', 'UNDO'}
1542 def poll(cls
, context
):
1544 if nw_check(context
):
1545 if context
.selected_nodes
:
1546 valid
= len(context
.selected_nodes
) <= 2
1549 def execute(self
, context
):
1550 nodes
, links
= get_nodes_links(context
)
1551 selected_nodes
= context
.selected_nodes
1552 n1
= selected_nodes
[0]
1555 if len(selected_nodes
) == 2:
1556 n2
= selected_nodes
[1]
1557 if n1
.outputs
and n2
.outputs
:
1562 for output
in n1
.outputs
:
1564 for link
in output
.links
:
1565 n1_outputs
.append([out_index
, link
.to_socket
])
1570 for output
in n2
.outputs
:
1572 for link
in output
.links
:
1573 n2_outputs
.append([out_index
, link
.to_socket
])
1577 for connection
in n1_outputs
:
1579 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1581 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1582 for connection
in n2_outputs
:
1584 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1586 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1588 if n1
.outputs
or n2
.outputs
:
1589 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1591 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1594 elif len(selected_nodes
) == 1:
1595 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1596 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1601 for i1
in n1
.inputs
:
1602 if i1
.is_linked
and not i1
.is_multi_input
:
1604 for i2
in n1
.inputs
:
1605 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1607 types
.append ([i1
, similar_types
, i
])
1609 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1614 for i2
in n1
.inputs
:
1615 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1617 i1f
= pair
[0].links
[0].from_socket
1618 i1t
= pair
[0].links
[0].to_socket
1619 i2f
= pair
[1].links
[0].from_socket
1620 i2t
= pair
[1].links
[0].to_socket
1625 fs
= t
[0].links
[0].from_socket
1627 links
.remove(t
[0].links
[0])
1628 if i
+1 == len(n1
.inputs
):
1631 while n1
.inputs
[i
].is_linked
:
1633 links
.new(fs
, n1
.inputs
[i
])
1634 elif len(types
) == 2:
1635 i1f
= types
[0][0].links
[0].from_socket
1636 i1t
= types
[0][0].links
[0].to_socket
1637 i2f
= types
[1][0].links
[0].from_socket
1638 i2t
= types
[1][0].links
[0].to_socket
1643 self
.report({'WARNING'}, "This node has no input connections to swap!")
1645 self
.report({'WARNING'}, "This node has no inputs to swap!")
1647 force_update(context
)
1651 class NWResetBG(Operator
, NWBase
):
1652 """Reset the zoom and position of the background image"""
1653 bl_idname
= 'node.nw_bg_reset'
1654 bl_label
= 'Reset Backdrop'
1655 bl_options
= {'REGISTER', 'UNDO'}
1658 def poll(cls
, context
):
1660 if nw_check(context
):
1661 snode
= context
.space_data
1662 valid
= snode
.tree_type
== 'CompositorNodeTree'
1665 def execute(self
, context
):
1666 context
.space_data
.backdrop_zoom
= 1
1667 context
.space_data
.backdrop_offset
[0] = 0
1668 context
.space_data
.backdrop_offset
[1] = 0
1672 class NWAddAttrNode(Operator
, NWBase
):
1673 """Add an Attribute node with this name"""
1674 bl_idname
= 'node.nw_add_attr_node'
1675 bl_label
= 'Add UV map'
1676 bl_options
= {'REGISTER', 'UNDO'}
1678 attr_name
: StringProperty()
1680 def execute(self
, context
):
1681 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1682 nodes
, links
= get_nodes_links(context
)
1683 nodes
.active
.attribute_name
= self
.attr_name
1686 class NWPreviewNode(Operator
, NWBase
):
1687 bl_idname
= "node.nw_preview_node"
1688 bl_label
= "Preview Node"
1689 bl_description
= "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output"
1690 bl_options
= {'REGISTER', 'UNDO'}
1692 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1693 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1694 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1695 run_in_geometry_nodes
: BoolProperty(default
=True)
1698 self
.shader_output_type
= ""
1699 self
.shader_output_ident
= ""
1700 self
.shader_viewer_ident
= ""
1703 def poll(cls
, context
):
1704 if nw_check(context
):
1705 space
= context
.space_data
1706 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1707 if context
.active_node
:
1708 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1714 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1715 #check if a viewer output already exists in a node group otherwise create
1716 if hasattr(node
, "node_tree"):
1718 if len(node
.node_tree
.outputs
):
1720 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1721 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1722 #if viewer output is already used but leads to the same socket we can still use it
1723 is_used
= self
.is_socket_used_other_mats(socket
)
1725 if connect_socket
== None:
1727 groupout
= get_group_output_node(node
.node_tree
)
1728 groupout_input
= groupout
.inputs
[i
]
1729 links
= groupout_input
.links
1730 if connect_socket
not in [link
.from_socket
for link
in links
]:
1736 if not index
and free_socket
:
1740 #create viewer socket
1741 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1742 index
= len(node
.node_tree
.outputs
) - 1
1743 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1746 def init_shader_variables(self
, space
, shader_type
):
1747 if shader_type
== 'OBJECT':
1748 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1749 self
.shader_output_type
= "OUTPUT_MATERIAL"
1750 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1751 self
.shader_viewer_ident
= "ShaderNodeEmission"
1753 self
.shader_output_type
= "OUTPUT_LIGHT"
1754 self
.shader_output_ident
= "ShaderNodeOutputLight"
1755 self
.shader_viewer_ident
= "ShaderNodeEmission"
1757 elif shader_type
== 'WORLD':
1758 self
.shader_output_type
= "OUTPUT_WORLD"
1759 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1760 self
.shader_viewer_ident
= "ShaderNodeBackground"
1762 def get_shader_output_node(self
, tree
):
1763 for node
in tree
.nodes
:
1764 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1768 def ensure_group_output(cls
, tree
):
1769 #check if a group output node exists otherwise create
1770 groupout
= get_group_output_node(tree
)
1772 groupout
= tree
.nodes
.new('NodeGroupOutput')
1773 loc_x
, loc_y
= get_output_location(tree
)
1774 groupout
.location
.x
= loc_x
1775 groupout
.location
.y
= loc_y
1776 groupout
.select
= False
1777 # So that we don't keep on adding new group outputs
1778 groupout
.is_active_output
= True
1782 def search_sockets(cls
, node
, sockets
, index
=None):
1783 # recursively scan nodes for viewer sockets and store in list
1784 for i
, input_socket
in enumerate(node
.inputs
):
1785 if index
and i
!= index
:
1787 if len(input_socket
.links
):
1788 link
= input_socket
.links
[0]
1789 next_node
= link
.from_node
1790 external_socket
= link
.from_socket
1791 if hasattr(next_node
, "node_tree"):
1792 for socket_index
, s
in enumerate(next_node
.outputs
):
1793 if s
== external_socket
:
1795 socket
= next_node
.node_tree
.outputs
[socket_index
]
1796 if is_viewer_socket(socket
) and socket
not in sockets
:
1797 sockets
.append(socket
)
1798 #continue search inside of node group but restrict socket to where we came from
1799 groupout
= get_group_output_node(next_node
.node_tree
)
1800 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1803 def scan_nodes(cls
, tree
, sockets
):
1804 # get all viewer sockets in a material tree
1805 for node
in tree
.nodes
:
1806 if hasattr(node
, "node_tree"):
1807 for socket
in node
.node_tree
.outputs
:
1808 if is_viewer_socket(socket
) and (socket
not in sockets
):
1809 sockets
.append(socket
)
1810 cls
.scan_nodes(node
.node_tree
, sockets
)
1812 def link_leads_to_used_socket(self
, link
):
1813 #return True if link leads to a socket that is already used in this material
1814 socket
= get_internal_socket(link
.to_socket
)
1815 return (socket
and self
.is_socket_used_active_mat(socket
))
1817 def is_socket_used_active_mat(self
, socket
):
1818 #ensure used sockets in active material is calculated and check given socket
1819 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1820 self
.used_viewer_sockets_active_mat
= []
1821 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1823 emission
= self
.get_viewer_node(materialout
)
1824 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_active_mat
)
1825 return socket
in self
.used_viewer_sockets_active_mat
1827 def is_socket_used_other_mats(self
, socket
):
1828 #ensure used sockets in other materials are calculated and check given socket
1829 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1830 self
.used_viewer_sockets_other_mats
= []
1831 for mat
in bpy
.data
.materials
:
1832 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1835 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1837 emission
= self
.get_viewer_node(materialout
)
1838 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_other_mats
)
1839 return socket
in self
.used_viewer_sockets_other_mats
1842 def get_viewer_node(materialout
):
1843 input_socket
= materialout
.inputs
[0]
1844 if len(input_socket
.links
) > 0:
1845 node
= input_socket
.links
[0].from_node
1846 if node
.type == 'EMISSION' and node
.name
== "Emission Viewer":
1849 def invoke(self
, context
, event
):
1850 space
= context
.space_data
1851 # Ignore operator when running in wrong context.
1852 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1853 return {'PASS_THROUGH'}
1855 shader_type
= space
.shader_type
1856 self
.init_shader_variables(space
, shader_type
)
1857 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1858 mlocx
= event
.mouse_region_x
1859 mlocy
= event
.mouse_region_y
1860 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
1861 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1862 active_tree
, path_to_tree
= get_active_tree(context
)
1863 nodes
, links
= active_tree
.nodes
, active_tree
.links
1864 base_node_tree
= space
.node_tree
1865 active
= nodes
.active
1867 # For geometry node trees we just connect to the group output,
1868 # because there is no "viewer node" yet.
1869 if space
.tree_type
== "GeometryNodeTree":
1872 for out
in active
.outputs
:
1873 if is_visible_socket(out
):
1882 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1883 self
.scan_nodes(base_node_tree
, delete_sockets
)
1885 # Find (or create if needed) the output of this node tree
1886 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1888 # Analyze outputs, make links
1891 for i
, out
in enumerate(active
.outputs
):
1892 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1893 valid_outputs
.append(i
)
1895 out_i
= valid_outputs
[0] # Start index of node's outputs
1896 for i
, valid_i
in enumerate(valid_outputs
):
1897 for out_link
in active
.outputs
[valid_i
].links
:
1898 if is_viewer_link(out_link
, geometryoutput
):
1899 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1900 if i
< len(valid_outputs
) - 1:
1901 out_i
= valid_outputs
[i
+ 1]
1903 out_i
= valid_outputs
[0]
1905 make_links
= [] # store sockets for new links
1906 delete_nodes
= [] # store unused nodes to delete in the end
1908 # If there is no 'GEOMETRY' output type - We can't preview the node
1911 socket_type
= 'GEOMETRY'
1912 # Find an input socket of the output of type geometry
1913 geometryoutindex
= None
1914 for i
,inp
in enumerate(geometryoutput
.inputs
):
1915 if inp
.type == socket_type
:
1916 geometryoutindex
= i
1918 if geometryoutindex
is None:
1919 # Create geometry socket
1920 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1921 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1923 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1924 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1925 for li_from
, li_to
in make_links
:
1926 base_node_tree
.links
.new(li_from
, li_to
)
1927 tree
= base_node_tree
1928 link_end
= output_socket
1929 while tree
.nodes
.active
!= active
:
1930 node
= tree
.nodes
.active
1931 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1932 link_start
= node
.outputs
[index
]
1933 node_socket
= node
.node_tree
.outputs
[index
]
1934 if node_socket
in delete_sockets
:
1935 delete_sockets
.remove(node_socket
)
1936 tree
.links
.new(link_start
, link_end
)
1938 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1939 tree
= tree
.nodes
.active
.node_tree
1940 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1943 for socket
in delete_sockets
:
1944 tree
= socket
.id_data
1945 tree
.outputs
.remove(socket
)
1948 for tree
, node
in delete_nodes
:
1949 tree
.nodes
.remove(node
)
1951 nodes
.active
= active
1952 active
.select
= True
1953 force_update(context
)
1957 # What follows is code for the shader editor
1958 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1961 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
):
1962 for out
in active
.outputs
:
1963 if is_visible_socket(out
):
1967 # get material_output node
1968 materialout
= None # placeholder node
1971 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1972 self
.scan_nodes(base_node_tree
, delete_sockets
)
1974 materialout
= self
.get_shader_output_node(base_node_tree
)
1976 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1977 materialout
.location
= get_output_location(base_node_tree
)
1978 materialout
.select
= False
1979 # Analyze outputs, add "Emission Viewer" if needed, make links
1982 for i
, out
in enumerate(active
.outputs
):
1983 if is_visible_socket(out
):
1984 valid_outputs
.append(i
)
1986 out_i
= valid_outputs
[0] # Start index of node's outputs
1987 for i
, valid_i
in enumerate(valid_outputs
):
1988 for out_link
in active
.outputs
[valid_i
].links
:
1989 if is_viewer_link(out_link
, materialout
):
1990 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1991 if i
< len(valid_outputs
) - 1:
1992 out_i
= valid_outputs
[i
+ 1]
1994 out_i
= valid_outputs
[0]
1996 make_links
= [] # store sockets for new links
1997 delete_nodes
= [] # store unused nodes to delete in the end
1999 # If output type not 'SHADER' - "Emission Viewer" needed
2000 if active
.outputs
[out_i
].type != 'SHADER':
2001 socket_type
= 'NodeSocketColor'
2002 # get Emission Viewer node
2003 emission_exists
= False
2004 emission_placeholder
= base_node_tree
.nodes
[0]
2005 for node
in base_node_tree
.nodes
:
2006 if "Emission Viewer" in node
.name
:
2007 emission_exists
= True
2008 emission_placeholder
= node
2009 if not emission_exists
:
2010 emission
= base_node_tree
.nodes
.new(self
.shader_viewer_ident
)
2011 emission
.hide
= True
2012 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
2013 emission
.label
= "Viewer"
2014 emission
.name
= "Emission Viewer"
2015 emission
.use_custom_color
= True
2016 emission
.color
= (0.6, 0.5, 0.4)
2017 emission
.select
= False
2019 emission
= emission_placeholder
2020 output_socket
= emission
.inputs
[0]
2022 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2023 if emission
.outputs
[0].links
.__len
__() > 0:
2024 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
2025 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2027 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2029 # Set brightness of viewer to compensate for Film and CM exposure
2030 if context
.scene
.render
.engine
== 'CYCLES' and hasattr(context
.scene
, 'cycles'):
2031 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
2035 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
2036 emission
.inputs
[1].default_value
= intensity
2039 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2040 socket_type
= 'NodeSocketShader'
2041 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
2042 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
2043 output_socket
= materialout
.inputs
[materialout_index
]
2044 for node
in base_node_tree
.nodes
:
2045 if node
.name
== 'Emission Viewer':
2046 delete_nodes
.append((base_node_tree
, node
))
2047 for li_from
, li_to
in make_links
:
2048 base_node_tree
.links
.new(li_from
, li_to
)
2050 # Create links through node groups until we reach the active node
2051 tree
= base_node_tree
2052 link_end
= output_socket
2053 while tree
.nodes
.active
!= active
:
2054 node
= tree
.nodes
.active
2055 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
2056 link_start
= node
.outputs
[index
]
2057 node_socket
= node
.node_tree
.outputs
[index
]
2058 if node_socket
in delete_sockets
:
2059 delete_sockets
.remove(node_socket
)
2060 tree
.links
.new(link_start
, link_end
)
2062 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
2063 tree
= tree
.nodes
.active
.node_tree
2064 tree
.links
.new(active
.outputs
[out_i
], link_end
)
2067 for socket
in delete_sockets
:
2068 if not self
.is_socket_used_other_mats(socket
):
2069 tree
= socket
.id_data
2070 tree
.outputs
.remove(socket
)
2073 for tree
, node
in delete_nodes
:
2074 tree
.nodes
.remove(node
)
2076 nodes
.active
= active
2077 active
.select
= True
2079 force_update(context
)
2083 return {'CANCELLED'}
2086 class NWFrameSelected(Operator
, NWBase
):
2087 bl_idname
= "node.nw_frame_selected"
2088 bl_label
= "Frame Selected"
2089 bl_description
= "Add a frame node and parent the selected nodes to it"
2090 bl_options
= {'REGISTER', 'UNDO'}
2092 label_prop
: StringProperty(
2094 description
='The visual name of the frame node',
2097 color_prop
: FloatVectorProperty(
2099 description
="The color of the frame node",
2100 default
=(0.6, 0.6, 0.6),
2101 min=0, max=1, step
=1, precision
=3,
2102 subtype
='COLOR_GAMMA', size
=3
2105 def execute(self
, context
):
2106 nodes
, links
= get_nodes_links(context
)
2109 if node
.select
== True:
2110 selected
.append(node
)
2112 bpy
.ops
.node
.add_node(type='NodeFrame')
2114 frm
.label
= self
.label_prop
2115 frm
.use_custom_color
= True
2116 frm
.color
= self
.color_prop
2118 for node
in selected
:
2124 class NWReloadImages(Operator
):
2125 bl_idname
= "node.nw_reload_images"
2126 bl_label
= "Reload Images"
2127 bl_description
= "Update all the image nodes to match their files on disk"
2130 def poll(cls
, context
):
2132 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
2133 if context
.active_node
is not None:
2134 for out
in context
.active_node
.outputs
:
2135 if is_visible_socket(out
):
2140 def execute(self
, context
):
2141 nodes
, links
= get_nodes_links(context
)
2142 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2145 if node
.type in image_types
:
2146 if node
.type == "TEXTURE":
2147 if node
.texture
: # node has texture assigned
2148 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2149 if node
.texture
.image
: # texture has image assigned
2150 node
.texture
.image
.reload()
2158 self
.report({'INFO'}, "Reloaded images")
2159 print("Reloaded " + str(num_reloaded
) + " images")
2160 force_update(context
)
2163 self
.report({'WARNING'}, "No images found to reload in this node tree")
2164 return {'CANCELLED'}
2167 class NWSwitchNodeType(Operator
, NWBase
):
2168 """Switch type of selected nodes """
2169 bl_idname
= "node.nw_swtch_node_type"
2170 bl_label
= "Switch Node Type"
2171 bl_options
= {'REGISTER', 'UNDO'}
2173 to_type
: EnumProperty(
2174 name
="Switch to type",
2175 items
=list(shaders_input_nodes_props
) +
2176 list(shaders_output_nodes_props
) +
2177 list(shaders_shader_nodes_props
) +
2178 list(shaders_texture_nodes_props
) +
2179 list(shaders_color_nodes_props
) +
2180 list(shaders_vector_nodes_props
) +
2181 list(shaders_converter_nodes_props
) +
2182 list(shaders_layout_nodes_props
) +
2183 list(compo_input_nodes_props
) +
2184 list(compo_output_nodes_props
) +
2185 list(compo_color_nodes_props
) +
2186 list(compo_converter_nodes_props
) +
2187 list(compo_filter_nodes_props
) +
2188 list(compo_vector_nodes_props
) +
2189 list(compo_matte_nodes_props
) +
2190 list(compo_distort_nodes_props
) +
2191 list(compo_layout_nodes_props
) +
2192 list(blender_mat_input_nodes_props
) +
2193 list(blender_mat_output_nodes_props
) +
2194 list(blender_mat_color_nodes_props
) +
2195 list(blender_mat_vector_nodes_props
) +
2196 list(blender_mat_converter_nodes_props
) +
2197 list(blender_mat_layout_nodes_props
) +
2198 list(texture_input_nodes_props
) +
2199 list(texture_output_nodes_props
) +
2200 list(texture_color_nodes_props
) +
2201 list(texture_pattern_nodes_props
) +
2202 list(texture_textures_nodes_props
) +
2203 list(texture_converter_nodes_props
) +
2204 list(texture_distort_nodes_props
) +
2205 list(texture_layout_nodes_props
)
2208 geo_to_type
: StringProperty(
2209 name
="Switch to type",
2213 def execute(self
, context
):
2214 nodes
, links
= get_nodes_links(context
)
2215 to_type
= self
.to_type
2216 if self
.geo_to_type
!= '':
2217 to_type
= self
.geo_to_type
2218 # Those types of nodes will not swap.
2219 src_excludes
= ('NodeFrame')
2220 # Those attributes of nodes will be copied if possible
2221 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
2222 'show_options', 'show_preview', 'show_texture',
2223 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2225 selected
= [n
for n
in nodes
if n
.select
]
2227 for node
in [n
for n
in selected
if
2228 n
.rna_type
.identifier
not in src_excludes
and
2229 n
.rna_type
.identifier
!= to_type
]:
2230 new_node
= nodes
.new(to_type
)
2231 for attr
in attrs_to_pass
:
2232 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
2233 setattr(new_node
, attr
, getattr(node
, attr
))
2234 # set image datablock of dst to image of src
2235 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
2237 new_node
.image
= node
.image
2239 if new_node
.type == 'SWITCH':
2240 new_node
.hide
= True
2241 # Dictionaries: src_sockets and dst_sockets:
2242 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2243 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2244 # in 'INPUTS' and 'OUTPUTS':
2245 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2247 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2249 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2250 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2253 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2254 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2256 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2257 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2258 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2259 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
2260 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2261 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
2262 # enumerate in inputs, then in outputs
2263 # find name, default value and links of socket
2264 for i
, socket
in enumerate(in_out
):
2265 the_name
= socket
.name
2267 # Not every socket, especially in outputs has "default_value"
2268 if hasattr(socket
, 'default_value'):
2269 dval
= socket
.default_value
2271 for lnk
in socket
.links
:
2272 socket_links
.append(lnk
)
2273 # check type of socket to fill proper keys.
2274 for the_type
in types_order_one
:
2275 if socket
.type == the_type
:
2276 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2277 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2278 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
2279 # Check which of the types in inputs/outputs is considered to be "main".
2280 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2281 for type_check
in types_order_one
:
2282 if sockets
[in_out_name
][type_check
]:
2283 sockets
[in_out_name
]['MAIN'] = type_check
2287 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2288 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2291 for inout
, soctype
in (
2292 ('INPUTS', 'MAIN',),
2293 ('INPUTS', 'SHADER',),
2294 ('INPUTS', 'RGBA',),
2295 ('INPUTS', 'VECTOR',),
2296 ('INPUTS', 'VALUE',),
2297 ('OUTPUTS', 'MAIN',),
2298 ('OUTPUTS', 'SHADER',),
2299 ('OUTPUTS', 'RGBA',),
2300 ('OUTPUTS', 'VECTOR',),
2301 ('OUTPUTS', 'VALUE',),
2303 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
2304 if soctype
== 'MAIN':
2305 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
2306 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
2308 sc
= src_sockets
[inout
][soctype
]
2309 dt
= dst_sockets
[inout
][soctype
]
2310 # start with 'dt' to determine number of possibilities.
2311 for i
, soc
in enumerate(dt
):
2312 # if src main has enough entries - match them with dst main sockets by indexes.
2314 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
2315 # add 'VALUE_NAME' criterion to inputs.
2316 if inout
== 'INPUTS' and soctype
== 'VALUE':
2318 if s
[2] == soc
[2]: # if names match
2319 # append src (index, dval), dst (index, dval)
2320 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
2322 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2323 # This creates better links when relinking textures.
2324 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
2325 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
2327 # Pass default values and RELINK:
2328 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2329 # INPUTS: Base on matches in proper order.
2330 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
2332 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
2333 new_node
.inputs
[dst_i
].default_value
= src_dval
2334 # Special case: switch to math
2335 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2336 new_node
.type == 'MATH' and\
2338 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
2339 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
2340 if node
.type == 'MIX_RGB':
2341 if node
.blend_type
in [o
[0] for o
in operations
]:
2342 new_node
.operation
= node
.blend_type
2343 # Special case: switch from math to some types
2344 if node
.type == 'MATH' and\
2345 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2348 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
2349 if new_node
.type == 'MIX_RGB':
2350 if node
.operation
in [t
[0] for t
in blend_types
]:
2351 new_node
.blend_type
= node
.operation
2352 # Set Fac of MIX_RGB to 1.0
2353 new_node
.inputs
[0].default_value
= 1.0
2354 # make link only when dst matching input is not linked already.
2355 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
2356 in_src_link
= node
.inputs
[src_i
].links
[0]
2357 in_dst_socket
= new_node
.inputs
[dst_i
]
2358 links
.new(in_src_link
.from_socket
, in_dst_socket
)
2359 links
.remove(in_src_link
)
2360 # OUTPUTS: Base on matches in proper order.
2361 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
2362 for out_src_link
in node
.outputs
[src_i
].links
:
2363 out_dst_socket
= new_node
.outputs
[dst_i
]
2364 links
.new(out_dst_socket
, out_src_link
.to_socket
)
2365 # relink rest inputs if possible, no criteria
2366 for src_inp
in node
.inputs
:
2367 for dst_inp
in new_node
.inputs
:
2368 if src_inp
.links
and not dst_inp
.links
:
2369 src_link
= src_inp
.links
[0]
2370 links
.new(src_link
.from_socket
, dst_inp
)
2371 links
.remove(src_link
)
2372 # relink rest outputs if possible, base on node kind if any left.
2373 for src_o
in node
.outputs
:
2374 for out_src_link
in src_o
.links
:
2375 for dst_o
in new_node
.outputs
:
2376 if src_o
.type == dst_o
.type:
2377 links
.new(dst_o
, out_src_link
.to_socket
)
2378 # relink rest outputs no criteria if any left. Link all from first output.
2379 for src_o
in node
.outputs
:
2380 for out_src_link
in src_o
.links
:
2381 if new_node
.outputs
:
2382 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2384 force_update(context
)
2388 class NWMergeNodes(Operator
, NWBase
):
2389 bl_idname
= "node.nw_merge_nodes"
2390 bl_label
= "Merge Nodes"
2391 bl_description
= "Merge Selected Nodes"
2392 bl_options
= {'REGISTER', 'UNDO'}
2396 description
="All possible blend types, boolean operations and math operations",
2397 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
],
2399 merge_type
: EnumProperty(
2401 description
="Type of Merge to be used",
2403 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2404 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2405 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2406 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2407 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2408 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2409 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2413 # Check if the link connects to a node that is in selected_nodes
2414 # If not, then check recursively for each link in the nodes outputs.
2415 # If yes, return True. If the recursion stops without finding a node
2416 # in selected_nodes, it returns False. The depth is used to prevent
2417 # getting stuck in a loop because of an already present cycle.
2419 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
2421 # We're stuck in a cycle, but that cycle was already present,
2422 # so we return False.
2423 # NOTE: The number 255 is arbitrary, but seems to work well.
2426 if node
in selected_nodes
:
2428 if not node
.outputs
:
2430 for output
in node
.outputs
:
2431 if output
.is_linked
:
2432 for olink
in output
.links
:
2433 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
2435 # None of the outputs found a node in selected_nodes, so there is no cycle.
2438 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2439 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2440 # be connected. The last one is assumed to be a multi input socket.
2441 # For convenience the node is returned.
2443 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
2444 # The y-location of the last node
2445 loc_y
= nodes_list
[-1][2]
2446 if merge_position
== 'CENTER':
2447 # Average the y-location
2448 for i
in range(len(nodes_list
)-1):
2449 loc_y
+= nodes_list
[i
][2]
2450 loc_y
= loc_y
/len(nodes_list
)
2451 new_node
= nodes
.new(node_name
)
2452 new_node
.hide
= do_hide
2453 new_node
.location
.x
= loc_x
2454 new_node
.location
.y
= loc_y
2455 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
2457 outputs_for_multi_input
= []
2458 for i
,node
in enumerate(selected_nodes
):
2460 # Search for the first node which had output links that do not create
2461 # a cycle, which we can then reconnect afterwards.
2462 if prev_links
== [] and node
.outputs
[0].is_linked
:
2463 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
2464 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2465 # To get the placement to look right we need to reverse the order in which we connect the
2466 # outputs to the multi input socket.
2467 if i
< len(socket_indices
) - 1:
2468 ind
= socket_indices
[i
]
2469 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
2471 outputs_for_multi_input
.insert(0, node
.outputs
[0])
2472 if outputs_for_multi_input
!= []:
2473 ind
= socket_indices
[-1]
2474 for output
in outputs_for_multi_input
:
2475 links
.new(output
, new_node
.inputs
[ind
])
2476 if prev_links
!= []:
2477 for link
in prev_links
:
2478 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
2481 def execute(self
, context
):
2482 settings
= context
.preferences
.addons
[__name__
].preferences
2483 merge_hide
= settings
.merge_hide
2484 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2487 do_hide_shader
= False
2488 if merge_hide
== 'ALWAYS':
2490 do_hide_shader
= True
2491 elif merge_hide
== 'NON_SHADER':
2494 tree_type
= context
.space_data
.node_tree
.type
2495 if tree_type
== 'GEOMETRY':
2496 node_type
= 'GeometryNode'
2497 if tree_type
== 'COMPOSITING':
2498 node_type
= 'CompositorNode'
2499 elif tree_type
== 'SHADER':
2500 node_type
= 'ShaderNode'
2501 elif tree_type
== 'TEXTURE':
2502 node_type
= 'TextureNode'
2503 nodes
, links
= get_nodes_links(context
)
2505 merge_type
= self
.merge_type
2506 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2507 # 'ZCOMBINE' works only if mode == 'MIX'
2508 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2509 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2512 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2514 # The math nodes used for geometry nodes are of type 'ShaderNode'
2515 if merge_type
== 'MATH' and tree_type
== 'GEOMETRY':
2516 node_type
= 'ShaderNode'
2517 selected_mix
= [] # entry = [index, loc]
2518 selected_shader
= [] # entry = [index, loc]
2519 selected_geometry
= [] # entry = [index, loc]
2520 selected_math
= [] # entry = [index, loc]
2521 selected_vector
= [] # entry = [index, loc]
2522 selected_z
= [] # entry = [index, loc]
2523 selected_alphaover
= [] # entry = [index, loc]
2525 for i
, node
in enumerate(nodes
):
2526 if node
.select
and node
.outputs
:
2527 if merge_type
== 'AUTO':
2528 for (type, types_list
, dst
) in (
2529 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2530 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2531 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2532 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2533 ('VECTOR', [], selected_vector
),
2535 output_type
= node
.outputs
[0].type
2536 valid_mode
= mode
in types_list
2537 # When mode is 'MIX' we have to cheat since the mix node is not used in
2539 if tree_type
== 'GEOMETRY':
2541 if output_type
== 'VALUE' and type == 'VALUE':
2543 elif output_type
== 'VECTOR' and type == 'VECTOR':
2545 elif type == 'GEOMETRY':
2547 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2548 # Cheat that output type is 'RGBA',
2549 # and that 'MIX' exists in math operations list.
2550 # This way when selected_mix list is analyzed:
2551 # Node data will be appended even though it doesn't meet requirements.
2552 elif output_type
!= 'SHADER' and mode
== 'MIX':
2553 output_type
= 'RGBA'
2555 if output_type
== type and valid_mode
:
2556 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2558 for (type, types_list
, dst
) in (
2559 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2560 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2561 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2562 ('MATH', [t
[0] for t
in operations
], selected_math
),
2563 ('ZCOMBINE', ('MIX', ), selected_z
),
2564 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2566 if merge_type
== type and mode
in types_list
:
2567 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2568 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2569 # use only 'Mix' nodes for merging.
2570 # For that we add selected_math list to selected_mix list and clear selected_math.
2571 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2572 selected_mix
+= selected_math
2574 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2577 count_before
= len(nodes
)
2578 # sort list by loc_x - reversed
2579 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2581 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2582 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2584 # Change the node type for math nodes in a geometry node tree.
2585 if tree_type
== 'GEOMETRY':
2586 if nodes_list
is selected_math
or nodes_list
is selected_vector
:
2587 node_type
= 'ShaderNode'
2591 node_type
= 'GeometryNode'
2592 if merge_position
== 'CENTER':
2593 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)
2594 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2600 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2604 if nodes_list
== selected_shader
and not do_hide_shader
:
2606 the_range
= len(nodes_list
) - 1
2607 if len(nodes_list
) == 1:
2610 for i
in range(the_range
):
2611 if nodes_list
== selected_mix
:
2612 add_type
= node_type
+ 'MixRGB'
2613 add
= nodes
.new(add_type
)
2614 add
.blend_type
= mode
2616 add
.inputs
[0].default_value
= 1.0
2617 add
.show_preview
= False
2623 add
.width_hidden
= 100.0
2624 elif nodes_list
== selected_math
:
2625 add_type
= node_type
+ 'Math'
2626 add
= nodes
.new(add_type
)
2627 add
.operation
= mode
2633 add
.width_hidden
= 100.0
2634 elif nodes_list
== selected_shader
:
2636 add_type
= node_type
+ 'MixShader'
2637 add
= nodes
.new(add_type
)
2638 add
.hide
= do_hide_shader
2643 add
.width_hidden
= 100.0
2645 add_type
= node_type
+ 'AddShader'
2646 add
= nodes
.new(add_type
)
2647 add
.hide
= do_hide_shader
2652 add
.width_hidden
= 100.0
2653 elif nodes_list
== selected_geometry
:
2654 if mode
in ('JOIN', 'MIX'):
2655 add_type
= node_type
+ 'JoinGeometry'
2656 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2658 add_type
= node_type
+ 'Boolean'
2659 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2660 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2661 add
.operation
= mode
2664 elif nodes_list
== selected_vector
:
2665 add_type
= node_type
+ 'VectorMath'
2666 add
= nodes
.new(add_type
)
2667 add
.operation
= mode
2673 add
.width_hidden
= 100.0
2674 elif nodes_list
== selected_z
:
2675 add
= nodes
.new('CompositorNodeZcombine')
2676 add
.show_preview
= False
2682 add
.width_hidden
= 100.0
2683 elif nodes_list
== selected_alphaover
:
2684 add
= nodes
.new('CompositorNodeAlphaOver')
2685 add
.show_preview
= False
2691 add
.width_hidden
= 100.0
2692 add
.location
= loc_x
, loc_y
2696 # This has already been handled separately
2700 count_after
= len(nodes
)
2701 index
= count_after
- 1
2702 first_selected
= nodes
[nodes_list
[0][0]]
2703 # "last" node has been added as first, so its index is count_before.
2704 last_add
= nodes
[count_before
]
2705 # Create list of invalid indexes.
2706 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2709 # Two nodes were selected and first selected has no output links, second selected has output links.
2710 # Then add links from last add to all links 'to_socket' of out links of second selected.
2711 if len(nodes_list
) == 2:
2712 if not first_selected
.outputs
[0].links
:
2713 second_selected
= nodes
[nodes_list
[1][0]]
2714 for ss_link
in second_selected
.outputs
[0].links
:
2715 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2716 # Link only if "to_node" index not in invalid indexes list.
2717 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2718 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2719 # add links from last_add to all links 'to_socket' of out links of first selected.
2720 for fs_link
in first_selected
.outputs
[0].links
:
2721 # Link only if "to_node" index not in invalid indexes list.
2722 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2723 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2724 # add link from "first" selected and "first" add node
2725 node_to
= nodes
[count_after
- 1]
2726 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2727 if node_to
.type == 'ZCOMBINE':
2728 for fs_out
in first_selected
.outputs
:
2729 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2730 links
.new(fs_out
, node_to
.inputs
[1])
2732 # add links between added ADD nodes and between selected and ADD nodes
2733 for i
in range(count_adds
):
2734 if i
< count_adds
- 1:
2735 node_from
= nodes
[index
]
2736 node_to
= nodes
[index
- 1]
2737 node_to_input_i
= first
2738 node_to_z_i
= 1 # if z combine - link z to first z input
2739 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2740 if node_to
.type == 'ZCOMBINE':
2741 for from_out
in node_from
.outputs
:
2742 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2743 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2744 if len(nodes_list
) > 1:
2745 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2746 node_to
= nodes
[index
]
2747 node_to_input_i
= second
2748 node_to_z_i
= 3 # if z combine - link z to second z input
2749 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2750 if node_to
.type == 'ZCOMBINE':
2751 for from_out
in node_from
.outputs
:
2752 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2753 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2755 # set "last" of added nodes as active
2756 nodes
.active
= last_add
2757 for i
, x
, y
, dx
, h
in nodes_list
:
2758 nodes
[i
].select
= False
2763 class NWBatchChangeNodes(Operator
, NWBase
):
2764 bl_idname
= "node.nw_batch_change"
2765 bl_label
= "Batch Change"
2766 bl_description
= "Batch Change Blend Type and Math Operation"
2767 bl_options
= {'REGISTER', 'UNDO'}
2769 blend_type
: EnumProperty(
2771 items
=blend_types
+ navs
,
2773 operation
: EnumProperty(
2775 items
=operations
+ navs
,
2778 def execute(self
, context
):
2779 blend_type
= self
.blend_type
2780 operation
= self
.operation
2781 for node
in context
.selected_nodes
:
2782 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2783 if not blend_type
in [nav
[0] for nav
in navs
]:
2784 node
.blend_type
= blend_type
2786 if blend_type
== 'NEXT':
2787 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2788 #index = blend_types.index(node.blend_type)
2789 if index
== len(blend_types
) - 1:
2790 node
.blend_type
= blend_types
[0][0]
2792 node
.blend_type
= blend_types
[index
+ 1][0]
2794 if blend_type
== 'PREV':
2795 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2797 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2799 node
.blend_type
= blend_types
[index
- 1][0]
2801 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2802 if not operation
in [nav
[0] for nav
in navs
]:
2803 node
.operation
= operation
2805 if operation
== 'NEXT':
2806 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2807 #index = operations.index(node.operation)
2808 if index
== len(operations
) - 1:
2809 node
.operation
= operations
[0][0]
2811 node
.operation
= operations
[index
+ 1][0]
2813 if operation
== 'PREV':
2814 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2815 #index = operations.index(node.operation)
2817 node
.operation
= operations
[len(operations
) - 1][0]
2819 node
.operation
= operations
[index
- 1][0]
2824 class NWChangeMixFactor(Operator
, NWBase
):
2825 bl_idname
= "node.nw_factor"
2826 bl_label
= "Change Factor"
2827 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2828 bl_options
= {'REGISTER', 'UNDO'}
2830 # option: Change factor.
2831 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2832 # Else - change factor by option value.
2833 option
: FloatProperty()
2835 def execute(self
, context
):
2836 nodes
, links
= get_nodes_links(context
)
2837 option
= self
.option
2838 selected
= [] # entry = index
2839 for si
, node
in enumerate(nodes
):
2841 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2845 fac
= nodes
[si
].inputs
[0]
2846 nodes
[si
].hide
= False
2847 if option
in {0.0, 1.0}:
2848 fac
.default_value
= option
2850 fac
.default_value
+= option
2855 class NWCopySettings(Operator
, NWBase
):
2856 bl_idname
= "node.nw_copy_settings"
2857 bl_label
= "Copy Settings"
2858 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2859 bl_options
= {'REGISTER', 'UNDO'}
2862 def poll(cls
, context
):
2864 if nw_check(context
):
2866 context
.active_node
is not None and
2867 context
.active_node
.type != 'FRAME'
2872 def execute(self
, context
):
2873 node_active
= context
.active_node
2874 node_selected
= context
.selected_nodes
2877 if not (len(node_selected
) > 1):
2878 self
.report({'ERROR'}, "2 nodes must be selected at least")
2879 return {'CANCELLED'}
2881 # Check if active node is in the selection
2882 selected_node_names
= [n
.name
for n
in node_selected
]
2883 if node_active
.name
not in selected_node_names
:
2884 self
.report({'ERROR'}, "No active node")
2885 return {'CANCELLED'}
2887 # Get nodes in selection by type
2888 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2890 if not (len(valid_nodes
) > 1) and node_active
:
2891 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2892 return {'CANCELLED'}
2894 if len(valid_nodes
) != len(node_selected
):
2895 # Report nodes that are not valid
2896 valid_node_names
= [n
.name
for n
in valid_nodes
]
2897 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2898 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2900 # Reference original
2902 #node_selected_names = [n.name for n in node_selected]
2907 # Deselect all nodes
2908 for i
in node_selected
:
2911 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2912 # Run through all other nodes
2913 for node
in valid_nodes
[1:]:
2915 # Check for frame node
2916 parent
= node
.parent
if node
.parent
else None
2917 node_loc
= [node
.location
.x
, node
.location
.y
]
2919 # Select original to duplicate
2922 # Duplicate selected node
2923 bpy
.ops
.node
.duplicate()
2924 new_node
= context
.selected_nodes
[0]
2927 new_node
.select
= False
2929 # Properties to copy
2930 node_tree
= node
.id_data
2931 props_to_copy
= 'bl_idname name location height width'.split(' ')
2935 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2936 for i
in (i
for i
in mappings
if i
.is_linked
):
2938 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2941 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2942 props_to_copy
.pop(0)
2944 for prop
in props_to_copy
:
2945 setattr(new_node
, prop
, props
[prop
])
2947 # Get the node tree to remove the old node
2948 nodes
= node_tree
.nodes
2950 new_node
.name
= props
['name']
2953 new_node
.parent
= parent
2954 new_node
.location
= node_loc
2956 for str_from
, str_to
in reconnections
:
2957 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2959 success_names
.append(new_node
.name
)
2962 node_tree
.nodes
.active
= orig
2963 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2967 class NWCopyLabel(Operator
, NWBase
):
2968 bl_idname
= "node.nw_copy_label"
2969 bl_label
= "Copy Label"
2970 bl_options
= {'REGISTER', 'UNDO'}
2972 option
: EnumProperty(
2974 description
="Source of name of label",
2976 ('FROM_ACTIVE', 'from active', 'from active node',),
2977 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2978 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2982 def execute(self
, context
):
2983 nodes
, links
= get_nodes_links(context
)
2984 option
= self
.option
2985 active
= nodes
.active
2986 if option
== 'FROM_ACTIVE':
2988 src_label
= active
.label
2989 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2990 node
.label
= src_label
2991 elif option
== 'FROM_NODE':
2992 selected
= [n
for n
in nodes
if n
.select
]
2993 for node
in selected
:
2994 for input in node
.inputs
:
2996 src
= input.links
[0].from_node
2997 node
.label
= src
.label
2999 elif option
== 'FROM_SOCKET':
3000 selected
= [n
for n
in nodes
if n
.select
]
3001 for node
in selected
:
3002 for input in node
.inputs
:
3004 src
= input.links
[0].from_socket
3005 node
.label
= src
.name
3011 class NWClearLabel(Operator
, NWBase
):
3012 bl_idname
= "node.nw_clear_label"
3013 bl_label
= "Clear Label"
3014 bl_options
= {'REGISTER', 'UNDO'}
3016 option
: BoolProperty()
3018 def execute(self
, context
):
3019 nodes
, links
= get_nodes_links(context
)
3020 for node
in [n
for n
in nodes
if n
.select
]:
3025 def invoke(self
, context
, event
):
3027 return self
.execute(context
)
3029 return context
.window_manager
.invoke_confirm(self
, event
)
3032 class NWModifyLabels(Operator
, NWBase
):
3033 """Modify Labels of all selected nodes"""
3034 bl_idname
= "node.nw_modify_labels"
3035 bl_label
= "Modify Labels"
3036 bl_options
= {'REGISTER', 'UNDO'}
3038 prepend
: StringProperty(
3039 name
="Add to Beginning"
3041 append
: StringProperty(
3044 replace_from
: StringProperty(
3045 name
="Text to Replace"
3047 replace_to
: StringProperty(
3051 def execute(self
, context
):
3052 nodes
, links
= get_nodes_links(context
)
3053 for node
in [n
for n
in nodes
if n
.select
]:
3054 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
3058 def invoke(self
, context
, event
):
3062 return context
.window_manager
.invoke_props_dialog(self
)
3065 class NWAddTextureSetup(Operator
, NWBase
):
3066 bl_idname
= "node.nw_add_texture"
3067 bl_label
= "Texture Setup"
3068 bl_description
= "Add Texture Node Setup to Selected Shaders"
3069 bl_options
= {'REGISTER', 'UNDO'}
3071 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
3074 def poll(cls
, context
):
3076 if nw_check(context
):
3077 space
= context
.space_data
3078 if space
.tree_type
== 'ShaderNodeTree':
3082 def execute(self
, context
):
3083 nodes
, links
= get_nodes_links(context
)
3084 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3085 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
3086 selected_nodes
= [n
for n
in nodes
if n
.select
]
3087 for t_node
in selected_nodes
:
3091 for index
, i
in enumerate(t_node
.inputs
):
3097 locx
= t_node
.location
.x
3098 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
3100 xoffset
= [500, 700]
3102 if t_node
.type in texture_types
+ ['MAPPING']:
3103 xoffset
= [290, 500]
3107 image_type
= 'ShaderNodeTexImage'
3109 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
3110 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
3111 if t_node
.type == 'BACKGROUND':
3112 image_type
= 'ShaderNodeTexEnvironment'
3115 tex
= nodes
.new(image_type
)
3116 tex
.location
= [locx
- 200, locy
+ 112]
3118 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
3120 t_node
.select
= False
3121 if self
.add_mapping
or is_texture
:
3122 if t_node
.type != 'MAPPING':
3123 m
= nodes
.new('ShaderNodeMapping')
3124 m
.location
= [locx
- xoffset
[0], locy
+ 141]
3128 coord
= nodes
.new('ShaderNodeTexCoord')
3129 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
3132 links
.new(m
.outputs
[0], tex
.inputs
[0])
3133 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3136 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
3137 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3139 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
3143 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
3144 bl_idname
= "node.nw_add_textures_for_principled"
3145 bl_label
= "Principled Texture Setup"
3146 bl_description
= "Add Texture Node Setup for Principled BSDF"
3147 bl_options
= {'REGISTER', 'UNDO'}
3149 directory
: StringProperty(
3153 description
='Folder to search in for image files'
3155 files
: CollectionProperty(
3156 type=bpy
.types
.OperatorFileListElement
,
3157 options
={'HIDDEN', 'SKIP_SAVE'}
3160 relative_path
: BoolProperty(
3161 name
='Relative Path',
3162 description
='Set the file path relative to the blend file, when possible',
3171 def draw(self
, context
):
3172 layout
= self
.layout
3173 layout
.alignment
= 'LEFT'
3175 layout
.prop(self
, 'relative_path')
3178 def poll(cls
, context
):
3180 if nw_check(context
):
3181 space
= context
.space_data
3182 if space
.tree_type
== 'ShaderNodeTree':
3186 def execute(self
, context
):
3187 # Check if everything is ok
3188 if not self
.directory
:
3189 self
.report({'INFO'}, 'No Folder Selected')
3190 return {'CANCELLED'}
3191 if not self
.files
[:]:
3192 self
.report({'INFO'}, 'No Files Selected')
3193 return {'CANCELLED'}
3195 nodes
, links
= get_nodes_links(context
)
3196 active_node
= nodes
.active
3197 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
3198 self
.report({'INFO'}, 'Select Principled BSDF')
3199 return {'CANCELLED'}
3202 def split_into__components(fname
):
3203 # Split filename into components
3204 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3206 fname
= path
.splitext(fname
)[0]
3208 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
3209 # Separate CamelCase by space
3210 fname
= re
.sub(r
"([a-z])([A-Z])", r
"\g<1> \g<2>",fname
)
3211 # Replace common separators with SPACE
3212 separators
= ['_', '.', '-', '__', '--', '#']
3213 for sep
in separators
:
3214 fname
= fname
.replace(sep
, ' ')
3216 components
= fname
.split(' ')
3217 components
= [c
.lower() for c
in components
]
3220 # Filter textures names for texturetypes in filenames
3221 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3222 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
3223 normal_abbr
= tags
.normal
.split(' ')
3224 bump_abbr
= tags
.bump
.split(' ')
3225 gloss_abbr
= tags
.gloss
.split(' ')
3226 rough_abbr
= tags
.rough
.split(' ')
3228 ['Displacement', tags
.displacement
.split(' '), None],
3229 ['Base Color', tags
.base_color
.split(' '), None],
3230 ['Subsurface Color', tags
.sss_color
.split(' '), None],
3231 ['Metallic', tags
.metallic
.split(' '), None],
3232 ['Specular', tags
.specular
.split(' '), None],
3233 ['Roughness', rough_abbr
+ gloss_abbr
, None],
3234 ['Normal', normal_abbr
+ bump_abbr
, None],
3235 ['Transmission', tags
.transmission
.split(' '), None],
3236 ['Emission', tags
.emission
.split(' '), None],
3237 ['Alpha', tags
.alpha
.split(' '), None],
3238 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
3241 # Look through texture_types and set value as filename of first matched file
3242 def match_files_to_socket_names():
3243 for sname
in socketnames
:
3244 for file in self
.files
:
3246 filenamecomponents
= split_into__components(fname
)
3247 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
3248 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3253 match_files_to_socket_names()
3254 # Remove socketnames without found files
3255 socketnames
= [s
for s
in socketnames
if s
[2]
3256 and path
.exists(self
.directory
+s
[2])]
3258 self
.report({'INFO'}, 'No matching images found')
3259 print('No matching images found')
3260 return {'CANCELLED'}
3262 # Don't override path earlier as os.path is used to check the absolute path
3263 import_path
= self
.directory
3264 if self
.relative_path
:
3265 if bpy
.data
.filepath
:
3267 import_path
= bpy
.path
.relpath(self
.directory
)
3272 print('\nMatched Textures:')
3277 roughness_node
= None
3278 for i
, sname
in enumerate(socketnames
):
3279 print(i
, sname
[0], sname
[2])
3281 # DISPLACEMENT NODES
3282 if sname
[0] == 'Displacement':
3283 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
3284 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3285 disp_texture
.image
= img
3286 disp_texture
.label
= 'Displacement'
3287 if disp_texture
.image
:
3288 disp_texture
.image
.colorspace_settings
.is_data
= True
3290 # Add displacement offset nodes
3291 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
3292 # Align the Displacement node under the active Principled BSDF node
3293 disp_node
.location
= active_node
.location
+ Vector((100, -700))
3294 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
3296 # TODO Turn on true displacement in the material
3297 # Too complicated for now
3300 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
3302 if not output_node
[0].inputs
[2].is_linked
:
3303 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
3307 # AMBIENT OCCLUSION TEXTURE
3308 if sname
[0] == 'Ambient Occlusion':
3309 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
3310 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3311 ao_texture
.image
= img
3312 ao_texture
.label
= sname
[0]
3313 if ao_texture
.image
:
3314 ao_texture
.image
.colorspace_settings
.is_data
= True
3318 if not active_node
.inputs
[sname
[0]].is_linked
:
3319 # No texture node connected -> add texture node with new image
3320 texture_node
= nodes
.new(type='ShaderNodeTexImage')
3321 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3322 texture_node
.image
= img
3325 if sname
[0] == 'Normal':
3326 # Test if new texture node is normal or bump map
3327 fname_components
= split_into__components(sname
[2])
3328 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
3329 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
3331 # If Normal add normal node in between
3332 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
3333 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
3335 # If Bump add bump node in between
3336 normal_node
= nodes
.new(type='ShaderNodeBump')
3337 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
3339 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
3340 normal_node_texture
= texture_node
3342 elif sname
[0] == 'Roughness':
3343 # Test if glossy or roughness map
3344 fname_components
= split_into__components(sname
[2])
3345 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
3346 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
3349 # If Roughness nothing to to
3350 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3353 # If Gloss Map add invert node
3354 invert_node
= nodes
.new(type='ShaderNodeInvert')
3355 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
3357 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
3358 roughness_node
= texture_node
3361 # This is a simple connection Texture --> Input slot
3362 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3364 # Use non-color for all but 'Base Color' Textures
3365 if not sname
[0] in ['Base Color', 'Emission'] and texture_node
.image
:
3366 texture_node
.image
.colorspace_settings
.is_data
= True
3369 # If already texture connected. add to node list for alignment
3370 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
3372 # This are all connected texture nodes
3373 texture_nodes
.append(texture_node
)
3374 texture_node
.label
= sname
[0]
3377 texture_nodes
.append(disp_texture
)
3380 # We want the ambient occlusion texture to be the top most texture node
3381 texture_nodes
.insert(0, ao_texture
)
3384 for i
, texture_node
in enumerate(texture_nodes
):
3385 offset
= Vector((-550, (i
* -280) + 200))
3386 texture_node
.location
= active_node
.location
+ offset
3389 # Extra alignment if normal node was added
3390 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
3393 # Alignment of invert node if glossy map
3394 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
3396 # Add texture input + mapping
3397 mapping
= nodes
.new(type='ShaderNodeMapping')
3398 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
3399 if len(texture_nodes
) > 1:
3400 # If more than one texture add reroute node in between
3401 reroute
= nodes
.new(type='NodeReroute')
3402 texture_nodes
.append(reroute
)
3403 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
3404 reroute
.location
= tex_coords
+ Vector((-50, -120))
3405 for texture_node
in texture_nodes
:
3406 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
3407 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
3409 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
3411 # Connect texture_coordiantes to mapping node
3412 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
3413 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
3414 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
3416 # Create frame around tex coords and mapping
3417 frame
= nodes
.new(type='NodeFrame')
3418 frame
.label
= 'Mapping'
3419 mapping
.parent
= frame
3420 texture_input
.parent
= frame
3423 # Create frame around texture nodes
3424 frame
= nodes
.new(type='NodeFrame')
3425 frame
.label
= 'Textures'
3426 for tnode
in texture_nodes
:
3427 tnode
.parent
= frame
3431 active_node
.select
= False
3434 force_update(context
)
3438 class NWAddReroutes(Operator
, NWBase
):
3439 """Add Reroute Nodes and link them to outputs of selected nodes"""
3440 bl_idname
= "node.nw_add_reroutes"
3441 bl_label
= "Add Reroutes"
3442 bl_description
= "Add Reroutes to Outputs"
3443 bl_options
= {'REGISTER', 'UNDO'}
3445 option
: EnumProperty(
3448 ('ALL', 'to all', 'Add to all outputs'),
3449 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3450 ('LINKED', 'to linked', 'Add only to linked outputs'),
3454 def execute(self
, context
):
3455 tree_type
= context
.space_data
.node_tree
.type
3456 option
= self
.option
3457 nodes
, links
= get_nodes_links(context
)
3458 # output valid when option is 'all' or when 'loose' output has no links
3460 post_select
= [] # nodes to be selected after execution
3461 # create reroutes and recreate links
3462 for node
in [n
for n
in nodes
if n
.select
]:
3467 # unhide 'REROUTE' nodes to avoid issues with location.y
3468 if node
.type == 'REROUTE':
3470 # When node is hidden - width_hidden not usable.
3471 # Hack needed to calculate real width
3473 bpy
.ops
.node
.select_all(action
='DESELECT')
3474 helper
= nodes
.new('NodeReroute')
3475 helper
.select
= True
3477 # resize node and helper to zero. Then check locations to calculate width
3478 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
3479 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
3480 # restore node location
3481 node
.location
= x
, y
3484 # only helper is selected now
3485 bpy
.ops
.node
.delete()
3486 x
= node
.location
.x
+ width
+ 20.0
3487 if node
.type != 'REROUTE':
3491 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3492 for out_i
, output
in enumerate(node
.outputs
):
3493 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3494 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3495 if node
.type != 'R_LAYERS':
3497 else: # if 'R_LAYERS' check if output represent used render pass
3498 node_scene
= node
.scene
3499 node_layer
= node
.layer
3500 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3501 if output
.name
== 'Alpha':
3504 # check entries in global 'rl_outputs' variable
3505 for rlo
in rl_outputs
:
3506 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3507 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3510 valid
= ((option
== 'ALL') or
3511 (option
== 'LOOSE' and not output
.links
) or
3512 (option
== 'LINKED' and output
.links
))
3513 # Add reroutes only if valid, but offset location in all cases.
3515 n
= nodes
.new('NodeReroute')
3517 for link
in output
.links
:
3518 links
.new(n
.outputs
[0], link
.to_socket
)
3519 links
.new(output
, n
.inputs
[0])
3521 post_select
.append(n
)
3525 # disselect the node so that after execution of script only newly created nodes are selected
3527 # nicer reroutes distribution along y when node.hide
3529 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3530 for reroute
in [r
for r
in nodes
if r
.select
]:
3531 reroute
.location
.y
-= y_translate
3532 for node
in post_select
:
3538 class NWLinkActiveToSelected(Operator
, NWBase
):
3539 """Link active node to selected nodes basing on various criteria"""
3540 bl_idname
= "node.nw_link_active_to_selected"
3541 bl_label
= "Link Active Node to Selected"
3542 bl_options
= {'REGISTER', 'UNDO'}
3544 replace
: BoolProperty()
3545 use_node_name
: BoolProperty()
3546 use_outputs_names
: BoolProperty()
3549 def poll(cls
, context
):
3551 if nw_check(context
):
3552 if context
.active_node
is not None:
3553 if context
.active_node
.select
:
3557 def execute(self
, context
):
3558 nodes
, links
= get_nodes_links(context
)
3559 replace
= self
.replace
3560 use_node_name
= self
.use_node_name
3561 use_outputs_names
= self
.use_outputs_names
3562 active
= nodes
.active
3563 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3564 outputs
= [] # Only usable outputs of active nodes will be stored here.
3565 for out
in active
.outputs
:
3566 if active
.type != 'R_LAYERS':
3569 # 'R_LAYERS' node type needs special handling.
3570 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3571 # Only outputs that represent used passes should be taken into account
3572 # Check if pass represented by output is used.
3573 # global 'rl_outputs' list will be used for that
3574 for rlo
in rl_outputs
:
3575 pass_used
= False # initial value. Will be set to True if pass is used
3576 if out
.name
== 'Alpha':
3577 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3579 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3580 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3581 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3585 doit
= True # Will be changed to False when links successfully added to previous output.
3588 for node
in selected
:
3589 dst_name
= node
.name
# Will be compared with src_name if needed.
3590 # When node has label - use it as dst_name
3592 dst_name
= node
.label
3593 valid
= True # Initial value. Will be changed to False if names don't match.
3594 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
3596 # Set src_name to source node name or label
3597 src_name
= active
.name
3599 src_name
= active
.label
3600 elif use_outputs_names
:
3601 src_name
= (out
.name
, )
3602 for rlo
in rl_outputs
:
3603 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3604 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3605 if dst_name
not in src_name
:
3608 for input in node
.inputs
:
3609 if input.type == out
.type or node
.type == 'REROUTE':
3610 if replace
or not input.is_linked
:
3611 links
.new(out
, input)
3612 if not use_node_name
and not use_outputs_names
:
3619 class NWAlignNodes(Operator
, NWBase
):
3620 '''Align the selected nodes neatly in a row/column'''
3621 bl_idname
= "node.nw_align_nodes"
3622 bl_label
= "Align Nodes"
3623 bl_options
= {'REGISTER', 'UNDO'}
3624 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3626 def execute(self
, context
):
3627 nodes
, links
= get_nodes_links(context
)
3628 margin
= self
.margin
3632 if node
.select
and node
.type != 'FRAME':
3633 selection
.append(node
)
3635 # If no nodes are selected, align all nodes
3639 elif nodes
.active
in selection
:
3640 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3642 # Check if nodes should be laid out horizontally or vertically
3643 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3644 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3645 x_range
= max(x_locs
) - min(x_locs
)
3646 y_range
= max(y_locs
) - min(y_locs
)
3647 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3648 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3649 horizontal
= x_range
> y_range
3651 # Sort selection by location of node mid-point
3653 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3655 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3659 for node
in selection
:
3660 current_margin
= margin
3661 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3664 node
.location
.x
= current_pos
3665 current_pos
+= current_margin
+ node
.dimensions
.x
3666 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3668 node
.location
.y
= current_pos
3669 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3670 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3672 # If active node is selected, center nodes around it
3673 if active_loc
is not None:
3674 active_loc_diff
= active_loc
- nodes
.active
.location
3675 for node
in selection
:
3676 node
.location
+= active_loc_diff
3677 else: # Position nodes centered around where they used to be
3678 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
3679 new_mid
= (max(locs
) + min(locs
)) / 2
3680 for node
in selection
:
3682 node
.location
.x
+= (mid_x
- new_mid
)
3684 node
.location
.y
+= (mid_y
- new_mid
)
3689 class NWSelectParentChildren(Operator
, NWBase
):
3690 bl_idname
= "node.nw_select_parent_child"
3691 bl_label
= "Select Parent or Children"
3692 bl_options
= {'REGISTER', 'UNDO'}
3694 option
: EnumProperty(
3697 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3698 ('CHILD', 'Select Children', 'Select members of selected frame'),
3702 def execute(self
, context
):
3703 nodes
, links
= get_nodes_links(context
)
3704 option
= self
.option
3705 selected
= [node
for node
in nodes
if node
.select
]
3706 if option
== 'PARENT':
3707 for sel
in selected
:
3710 parent
.select
= True
3711 else: # option == 'CHILD'
3712 for sel
in selected
:
3713 children
= [node
for node
in nodes
if node
.parent
== sel
]
3714 for kid
in children
:
3720 class NWDetachOutputs(Operator
, NWBase
):
3721 """Detach outputs of selected node leaving inputs linked"""
3722 bl_idname
= "node.nw_detach_outputs"
3723 bl_label
= "Detach Outputs"
3724 bl_options
= {'REGISTER', 'UNDO'}
3726 def execute(self
, context
):
3727 nodes
, links
= get_nodes_links(context
)
3728 selected
= context
.selected_nodes
3729 bpy
.ops
.node
.duplicate_move_keep_inputs()
3730 new_nodes
= context
.selected_nodes
3731 bpy
.ops
.node
.select_all(action
="DESELECT")
3732 for node
in selected
:
3734 bpy
.ops
.node
.delete_reconnect()
3735 for new_node
in new_nodes
:
3736 new_node
.select
= True
3737 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3742 class NWLinkToOutputNode(Operator
):
3743 """Link to Composite node or Material Output node"""
3744 bl_idname
= "node.nw_link_out"
3745 bl_label
= "Connect to Output"
3746 bl_options
= {'REGISTER', 'UNDO'}
3749 def poll(cls
, context
):
3751 if nw_check(context
):
3752 if context
.active_node
is not None:
3753 for out
in context
.active_node
.outputs
:
3754 if is_visible_socket(out
):
3759 def execute(self
, context
):
3760 nodes
, links
= get_nodes_links(context
)
3761 active
= nodes
.active
3764 tree_type
= context
.space_data
.tree_type
3765 if tree_type
== 'ShaderNodeTree':
3766 output_types
= [x
[1] for x
in shaders_output_nodes_props
] + ['OUTPUT']
3767 elif tree_type
== 'CompositorNodeTree':
3768 output_types
= ['COMPOSITE']
3769 elif tree_type
== 'TextureNodeTree':
3770 output_types
= ['OUTPUT']
3771 elif tree_type
== 'GeometryNodeTree':
3772 output_types
= ['GROUP_OUTPUT']
3774 if node
.type in output_types
:
3778 bpy
.ops
.node
.select_all(action
="DESELECT")
3779 if tree_type
== 'ShaderNodeTree':
3780 if context
.space_data
.shader_type
== 'OBJECT':
3781 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3782 elif context
.space_data
.shader_type
== 'WORLD':
3783 output_node
= nodes
.new('ShaderNodeOutputWorld')
3784 elif tree_type
== 'CompositorNodeTree':
3785 output_node
= nodes
.new('CompositorNodeComposite')
3786 elif tree_type
== 'TextureNodeTree':
3787 output_node
= nodes
.new('TextureNodeOutput')
3788 elif tree_type
== 'GeometryNodeTree':
3789 output_node
= nodes
.new('NodeGroupOutput')
3790 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3791 output_node
.location
.y
= active
.location
.y
3792 if (output_node
and active
.outputs
):
3793 for i
, output
in enumerate(active
.outputs
):
3794 if is_visible_socket(output
):
3797 for i
, output
in enumerate(active
.outputs
):
3798 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3803 if tree_type
== 'ShaderNodeTree':
3804 if active
.outputs
[output_index
].name
== 'Volume':
3806 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3808 elif tree_type
== 'GeometryNodeTree':
3809 if active
.outputs
[output_index
].type != 'GEOMETRY':
3810 return {'CANCELLED'}
3811 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3813 force_update(context
) # viewport render does not update
3818 class NWMakeLink(Operator
, NWBase
):
3819 """Make a link from one socket to another"""
3820 bl_idname
= 'node.nw_make_link'
3821 bl_label
= 'Make Link'
3822 bl_options
= {'REGISTER', 'UNDO'}
3823 from_socket
: IntProperty()
3824 to_socket
: IntProperty()
3826 def execute(self
, context
):
3827 nodes
, links
= get_nodes_links(context
)
3829 n1
= nodes
[context
.scene
.NWLazySource
]
3830 n2
= nodes
[context
.scene
.NWLazyTarget
]
3832 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3834 force_update(context
)
3839 class NWCallInputsMenu(Operator
, NWBase
):
3840 """Link from this output"""
3841 bl_idname
= 'node.nw_call_inputs_menu'
3842 bl_label
= 'Make Link'
3843 bl_options
= {'REGISTER', 'UNDO'}
3844 from_socket
: IntProperty()
3846 def execute(self
, context
):
3847 nodes
, links
= get_nodes_links(context
)
3849 context
.scene
.NWSourceSocket
= self
.from_socket
3851 n1
= nodes
[context
.scene
.NWLazySource
]
3852 n2
= nodes
[context
.scene
.NWLazyTarget
]
3853 if len(n2
.inputs
) > 1:
3854 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3855 elif len(n2
.inputs
) == 1:
3856 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3860 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3861 """Add an Image Sequence"""
3862 bl_idname
= 'node.nw_add_sequence'
3863 bl_label
= 'Import Image Sequence'
3864 bl_options
= {'REGISTER', 'UNDO'}
3866 directory
: StringProperty(
3869 filename
: StringProperty(
3872 files
: CollectionProperty(
3873 type=bpy
.types
.OperatorFileListElement
,
3874 options
={'HIDDEN', 'SKIP_SAVE'}
3876 relative_path
: BoolProperty(
3877 name
='Relative Path',
3878 description
='Set the file path relative to the blend file, when possible',
3882 def draw(self
, context
):
3883 layout
= self
.layout
3884 layout
.alignment
= 'LEFT'
3886 layout
.prop(self
, 'relative_path')
3888 def execute(self
, context
):
3889 nodes
, links
= get_nodes_links(context
)
3890 directory
= self
.directory
3891 filename
= self
.filename
3893 tree
= context
.space_data
.node_tree
3896 # print ("\nDIR:", directory)
3897 # print ("FN:", filename)
3898 # print ("Fs:", list(f.name for f in files), '\n')
3900 if tree
.type == 'SHADER':
3901 node_type
= "ShaderNodeTexImage"
3902 elif tree
.type == 'COMPOSITING':
3903 node_type
= "CompositorNodeImage"
3905 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3906 return {'CANCELLED'}
3908 if not files
[0].name
and not filename
:
3909 self
.report({'ERROR'}, "No file chosen")
3910 return {'CANCELLED'}
3911 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3912 # User has selected multiple files without an active one, or the active one is non-existant
3913 filename
= files
[0].name
3915 if not path
.exists(directory
+filename
):
3916 self
.report({'ERROR'}, filename
+" does not exist!")
3917 return {'CANCELLED'}
3919 without_ext
= '.'.join(filename
.split('.')[:-1])
3921 # if last digit isn't a number, it's not a sequence
3922 if not without_ext
[-1].isdigit():
3923 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3924 return {'CANCELLED'}
3927 extension
= filename
.split('.')[-1]
3928 reverse
= without_ext
[::-1] # reverse string
3931 for char
in reverse
:
3937 without_num
= without_ext
[:count_numbers
*-1]
3939 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3941 num_frames
= len(files
)
3943 nodes_list
= [node
for node
in nodes
]
3945 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3946 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3950 yloc
+= node_mid_pt(node
, 'y')
3951 yloc
= yloc
/len(nodes
)
3956 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3958 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3960 node
.label
= name_with_hashes
3962 filepath
= directory
+(without_ext
+'.'+extension
)
3963 if self
.relative_path
:
3964 if bpy
.data
.filepath
:
3966 filepath
= bpy
.path
.relpath(filepath
)
3970 img
= bpy
.data
.images
.load(filepath
)
3971 img
.source
= 'SEQUENCE'
3972 img
.name
= name_with_hashes
3974 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3975 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
3976 image_user
.frame_duration
= num_frames
3981 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3982 """Add multiple images at once"""
3983 bl_idname
= 'node.nw_add_multiple_images'
3984 bl_label
= 'Open Selected Images'
3985 bl_options
= {'REGISTER', 'UNDO'}
3986 directory
: StringProperty(
3989 files
: CollectionProperty(
3990 type=bpy
.types
.OperatorFileListElement
,
3991 options
={'HIDDEN', 'SKIP_SAVE'}
3994 def execute(self
, context
):
3995 nodes
, links
= get_nodes_links(context
)
3997 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3999 if context
.space_data
.node_tree
.type == 'SHADER':
4000 node_type
= "ShaderNodeTexImage"
4001 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
4002 node_type
= "CompositorNodeImage"
4004 self
.report({'ERROR'}, "Unsupported Node Tree type!")
4005 return {'CANCELLED'}
4008 for f
in self
.files
:
4011 node
= nodes
.new(node_type
)
4012 new_nodes
.append(node
)
4015 node
.width_hidden
= 100
4016 node
.location
.x
= xloc
4017 node
.location
.y
= yloc
4020 img
= bpy
.data
.images
.load(self
.directory
+fname
)
4023 # shift new nodes up to center of tree
4024 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
4026 if node
in new_nodes
:
4028 node
.location
.y
+= (list_size
/2)
4034 class NWViewerFocus(bpy
.types
.Operator
):
4035 """Set the viewer tile center to the mouse position"""
4036 bl_idname
= "node.nw_viewer_focus"
4037 bl_label
= "Viewer Focus"
4039 x
: bpy
.props
.IntProperty()
4040 y
: bpy
.props
.IntProperty()
4043 def poll(cls
, context
):
4044 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
4046 def execute(self
, context
):
4049 def invoke(self
, context
, event
):
4050 render
= context
.scene
.render
4051 space
= context
.space_data
4052 percent
= render
.resolution_percentage
*0.01
4054 nodes
, links
= get_nodes_links(context
)
4055 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
4058 mlocx
= event
.mouse_region_x
4059 mlocy
= event
.mouse_region_y
4060 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
4062 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
4063 region_x
= context
.region
.width
4064 region_y
= context
.region
.height
4066 region_center_x
= context
.region
.width
/ 2
4067 region_center_y
= context
.region
.height
/ 2
4069 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
4070 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
4072 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
4073 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
4075 margin_x
= region_center_x
- backdrop_center_x
4076 margin_y
= region_center_y
- backdrop_center_y
4078 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
4079 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
4081 for node
in viewers
:
4082 node
.center_x
= abs_mouse_x
4083 node
.center_y
= abs_mouse_y
4085 return {'PASS_THROUGH'}
4087 return self
.execute(context
)
4090 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
4091 """Save the current viewer node to an image file"""
4092 bl_idname
= "node.nw_save_viewer"
4093 bl_label
= "Save This Image"
4094 filepath
: StringProperty(subtype
="FILE_PATH")
4095 filename_ext
: EnumProperty(
4097 description
="Choose the file format to save to",
4098 items
=(('.bmp', "BMP", ""),
4099 ('.rgb', 'IRIS', ""),
4100 ('.png', 'PNG', ""),
4101 ('.jpg', 'JPEG', ""),
4102 ('.jp2', 'JPEG2000', ""),
4103 ('.tga', 'TARGA', ""),
4104 ('.cin', 'CINEON', ""),
4105 ('.dpx', 'DPX', ""),
4106 ('.exr', 'OPEN_EXR', ""),
4107 ('.hdr', 'HDR', ""),
4108 ('.tif', 'TIFF', "")),
4113 def poll(cls
, context
):
4115 if nw_check(context
):
4116 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4117 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
4118 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
4122 def execute(self
, context
):
4139 basename
, ext
= path
.splitext(fp
)
4140 old_render_format
= context
.scene
.render
.image_settings
.file_format
4141 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
4142 context
.area
.type = "IMAGE_EDITOR"
4143 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
4144 context
.area
.spaces
[0].image
.save_render(fp
)
4145 context
.area
.type = "NODE_EDITOR"
4146 context
.scene
.render
.image_settings
.file_format
= old_render_format
4150 class NWResetNodes(bpy
.types
.Operator
):
4151 """Reset Nodes in Selection"""
4152 bl_idname
= "node.nw_reset_nodes"
4153 bl_label
= "Reset Nodes"
4154 bl_options
= {'REGISTER', 'UNDO'}
4157 def poll(cls
, context
):
4158 space
= context
.space_data
4159 return space
.type == 'NODE_EDITOR'
4161 def execute(self
, context
):
4162 node_active
= context
.active_node
4163 node_selected
= context
.selected_nodes
4164 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4166 # Check if one node is selected at least
4167 if not (len(node_selected
) > 0):
4168 self
.report({'ERROR'}, "1 node must be selected at least")
4169 return {'CANCELLED'}
4171 active_node_name
= node_active
.name
if node_active
.select
else None
4172 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
4174 # Create output lists
4175 selected_node_names
= [n
.name
for n
in node_selected
]
4178 # Reset all valid children in a frame
4179 node_active_is_frame
= False
4180 if len(node_selected
) == 1 and node_active
.type == "FRAME":
4181 node_tree
= node_active
.id_data
4182 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
4184 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
4185 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
4186 node_active_is_frame
= True
4188 # Check if valid nodes in selection
4189 if not (len(valid_nodes
) > 0):
4190 # Check for frames only
4191 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
4192 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
4193 self
.report({'ERROR'}, "Please select only 1 frame to reset")
4195 self
.report({'ERROR'}, "No valid node(s) in selection")
4196 return {'CANCELLED'}
4198 # Report nodes that are not valid
4199 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
4200 valid_node_names
= [n
.name
for n
in valid_nodes
]
4201 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
4202 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
4204 # Deselect all nodes
4205 for i
in node_selected
:
4208 # Run through all valid nodes
4209 for node
in valid_nodes
:
4211 parent
= node
.parent
if node
.parent
else None
4212 node_loc
= [node
.location
.x
, node
.location
.y
]
4214 node_tree
= node
.id_data
4215 props_to_copy
= 'bl_idname name location height width'.split(' ')
4218 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
4219 for i
in (i
for i
in mappings
if i
.is_linked
):
4221 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
4223 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
4225 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
4226 props_to_copy
.pop(0)
4228 for prop
in props_to_copy
:
4229 setattr(new_node
, prop
, props
[prop
])
4231 nodes
= node_tree
.nodes
4233 new_node
.name
= props
['name']
4236 new_node
.parent
= parent
4237 new_node
.location
= node_loc
4239 for str_from
, str_to
in reconnections
:
4240 node_tree
.links
.new(eval(str_from
), eval(str_to
))
4242 new_node
.select
= False
4243 success_names
.append(new_node
.name
)
4245 # Reselect all nodes
4246 if selected_node_names
and node_active_is_frame
is False:
4247 for i
in selected_node_names
:
4248 node_tree
.nodes
[i
].select
= True
4250 if active_node_name
is not None:
4251 node_tree
.nodes
[active_node_name
].select
= True
4252 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
4254 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
4262 def drawlayout(context
, layout
, mode
='non-panel'):
4263 tree_type
= context
.space_data
.tree_type
4265 col
= layout
.column(align
=True)
4266 col
.menu(NWMergeNodesMenu
.bl_idname
)
4269 col
= layout
.column(align
=True)
4270 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
4273 if tree_type
== 'ShaderNodeTree':
4274 col
= layout
.column(align
=True)
4275 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
4276 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
4279 col
= layout
.column(align
=True)
4280 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
4281 col
.operator(NWSwapLinks
.bl_idname
)
4282 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
4285 col
= layout
.column(align
=True)
4286 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
4287 if tree_type
!= 'GeometryNodeTree':
4288 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
4291 col
= layout
.column(align
=True)
4293 row
= col
.row(align
=True)
4294 row
.operator(NWClearLabel
.bl_idname
).option
= True
4295 row
.operator(NWModifyLabels
.bl_idname
)
4297 col
.operator(NWClearLabel
.bl_idname
).option
= True
4298 col
.operator(NWModifyLabels
.bl_idname
)
4299 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
4301 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
4304 col
= layout
.column(align
=True)
4305 if tree_type
== 'CompositorNodeTree':
4306 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
4307 if tree_type
!= 'GeometryNodeTree':
4308 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
4311 col
= layout
.column(align
=True)
4312 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
4315 col
= layout
.column(align
=True)
4316 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
4319 col
= layout
.column(align
=True)
4320 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
4324 class NodeWranglerPanel(Panel
, NWBase
):
4325 bl_idname
= "NODE_PT_nw_node_wrangler"
4326 bl_space_type
= 'NODE_EDITOR'
4327 bl_label
= "Node Wrangler"
4328 bl_region_type
= "UI"
4329 bl_category
= "Node Wrangler"
4331 prepend
: StringProperty(
4334 append
: StringProperty()
4335 remove
: StringProperty()
4337 def draw(self
, context
):
4338 self
.layout
.label(text
="(Quick access: Shift+W)")
4339 drawlayout(context
, self
.layout
, mode
='panel')
4345 class NodeWranglerMenu(Menu
, NWBase
):
4346 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
4347 bl_label
= "Node Wrangler"
4349 def draw(self
, context
):
4350 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
4351 drawlayout(context
, self
.layout
)
4354 class NWMergeNodesMenu(Menu
, NWBase
):
4355 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
4356 bl_label
= "Merge Selected Nodes"
4358 def draw(self
, context
):
4359 type = context
.space_data
.tree_type
4360 layout
= self
.layout
4361 if type == 'ShaderNodeTree':
4362 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
4363 if type == 'GeometryNodeTree':
4364 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
4365 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4367 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
4368 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4369 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
4371 props
.merge_type
= 'ZCOMBINE'
4372 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
4374 props
.merge_type
= 'ALPHAOVER'
4376 class NWMergeGeometryMenu(Menu
, NWBase
):
4377 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
4378 bl_label
= "Merge Selected Nodes using Geometry Nodes"
4379 def draw(self
, context
):
4380 layout
= self
.layout
4381 # The boolean node + Join Geometry node
4382 for type, name
, description
in geo_combine_operations
:
4383 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4385 props
.merge_type
= 'GEOMETRY'
4387 class NWMergeShadersMenu(Menu
, NWBase
):
4388 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
4389 bl_label
= "Merge Selected Nodes using Shaders"
4391 def draw(self
, context
):
4392 layout
= self
.layout
4393 for type in ('MIX', 'ADD'):
4394 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
4396 props
.merge_type
= 'SHADER'
4399 class NWMergeMixMenu(Menu
, NWBase
):
4400 bl_idname
= "NODE_MT_nw_merge_mix_menu"
4401 bl_label
= "Merge Selected Nodes using Mix"
4403 def draw(self
, context
):
4404 layout
= self
.layout
4405 for type, name
, description
in blend_types
:
4406 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4408 props
.merge_type
= 'MIX'
4411 class NWConnectionListOutputs(Menu
, NWBase
):
4412 bl_idname
= "NODE_MT_nw_connection_list_out"
4415 def draw(self
, context
):
4416 layout
= self
.layout
4417 nodes
, links
= get_nodes_links(context
)
4419 n1
= nodes
[context
.scene
.NWLazySource
]
4421 for o
in n1
.outputs
:
4422 # Only show sockets that are exposed.
4424 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
4428 class NWConnectionListInputs(Menu
, NWBase
):
4429 bl_idname
= "NODE_MT_nw_connection_list_in"
4432 def draw(self
, context
):
4433 layout
= self
.layout
4434 nodes
, links
= get_nodes_links(context
)
4436 n2
= nodes
[context
.scene
.NWLazyTarget
]
4440 # Only show sockets that are exposed.
4441 # This prevents, for example, the scale value socket
4442 # of the vector math node being added to the list when
4443 # the mode is not 'SCALE'.
4445 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
4446 op
.from_socket
= context
.scene
.NWSourceSocket
4447 op
.to_socket
= index
4451 class NWMergeMathMenu(Menu
, NWBase
):
4452 bl_idname
= "NODE_MT_nw_merge_math_menu"
4453 bl_label
= "Merge Selected Nodes using Math"
4455 def draw(self
, context
):
4456 layout
= self
.layout
4457 for type, name
, description
in operations
:
4458 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4460 props
.merge_type
= 'MATH'
4463 class NWBatchChangeNodesMenu(Menu
, NWBase
):
4464 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
4465 bl_label
= "Batch Change Selected Nodes"
4467 def draw(self
, context
):
4468 layout
= self
.layout
4469 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
4470 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
4473 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
4474 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
4475 bl_label
= "Batch Change Blend Type"
4477 def draw(self
, context
):
4478 layout
= self
.layout
4479 for type, name
, description
in blend_types
:
4480 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4481 props
.blend_type
= type
4482 props
.operation
= 'CURRENT'
4485 class NWBatchChangeOperationMenu(Menu
, NWBase
):
4486 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
4487 bl_label
= "Batch Change Math Operation"
4489 def draw(self
, context
):
4490 layout
= self
.layout
4491 for type, name
, description
in operations
:
4492 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4493 props
.blend_type
= 'CURRENT'
4494 props
.operation
= type
4497 class NWCopyToSelectedMenu(Menu
, NWBase
):
4498 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4499 bl_label
= "Copy to Selected"
4501 def draw(self
, context
):
4502 layout
= self
.layout
4503 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4504 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4507 class NWCopyLabelMenu(Menu
, NWBase
):
4508 bl_idname
= "NODE_MT_nw_copy_label_menu"
4509 bl_label
= "Copy Label"
4511 def draw(self
, context
):
4512 layout
= self
.layout
4513 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4514 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4515 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4518 class NWAddReroutesMenu(Menu
, NWBase
):
4519 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4520 bl_label
= "Add Reroutes"
4521 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4523 def draw(self
, context
):
4524 layout
= self
.layout
4525 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4526 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4527 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4530 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4531 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4532 bl_label
= "Link Active to Selected"
4534 def draw(self
, context
):
4535 layout
= self
.layout
4536 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4537 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4538 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4541 class NWLinkStandardMenu(Menu
, NWBase
):
4542 bl_idname
= "NODE_MT_nw_link_standard_menu"
4543 bl_label
= "To All Selected"
4545 def draw(self
, context
):
4546 layout
= self
.layout
4547 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4548 props
.replace
= False
4549 props
.use_node_name
= False
4550 props
.use_outputs_names
= False
4551 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4552 props
.replace
= True
4553 props
.use_node_name
= False
4554 props
.use_outputs_names
= False
4557 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4558 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4559 bl_label
= "Use Node Name/Label"
4561 def draw(self
, context
):
4562 layout
= self
.layout
4563 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4564 props
.replace
= False
4565 props
.use_node_name
= True
4566 props
.use_outputs_names
= False
4567 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4568 props
.replace
= True
4569 props
.use_node_name
= True
4570 props
.use_outputs_names
= False
4573 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4574 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4575 bl_label
= "Use Outputs Names"
4577 def draw(self
, context
):
4578 layout
= self
.layout
4579 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4580 props
.replace
= False
4581 props
.use_node_name
= False
4582 props
.use_outputs_names
= True
4583 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4584 props
.replace
= True
4585 props
.use_node_name
= False
4586 props
.use_outputs_names
= True
4589 class NWAttributeMenu(bpy
.types
.Menu
):
4590 bl_idname
= "NODE_MT_nw_node_attribute_menu"
4591 bl_label
= "Attributes"
4594 def poll(cls
, context
):
4596 if nw_check(context
):
4597 snode
= context
.space_data
4598 valid
= snode
.tree_type
== 'ShaderNodeTree'
4601 def draw(self
, context
):
4603 nodes
, links
= get_nodes_links(context
)
4604 mat
= context
.object.active_material
4607 for obj
in bpy
.data
.objects
:
4608 for slot
in obj
.material_slots
:
4609 if slot
.material
== mat
:
4613 if obj
.data
.attributes
:
4614 for attr
in obj
.data
.attributes
:
4615 attrs
.append(attr
.name
)
4616 attrs
= list(set(attrs
)) # get a unique list
4620 l
.operator(NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
4622 l
.label(text
="No attributes on objects with this material")
4625 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4626 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4627 bl_label
= "Switch Type to..."
4629 def draw(self
, context
):
4630 layout
= self
.layout
4631 tree
= context
.space_data
.node_tree
4632 if tree
.type == 'SHADER':
4633 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4634 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4635 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4636 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4637 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4638 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4639 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4640 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4641 if tree
.type == 'COMPOSITING':
4642 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4643 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4644 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4645 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4646 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4647 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4648 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4649 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4650 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4651 if tree
.type == 'TEXTURE':
4652 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4653 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4654 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4655 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4656 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4657 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4658 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4659 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4660 if tree
.type == 'GEOMETRY':
4661 categories
= [c
for c
in node_categories_iter(context
)
4662 if c
.name
not in ['Group', 'Script']]
4663 for cat
in categories
:
4664 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4665 if hasattr(bpy
.types
, idname
):
4668 layout
.label(text
="Unable to load altered node lists.")
4669 layout
.label(text
="Please re-enable Node Wrangler.")
4673 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4674 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4677 def draw(self
, context
):
4678 layout
= self
.layout
4679 for ident
, node_type
, rna_name
in shaders_input_nodes_props
:
4680 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4681 props
.to_type
= ident
4684 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4685 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4688 def draw(self
, context
):
4689 layout
= self
.layout
4690 for ident
, node_type
, rna_name
in shaders_output_nodes_props
:
4691 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4692 props
.to_type
= ident
4695 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4696 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4699 def draw(self
, context
):
4700 layout
= self
.layout
4701 for ident
, node_type
, rna_name
in shaders_shader_nodes_props
:
4702 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4703 props
.to_type
= ident
4706 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4707 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4708 bl_label
= "Texture"
4710 def draw(self
, context
):
4711 layout
= self
.layout
4712 for ident
, node_type
, rna_name
in shaders_texture_nodes_props
:
4713 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4714 props
.to_type
= ident
4717 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4718 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4721 def draw(self
, context
):
4722 layout
= self
.layout
4723 for ident
, node_type
, rna_name
in shaders_color_nodes_props
:
4724 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4725 props
.to_type
= ident
4728 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4729 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4732 def draw(self
, context
):
4733 layout
= self
.layout
4734 for ident
, node_type
, rna_name
in shaders_vector_nodes_props
:
4735 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4736 props
.to_type
= ident
4739 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4740 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4741 bl_label
= "Converter"
4743 def draw(self
, context
):
4744 layout
= self
.layout
4745 for ident
, node_type
, rna_name
in shaders_converter_nodes_props
:
4746 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4747 props
.to_type
= ident
4750 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4751 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4754 def draw(self
, context
):
4755 layout
= self
.layout
4756 for ident
, node_type
, rna_name
in shaders_layout_nodes_props
:
4757 if node_type
!= 'FRAME':
4758 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4759 props
.to_type
= ident
4762 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4763 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4766 def draw(self
, context
):
4767 layout
= self
.layout
4768 for ident
, node_type
, rna_name
in compo_input_nodes_props
:
4769 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4770 props
.to_type
= ident
4773 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4774 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4777 def draw(self
, context
):
4778 layout
= self
.layout
4779 for ident
, node_type
, rna_name
in compo_output_nodes_props
:
4780 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4781 props
.to_type
= ident
4784 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4785 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4788 def draw(self
, context
):
4789 layout
= self
.layout
4790 for ident
, node_type
, rna_name
in compo_color_nodes_props
:
4791 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4792 props
.to_type
= ident
4795 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4796 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4797 bl_label
= "Converter"
4799 def draw(self
, context
):
4800 layout
= self
.layout
4801 for ident
, node_type
, rna_name
in compo_converter_nodes_props
:
4802 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4803 props
.to_type
= ident
4806 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4807 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4810 def draw(self
, context
):
4811 layout
= self
.layout
4812 for ident
, node_type
, rna_name
in compo_filter_nodes_props
:
4813 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4814 props
.to_type
= ident
4817 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4818 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4821 def draw(self
, context
):
4822 layout
= self
.layout
4823 for ident
, node_type
, rna_name
in compo_vector_nodes_props
:
4824 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4825 props
.to_type
= ident
4828 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4829 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4832 def draw(self
, context
):
4833 layout
= self
.layout
4834 for ident
, node_type
, rna_name
in compo_matte_nodes_props
:
4835 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4836 props
.to_type
= ident
4839 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4840 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4841 bl_label
= "Distort"
4843 def draw(self
, context
):
4844 layout
= self
.layout
4845 for ident
, node_type
, rna_name
in compo_distort_nodes_props
:
4846 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4847 props
.to_type
= ident
4850 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4851 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4854 def draw(self
, context
):
4855 layout
= self
.layout
4856 for ident
, node_type
, rna_name
in compo_layout_nodes_props
:
4857 if node_type
!= 'FRAME':
4858 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4859 props
.to_type
= ident
4862 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4863 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4866 def draw(self
, context
):
4867 layout
= self
.layout
4868 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4869 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4870 props
.to_type
= ident
4873 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4874 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4877 def draw(self
, context
):
4878 layout
= self
.layout
4879 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4880 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4881 props
.to_type
= ident
4884 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4885 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4888 def draw(self
, context
):
4889 layout
= self
.layout
4890 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4891 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4892 props
.to_type
= ident
4895 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4896 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4899 def draw(self
, context
):
4900 layout
= self
.layout
4901 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4902 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4903 props
.to_type
= ident
4906 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4907 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4908 bl_label
= "Converter"
4910 def draw(self
, context
):
4911 layout
= self
.layout
4912 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4913 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4914 props
.to_type
= ident
4917 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4918 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4921 def draw(self
, context
):
4922 layout
= self
.layout
4923 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4924 if node_type
!= 'FRAME':
4925 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4926 props
.to_type
= ident
4929 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4930 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4933 def draw(self
, context
):
4934 layout
= self
.layout
4935 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4936 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4937 props
.to_type
= ident
4940 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4941 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4944 def draw(self
, context
):
4945 layout
= self
.layout
4946 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4947 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4948 props
.to_type
= ident
4951 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4952 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4955 def draw(self
, context
):
4956 layout
= self
.layout
4957 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4958 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4959 props
.to_type
= ident
4962 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4963 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4964 bl_label
= "Pattern"
4966 def draw(self
, context
):
4967 layout
= self
.layout
4968 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4969 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4970 props
.to_type
= ident
4973 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4974 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4975 bl_label
= "Textures"
4977 def draw(self
, context
):
4978 layout
= self
.layout
4979 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4980 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4981 props
.to_type
= ident
4984 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4985 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4986 bl_label
= "Converter"
4988 def draw(self
, context
):
4989 layout
= self
.layout
4990 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4991 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4992 props
.to_type
= ident
4995 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4996 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4997 bl_label
= "Distort"
4999 def draw(self
, context
):
5000 layout
= self
.layout
5001 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
5002 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
5003 props
.to_type
= ident
5006 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
5007 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
5010 def draw(self
, context
):
5011 layout
= self
.layout
5012 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
5013 if node_type
!= 'FRAME':
5014 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
5015 props
.to_type
= ident
5017 def draw_switch_category_submenu(self
, context
):
5018 layout
= self
.layout
5019 if self
.category
.name
== 'Layout':
5020 for node
in self
.category
.items(context
):
5021 if node
.nodetype
!= 'NodeFrame':
5022 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
5023 props
.to_type
= node
.nodetype
5025 for node
in self
.category
.items(context
):
5026 if isinstance(node
, NodeItemCustom
):
5027 node
.draw(self
, layout
, context
)
5029 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
5030 props
.geo_to_type
= node
.nodetype
5033 # APPENDAGES TO EXISTING UI
5037 def select_parent_children_buttons(self
, context
):
5038 layout
= self
.layout
5039 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
5040 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
5043 def attr_nodes_menu_func(self
, context
):
5044 col
= self
.layout
.column(align
=True)
5045 col
.menu("NODE_MT_nw_node_attribute_menu")
5049 def multipleimages_menu_func(self
, context
):
5050 col
= self
.layout
.column(align
=True)
5051 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
5052 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
5056 def bgreset_menu_func(self
, context
):
5057 self
.layout
.operator(NWResetBG
.bl_idname
)
5060 def save_viewer_menu_func(self
, context
):
5061 if nw_check(context
):
5062 if context
.space_data
.tree_type
== 'CompositorNodeTree':
5063 if context
.scene
.node_tree
.nodes
.active
:
5064 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
5065 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
5068 def reset_nodes_button(self
, context
):
5069 node_active
= context
.active_node
5070 node_selected
= context
.selected_nodes
5071 node_ignore
= ["FRAME","REROUTE", "GROUP"]
5073 # Check if active node is in the selection and respective type
5074 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
5075 row
= self
.layout
.row()
5076 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
5077 self
.layout
.separator()
5079 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
5080 row
= self
.layout
.row()
5081 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
5082 self
.layout
.separator()
5086 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5088 switch_category_menus
= []
5090 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5091 # props entry: (property name, property value)
5094 # NWMergeNodes with Ctrl (AUTO).
5095 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
5096 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5097 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
5098 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5099 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5100 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5101 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
5102 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5103 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5104 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5105 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
5106 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5107 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5108 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5109 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
5110 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5111 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5112 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5113 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
5114 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5115 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
5116 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5117 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
5118 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5119 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5120 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5121 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5122 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
5123 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5124 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
5125 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5126 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5127 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5128 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
5129 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5130 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5131 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5132 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
5133 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5134 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5135 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5136 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
5137 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5138 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5139 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5140 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
5141 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5142 # NWMergeNodes with Ctrl Shift (MATH)
5143 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5144 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5145 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
5146 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5147 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5148 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5149 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
5150 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5151 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5152 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5153 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
5154 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5155 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5156 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5157 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
5158 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5159 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
5160 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5161 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
5162 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5163 # BATCH CHANGE NODES
5164 # NWBatchChangeNodes with Alt
5165 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
5166 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5167 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
5168 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5169 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5170 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5171 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
5172 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5173 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5174 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5175 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
5176 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5177 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5178 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5179 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
5180 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5181 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5182 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5183 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
5184 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5185 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
5186 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5187 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
5188 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5189 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
5190 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5191 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
5192 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5193 # LINK ACTIVE TO SELECTED
5194 # Don't use names, don't replace links (K)
5195 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
5196 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5197 # Don't use names, replace links (Shift K)
5198 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
5199 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5200 # Use node name, don't replace links (')
5201 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
5202 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5203 # Use node name, replace links (Shift ')
5204 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
5205 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5206 # Don't use names, don't replace links (;)
5207 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
5208 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5209 # Don't use names, replace links (')
5210 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
5211 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5213 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5214 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5215 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5216 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5217 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5218 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5219 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5220 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5221 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5222 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5223 # CLEAR LABEL (Alt L)
5224 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5225 # MODIFY LABEL (Alt Shift L)
5226 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5227 # Copy Label from active to selected
5228 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5229 # DETACH OUTPUTS (Alt Shift D)
5230 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5231 # LINK TO OUTPUT NODE (O)
5232 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5233 # SELECT PARENT/CHILDREN
5235 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5237 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5239 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5240 # Add Principled BSDF Texture Setup
5241 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5243 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5245 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5247 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5249 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5251 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5252 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5254 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
5256 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5258 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5259 # Lazy Connect with Menu
5260 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5261 # Viewer Tile Center
5262 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5264 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5265 # Reset Nodes (Back Space)
5266 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5268 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
5269 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5270 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5271 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
5272 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
5273 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
5278 NWPrincipledPreferences
,
5298 NWAddPrincipledSetup
,
5300 NWLinkActiveToSelected
,
5302 NWSelectParentChildren
,
5308 NWAddMultipleImages
,
5316 NWMergeGeometryMenu
,
5318 NWConnectionListOutputs
,
5319 NWConnectionListInputs
,
5321 NWBatchChangeNodesMenu
,
5322 NWBatchChangeBlendTypeMenu
,
5323 NWBatchChangeOperationMenu
,
5324 NWCopyToSelectedMenu
,
5327 NWLinkActiveToSelectedMenu
,
5329 NWLinkUseNodeNameMenu
,
5330 NWLinkUseOutputsNamesMenu
,
5332 NWSwitchNodeTypeMenu
,
5333 NWSwitchShadersInputSubmenu
,
5334 NWSwitchShadersOutputSubmenu
,
5335 NWSwitchShadersShaderSubmenu
,
5336 NWSwitchShadersTextureSubmenu
,
5337 NWSwitchShadersColorSubmenu
,
5338 NWSwitchShadersVectorSubmenu
,
5339 NWSwitchShadersConverterSubmenu
,
5340 NWSwitchShadersLayoutSubmenu
,
5341 NWSwitchCompoInputSubmenu
,
5342 NWSwitchCompoOutputSubmenu
,
5343 NWSwitchCompoColorSubmenu
,
5344 NWSwitchCompoConverterSubmenu
,
5345 NWSwitchCompoFilterSubmenu
,
5346 NWSwitchCompoVectorSubmenu
,
5347 NWSwitchCompoMatteSubmenu
,
5348 NWSwitchCompoDistortSubmenu
,
5349 NWSwitchCompoLayoutSubmenu
,
5350 NWSwitchMatInputSubmenu
,
5351 NWSwitchMatOutputSubmenu
,
5352 NWSwitchMatColorSubmenu
,
5353 NWSwitchMatVectorSubmenu
,
5354 NWSwitchMatConverterSubmenu
,
5355 NWSwitchMatLayoutSubmenu
,
5356 NWSwitchTexInputSubmenu
,
5357 NWSwitchTexOutputSubmenu
,
5358 NWSwitchTexColorSubmenu
,
5359 NWSwitchTexPatternSubmenu
,
5360 NWSwitchTexTexturesSubmenu
,
5361 NWSwitchTexConverterSubmenu
,
5362 NWSwitchTexDistortSubmenu
,
5363 NWSwitchTexLayoutSubmenu
,
5367 from bpy
.utils
import register_class
5370 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
5371 name
="Busy Drawing!",
5373 description
="An internal property used to store only the first mouse position")
5374 bpy
.types
.Scene
.NWLazySource
= StringProperty(
5375 name
="Lazy Source!",
5377 description
="An internal property used to store the first node in a Lazy Connect operation")
5378 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
5379 name
="Lazy Target!",
5381 description
="An internal property used to store the last node in a Lazy Connect operation")
5382 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
5383 name
="Source Socket!",
5385 description
="An internal property used to store the source socket in a Lazy Connect operation")
5386 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
5389 description
="An internal property used to determine if a socket is generated by the addon"
5396 addon_keymaps
.clear()
5397 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
5399 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
5400 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
5401 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
5403 for prop
, value
in props
:
5404 setattr(kmi
.properties
, prop
, value
)
5405 addon_keymaps
.append((km
, kmi
))
5408 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
5409 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
5410 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
5411 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
5412 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
5413 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
5414 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
5415 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
5418 switch_category_menus
.clear()
5419 for cat
in node_categories_iter(None):
5420 if cat
.name
not in ['Group', 'Script'] and cat
.identifier
.startswith('GEO'):
5421 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
5422 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
5423 "bl_space_type": 'NODE_EDITOR',
5424 "bl_label": cat
.name
,
5427 "draw": draw_switch_category_submenu
,
5430 switch_category_menus
.append(switch_category_type
)
5432 bpy
.utils
.register_class(switch_category_type
)
5436 from bpy
.utils
import unregister_class
5439 del bpy
.types
.Scene
.NWBusyDrawing
5440 del bpy
.types
.Scene
.NWLazySource
5441 del bpy
.types
.Scene
.NWLazyTarget
5442 del bpy
.types
.Scene
.NWSourceSocket
5443 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
5445 for cat_types
in switch_category_menus
:
5446 bpy
.utils
.unregister_class(cat_types
)
5447 switch_category_menus
.clear()
5450 for km
, kmi
in addon_keymaps
:
5451 km
.keymap_items
.remove(kmi
)
5452 addon_keymaps
.clear()
5455 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
5456 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
5457 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
5458 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
5459 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
5460 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
5461 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
5462 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
5465 unregister_class(cls
)
5467 if __name__
== "__main__":