1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
17 from bpy
.types
import Operator
, Panel
, Menu
18 from bpy
.props
import (
27 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
28 from gpu_extras
.batch
import batch_for_shader
29 from mathutils
import Vector
30 from nodeitems_utils
import node_categories_iter
, NodeItemCustom
31 from math
import cos
, sin
, pi
, hypot
35 from itertools
import chain
37 from collections
import namedtuple
41 # list of outputs of Input Render Layer
42 # with attributes determining if pass is used,
43 # and MultiLayer EXR outputs names and corresponding render engines
45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
46 RL_entry
= namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
48 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
49 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
50 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
51 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
52 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
53 RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
54 RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
55 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
56 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
57 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
58 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
59 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
60 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
61 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
62 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
63 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
64 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
65 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
66 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
67 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
68 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
69 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
70 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
71 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
72 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
76 # (rna_type.identifier, type, rna_type.name)
77 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
78 # Keeping things in alphabetical order so we don't need to sort later.
79 shaders_input_nodes_props
= (
80 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
81 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
82 ('ShaderNodeBevel', 'BEVEL', 'Bevel'),
83 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
84 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
85 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
86 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
87 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
88 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
89 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
90 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
91 ('ShaderNodeRGB', 'RGB', 'RGB'),
92 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
93 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
94 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
95 ('ShaderNodeValue', 'VALUE', 'Value'),
96 ('ShaderNodeVertexColor', 'VERTEX_COLOR', 'Vertex Color'),
97 ('ShaderNodeVolumeInfo', 'VOLUME_INFO', 'Volume Info'),
98 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
101 # (rna_type.identifier, type, rna_type.name)
102 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
103 # Keeping things in alphabetical order so we don't need to sort later.
104 shaders_output_nodes_props
= (
105 ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'),
106 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
107 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
108 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
110 # (rna_type.identifier, type, rna_type.name)
111 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
112 # Keeping things in alphabetical order so we don't need to sort later.
113 shaders_shader_nodes_props
= (
114 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
115 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
116 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
117 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
118 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
119 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
120 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
121 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
122 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
123 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
124 ('ShaderNodeBsdfHairPrincipled', 'BSDF_HAIR_PRINCIPLED', 'Principled Hair BSDF'),
125 ('ShaderNodeVolumePrincipled', 'PRINCIPLED_VOLUME', 'Principled Volume'),
126 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
127 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
128 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
129 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
130 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
131 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
132 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
133 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
134 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
136 # (rna_type.identifier, type, rna_type.name)
137 # Keeping things in alphabetical order so we don't need to sort later.
138 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
139 shaders_texture_nodes_props
= (
140 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
141 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
142 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
143 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
144 ('ShaderNodeTexIES', 'TEX_IES', 'IES Texture'),
145 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
146 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
147 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
148 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
149 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
150 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
151 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
152 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
153 ('ShaderNodeTexWhiteNoise', 'TEX_WHITE_NOISE', 'White Noise'),
155 # (rna_type.identifier, type, rna_type.name)
156 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
157 # Keeping things in alphabetical order so we don't need to sort later.
158 shaders_color_nodes_props
= (
159 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
160 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
161 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
162 ('ShaderNodeInvert', 'INVERT', 'Invert'),
163 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
164 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
165 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
167 # (rna_type.identifier, type, rna_type.name)
168 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
169 # Keeping things in alphabetical order so we don't need to sort later.
170 shaders_vector_nodes_props
= (
171 ('ShaderNodeBump', 'BUMP', 'Bump'),
172 ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'),
173 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
174 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
175 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
176 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
177 ('ShaderNodeVectorDisplacement', 'VECTOR_DISPLACEMENT', 'Vector Displacement'),
178 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
180 # (rna_type.identifier, type, rna_type.name)
181 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
182 # Keeping things in alphabetical order so we don't need to sort later.
183 shaders_converter_nodes_props
= (
184 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
185 ('ShaderNodeClamp', 'CLAMP', 'Clamp'),
186 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
187 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
188 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
189 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
190 ('ShaderNodeMapRange', 'MAP_RANGE', 'Map Range'),
191 ('ShaderNodeMath', 'MATH', 'Math'),
192 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
193 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
194 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
195 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
196 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
197 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
199 # (rna_type.identifier, type, rna_type.name)
200 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
201 # Keeping things in alphabetical order so we don't need to sort later.
202 shaders_layout_nodes_props
= (
203 ('NodeFrame', 'FRAME', 'Frame'),
204 ('NodeReroute', 'REROUTE', 'Reroute'),
208 # (rna_type.identifier, type, rna_type.name)
209 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
210 # Keeping things in alphabetical order so we don't need to sort later.
211 compo_input_nodes_props
= (
212 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
213 ('CompositorNodeImage', 'IMAGE', 'Image'),
214 ('CompositorNodeMask', 'MASK', 'Mask'),
215 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
216 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
217 ('CompositorNodeRGB', 'RGB', 'RGB'),
218 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
219 ('CompositorNodeTime', 'TIME', 'Time'),
220 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
221 ('CompositorNodeValue', 'VALUE', 'Value'),
223 # (rna_type.identifier, type, rna_type.name)
224 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
225 # Keeping things in alphabetical order so we don't need to sort later.
226 compo_output_nodes_props
= (
227 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
228 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
229 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
230 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
231 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
233 # (rna_type.identifier, type, rna_type.name)
234 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
235 # Keeping things in alphabetical order so we don't need to sort later.
236 compo_color_nodes_props
= (
237 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
238 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
239 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
240 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
241 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
242 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
243 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
244 ('CompositorNodeInvert', 'INVERT', 'Invert'),
245 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
246 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
247 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
248 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
250 # (rna_type.identifier, type, rna_type.name)
251 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
252 # Keeping things in alphabetical order so we don't need to sort later.
253 compo_converter_nodes_props
= (
254 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
255 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
256 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
257 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
258 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
259 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
260 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
261 ('CompositorNodeMath', 'MATH', 'Math'),
262 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
263 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
264 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
265 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
266 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
267 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
268 ('CompositorNodeSwitchView', 'VIEWSWITCH', 'View Switch'),
270 # (rna_type.identifier, type, rna_type.name)
271 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
272 # Keeping things in alphabetical order so we don't need to sort later.
273 compo_filter_nodes_props
= (
274 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
275 ('CompositorNodeBlur', 'BLUR', 'Blur'),
276 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
277 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
278 ('CompositorNodeDenoise', 'DENOISE', 'Denoise'),
279 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
280 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
281 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
282 ('CompositorNodeFilter', 'FILTER', 'Filter'),
283 ('CompositorNodeGlare', 'GLARE', 'Glare'),
284 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
285 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
286 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
287 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
289 # (rna_type.identifier, type, rna_type.name)
290 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
291 # Keeping things in alphabetical order so we don't need to sort later.
292 compo_vector_nodes_props
= (
293 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
294 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
295 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
296 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
297 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
299 # (rna_type.identifier, type, rna_type.name)
300 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
301 # Keeping things in alphabetical order so we don't need to sort later.
302 compo_matte_nodes_props
= (
303 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
304 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
305 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
306 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
307 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
308 ('CompositorNodeCryptomatte', 'CRYPTOMATTE', 'Cryptomatte'),
309 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
310 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
311 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
312 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
313 ('CompositorNodeKeying', 'KEYING', 'Keying'),
314 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
315 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
317 # (rna_type.identifier, type, rna_type.name)
318 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
319 # Keeping things in alphabetical order so we don't need to sort later.
320 compo_distort_nodes_props
= (
321 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
322 ('CompositorNodeCrop', 'CROP', 'Crop'),
323 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
324 ('CompositorNodeFlip', 'FLIP', 'Flip'),
325 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
326 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
327 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
328 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
329 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
330 ('CompositorNodeScale', 'SCALE', 'Scale'),
331 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
332 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
333 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
335 # (rna_type.identifier, type, rna_type.name)
336 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
337 # Keeping things in alphabetical order so we don't need to sort later.
338 compo_layout_nodes_props
= (
339 ('NodeFrame', 'FRAME', 'Frame'),
340 ('NodeReroute', 'REROUTE', 'Reroute'),
341 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
343 # Blender Render material nodes
344 # (rna_type.identifier, type, rna_type.name)
345 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
346 blender_mat_input_nodes_props
= (
347 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
348 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
349 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
350 ('ShaderNodeValue', 'VALUE', 'Value'),
351 ('ShaderNodeRGB', 'RGB', 'RGB'),
352 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
353 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
354 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
357 # (rna_type.identifier, type, rna_type.name)
358 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
359 blender_mat_output_nodes_props
= (
360 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
363 # (rna_type.identifier, type, rna_type.name)
364 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
365 blender_mat_color_nodes_props
= (
366 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
367 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
368 ('ShaderNodeInvert', 'INVERT', 'Invert'),
369 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
372 # (rna_type.identifier, type, rna_type.name)
373 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
374 blender_mat_vector_nodes_props
= (
375 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
376 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
377 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
380 # (rna_type.identifier, type, rna_type.name)
381 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
382 blender_mat_converter_nodes_props
= (
383 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
384 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
385 ('ShaderNodeMath', 'MATH', 'Math'),
386 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
387 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
388 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
389 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
390 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
391 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
394 # (rna_type.identifier, type, rna_type.name)
395 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
396 blender_mat_layout_nodes_props
= (
397 ('NodeReroute', 'REROUTE', 'Reroute'),
401 # (rna_type.identifier, type, rna_type.name)
402 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
403 texture_input_nodes_props
= (
404 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
405 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
406 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
407 ('TextureNodeImage', 'IMAGE', 'Image'),
410 # (rna_type.identifier, type, rna_type.name)
411 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
412 texture_output_nodes_props
= (
413 ('TextureNodeOutput', 'OUTPUT', 'Output'),
414 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
417 # (rna_type.identifier, type, rna_type.name)
418 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
419 texture_color_nodes_props
= (
420 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
421 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
422 ('TextureNodeInvert', 'INVERT', 'Invert'),
423 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
424 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
425 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
428 # (rna_type.identifier, type, rna_type.name)
429 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
430 texture_pattern_nodes_props
= (
431 ('TextureNodeChecker', 'CHECKER', 'Checker'),
432 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
435 # (rna_type.identifier, type, rna_type.name)
436 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
437 texture_textures_nodes_props
= (
438 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
439 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
440 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
441 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
442 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
443 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
444 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
445 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
446 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
447 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
450 # (rna_type.identifier, type, rna_type.name)
451 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
452 texture_converter_nodes_props
= (
453 ('TextureNodeMath', 'MATH', 'Math'),
454 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
455 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
456 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
457 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
460 # (rna_type.identifier, type, rna_type.name)
461 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
462 texture_distort_nodes_props
= (
463 ('TextureNodeScale', 'SCALE', 'Scale'),
464 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
465 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
466 ('TextureNodeAt', 'AT', 'At'),
469 # (rna_type.identifier, type, rna_type.name)
470 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
471 texture_layout_nodes_props
= (
472 ('NodeReroute', 'REROUTE', 'Reroute'),
475 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
476 # used list, not tuple for easy merging with other lists.
478 ('MIX', 'Mix', 'Mix Mode'),
479 ('ADD', 'Add', 'Add Mode'),
480 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
481 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
482 ('SCREEN', 'Screen', 'Screen Mode'),
483 ('DIVIDE', 'Divide', 'Divide Mode'),
484 ('DIFFERENCE', 'Difference', 'Difference Mode'),
485 ('DARKEN', 'Darken', 'Darken Mode'),
486 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
487 ('OVERLAY', 'Overlay', 'Overlay Mode'),
488 ('DODGE', 'Dodge', 'Dodge Mode'),
489 ('BURN', 'Burn', 'Burn Mode'),
490 ('HUE', 'Hue', 'Hue Mode'),
491 ('SATURATION', 'Saturation', 'Saturation Mode'),
492 ('VALUE', 'Value', 'Value Mode'),
493 ('COLOR', 'Color', 'Color Mode'),
494 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
495 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
498 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
499 # used list, not tuple for easy merging with other lists.
501 ('ADD', 'Add', 'Add Mode'),
502 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
503 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
504 ('DIVIDE', 'Divide', 'Divide Mode'),
505 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
506 ('SINE', 'Sine', 'Sine Mode'),
507 ('COSINE', 'Cosine', 'Cosine Mode'),
508 ('TANGENT', 'Tangent', 'Tangent Mode'),
509 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
510 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
511 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
512 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
513 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
514 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
515 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
516 ('POWER', 'Power', 'Power Mode'),
517 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
518 ('SQRT', 'Square Root', 'Square Root Mode'),
519 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
520 ('EXPONENT', 'Exponent', 'Exponent Mode'),
521 ('MINIMUM', 'Minimum', 'Minimum Mode'),
522 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
523 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
524 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
525 ('SIGN', 'Sign', 'Sign Mode'),
526 ('COMPARE', 'Compare', 'Compare Mode'),
527 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
528 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
529 ('FRACT', 'Fraction', 'Fraction Mode'),
530 ('MODULO', 'Modulo', 'Modulo Mode'),
531 ('SNAP', 'Snap', 'Snap Mode'),
532 ('WRAP', 'Wrap', 'Wrap Mode'),
533 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
534 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
535 ('ROUND', 'Round', 'Round Mode'),
536 ('FLOOR', 'Floor', 'Floor Mode'),
537 ('CEIL', 'Ceil', 'Ceil Mode'),
538 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
539 ('RADIANS', 'To Radians', 'To Radians Mode'),
540 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
543 # Operations used by the geometry boolean node and join geometry node
544 geo_combine_operations
= [
545 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
546 ('INTERSECT', 'Intersect', 'Intersect Mode'),
547 ('UNION', 'Union', 'Union Mode'),
548 ('DIFFERENCE', 'Difference', 'Difference Mode'),
551 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
552 # used list, not tuple for easy merging with other lists.
554 ('CURRENT', 'Current', 'Leave at current state'),
555 ('NEXT', 'Next', 'Next blend type/operation'),
556 ('PREV', 'Prev', 'Previous blend type/operation'),
561 (1.0, 1.0, 1.0, 0.7),
562 (1.0, 0.0, 0.0, 0.7),
566 (0.0, 0.0, 0.0, 1.0),
567 (0.38, 0.77, 0.38, 1.0),
568 (0.38, 0.77, 0.38, 1.0)
571 (0.0, 0.0, 0.0, 1.0),
572 (0.77, 0.77, 0.16, 1.0),
573 (0.77, 0.77, 0.16, 1.0)
576 (0.0, 0.0, 0.0, 1.0),
577 (0.38, 0.38, 0.77, 1.0),
578 (0.38, 0.38, 0.77, 1.0)
581 (0.0, 0.0, 0.0, 1.0),
582 (0.63, 0.63, 0.63, 1.0),
583 (0.63, 0.63, 0.63, 1.0)
586 (1.0, 1.0, 1.0, 0.7),
587 (0.0, 0.0, 0.0, 0.7),
592 viewer_socket_name
= "tmp_viewer"
594 def get_nodes_from_category(category_name
, context
):
595 for category
in node_categories_iter(context
):
596 if category
.name
== category_name
:
597 return sorted(category
.items(context
), key
=lambda node
: node
.label
)
599 def is_visible_socket(socket
):
600 return not socket
.hide
and socket
.enabled
and socket
.type != 'CUSTOM'
602 def nice_hotkey_name(punc
):
603 # convert the ugly string name into the actual character
606 'MIDDLEMOUSE': "MMB",
608 'WHEELUPMOUSE': "Wheel Up",
609 'WHEELDOWNMOUSE': "Wheel Down",
610 'WHEELINMOUSE': "Wheel In",
611 'WHEELOUTMOUSE': "Wheel Out",
624 'LINE_FEED': "Enter",
633 'NUMPAD_1': "Numpad 1",
634 'NUMPAD_2': "Numpad 2",
635 'NUMPAD_3': "Numpad 3",
636 'NUMPAD_4': "Numpad 4",
637 'NUMPAD_5': "Numpad 5",
638 'NUMPAD_6': "Numpad 6",
639 'NUMPAD_7': "Numpad 7",
640 'NUMPAD_8': "Numpad 8",
641 'NUMPAD_9': "Numpad 9",
642 'NUMPAD_0': "Numpad 0",
643 'NUMPAD_PERIOD': "Numpad .",
644 'NUMPAD_SLASH': "Numpad /",
645 'NUMPAD_ASTERIX': "Numpad *",
646 'NUMPAD_MINUS': "Numpad -",
647 'NUMPAD_ENTER': "Numpad Enter",
648 'NUMPAD_PLUS': "Numpad +",
651 return nice_name
[punc
]
653 return punc
.replace("_", " ").title()
656 def force_update(context
):
657 context
.space_data
.node_tree
.update_tag()
661 prefs
= bpy
.context
.preferences
.system
662 return prefs
.dpi
* prefs
.pixel_size
/ 72
665 def node_mid_pt(node
, axis
):
667 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
669 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
675 def autolink(node1
, node2
, links
):
677 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
678 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
679 for outp
in available_outputs
:
680 for inp
in available_inputs
:
681 if not inp
.is_linked
and inp
.name
== outp
.name
:
686 for outp
in available_outputs
:
687 for inp
in available_inputs
:
688 if not inp
.is_linked
and inp
.type == outp
.type:
693 # force some connection even if the type doesn't match
694 if available_outputs
:
695 for inp
in available_inputs
:
696 if not inp
.is_linked
:
698 links
.new(available_outputs
[0], inp
)
701 # even if no sockets are open, force one of matching type
702 for outp
in available_outputs
:
703 for inp
in available_inputs
:
704 if inp
.type == outp
.type:
710 for outp
in available_outputs
:
711 for inp
in available_inputs
:
716 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
720 def node_at_pos(nodes
, context
, event
):
721 nodes_under_mouse
= []
724 store_mouse_cursor(context
, event
)
725 x
, y
= context
.space_data
.cursor_location
727 # Make a list of each corner (and middle of border) for each node.
728 # Will be sorted to find nearest point and thus nearest node
729 node_points_with_dist
= []
732 if node
.type != 'FRAME': # no point trying to link to a frame node
733 locx
= node
.location
.x
734 locy
= node
.location
.y
735 dimx
= node
.dimensions
.x
/dpifac()
736 dimy
= node
.dimensions
.y
/dpifac()
738 locx
+= node
.parent
.location
.x
739 locy
+= node
.parent
.location
.y
740 if node
.parent
.parent
:
741 locx
+= node
.parent
.parent
.location
.x
742 locy
+= node
.parent
.parent
.location
.y
743 if node
.parent
.parent
.parent
:
744 locx
+= node
.parent
.parent
.parent
.location
.x
745 locy
+= node
.parent
.parent
.parent
.location
.y
746 if node
.parent
.parent
.parent
.parent
:
747 # Support three levels or parenting
748 # There's got to be a better way to do this...
751 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
752 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
753 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
754 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
756 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
757 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
758 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
759 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
761 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
764 if node
.type != 'FRAME' and skipnode
== False:
765 locx
= node
.location
.x
766 locy
= node
.location
.y
767 dimx
= node
.dimensions
.x
/dpifac()
768 dimy
= node
.dimensions
.y
/dpifac()
770 locx
+= node
.parent
.location
.x
771 locy
+= node
.parent
.location
.y
772 if (locx
<= x
<= locx
+ dimx
) and \
773 (locy
- dimy
<= y
<= locy
):
774 nodes_under_mouse
.append(node
)
776 if len(nodes_under_mouse
) == 1:
777 if nodes_under_mouse
[0] != nearest_node
:
778 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
780 target_node
= nearest_node
# else use the nearest node
782 target_node
= nearest_node
786 def store_mouse_cursor(context
, event
):
787 space
= context
.space_data
788 v2d
= context
.region
.view2d
789 tree
= space
.edit_tree
791 # convert mouse position to the View2D for later node placement
792 if context
.region
.type == 'WINDOW':
793 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
795 space
.cursor_location
= tree
.view_center
797 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
798 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
800 vertices
= ((x1
, y1
), (x2
, y2
))
801 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
802 colour
[1]+(1.0-colour
[1])/4,
803 colour
[2]+(1.0-colour
[2])/4,
804 colour
[3]+(1.0-colour
[3])/4),
807 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
808 bgl
.glLineWidth(size
* dpifac())
814 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
815 radius
= radius
* dpifac()
817 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
818 radius
* sin(i
* 2 * pi
/ sides
) + my
)
819 for i
in range(sides
+ 1)]
821 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
823 shader
.uniform_float("color", colour
)
826 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
827 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
828 bottom_bar
= (16*dpifac()) + 1
830 radius
= radius
*dpifac()
832 nlocx
= (node
.location
.x
+1)*dpifac()
833 nlocy
= (node
.location
.y
+1)*dpifac()
834 ndimx
= node
.dimensions
.x
835 ndimy
= node
.dimensions
.y
836 # This is a stupid way to do this... TODO use while loop
838 nlocx
+= node
.parent
.location
.x
839 nlocy
+= node
.parent
.location
.y
840 if node
.parent
.parent
:
841 nlocx
+= node
.parent
.parent
.location
.x
842 nlocy
+= node
.parent
.parent
.location
.y
843 if node
.parent
.parent
.parent
:
844 nlocx
+= node
.parent
.parent
.parent
.location
.x
845 nlocy
+= node
.parent
.parent
.parent
.location
.y
850 if node
.type == 'REROUTE':
858 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
860 for i
in range(sides
+1):
862 if my
> bottom_bar
and mx
< area_width
:
863 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
864 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
865 vertices
.append((cosine
,sine
))
866 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
868 shader
.uniform_float("color", colour
)
872 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
874 for i
in range(sides
+1):
876 if my
> bottom_bar
and mx
< area_width
:
877 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
878 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
879 vertices
.append((cosine
,sine
))
880 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
882 shader
.uniform_float("color", colour
)
886 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
888 for i
in range(sides
+1):
890 if my
> bottom_bar
and mx
< area_width
:
891 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
892 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
893 vertices
.append((cosine
,sine
))
894 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
896 shader
.uniform_float("color", colour
)
899 # Bottom right corner
900 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
902 for i
in range(sides
+1):
904 if my
> bottom_bar
and mx
< area_width
:
905 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
906 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
907 vertices
.append((cosine
,sine
))
908 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
910 shader
.uniform_float("color", colour
)
913 # prepare drawing all edges in one batch
919 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
920 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
921 if m1x
< area_width
and m2x
< area_width
:
922 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
923 (m1x
,m1y
), (m1x
-radius
,m1y
)])
924 indices
.extend([(id_last
, id_last
+1, id_last
+3),
925 (id_last
+3, id_last
+1, id_last
+2)])
929 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
930 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
931 m1x
= min(m1x
, area_width
)
932 m2x
= min(m2x
, area_width
)
933 if m1y
> bottom_bar
and m2y
> bottom_bar
:
934 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
935 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
936 indices
.extend([(id_last
, id_last
+1, id_last
+3),
937 (id_last
+3, id_last
+1, id_last
+2)])
941 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
942 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
943 m1y
= max(m1y
, bottom_bar
)
944 m2y
= max(m2y
, bottom_bar
)
945 if m1x
< area_width
and m2x
< area_width
:
946 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
947 (m1x
+radius
,m1y
), (m1x
,m1y
)])
948 indices
.extend([(id_last
, id_last
+1, id_last
+3),
949 (id_last
+3, id_last
+1, id_last
+2)])
953 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
954 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
955 m1x
= min(m1x
, area_width
)
956 m2x
= min(m2x
, area_width
)
957 if m1y
> bottom_bar
and m2y
> bottom_bar
:
958 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
959 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
960 indices
.extend([(id_last
, id_last
+1, id_last
+3),
961 (id_last
+3, id_last
+1, id_last
+2)])
963 # now draw all edges in one batch
964 if len(vertices
) != 0:
965 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
967 shader
.uniform_float("color", colour
)
970 def draw_callback_nodeoutline(self
, context
, mode
):
974 bgl
.glEnable(bgl
.GL_BLEND
)
975 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
976 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
978 nodes
, links
= get_nodes_links(context
)
980 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
983 col_outer
= (1.0, 0.2, 0.2, 0.4)
984 col_inner
= (0.0, 0.0, 0.0, 0.5)
985 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
986 elif mode
== "LINKMENU":
987 col_outer
= (0.4, 0.6, 1.0, 0.4)
988 col_inner
= (0.0, 0.0, 0.0, 0.5)
989 col_circle_inner
= (0.08, 0.15, .3, 1.0)
991 col_outer
= (0.2, 1.0, 0.2, 0.4)
992 col_inner
= (0.0, 0.0, 0.0, 0.5)
993 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
995 m1x
= self
.mouse_path
[0][0]
996 m1y
= self
.mouse_path
[0][1]
997 m2x
= self
.mouse_path
[-1][0]
998 m2y
= self
.mouse_path
[-1][1]
1000 n1
= nodes
[context
.scene
.NWLazySource
]
1001 n2
= nodes
[context
.scene
.NWLazyTarget
]
1004 col_outer
= (0.4, 0.4, 0.4, 0.4)
1005 col_inner
= (0.0, 0.0, 0.0, 0.5)
1006 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
1008 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
1009 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
1010 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
1011 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
1013 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
1014 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
1017 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
1018 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
1021 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
1022 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
1024 bgl
.glDisable(bgl
.GL_BLEND
)
1025 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
1026 def get_active_tree(context
):
1027 tree
= context
.space_data
.node_tree
1029 # Get nodes from currently edited tree.
1030 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1031 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1032 # the same as context.active_node, the user is in a group.
1033 # Check recursively until we find the real active node_tree:
1034 if tree
.nodes
.active
:
1035 while tree
.nodes
.active
!= context
.active_node
:
1036 tree
= tree
.nodes
.active
.node_tree
1040 def get_nodes_links(context
):
1041 tree
, path
= get_active_tree(context
)
1042 return tree
.nodes
, tree
.links
1044 def is_viewer_socket(socket
):
1045 # checks if a internal socket is a valid viewer socket
1046 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
1048 def get_internal_socket(socket
):
1049 #get the internal socket from a socket inside or outside the group
1051 if node
.type == 'GROUP_OUTPUT':
1052 source_iterator
= node
.inputs
1053 iterator
= node
.id_data
.outputs
1054 elif node
.type == 'GROUP_INPUT':
1055 source_iterator
= node
.outputs
1056 iterator
= node
.id_data
.inputs
1057 elif hasattr(node
, "node_tree"):
1058 if socket
.is_output
:
1059 source_iterator
= node
.outputs
1060 iterator
= node
.node_tree
.outputs
1062 source_iterator
= node
.inputs
1063 iterator
= node
.node_tree
.inputs
1067 for i
, s
in enumerate(source_iterator
):
1072 def is_viewer_link(link
, output_node
):
1073 if "Emission Viewer" in link
.to_node
.name
or link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
1075 if link
.to_node
.type == 'GROUP_OUTPUT':
1076 socket
= get_internal_socket(link
.to_socket
)
1077 if is_viewer_socket(socket
):
1081 def get_group_output_node(tree
):
1082 for node
in tree
.nodes
:
1083 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
1086 def get_output_location(tree
):
1087 # get right-most location
1088 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
1089 max_xloc_node
= sorted_by_xloc
[-1]
1090 if max_xloc_node
.name
== 'Emission Viewer':
1091 max_xloc_node
= sorted_by_xloc
[-2]
1093 # get average y location
1095 for node
in tree
.nodes
:
1096 sum_yloc
+= node
.location
.y
1098 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1099 loc_y
= sum_yloc
/ len(tree
.nodes
)
1103 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
1104 base_color
: StringProperty(
1106 default
='diffuse diff albedo base col color',
1107 description
='Naming Components for Base Color maps')
1108 sss_color
: StringProperty(
1109 name
='Subsurface Color',
1110 default
='sss subsurface',
1111 description
='Naming Components for Subsurface Color maps')
1112 metallic
: StringProperty(
1114 default
='metallic metalness metal mtl',
1115 description
='Naming Components for metallness maps')
1116 specular
: StringProperty(
1118 default
='specularity specular spec spc',
1119 description
='Naming Components for Specular maps')
1120 normal
: StringProperty(
1122 default
='normal nor nrm nrml norm',
1123 description
='Naming Components for Normal maps')
1124 bump
: StringProperty(
1127 description
='Naming Components for bump maps')
1128 rough
: StringProperty(
1130 default
='roughness rough rgh',
1131 description
='Naming Components for roughness maps')
1132 gloss
: StringProperty(
1134 default
='gloss glossy glossiness',
1135 description
='Naming Components for glossy maps')
1136 displacement
: StringProperty(
1137 name
='Displacement',
1138 default
='displacement displace disp dsp height heightmap',
1139 description
='Naming Components for displacement maps')
1140 transmission
: StringProperty(
1141 name
='Transmission',
1142 default
='transmission transparency',
1143 description
='Naming Components for transmission maps')
1144 emission
: StringProperty(
1146 default
='emission emissive emit',
1147 description
='Naming Components for emission maps')
1148 alpha
: StringProperty(
1150 default
='alpha opacity',
1151 description
='Naming Components for alpha maps')
1152 ambient_occlusion
: StringProperty(
1153 name
='Ambient Occlusion',
1154 default
='ao ambient occlusion',
1155 description
='Naming Components for AO maps')
1158 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1159 bl_idname
= __name__
1161 merge_hide
: EnumProperty(
1162 name
="Hide Mix nodes",
1164 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1165 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1166 ("NEVER", "Never", "Never collapse the new merge nodes")
1168 default
='NON_SHADER',
1169 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1170 merge_position
: EnumProperty(
1171 name
="Mix Node Position",
1173 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1174 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1177 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1179 show_hotkey_list
: BoolProperty(
1180 name
="Show Hotkey List",
1182 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1184 hotkey_list_filter
: StringProperty(
1185 name
=" Filter by Name",
1187 description
="Show only hotkeys that have this text in their name"
1189 show_principled_lists
: BoolProperty(
1190 name
="Show Principled naming tags",
1192 description
="Expand this box into a list of all naming tags for principled texture setup"
1194 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1196 def draw(self
, context
):
1197 layout
= self
.layout
1198 col
= layout
.column()
1199 col
.prop(self
, "merge_position")
1200 col
.prop(self
, "merge_hide")
1203 col
= box
.column(align
=True)
1204 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1205 if self
.show_principled_lists
:
1206 tags
= self
.principled_tags
1208 col
.prop(tags
, "base_color")
1209 col
.prop(tags
, "sss_color")
1210 col
.prop(tags
, "metallic")
1211 col
.prop(tags
, "specular")
1212 col
.prop(tags
, "rough")
1213 col
.prop(tags
, "gloss")
1214 col
.prop(tags
, "normal")
1215 col
.prop(tags
, "bump")
1216 col
.prop(tags
, "displacement")
1217 col
.prop(tags
, "transmission")
1218 col
.prop(tags
, "emission")
1219 col
.prop(tags
, "alpha")
1220 col
.prop(tags
, "ambient_occlusion")
1223 col
= box
.column(align
=True)
1224 hotkey_button_name
= "Show Hotkey List"
1225 if self
.show_hotkey_list
:
1226 hotkey_button_name
= "Hide Hotkey List"
1227 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1228 if self
.show_hotkey_list
:
1229 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1231 for hotkey
in kmi_defs
:
1233 hotkey_name
= hotkey
[7]
1235 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1236 row
= col
.row(align
=True)
1237 row
.label(text
=hotkey_name
)
1238 keystr
= nice_hotkey_name(hotkey
[1])
1240 keystr
= "Shift " + keystr
1242 keystr
= "Alt " + keystr
1244 keystr
= "Ctrl " + keystr
1245 row
.label(text
=keystr
)
1249 def nw_check(context
):
1250 space
= context
.space_data
1251 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1254 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1261 def poll(cls
, context
):
1262 return nw_check(context
)
1266 class NWLazyMix(Operator
, NWBase
):
1267 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1268 bl_idname
= "node.nw_lazy_mix"
1269 bl_label
= "Mix Nodes"
1270 bl_options
= {'REGISTER', 'UNDO'}
1272 def modal(self
, context
, event
):
1273 context
.area
.tag_redraw()
1274 nodes
, links
= get_nodes_links(context
)
1277 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1280 if not context
.scene
.NWBusyDrawing
:
1281 node1
= node_at_pos(nodes
, context
, event
)
1283 context
.scene
.NWBusyDrawing
= node1
.name
1285 if context
.scene
.NWBusyDrawing
!= 'STOP':
1286 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1288 context
.scene
.NWLazySource
= node1
.name
1289 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1291 if event
.type == 'MOUSEMOVE':
1292 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1294 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1295 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1296 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1299 node2
= node_at_pos(nodes
, context
, event
)
1301 context
.scene
.NWBusyDrawing
= node2
.name
1313 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1315 context
.scene
.NWBusyDrawing
= ""
1318 elif event
.type == 'ESC':
1320 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1321 return {'CANCELLED'}
1323 return {'RUNNING_MODAL'}
1325 def invoke(self
, context
, event
):
1326 if context
.area
.type == 'NODE_EDITOR':
1327 # the arguments we pass the the callback
1328 args
= (self
, context
, 'MIX')
1329 # Add the region OpenGL drawing callback
1330 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1331 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1333 self
.mouse_path
= []
1335 context
.window_manager
.modal_handler_add(self
)
1336 return {'RUNNING_MODAL'}
1338 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1339 return {'CANCELLED'}
1342 class NWLazyConnect(Operator
, NWBase
):
1343 """Connect two nodes without clicking a specific socket (automatically determined"""
1344 bl_idname
= "node.nw_lazy_connect"
1345 bl_label
= "Lazy Connect"
1346 bl_options
= {'REGISTER', 'UNDO'}
1347 with_menu
: BoolProperty()
1349 def modal(self
, context
, event
):
1350 context
.area
.tag_redraw()
1351 nodes
, links
= get_nodes_links(context
)
1354 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1357 if not context
.scene
.NWBusyDrawing
:
1358 node1
= node_at_pos(nodes
, context
, event
)
1360 context
.scene
.NWBusyDrawing
= node1
.name
1362 if context
.scene
.NWBusyDrawing
!= 'STOP':
1363 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1365 context
.scene
.NWLazySource
= node1
.name
1366 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1368 if event
.type == 'MOUSEMOVE':
1369 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1371 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1372 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1373 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1376 node2
= node_at_pos(nodes
, context
, event
)
1378 context
.scene
.NWBusyDrawing
= node2
.name
1383 link_success
= False
1389 if node
.select
== True:
1391 original_sel
.append(node
)
1393 original_unsel
.append(node
)
1397 #link_success = autolink(node1, node2, links)
1399 if len(node1
.outputs
) > 1 and node2
.inputs
:
1400 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1401 elif len(node1
.outputs
) == 1:
1402 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1404 link_success
= autolink(node1
, node2
, links
)
1406 for node
in original_sel
:
1408 for node
in original_unsel
:
1412 force_update(context
)
1413 context
.scene
.NWBusyDrawing
= ""
1416 elif event
.type == 'ESC':
1417 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1418 return {'CANCELLED'}
1420 return {'RUNNING_MODAL'}
1422 def invoke(self
, context
, event
):
1423 if context
.area
.type == 'NODE_EDITOR':
1424 nodes
, links
= get_nodes_links(context
)
1425 node
= node_at_pos(nodes
, context
, event
)
1427 context
.scene
.NWBusyDrawing
= node
.name
1429 # the arguments we pass the the callback
1433 args
= (self
, context
, mode
)
1434 # Add the region OpenGL drawing callback
1435 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1436 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1438 self
.mouse_path
= []
1440 context
.window_manager
.modal_handler_add(self
)
1441 return {'RUNNING_MODAL'}
1443 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1444 return {'CANCELLED'}
1447 class NWDeleteUnused(Operator
, NWBase
):
1448 """Delete all nodes whose output is not used"""
1449 bl_idname
= 'node.nw_del_unused'
1450 bl_label
= 'Delete Unused Nodes'
1451 bl_options
= {'REGISTER', 'UNDO'}
1453 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1454 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1456 def is_unused_node(self
, node
):
1457 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1458 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1459 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1460 if node
.type in end_types
:
1463 for output
in node
.outputs
:
1469 def poll(cls
, context
):
1471 if nw_check(context
):
1472 if context
.space_data
.node_tree
.nodes
:
1476 def execute(self
, context
):
1477 nodes
, links
= get_nodes_links(context
)
1482 if node
.select
== True:
1483 selection
.append(node
.name
)
1489 temp_deleted_nodes
= []
1490 del_unused_iterations
= len(nodes
)
1491 for it
in range(0, del_unused_iterations
):
1492 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1494 if self
.is_unused_node(node
):
1496 deleted_nodes
.append(node
.name
)
1497 bpy
.ops
.node
.delete()
1499 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1502 if self
.delete_frames
:
1510 frames_in_use
.append(node
.parent
)
1512 if node
.type == 'FRAME' and node
not in frames_in_use
:
1515 repeat
= True # repeat for nested frames
1517 if node
not in frames_in_use
:
1519 deleted_nodes
.append(node
.name
)
1520 bpy
.ops
.node
.delete()
1522 if self
.delete_muted
:
1526 deleted_nodes
.append(node
.name
)
1527 bpy
.ops
.node
.delete_reconnect()
1529 # get unique list of deleted nodes (iterations would count the same node more than once)
1530 deleted_nodes
= list(set(deleted_nodes
))
1531 for n
in deleted_nodes
:
1532 self
.report({'INFO'}, "Node " + n
+ " deleted")
1533 num_deleted
= len(deleted_nodes
)
1538 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1540 self
.report({'INFO'}, "Nothing deleted")
1543 nodes
, links
= get_nodes_links(context
)
1545 if node
.name
in selection
:
1549 def invoke(self
, context
, event
):
1550 return context
.window_manager
.invoke_confirm(self
, event
)
1553 class NWSwapLinks(Operator
, NWBase
):
1554 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1555 bl_idname
= 'node.nw_swap_links'
1556 bl_label
= 'Swap Links'
1557 bl_options
= {'REGISTER', 'UNDO'}
1560 def poll(cls
, context
):
1562 if nw_check(context
):
1563 if context
.selected_nodes
:
1564 valid
= len(context
.selected_nodes
) <= 2
1567 def execute(self
, context
):
1568 nodes
, links
= get_nodes_links(context
)
1569 selected_nodes
= context
.selected_nodes
1570 n1
= selected_nodes
[0]
1573 if len(selected_nodes
) == 2:
1574 n2
= selected_nodes
[1]
1575 if n1
.outputs
and n2
.outputs
:
1580 for output
in n1
.outputs
:
1582 for link
in output
.links
:
1583 n1_outputs
.append([out_index
, link
.to_socket
])
1588 for output
in n2
.outputs
:
1590 for link
in output
.links
:
1591 n2_outputs
.append([out_index
, link
.to_socket
])
1595 for connection
in n1_outputs
:
1597 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1599 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1600 for connection
in n2_outputs
:
1602 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1604 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1606 if n1
.outputs
or n2
.outputs
:
1607 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1609 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1612 elif len(selected_nodes
) == 1:
1613 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1614 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1619 for i1
in n1
.inputs
:
1620 if i1
.is_linked
and not i1
.is_multi_input
:
1622 for i2
in n1
.inputs
:
1623 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1625 types
.append ([i1
, similar_types
, i
])
1627 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1632 for i2
in n1
.inputs
:
1633 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1635 i1f
= pair
[0].links
[0].from_socket
1636 i1t
= pair
[0].links
[0].to_socket
1637 i2f
= pair
[1].links
[0].from_socket
1638 i2t
= pair
[1].links
[0].to_socket
1643 fs
= t
[0].links
[0].from_socket
1645 links
.remove(t
[0].links
[0])
1646 if i
+1 == len(n1
.inputs
):
1649 while n1
.inputs
[i
].is_linked
:
1651 links
.new(fs
, n1
.inputs
[i
])
1652 elif len(types
) == 2:
1653 i1f
= types
[0][0].links
[0].from_socket
1654 i1t
= types
[0][0].links
[0].to_socket
1655 i2f
= types
[1][0].links
[0].from_socket
1656 i2t
= types
[1][0].links
[0].to_socket
1661 self
.report({'WARNING'}, "This node has no input connections to swap!")
1663 self
.report({'WARNING'}, "This node has no inputs to swap!")
1665 force_update(context
)
1669 class NWResetBG(Operator
, NWBase
):
1670 """Reset the zoom and position of the background image"""
1671 bl_idname
= 'node.nw_bg_reset'
1672 bl_label
= 'Reset Backdrop'
1673 bl_options
= {'REGISTER', 'UNDO'}
1676 def poll(cls
, context
):
1678 if nw_check(context
):
1679 snode
= context
.space_data
1680 valid
= snode
.tree_type
== 'CompositorNodeTree'
1683 def execute(self
, context
):
1684 context
.space_data
.backdrop_zoom
= 1
1685 context
.space_data
.backdrop_offset
[0] = 0
1686 context
.space_data
.backdrop_offset
[1] = 0
1690 class NWAddAttrNode(Operator
, NWBase
):
1691 """Add an Attribute node with this name"""
1692 bl_idname
= 'node.nw_add_attr_node'
1693 bl_label
= 'Add UV map'
1694 bl_options
= {'REGISTER', 'UNDO'}
1696 attr_name
: StringProperty()
1698 def execute(self
, context
):
1699 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1700 nodes
, links
= get_nodes_links(context
)
1701 nodes
.active
.attribute_name
= self
.attr_name
1704 class NWPreviewNode(Operator
, NWBase
):
1705 bl_idname
= "node.nw_preview_node"
1706 bl_label
= "Preview Node"
1707 bl_description
= "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output"
1708 bl_options
= {'REGISTER', 'UNDO'}
1710 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1711 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1712 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1713 run_in_geometry_nodes
: BoolProperty(default
=True)
1716 self
.shader_output_type
= ""
1717 self
.shader_output_ident
= ""
1718 self
.shader_viewer_ident
= ""
1721 def poll(cls
, context
):
1722 if nw_check(context
):
1723 space
= context
.space_data
1724 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1725 if context
.active_node
:
1726 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1732 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1733 #check if a viewer output already exists in a node group otherwise create
1734 if hasattr(node
, "node_tree"):
1736 if len(node
.node_tree
.outputs
):
1738 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1739 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1740 #if viewer output is already used but leads to the same socket we can still use it
1741 is_used
= self
.is_socket_used_other_mats(socket
)
1743 if connect_socket
== None:
1745 groupout
= get_group_output_node(node
.node_tree
)
1746 groupout_input
= groupout
.inputs
[i
]
1747 links
= groupout_input
.links
1748 if connect_socket
not in [link
.from_socket
for link
in links
]:
1754 if not index
and free_socket
:
1758 #create viewer socket
1759 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1760 index
= len(node
.node_tree
.outputs
) - 1
1761 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1764 def init_shader_variables(self
, space
, shader_type
):
1765 if shader_type
== 'OBJECT':
1766 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1767 self
.shader_output_type
= "OUTPUT_MATERIAL"
1768 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1769 self
.shader_viewer_ident
= "ShaderNodeEmission"
1771 self
.shader_output_type
= "OUTPUT_LIGHT"
1772 self
.shader_output_ident
= "ShaderNodeOutputLight"
1773 self
.shader_viewer_ident
= "ShaderNodeEmission"
1775 elif shader_type
== 'WORLD':
1776 self
.shader_output_type
= "OUTPUT_WORLD"
1777 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1778 self
.shader_viewer_ident
= "ShaderNodeBackground"
1780 def get_shader_output_node(self
, tree
):
1781 for node
in tree
.nodes
:
1782 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1786 def ensure_group_output(cls
, tree
):
1787 #check if a group output node exists otherwise create
1788 groupout
= get_group_output_node(tree
)
1790 groupout
= tree
.nodes
.new('NodeGroupOutput')
1791 loc_x
, loc_y
= get_output_location(tree
)
1792 groupout
.location
.x
= loc_x
1793 groupout
.location
.y
= loc_y
1794 groupout
.select
= False
1795 # So that we don't keep on adding new group outputs
1796 groupout
.is_active_output
= True
1800 def search_sockets(cls
, node
, sockets
, index
=None):
1801 # recursively scan nodes for viewer sockets and store in list
1802 for i
, input_socket
in enumerate(node
.inputs
):
1803 if index
and i
!= index
:
1805 if len(input_socket
.links
):
1806 link
= input_socket
.links
[0]
1807 next_node
= link
.from_node
1808 external_socket
= link
.from_socket
1809 if hasattr(next_node
, "node_tree"):
1810 for socket_index
, s
in enumerate(next_node
.outputs
):
1811 if s
== external_socket
:
1813 socket
= next_node
.node_tree
.outputs
[socket_index
]
1814 if is_viewer_socket(socket
) and socket
not in sockets
:
1815 sockets
.append(socket
)
1816 #continue search inside of node group but restrict socket to where we came from
1817 groupout
= get_group_output_node(next_node
.node_tree
)
1818 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1821 def scan_nodes(cls
, tree
, sockets
):
1822 # get all viewer sockets in a material tree
1823 for node
in tree
.nodes
:
1824 if hasattr(node
, "node_tree"):
1825 for socket
in node
.node_tree
.outputs
:
1826 if is_viewer_socket(socket
) and (socket
not in sockets
):
1827 sockets
.append(socket
)
1828 cls
.scan_nodes(node
.node_tree
, sockets
)
1830 def link_leads_to_used_socket(self
, link
):
1831 #return True if link leads to a socket that is already used in this material
1832 socket
= get_internal_socket(link
.to_socket
)
1833 return (socket
and self
.is_socket_used_active_mat(socket
))
1835 def is_socket_used_active_mat(self
, socket
):
1836 #ensure used sockets in active material is calculated and check given socket
1837 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1838 self
.used_viewer_sockets_active_mat
= []
1839 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1841 emission
= self
.get_viewer_node(materialout
)
1842 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_active_mat
)
1843 return socket
in self
.used_viewer_sockets_active_mat
1845 def is_socket_used_other_mats(self
, socket
):
1846 #ensure used sockets in other materials are calculated and check given socket
1847 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1848 self
.used_viewer_sockets_other_mats
= []
1849 for mat
in bpy
.data
.materials
:
1850 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1853 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1855 emission
= self
.get_viewer_node(materialout
)
1856 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_other_mats
)
1857 return socket
in self
.used_viewer_sockets_other_mats
1860 def get_viewer_node(materialout
):
1861 input_socket
= materialout
.inputs
[0]
1862 if len(input_socket
.links
) > 0:
1863 node
= input_socket
.links
[0].from_node
1864 if node
.type == 'EMISSION' and node
.name
== "Emission Viewer":
1867 def invoke(self
, context
, event
):
1868 space
= context
.space_data
1869 # Ignore operator when running in wrong context.
1870 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1871 return {'PASS_THROUGH'}
1873 shader_type
= space
.shader_type
1874 self
.init_shader_variables(space
, shader_type
)
1875 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1876 mlocx
= event
.mouse_region_x
1877 mlocy
= event
.mouse_region_y
1878 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
1879 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1880 active_tree
, path_to_tree
= get_active_tree(context
)
1881 nodes
, links
= active_tree
.nodes
, active_tree
.links
1882 base_node_tree
= space
.node_tree
1883 active
= nodes
.active
1885 # For geometry node trees we just connect to the group output,
1886 # because there is no "viewer node" yet.
1887 if space
.tree_type
== "GeometryNodeTree":
1890 for out
in active
.outputs
:
1891 if is_visible_socket(out
):
1900 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1901 self
.scan_nodes(base_node_tree
, delete_sockets
)
1903 # Find (or create if needed) the output of this node tree
1904 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1906 # Analyze outputs, make links
1909 for i
, out
in enumerate(active
.outputs
):
1910 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1911 valid_outputs
.append(i
)
1913 out_i
= valid_outputs
[0] # Start index of node's outputs
1914 for i
, valid_i
in enumerate(valid_outputs
):
1915 for out_link
in active
.outputs
[valid_i
].links
:
1916 if is_viewer_link(out_link
, geometryoutput
):
1917 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1918 if i
< len(valid_outputs
) - 1:
1919 out_i
= valid_outputs
[i
+ 1]
1921 out_i
= valid_outputs
[0]
1923 make_links
= [] # store sockets for new links
1924 delete_nodes
= [] # store unused nodes to delete in the end
1926 # If there is no 'GEOMETRY' output type - We can't preview the node
1929 socket_type
= 'GEOMETRY'
1930 # Find an input socket of the output of type geometry
1931 geometryoutindex
= None
1932 for i
,inp
in enumerate(geometryoutput
.inputs
):
1933 if inp
.type == socket_type
:
1934 geometryoutindex
= i
1936 if geometryoutindex
is None:
1937 # Create geometry socket
1938 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1939 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1941 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1942 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1943 for li_from
, li_to
in make_links
:
1944 base_node_tree
.links
.new(li_from
, li_to
)
1945 tree
= base_node_tree
1946 link_end
= output_socket
1947 while tree
.nodes
.active
!= active
:
1948 node
= tree
.nodes
.active
1949 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1950 link_start
= node
.outputs
[index
]
1951 node_socket
= node
.node_tree
.outputs
[index
]
1952 if node_socket
in delete_sockets
:
1953 delete_sockets
.remove(node_socket
)
1954 tree
.links
.new(link_start
, link_end
)
1956 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1957 tree
= tree
.nodes
.active
.node_tree
1958 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1961 for socket
in delete_sockets
:
1962 tree
= socket
.id_data
1963 tree
.outputs
.remove(socket
)
1966 for tree
, node
in delete_nodes
:
1967 tree
.nodes
.remove(node
)
1969 nodes
.active
= active
1970 active
.select
= True
1971 force_update(context
)
1975 # What follows is code for the shader editor
1976 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1979 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
):
1980 for out
in active
.outputs
:
1981 if is_visible_socket(out
):
1985 # get material_output node
1986 materialout
= None # placeholder node
1989 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1990 self
.scan_nodes(base_node_tree
, delete_sockets
)
1992 materialout
= self
.get_shader_output_node(base_node_tree
)
1994 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1995 materialout
.location
= get_output_location(base_node_tree
)
1996 materialout
.select
= False
1997 # Analyze outputs, add "Emission Viewer" if needed, make links
2000 for i
, out
in enumerate(active
.outputs
):
2001 if is_visible_socket(out
):
2002 valid_outputs
.append(i
)
2004 out_i
= valid_outputs
[0] # Start index of node's outputs
2005 for i
, valid_i
in enumerate(valid_outputs
):
2006 for out_link
in active
.outputs
[valid_i
].links
:
2007 if is_viewer_link(out_link
, materialout
):
2008 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
2009 if i
< len(valid_outputs
) - 1:
2010 out_i
= valid_outputs
[i
+ 1]
2012 out_i
= valid_outputs
[0]
2014 make_links
= [] # store sockets for new links
2015 delete_nodes
= [] # store unused nodes to delete in the end
2017 # If output type not 'SHADER' - "Emission Viewer" needed
2018 if active
.outputs
[out_i
].type != 'SHADER':
2019 socket_type
= 'NodeSocketColor'
2020 # get Emission Viewer node
2021 emission_exists
= False
2022 emission_placeholder
= base_node_tree
.nodes
[0]
2023 for node
in base_node_tree
.nodes
:
2024 if "Emission Viewer" in node
.name
:
2025 emission_exists
= True
2026 emission_placeholder
= node
2027 if not emission_exists
:
2028 emission
= base_node_tree
.nodes
.new(self
.shader_viewer_ident
)
2029 emission
.hide
= True
2030 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
2031 emission
.label
= "Viewer"
2032 emission
.name
= "Emission Viewer"
2033 emission
.use_custom_color
= True
2034 emission
.color
= (0.6, 0.5, 0.4)
2035 emission
.select
= False
2037 emission
= emission_placeholder
2038 output_socket
= emission
.inputs
[0]
2040 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2041 if emission
.outputs
[0].links
.__len
__() > 0:
2042 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
2043 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2045 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2047 # Set brightness of viewer to compensate for Film and CM exposure
2048 if context
.scene
.render
.engine
== 'CYCLES' and hasattr(context
.scene
, 'cycles'):
2049 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
2053 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
2054 emission
.inputs
[1].default_value
= intensity
2057 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2058 socket_type
= 'NodeSocketShader'
2059 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
2060 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
2061 output_socket
= materialout
.inputs
[materialout_index
]
2062 for node
in base_node_tree
.nodes
:
2063 if node
.name
== 'Emission Viewer':
2064 delete_nodes
.append((base_node_tree
, node
))
2065 for li_from
, li_to
in make_links
:
2066 base_node_tree
.links
.new(li_from
, li_to
)
2068 # Create links through node groups until we reach the active node
2069 tree
= base_node_tree
2070 link_end
= output_socket
2071 while tree
.nodes
.active
!= active
:
2072 node
= tree
.nodes
.active
2073 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
2074 link_start
= node
.outputs
[index
]
2075 node_socket
= node
.node_tree
.outputs
[index
]
2076 if node_socket
in delete_sockets
:
2077 delete_sockets
.remove(node_socket
)
2078 tree
.links
.new(link_start
, link_end
)
2080 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
2081 tree
= tree
.nodes
.active
.node_tree
2082 tree
.links
.new(active
.outputs
[out_i
], link_end
)
2085 for socket
in delete_sockets
:
2086 if not self
.is_socket_used_other_mats(socket
):
2087 tree
= socket
.id_data
2088 tree
.outputs
.remove(socket
)
2091 for tree
, node
in delete_nodes
:
2092 tree
.nodes
.remove(node
)
2094 nodes
.active
= active
2095 active
.select
= True
2097 force_update(context
)
2101 return {'CANCELLED'}
2104 class NWFrameSelected(Operator
, NWBase
):
2105 bl_idname
= "node.nw_frame_selected"
2106 bl_label
= "Frame Selected"
2107 bl_description
= "Add a frame node and parent the selected nodes to it"
2108 bl_options
= {'REGISTER', 'UNDO'}
2110 label_prop
: StringProperty(
2112 description
='The visual name of the frame node',
2115 color_prop
: FloatVectorProperty(
2117 description
="The color of the frame node",
2118 default
=(0.6, 0.6, 0.6),
2119 min=0, max=1, step
=1, precision
=3,
2120 subtype
='COLOR_GAMMA', size
=3
2123 def execute(self
, context
):
2124 nodes
, links
= get_nodes_links(context
)
2127 if node
.select
== True:
2128 selected
.append(node
)
2130 bpy
.ops
.node
.add_node(type='NodeFrame')
2132 frm
.label
= self
.label_prop
2133 frm
.use_custom_color
= True
2134 frm
.color
= self
.color_prop
2136 for node
in selected
:
2142 class NWReloadImages(Operator
):
2143 bl_idname
= "node.nw_reload_images"
2144 bl_label
= "Reload Images"
2145 bl_description
= "Update all the image nodes to match their files on disk"
2148 def poll(cls
, context
):
2150 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
2151 if context
.active_node
is not None:
2152 for out
in context
.active_node
.outputs
:
2153 if is_visible_socket(out
):
2158 def execute(self
, context
):
2159 nodes
, links
= get_nodes_links(context
)
2160 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2163 if node
.type in image_types
:
2164 if node
.type == "TEXTURE":
2165 if node
.texture
: # node has texture assigned
2166 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2167 if node
.texture
.image
: # texture has image assigned
2168 node
.texture
.image
.reload()
2176 self
.report({'INFO'}, "Reloaded images")
2177 print("Reloaded " + str(num_reloaded
) + " images")
2178 force_update(context
)
2181 self
.report({'WARNING'}, "No images found to reload in this node tree")
2182 return {'CANCELLED'}
2185 class NWSwitchNodeType(Operator
, NWBase
):
2186 """Switch type of selected nodes """
2187 bl_idname
= "node.nw_swtch_node_type"
2188 bl_label
= "Switch Node Type"
2189 bl_options
= {'REGISTER', 'UNDO'}
2191 to_type
: EnumProperty(
2192 name
="Switch to type",
2193 items
=list(shaders_input_nodes_props
) +
2194 list(shaders_output_nodes_props
) +
2195 list(shaders_shader_nodes_props
) +
2196 list(shaders_texture_nodes_props
) +
2197 list(shaders_color_nodes_props
) +
2198 list(shaders_vector_nodes_props
) +
2199 list(shaders_converter_nodes_props
) +
2200 list(shaders_layout_nodes_props
) +
2201 list(compo_input_nodes_props
) +
2202 list(compo_output_nodes_props
) +
2203 list(compo_color_nodes_props
) +
2204 list(compo_converter_nodes_props
) +
2205 list(compo_filter_nodes_props
) +
2206 list(compo_vector_nodes_props
) +
2207 list(compo_matte_nodes_props
) +
2208 list(compo_distort_nodes_props
) +
2209 list(compo_layout_nodes_props
) +
2210 list(blender_mat_input_nodes_props
) +
2211 list(blender_mat_output_nodes_props
) +
2212 list(blender_mat_color_nodes_props
) +
2213 list(blender_mat_vector_nodes_props
) +
2214 list(blender_mat_converter_nodes_props
) +
2215 list(blender_mat_layout_nodes_props
) +
2216 list(texture_input_nodes_props
) +
2217 list(texture_output_nodes_props
) +
2218 list(texture_color_nodes_props
) +
2219 list(texture_pattern_nodes_props
) +
2220 list(texture_textures_nodes_props
) +
2221 list(texture_converter_nodes_props
) +
2222 list(texture_distort_nodes_props
) +
2223 list(texture_layout_nodes_props
)
2226 geo_to_type
: StringProperty(
2227 name
="Switch to type",
2231 def execute(self
, context
):
2232 nodes
, links
= get_nodes_links(context
)
2233 to_type
= self
.to_type
2234 if self
.geo_to_type
!= '':
2235 to_type
= self
.geo_to_type
2236 # Those types of nodes will not swap.
2237 src_excludes
= ('NodeFrame')
2238 # Those attributes of nodes will be copied if possible
2239 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
2240 'show_options', 'show_preview', 'show_texture',
2241 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2243 selected
= [n
for n
in nodes
if n
.select
]
2245 for node
in [n
for n
in selected
if
2246 n
.rna_type
.identifier
not in src_excludes
and
2247 n
.rna_type
.identifier
!= to_type
]:
2248 new_node
= nodes
.new(to_type
)
2249 for attr
in attrs_to_pass
:
2250 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
2251 setattr(new_node
, attr
, getattr(node
, attr
))
2252 # set image datablock of dst to image of src
2253 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
2255 new_node
.image
= node
.image
2257 if new_node
.type == 'SWITCH':
2258 new_node
.hide
= True
2259 # Dictionaries: src_sockets and dst_sockets:
2260 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2261 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2262 # in 'INPUTS' and 'OUTPUTS':
2263 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2265 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2267 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2268 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2271 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2272 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2274 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2275 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2276 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2277 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
2278 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2279 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
2280 # enumerate in inputs, then in outputs
2281 # find name, default value and links of socket
2282 for i
, socket
in enumerate(in_out
):
2283 the_name
= socket
.name
2285 # Not every socket, especially in outputs has "default_value"
2286 if hasattr(socket
, 'default_value'):
2287 dval
= socket
.default_value
2289 for lnk
in socket
.links
:
2290 socket_links
.append(lnk
)
2291 # check type of socket to fill proper keys.
2292 for the_type
in types_order_one
:
2293 if socket
.type == the_type
:
2294 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2295 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2296 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
2297 # Check which of the types in inputs/outputs is considered to be "main".
2298 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2299 for type_check
in types_order_one
:
2300 if sockets
[in_out_name
][type_check
]:
2301 sockets
[in_out_name
]['MAIN'] = type_check
2305 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2306 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2309 for inout
, soctype
in (
2310 ('INPUTS', 'MAIN',),
2311 ('INPUTS', 'SHADER',),
2312 ('INPUTS', 'RGBA',),
2313 ('INPUTS', 'VECTOR',),
2314 ('INPUTS', 'VALUE',),
2315 ('OUTPUTS', 'MAIN',),
2316 ('OUTPUTS', 'SHADER',),
2317 ('OUTPUTS', 'RGBA',),
2318 ('OUTPUTS', 'VECTOR',),
2319 ('OUTPUTS', 'VALUE',),
2321 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
2322 if soctype
== 'MAIN':
2323 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
2324 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
2326 sc
= src_sockets
[inout
][soctype
]
2327 dt
= dst_sockets
[inout
][soctype
]
2328 # start with 'dt' to determine number of possibilities.
2329 for i
, soc
in enumerate(dt
):
2330 # if src main has enough entries - match them with dst main sockets by indexes.
2332 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
2333 # add 'VALUE_NAME' criterion to inputs.
2334 if inout
== 'INPUTS' and soctype
== 'VALUE':
2336 if s
[2] == soc
[2]: # if names match
2337 # append src (index, dval), dst (index, dval)
2338 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
2340 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2341 # This creates better links when relinking textures.
2342 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
2343 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
2345 # Pass default values and RELINK:
2346 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2347 # INPUTS: Base on matches in proper order.
2348 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
2350 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
2351 new_node
.inputs
[dst_i
].default_value
= src_dval
2352 # Special case: switch to math
2353 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2354 new_node
.type == 'MATH' and\
2356 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
2357 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
2358 if node
.type == 'MIX_RGB':
2359 if node
.blend_type
in [o
[0] for o
in operations
]:
2360 new_node
.operation
= node
.blend_type
2361 # Special case: switch from math to some types
2362 if node
.type == 'MATH' and\
2363 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2366 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
2367 if new_node
.type == 'MIX_RGB':
2368 if node
.operation
in [t
[0] for t
in blend_types
]:
2369 new_node
.blend_type
= node
.operation
2370 # Set Fac of MIX_RGB to 1.0
2371 new_node
.inputs
[0].default_value
= 1.0
2372 # make link only when dst matching input is not linked already.
2373 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
2374 in_src_link
= node
.inputs
[src_i
].links
[0]
2375 in_dst_socket
= new_node
.inputs
[dst_i
]
2376 links
.new(in_src_link
.from_socket
, in_dst_socket
)
2377 links
.remove(in_src_link
)
2378 # OUTPUTS: Base on matches in proper order.
2379 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
2380 for out_src_link
in node
.outputs
[src_i
].links
:
2381 out_dst_socket
= new_node
.outputs
[dst_i
]
2382 links
.new(out_dst_socket
, out_src_link
.to_socket
)
2383 # relink rest inputs if possible, no criteria
2384 for src_inp
in node
.inputs
:
2385 for dst_inp
in new_node
.inputs
:
2386 if src_inp
.links
and not dst_inp
.links
:
2387 src_link
= src_inp
.links
[0]
2388 links
.new(src_link
.from_socket
, dst_inp
)
2389 links
.remove(src_link
)
2390 # relink rest outputs if possible, base on node kind if any left.
2391 for src_o
in node
.outputs
:
2392 for out_src_link
in src_o
.links
:
2393 for dst_o
in new_node
.outputs
:
2394 if src_o
.type == dst_o
.type:
2395 links
.new(dst_o
, out_src_link
.to_socket
)
2396 # relink rest outputs no criteria if any left. Link all from first output.
2397 for src_o
in node
.outputs
:
2398 for out_src_link
in src_o
.links
:
2399 if new_node
.outputs
:
2400 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2402 force_update(context
)
2406 class NWMergeNodes(Operator
, NWBase
):
2407 bl_idname
= "node.nw_merge_nodes"
2408 bl_label
= "Merge Nodes"
2409 bl_description
= "Merge Selected Nodes"
2410 bl_options
= {'REGISTER', 'UNDO'}
2414 description
="All possible blend types, boolean operations and math operations",
2415 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
],
2417 merge_type
: EnumProperty(
2419 description
="Type of Merge to be used",
2421 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2422 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2423 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2424 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2425 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2426 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2427 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2431 # Check if the link connects to a node that is in selected_nodes
2432 # If not, then check recursively for each link in the nodes outputs.
2433 # If yes, return True. If the recursion stops without finding a node
2434 # in selected_nodes, it returns False. The depth is used to prevent
2435 # getting stuck in a loop because of an already present cycle.
2437 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
2439 # We're stuck in a cycle, but that cycle was already present,
2440 # so we return False.
2441 # NOTE: The number 255 is arbitrary, but seems to work well.
2444 if node
in selected_nodes
:
2446 if not node
.outputs
:
2448 for output
in node
.outputs
:
2449 if output
.is_linked
:
2450 for olink
in output
.links
:
2451 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
2453 # None of the outputs found a node in selected_nodes, so there is no cycle.
2456 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2457 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2458 # be connected. The last one is assumed to be a multi input socket.
2459 # For convenience the node is returned.
2461 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
2462 # The y-location of the last node
2463 loc_y
= nodes_list
[-1][2]
2464 if merge_position
== 'CENTER':
2465 # Average the y-location
2466 for i
in range(len(nodes_list
)-1):
2467 loc_y
+= nodes_list
[i
][2]
2468 loc_y
= loc_y
/len(nodes_list
)
2469 new_node
= nodes
.new(node_name
)
2470 new_node
.hide
= do_hide
2471 new_node
.location
.x
= loc_x
2472 new_node
.location
.y
= loc_y
2473 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
2475 outputs_for_multi_input
= []
2476 for i
,node
in enumerate(selected_nodes
):
2478 # Search for the first node which had output links that do not create
2479 # a cycle, which we can then reconnect afterwards.
2480 if prev_links
== [] and node
.outputs
[0].is_linked
:
2481 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
2482 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2483 # To get the placement to look right we need to reverse the order in which we connect the
2484 # outputs to the multi input socket.
2485 if i
< len(socket_indices
) - 1:
2486 ind
= socket_indices
[i
]
2487 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
2489 outputs_for_multi_input
.insert(0, node
.outputs
[0])
2490 if outputs_for_multi_input
!= []:
2491 ind
= socket_indices
[-1]
2492 for output
in outputs_for_multi_input
:
2493 links
.new(output
, new_node
.inputs
[ind
])
2494 if prev_links
!= []:
2495 for link
in prev_links
:
2496 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
2499 def execute(self
, context
):
2500 settings
= context
.preferences
.addons
[__name__
].preferences
2501 merge_hide
= settings
.merge_hide
2502 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2505 do_hide_shader
= False
2506 if merge_hide
== 'ALWAYS':
2508 do_hide_shader
= True
2509 elif merge_hide
== 'NON_SHADER':
2512 tree_type
= context
.space_data
.node_tree
.type
2513 if tree_type
== 'GEOMETRY':
2514 node_type
= 'GeometryNode'
2515 if tree_type
== 'COMPOSITING':
2516 node_type
= 'CompositorNode'
2517 elif tree_type
== 'SHADER':
2518 node_type
= 'ShaderNode'
2519 elif tree_type
== 'TEXTURE':
2520 node_type
= 'TextureNode'
2521 nodes
, links
= get_nodes_links(context
)
2523 merge_type
= self
.merge_type
2524 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2525 # 'ZCOMBINE' works only if mode == 'MIX'
2526 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2527 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2530 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2532 # The math nodes used for geometry nodes are of type 'ShaderNode'
2533 if merge_type
== 'MATH' and tree_type
== 'GEOMETRY':
2534 node_type
= 'ShaderNode'
2535 selected_mix
= [] # entry = [index, loc]
2536 selected_shader
= [] # entry = [index, loc]
2537 selected_geometry
= [] # entry = [index, loc]
2538 selected_math
= [] # entry = [index, loc]
2539 selected_vector
= [] # entry = [index, loc]
2540 selected_z
= [] # entry = [index, loc]
2541 selected_alphaover
= [] # entry = [index, loc]
2543 for i
, node
in enumerate(nodes
):
2544 if node
.select
and node
.outputs
:
2545 if merge_type
== 'AUTO':
2546 for (type, types_list
, dst
) in (
2547 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2548 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2549 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2550 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2551 ('VECTOR', [], selected_vector
),
2553 output_type
= node
.outputs
[0].type
2554 valid_mode
= mode
in types_list
2555 # When mode is 'MIX' we have to cheat since the mix node is not used in
2557 if tree_type
== 'GEOMETRY':
2559 if output_type
== 'VALUE' and type == 'VALUE':
2561 elif output_type
== 'VECTOR' and type == 'VECTOR':
2563 elif type == 'GEOMETRY':
2565 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2566 # Cheat that output type is 'RGBA',
2567 # and that 'MIX' exists in math operations list.
2568 # This way when selected_mix list is analyzed:
2569 # Node data will be appended even though it doesn't meet requirements.
2570 elif output_type
!= 'SHADER' and mode
== 'MIX':
2571 output_type
= 'RGBA'
2573 if output_type
== type and valid_mode
:
2574 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2576 for (type, types_list
, dst
) in (
2577 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2578 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2579 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2580 ('MATH', [t
[0] for t
in operations
], selected_math
),
2581 ('ZCOMBINE', ('MIX', ), selected_z
),
2582 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2584 if merge_type
== type and mode
in types_list
:
2585 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2586 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2587 # use only 'Mix' nodes for merging.
2588 # For that we add selected_math list to selected_mix list and clear selected_math.
2589 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2590 selected_mix
+= selected_math
2592 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2595 count_before
= len(nodes
)
2596 # sort list by loc_x - reversed
2597 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2599 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2600 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2602 # Change the node type for math nodes in a geometry node tree.
2603 if tree_type
== 'GEOMETRY':
2604 if nodes_list
is selected_math
or nodes_list
is selected_vector
:
2605 node_type
= 'ShaderNode'
2609 node_type
= 'GeometryNode'
2610 if merge_position
== 'CENTER':
2611 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)
2612 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2618 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2622 if nodes_list
== selected_shader
and not do_hide_shader
:
2624 the_range
= len(nodes_list
) - 1
2625 if len(nodes_list
) == 1:
2628 for i
in range(the_range
):
2629 if nodes_list
== selected_mix
:
2630 add_type
= node_type
+ 'MixRGB'
2631 add
= nodes
.new(add_type
)
2632 add
.blend_type
= mode
2634 add
.inputs
[0].default_value
= 1.0
2635 add
.show_preview
= False
2641 add
.width_hidden
= 100.0
2642 elif nodes_list
== selected_math
:
2643 add_type
= node_type
+ 'Math'
2644 add
= nodes
.new(add_type
)
2645 add
.operation
= mode
2651 add
.width_hidden
= 100.0
2652 elif nodes_list
== selected_shader
:
2654 add_type
= node_type
+ 'MixShader'
2655 add
= nodes
.new(add_type
)
2656 add
.hide
= do_hide_shader
2661 add
.width_hidden
= 100.0
2663 add_type
= node_type
+ 'AddShader'
2664 add
= nodes
.new(add_type
)
2665 add
.hide
= do_hide_shader
2670 add
.width_hidden
= 100.0
2671 elif nodes_list
== selected_geometry
:
2672 if mode
in ('JOIN', 'MIX'):
2673 add_type
= node_type
+ 'JoinGeometry'
2674 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2676 add_type
= node_type
+ 'Boolean'
2677 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2678 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2679 add
.operation
= mode
2682 elif nodes_list
== selected_vector
:
2683 add_type
= node_type
+ 'VectorMath'
2684 add
= nodes
.new(add_type
)
2685 add
.operation
= mode
2691 add
.width_hidden
= 100.0
2692 elif nodes_list
== selected_z
:
2693 add
= nodes
.new('CompositorNodeZcombine')
2694 add
.show_preview
= False
2700 add
.width_hidden
= 100.0
2701 elif nodes_list
== selected_alphaover
:
2702 add
= nodes
.new('CompositorNodeAlphaOver')
2703 add
.show_preview
= False
2709 add
.width_hidden
= 100.0
2710 add
.location
= loc_x
, loc_y
2714 # This has already been handled separately
2718 count_after
= len(nodes
)
2719 index
= count_after
- 1
2720 first_selected
= nodes
[nodes_list
[0][0]]
2721 # "last" node has been added as first, so its index is count_before.
2722 last_add
= nodes
[count_before
]
2723 # Create list of invalid indexes.
2724 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2727 # Two nodes were selected and first selected has no output links, second selected has output links.
2728 # Then add links from last add to all links 'to_socket' of out links of second selected.
2729 if len(nodes_list
) == 2:
2730 if not first_selected
.outputs
[0].links
:
2731 second_selected
= nodes
[nodes_list
[1][0]]
2732 for ss_link
in second_selected
.outputs
[0].links
:
2733 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2734 # Link only if "to_node" index not in invalid indexes list.
2735 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2736 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2737 # add links from last_add to all links 'to_socket' of out links of first selected.
2738 for fs_link
in first_selected
.outputs
[0].links
:
2739 # Link only if "to_node" index not in invalid indexes list.
2740 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2741 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2742 # add link from "first" selected and "first" add node
2743 node_to
= nodes
[count_after
- 1]
2744 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2745 if node_to
.type == 'ZCOMBINE':
2746 for fs_out
in first_selected
.outputs
:
2747 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2748 links
.new(fs_out
, node_to
.inputs
[1])
2750 # add links between added ADD nodes and between selected and ADD nodes
2751 for i
in range(count_adds
):
2752 if i
< count_adds
- 1:
2753 node_from
= nodes
[index
]
2754 node_to
= nodes
[index
- 1]
2755 node_to_input_i
= first
2756 node_to_z_i
= 1 # if z combine - link z to first z input
2757 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2758 if node_to
.type == 'ZCOMBINE':
2759 for from_out
in node_from
.outputs
:
2760 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2761 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2762 if len(nodes_list
) > 1:
2763 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2764 node_to
= nodes
[index
]
2765 node_to_input_i
= second
2766 node_to_z_i
= 3 # if z combine - link z to second z input
2767 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2768 if node_to
.type == 'ZCOMBINE':
2769 for from_out
in node_from
.outputs
:
2770 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2771 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2773 # set "last" of added nodes as active
2774 nodes
.active
= last_add
2775 for i
, x
, y
, dx
, h
in nodes_list
:
2776 nodes
[i
].select
= False
2781 class NWBatchChangeNodes(Operator
, NWBase
):
2782 bl_idname
= "node.nw_batch_change"
2783 bl_label
= "Batch Change"
2784 bl_description
= "Batch Change Blend Type and Math Operation"
2785 bl_options
= {'REGISTER', 'UNDO'}
2787 blend_type
: EnumProperty(
2789 items
=blend_types
+ navs
,
2791 operation
: EnumProperty(
2793 items
=operations
+ navs
,
2796 def execute(self
, context
):
2797 blend_type
= self
.blend_type
2798 operation
= self
.operation
2799 for node
in context
.selected_nodes
:
2800 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2801 if not blend_type
in [nav
[0] for nav
in navs
]:
2802 node
.blend_type
= blend_type
2804 if blend_type
== 'NEXT':
2805 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2806 #index = blend_types.index(node.blend_type)
2807 if index
== len(blend_types
) - 1:
2808 node
.blend_type
= blend_types
[0][0]
2810 node
.blend_type
= blend_types
[index
+ 1][0]
2812 if blend_type
== 'PREV':
2813 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2815 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2817 node
.blend_type
= blend_types
[index
- 1][0]
2819 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2820 if not operation
in [nav
[0] for nav
in navs
]:
2821 node
.operation
= operation
2823 if operation
== 'NEXT':
2824 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2825 #index = operations.index(node.operation)
2826 if index
== len(operations
) - 1:
2827 node
.operation
= operations
[0][0]
2829 node
.operation
= operations
[index
+ 1][0]
2831 if operation
== 'PREV':
2832 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2833 #index = operations.index(node.operation)
2835 node
.operation
= operations
[len(operations
) - 1][0]
2837 node
.operation
= operations
[index
- 1][0]
2842 class NWChangeMixFactor(Operator
, NWBase
):
2843 bl_idname
= "node.nw_factor"
2844 bl_label
= "Change Factor"
2845 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2846 bl_options
= {'REGISTER', 'UNDO'}
2848 # option: Change factor.
2849 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2850 # Else - change factor by option value.
2851 option
: FloatProperty()
2853 def execute(self
, context
):
2854 nodes
, links
= get_nodes_links(context
)
2855 option
= self
.option
2856 selected
= [] # entry = index
2857 for si
, node
in enumerate(nodes
):
2859 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2863 fac
= nodes
[si
].inputs
[0]
2864 nodes
[si
].hide
= False
2865 if option
in {0.0, 1.0}:
2866 fac
.default_value
= option
2868 fac
.default_value
+= option
2873 class NWCopySettings(Operator
, NWBase
):
2874 bl_idname
= "node.nw_copy_settings"
2875 bl_label
= "Copy Settings"
2876 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2877 bl_options
= {'REGISTER', 'UNDO'}
2880 def poll(cls
, context
):
2882 if nw_check(context
):
2884 context
.active_node
is not None and
2885 context
.active_node
.type != 'FRAME'
2890 def execute(self
, context
):
2891 node_active
= context
.active_node
2892 node_selected
= context
.selected_nodes
2895 if not (len(node_selected
) > 1):
2896 self
.report({'ERROR'}, "2 nodes must be selected at least")
2897 return {'CANCELLED'}
2899 # Check if active node is in the selection
2900 selected_node_names
= [n
.name
for n
in node_selected
]
2901 if node_active
.name
not in selected_node_names
:
2902 self
.report({'ERROR'}, "No active node")
2903 return {'CANCELLED'}
2905 # Get nodes in selection by type
2906 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2908 if not (len(valid_nodes
) > 1) and node_active
:
2909 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2910 return {'CANCELLED'}
2912 if len(valid_nodes
) != len(node_selected
):
2913 # Report nodes that are not valid
2914 valid_node_names
= [n
.name
for n
in valid_nodes
]
2915 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2916 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2918 # Reference original
2920 #node_selected_names = [n.name for n in node_selected]
2925 # Deselect all nodes
2926 for i
in node_selected
:
2929 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2930 # Run through all other nodes
2931 for node
in valid_nodes
[1:]:
2933 # Check for frame node
2934 parent
= node
.parent
if node
.parent
else None
2935 node_loc
= [node
.location
.x
, node
.location
.y
]
2937 # Select original to duplicate
2940 # Duplicate selected node
2941 bpy
.ops
.node
.duplicate()
2942 new_node
= context
.selected_nodes
[0]
2945 new_node
.select
= False
2947 # Properties to copy
2948 node_tree
= node
.id_data
2949 props_to_copy
= 'bl_idname name location height width'.split(' ')
2953 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2954 for i
in (i
for i
in mappings
if i
.is_linked
):
2956 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2959 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2960 props_to_copy
.pop(0)
2962 for prop
in props_to_copy
:
2963 setattr(new_node
, prop
, props
[prop
])
2965 # Get the node tree to remove the old node
2966 nodes
= node_tree
.nodes
2968 new_node
.name
= props
['name']
2971 new_node
.parent
= parent
2972 new_node
.location
= node_loc
2974 for str_from
, str_to
in reconnections
:
2975 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2977 success_names
.append(new_node
.name
)
2980 node_tree
.nodes
.active
= orig
2981 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2985 class NWCopyLabel(Operator
, NWBase
):
2986 bl_idname
= "node.nw_copy_label"
2987 bl_label
= "Copy Label"
2988 bl_options
= {'REGISTER', 'UNDO'}
2990 option
: EnumProperty(
2992 description
="Source of name of label",
2994 ('FROM_ACTIVE', 'from active', 'from active node',),
2995 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2996 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
3000 def execute(self
, context
):
3001 nodes
, links
= get_nodes_links(context
)
3002 option
= self
.option
3003 active
= nodes
.active
3004 if option
== 'FROM_ACTIVE':
3006 src_label
= active
.label
3007 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
3008 node
.label
= src_label
3009 elif option
== 'FROM_NODE':
3010 selected
= [n
for n
in nodes
if n
.select
]
3011 for node
in selected
:
3012 for input in node
.inputs
:
3014 src
= input.links
[0].from_node
3015 node
.label
= src
.label
3017 elif option
== 'FROM_SOCKET':
3018 selected
= [n
for n
in nodes
if n
.select
]
3019 for node
in selected
:
3020 for input in node
.inputs
:
3022 src
= input.links
[0].from_socket
3023 node
.label
= src
.name
3029 class NWClearLabel(Operator
, NWBase
):
3030 bl_idname
= "node.nw_clear_label"
3031 bl_label
= "Clear Label"
3032 bl_options
= {'REGISTER', 'UNDO'}
3034 option
: BoolProperty()
3036 def execute(self
, context
):
3037 nodes
, links
= get_nodes_links(context
)
3038 for node
in [n
for n
in nodes
if n
.select
]:
3043 def invoke(self
, context
, event
):
3045 return self
.execute(context
)
3047 return context
.window_manager
.invoke_confirm(self
, event
)
3050 class NWModifyLabels(Operator
, NWBase
):
3051 """Modify Labels of all selected nodes"""
3052 bl_idname
= "node.nw_modify_labels"
3053 bl_label
= "Modify Labels"
3054 bl_options
= {'REGISTER', 'UNDO'}
3056 prepend
: StringProperty(
3057 name
="Add to Beginning"
3059 append
: StringProperty(
3062 replace_from
: StringProperty(
3063 name
="Text to Replace"
3065 replace_to
: StringProperty(
3069 def execute(self
, context
):
3070 nodes
, links
= get_nodes_links(context
)
3071 for node
in [n
for n
in nodes
if n
.select
]:
3072 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
3076 def invoke(self
, context
, event
):
3080 return context
.window_manager
.invoke_props_dialog(self
)
3083 class NWAddTextureSetup(Operator
, NWBase
):
3084 bl_idname
= "node.nw_add_texture"
3085 bl_label
= "Texture Setup"
3086 bl_description
= "Add Texture Node Setup to Selected Shaders"
3087 bl_options
= {'REGISTER', 'UNDO'}
3089 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
3092 def poll(cls
, context
):
3094 if nw_check(context
):
3095 space
= context
.space_data
3096 if space
.tree_type
== 'ShaderNodeTree':
3100 def execute(self
, context
):
3101 nodes
, links
= get_nodes_links(context
)
3102 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3103 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
3104 selected_nodes
= [n
for n
in nodes
if n
.select
]
3105 for t_node
in selected_nodes
:
3109 for index
, i
in enumerate(t_node
.inputs
):
3115 locx
= t_node
.location
.x
3116 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
3118 xoffset
= [500, 700]
3120 if t_node
.type in texture_types
+ ['MAPPING']:
3121 xoffset
= [290, 500]
3125 image_type
= 'ShaderNodeTexImage'
3127 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
3128 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
3129 if t_node
.type == 'BACKGROUND':
3130 image_type
= 'ShaderNodeTexEnvironment'
3133 tex
= nodes
.new(image_type
)
3134 tex
.location
= [locx
- 200, locy
+ 112]
3136 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
3138 t_node
.select
= False
3139 if self
.add_mapping
or is_texture
:
3140 if t_node
.type != 'MAPPING':
3141 m
= nodes
.new('ShaderNodeMapping')
3142 m
.location
= [locx
- xoffset
[0], locy
+ 141]
3146 coord
= nodes
.new('ShaderNodeTexCoord')
3147 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
3150 links
.new(m
.outputs
[0], tex
.inputs
[0])
3151 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3154 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
3155 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3157 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
3161 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
3162 bl_idname
= "node.nw_add_textures_for_principled"
3163 bl_label
= "Principled Texture Setup"
3164 bl_description
= "Add Texture Node Setup for Principled BSDF"
3165 bl_options
= {'REGISTER', 'UNDO'}
3167 directory
: StringProperty(
3171 description
='Folder to search in for image files'
3173 files
: CollectionProperty(
3174 type=bpy
.types
.OperatorFileListElement
,
3175 options
={'HIDDEN', 'SKIP_SAVE'}
3178 relative_path
: BoolProperty(
3179 name
='Relative Path',
3180 description
='Set the file path relative to the blend file, when possible',
3189 def draw(self
, context
):
3190 layout
= self
.layout
3191 layout
.alignment
= 'LEFT'
3193 layout
.prop(self
, 'relative_path')
3196 def poll(cls
, context
):
3198 if nw_check(context
):
3199 space
= context
.space_data
3200 if space
.tree_type
== 'ShaderNodeTree':
3204 def execute(self
, context
):
3205 # Check if everything is ok
3206 if not self
.directory
:
3207 self
.report({'INFO'}, 'No Folder Selected')
3208 return {'CANCELLED'}
3209 if not self
.files
[:]:
3210 self
.report({'INFO'}, 'No Files Selected')
3211 return {'CANCELLED'}
3213 nodes
, links
= get_nodes_links(context
)
3214 active_node
= nodes
.active
3215 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
3216 self
.report({'INFO'}, 'Select Principled BSDF')
3217 return {'CANCELLED'}
3220 def split_into__components(fname
):
3221 # Split filename into components
3222 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3224 fname
= path
.splitext(fname
)[0]
3226 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
3227 # Separate CamelCase by space
3228 fname
= re
.sub(r
"([a-z])([A-Z])", r
"\g<1> \g<2>",fname
)
3229 # Replace common separators with SPACE
3230 separators
= ['_', '.', '-', '__', '--', '#']
3231 for sep
in separators
:
3232 fname
= fname
.replace(sep
, ' ')
3234 components
= fname
.split(' ')
3235 components
= [c
.lower() for c
in components
]
3238 # Filter textures names for texturetypes in filenames
3239 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3240 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
3241 normal_abbr
= tags
.normal
.split(' ')
3242 bump_abbr
= tags
.bump
.split(' ')
3243 gloss_abbr
= tags
.gloss
.split(' ')
3244 rough_abbr
= tags
.rough
.split(' ')
3246 ['Displacement', tags
.displacement
.split(' '), None],
3247 ['Base Color', tags
.base_color
.split(' '), None],
3248 ['Subsurface Color', tags
.sss_color
.split(' '), None],
3249 ['Metallic', tags
.metallic
.split(' '), None],
3250 ['Specular', tags
.specular
.split(' '), None],
3251 ['Roughness', rough_abbr
+ gloss_abbr
, None],
3252 ['Normal', normal_abbr
+ bump_abbr
, None],
3253 ['Transmission', tags
.transmission
.split(' '), None],
3254 ['Emission', tags
.emission
.split(' '), None],
3255 ['Alpha', tags
.alpha
.split(' '), None],
3256 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
3259 # Look through texture_types and set value as filename of first matched file
3260 def match_files_to_socket_names():
3261 for sname
in socketnames
:
3262 for file in self
.files
:
3264 filenamecomponents
= split_into__components(fname
)
3265 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
3266 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3271 match_files_to_socket_names()
3272 # Remove socketnames without found files
3273 socketnames
= [s
for s
in socketnames
if s
[2]
3274 and path
.exists(self
.directory
+s
[2])]
3276 self
.report({'INFO'}, 'No matching images found')
3277 print('No matching images found')
3278 return {'CANCELLED'}
3280 # Don't override path earlier as os.path is used to check the absolute path
3281 import_path
= self
.directory
3282 if self
.relative_path
:
3283 if bpy
.data
.filepath
:
3285 import_path
= bpy
.path
.relpath(self
.directory
)
3290 print('\nMatched Textures:')
3295 roughness_node
= None
3296 for i
, sname
in enumerate(socketnames
):
3297 print(i
, sname
[0], sname
[2])
3299 # DISPLACEMENT NODES
3300 if sname
[0] == 'Displacement':
3301 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
3302 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3303 disp_texture
.image
= img
3304 disp_texture
.label
= 'Displacement'
3305 if disp_texture
.image
:
3306 disp_texture
.image
.colorspace_settings
.is_data
= True
3308 # Add displacement offset nodes
3309 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
3310 # Align the Displacement node under the active Principled BSDF node
3311 disp_node
.location
= active_node
.location
+ Vector((100, -700))
3312 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
3314 # TODO Turn on true displacement in the material
3315 # Too complicated for now
3318 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
3320 if not output_node
[0].inputs
[2].is_linked
:
3321 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
3325 # AMBIENT OCCLUSION TEXTURE
3326 if sname
[0] == 'Ambient Occlusion':
3327 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
3328 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3329 ao_texture
.image
= img
3330 ao_texture
.label
= sname
[0]
3331 if ao_texture
.image
:
3332 ao_texture
.image
.colorspace_settings
.is_data
= True
3336 if not active_node
.inputs
[sname
[0]].is_linked
:
3337 # No texture node connected -> add texture node with new image
3338 texture_node
= nodes
.new(type='ShaderNodeTexImage')
3339 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3340 texture_node
.image
= img
3343 if sname
[0] == 'Normal':
3344 # Test if new texture node is normal or bump map
3345 fname_components
= split_into__components(sname
[2])
3346 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
3347 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
3349 # If Normal add normal node in between
3350 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
3351 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
3353 # If Bump add bump node in between
3354 normal_node
= nodes
.new(type='ShaderNodeBump')
3355 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
3357 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
3358 normal_node_texture
= texture_node
3360 elif sname
[0] == 'Roughness':
3361 # Test if glossy or roughness map
3362 fname_components
= split_into__components(sname
[2])
3363 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
3364 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
3367 # If Roughness nothing to to
3368 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3371 # If Gloss Map add invert node
3372 invert_node
= nodes
.new(type='ShaderNodeInvert')
3373 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
3375 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
3376 roughness_node
= texture_node
3379 # This is a simple connection Texture --> Input slot
3380 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3382 # Use non-color for all but 'Base Color' Textures
3383 if not sname
[0] in ['Base Color', 'Emission'] and texture_node
.image
:
3384 texture_node
.image
.colorspace_settings
.is_data
= True
3387 # If already texture connected. add to node list for alignment
3388 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
3390 # This are all connected texture nodes
3391 texture_nodes
.append(texture_node
)
3392 texture_node
.label
= sname
[0]
3395 texture_nodes
.append(disp_texture
)
3398 # We want the ambient occlusion texture to be the top most texture node
3399 texture_nodes
.insert(0, ao_texture
)
3402 for i
, texture_node
in enumerate(texture_nodes
):
3403 offset
= Vector((-550, (i
* -280) + 200))
3404 texture_node
.location
= active_node
.location
+ offset
3407 # Extra alignment if normal node was added
3408 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
3411 # Alignment of invert node if glossy map
3412 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
3414 # Add texture input + mapping
3415 mapping
= nodes
.new(type='ShaderNodeMapping')
3416 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
3417 if len(texture_nodes
) > 1:
3418 # If more than one texture add reroute node in between
3419 reroute
= nodes
.new(type='NodeReroute')
3420 texture_nodes
.append(reroute
)
3421 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
3422 reroute
.location
= tex_coords
+ Vector((-50, -120))
3423 for texture_node
in texture_nodes
:
3424 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
3425 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
3427 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
3429 # Connect texture_coordiantes to mapping node
3430 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
3431 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
3432 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
3434 # Create frame around tex coords and mapping
3435 frame
= nodes
.new(type='NodeFrame')
3436 frame
.label
= 'Mapping'
3437 mapping
.parent
= frame
3438 texture_input
.parent
= frame
3441 # Create frame around texture nodes
3442 frame
= nodes
.new(type='NodeFrame')
3443 frame
.label
= 'Textures'
3444 for tnode
in texture_nodes
:
3445 tnode
.parent
= frame
3449 active_node
.select
= False
3452 force_update(context
)
3456 class NWAddReroutes(Operator
, NWBase
):
3457 """Add Reroute Nodes and link them to outputs of selected nodes"""
3458 bl_idname
= "node.nw_add_reroutes"
3459 bl_label
= "Add Reroutes"
3460 bl_description
= "Add Reroutes to Outputs"
3461 bl_options
= {'REGISTER', 'UNDO'}
3463 option
: EnumProperty(
3466 ('ALL', 'to all', 'Add to all outputs'),
3467 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3468 ('LINKED', 'to linked', 'Add only to linked outputs'),
3472 def execute(self
, context
):
3473 tree_type
= context
.space_data
.node_tree
.type
3474 option
= self
.option
3475 nodes
, links
= get_nodes_links(context
)
3476 # output valid when option is 'all' or when 'loose' output has no links
3478 post_select
= [] # nodes to be selected after execution
3479 # create reroutes and recreate links
3480 for node
in [n
for n
in nodes
if n
.select
]:
3485 # unhide 'REROUTE' nodes to avoid issues with location.y
3486 if node
.type == 'REROUTE':
3488 # When node is hidden - width_hidden not usable.
3489 # Hack needed to calculate real width
3491 bpy
.ops
.node
.select_all(action
='DESELECT')
3492 helper
= nodes
.new('NodeReroute')
3493 helper
.select
= True
3495 # resize node and helper to zero. Then check locations to calculate width
3496 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
3497 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
3498 # restore node location
3499 node
.location
= x
, y
3502 # only helper is selected now
3503 bpy
.ops
.node
.delete()
3504 x
= node
.location
.x
+ width
+ 20.0
3505 if node
.type != 'REROUTE':
3509 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3510 for out_i
, output
in enumerate(node
.outputs
):
3511 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3512 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3513 if node
.type != 'R_LAYERS':
3515 else: # if 'R_LAYERS' check if output represent used render pass
3516 node_scene
= node
.scene
3517 node_layer
= node
.layer
3518 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3519 if output
.name
== 'Alpha':
3522 # check entries in global 'rl_outputs' variable
3523 for rlo
in rl_outputs
:
3524 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3525 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3528 valid
= ((option
== 'ALL') or
3529 (option
== 'LOOSE' and not output
.links
) or
3530 (option
== 'LINKED' and output
.links
))
3531 # Add reroutes only if valid, but offset location in all cases.
3533 n
= nodes
.new('NodeReroute')
3535 for link
in output
.links
:
3536 links
.new(n
.outputs
[0], link
.to_socket
)
3537 links
.new(output
, n
.inputs
[0])
3539 post_select
.append(n
)
3543 # disselect the node so that after execution of script only newly created nodes are selected
3545 # nicer reroutes distribution along y when node.hide
3547 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3548 for reroute
in [r
for r
in nodes
if r
.select
]:
3549 reroute
.location
.y
-= y_translate
3550 for node
in post_select
:
3556 class NWLinkActiveToSelected(Operator
, NWBase
):
3557 """Link active node to selected nodes basing on various criteria"""
3558 bl_idname
= "node.nw_link_active_to_selected"
3559 bl_label
= "Link Active Node to Selected"
3560 bl_options
= {'REGISTER', 'UNDO'}
3562 replace
: BoolProperty()
3563 use_node_name
: BoolProperty()
3564 use_outputs_names
: BoolProperty()
3567 def poll(cls
, context
):
3569 if nw_check(context
):
3570 if context
.active_node
is not None:
3571 if context
.active_node
.select
:
3575 def execute(self
, context
):
3576 nodes
, links
= get_nodes_links(context
)
3577 replace
= self
.replace
3578 use_node_name
= self
.use_node_name
3579 use_outputs_names
= self
.use_outputs_names
3580 active
= nodes
.active
3581 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3582 outputs
= [] # Only usable outputs of active nodes will be stored here.
3583 for out
in active
.outputs
:
3584 if active
.type != 'R_LAYERS':
3587 # 'R_LAYERS' node type needs special handling.
3588 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3589 # Only outputs that represent used passes should be taken into account
3590 # Check if pass represented by output is used.
3591 # global 'rl_outputs' list will be used for that
3592 for rlo
in rl_outputs
:
3593 pass_used
= False # initial value. Will be set to True if pass is used
3594 if out
.name
== 'Alpha':
3595 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3597 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3598 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3599 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3603 doit
= True # Will be changed to False when links successfully added to previous output.
3606 for node
in selected
:
3607 dst_name
= node
.name
# Will be compared with src_name if needed.
3608 # When node has label - use it as dst_name
3610 dst_name
= node
.label
3611 valid
= True # Initial value. Will be changed to False if names don't match.
3612 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
3614 # Set src_name to source node name or label
3615 src_name
= active
.name
3617 src_name
= active
.label
3618 elif use_outputs_names
:
3619 src_name
= (out
.name
, )
3620 for rlo
in rl_outputs
:
3621 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3622 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3623 if dst_name
not in src_name
:
3626 for input in node
.inputs
:
3627 if input.type == out
.type or node
.type == 'REROUTE':
3628 if replace
or not input.is_linked
:
3629 links
.new(out
, input)
3630 if not use_node_name
and not use_outputs_names
:
3637 class NWAlignNodes(Operator
, NWBase
):
3638 '''Align the selected nodes neatly in a row/column'''
3639 bl_idname
= "node.nw_align_nodes"
3640 bl_label
= "Align Nodes"
3641 bl_options
= {'REGISTER', 'UNDO'}
3642 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3644 def execute(self
, context
):
3645 nodes
, links
= get_nodes_links(context
)
3646 margin
= self
.margin
3650 if node
.select
and node
.type != 'FRAME':
3651 selection
.append(node
)
3653 # If no nodes are selected, align all nodes
3657 elif nodes
.active
in selection
:
3658 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3660 # Check if nodes should be laid out horizontally or vertically
3661 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3662 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3663 x_range
= max(x_locs
) - min(x_locs
)
3664 y_range
= max(y_locs
) - min(y_locs
)
3665 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3666 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3667 horizontal
= x_range
> y_range
3669 # Sort selection by location of node mid-point
3671 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3673 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3677 for node
in selection
:
3678 current_margin
= margin
3679 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3682 node
.location
.x
= current_pos
3683 current_pos
+= current_margin
+ node
.dimensions
.x
3684 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3686 node
.location
.y
= current_pos
3687 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3688 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3690 # If active node is selected, center nodes around it
3691 if active_loc
is not None:
3692 active_loc_diff
= active_loc
- nodes
.active
.location
3693 for node
in selection
:
3694 node
.location
+= active_loc_diff
3695 else: # Position nodes centered around where they used to be
3696 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
])
3697 new_mid
= (max(locs
) + min(locs
)) / 2
3698 for node
in selection
:
3700 node
.location
.x
+= (mid_x
- new_mid
)
3702 node
.location
.y
+= (mid_y
- new_mid
)
3707 class NWSelectParentChildren(Operator
, NWBase
):
3708 bl_idname
= "node.nw_select_parent_child"
3709 bl_label
= "Select Parent or Children"
3710 bl_options
= {'REGISTER', 'UNDO'}
3712 option
: EnumProperty(
3715 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3716 ('CHILD', 'Select Children', 'Select members of selected frame'),
3720 def execute(self
, context
):
3721 nodes
, links
= get_nodes_links(context
)
3722 option
= self
.option
3723 selected
= [node
for node
in nodes
if node
.select
]
3724 if option
== 'PARENT':
3725 for sel
in selected
:
3728 parent
.select
= True
3729 else: # option == 'CHILD'
3730 for sel
in selected
:
3731 children
= [node
for node
in nodes
if node
.parent
== sel
]
3732 for kid
in children
:
3738 class NWDetachOutputs(Operator
, NWBase
):
3739 """Detach outputs of selected node leaving inputs linked"""
3740 bl_idname
= "node.nw_detach_outputs"
3741 bl_label
= "Detach Outputs"
3742 bl_options
= {'REGISTER', 'UNDO'}
3744 def execute(self
, context
):
3745 nodes
, links
= get_nodes_links(context
)
3746 selected
= context
.selected_nodes
3747 bpy
.ops
.node
.duplicate_move_keep_inputs()
3748 new_nodes
= context
.selected_nodes
3749 bpy
.ops
.node
.select_all(action
="DESELECT")
3750 for node
in selected
:
3752 bpy
.ops
.node
.delete_reconnect()
3753 for new_node
in new_nodes
:
3754 new_node
.select
= True
3755 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3760 class NWLinkToOutputNode(Operator
):
3761 """Link to Composite node or Material Output node"""
3762 bl_idname
= "node.nw_link_out"
3763 bl_label
= "Connect to Output"
3764 bl_options
= {'REGISTER', 'UNDO'}
3767 def poll(cls
, context
):
3769 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
3770 if context
.active_node
is not None:
3771 for out
in context
.active_node
.outputs
:
3772 if is_visible_socket(out
):
3777 def execute(self
, context
):
3778 nodes
, links
= get_nodes_links(context
)
3779 active
= nodes
.active
3782 tree_type
= context
.space_data
.tree_type
3783 output_types_shaders
= [x
[1] for x
in shaders_output_nodes_props
]
3784 output_types_compo
= ['COMPOSITE']
3785 output_types_blender_mat
= ['OUTPUT']
3786 output_types_textures
= ['OUTPUT']
3787 output_types
= output_types_shaders
+ output_types_compo
+ output_types_blender_mat
3789 if node
.type in output_types
:
3793 bpy
.ops
.node
.select_all(action
="DESELECT")
3794 if tree_type
== 'ShaderNodeTree':
3795 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3796 elif tree_type
== 'CompositorNodeTree':
3797 output_node
= nodes
.new('CompositorNodeComposite')
3798 elif tree_type
== 'TextureNodeTree':
3799 output_node
= nodes
.new('TextureNodeOutput')
3800 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3801 output_node
.location
.y
= active
.location
.y
3802 if (output_node
and active
.outputs
):
3803 for i
, output
in enumerate(active
.outputs
):
3804 if is_visible_socket(output
):
3807 for i
, output
in enumerate(active
.outputs
):
3808 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3813 if tree_type
== 'ShaderNodeTree':
3814 if active
.outputs
[output_index
].name
== 'Volume':
3816 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3818 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3820 force_update(context
) # viewport render does not update
3825 class NWMakeLink(Operator
, NWBase
):
3826 """Make a link from one socket to another"""
3827 bl_idname
= 'node.nw_make_link'
3828 bl_label
= 'Make Link'
3829 bl_options
= {'REGISTER', 'UNDO'}
3830 from_socket
: IntProperty()
3831 to_socket
: IntProperty()
3833 def execute(self
, context
):
3834 nodes
, links
= get_nodes_links(context
)
3836 n1
= nodes
[context
.scene
.NWLazySource
]
3837 n2
= nodes
[context
.scene
.NWLazyTarget
]
3839 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3841 force_update(context
)
3846 class NWCallInputsMenu(Operator
, NWBase
):
3847 """Link from this output"""
3848 bl_idname
= 'node.nw_call_inputs_menu'
3849 bl_label
= 'Make Link'
3850 bl_options
= {'REGISTER', 'UNDO'}
3851 from_socket
: IntProperty()
3853 def execute(self
, context
):
3854 nodes
, links
= get_nodes_links(context
)
3856 context
.scene
.NWSourceSocket
= self
.from_socket
3858 n1
= nodes
[context
.scene
.NWLazySource
]
3859 n2
= nodes
[context
.scene
.NWLazyTarget
]
3860 if len(n2
.inputs
) > 1:
3861 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3862 elif len(n2
.inputs
) == 1:
3863 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3867 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3868 """Add an Image Sequence"""
3869 bl_idname
= 'node.nw_add_sequence'
3870 bl_label
= 'Import Image Sequence'
3871 bl_options
= {'REGISTER', 'UNDO'}
3873 directory
: StringProperty(
3876 filename
: StringProperty(
3879 files
: CollectionProperty(
3880 type=bpy
.types
.OperatorFileListElement
,
3881 options
={'HIDDEN', 'SKIP_SAVE'}
3884 def execute(self
, context
):
3885 nodes
, links
= get_nodes_links(context
)
3886 directory
= self
.directory
3887 filename
= self
.filename
3889 tree
= context
.space_data
.node_tree
3892 # print ("\nDIR:", directory)
3893 # print ("FN:", filename)
3894 # print ("Fs:", list(f.name for f in files), '\n')
3896 if tree
.type == 'SHADER':
3897 node_type
= "ShaderNodeTexImage"
3898 elif tree
.type == 'COMPOSITING':
3899 node_type
= "CompositorNodeImage"
3901 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3902 return {'CANCELLED'}
3904 if not files
[0].name
and not filename
:
3905 self
.report({'ERROR'}, "No file chosen")
3906 return {'CANCELLED'}
3907 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3908 # User has selected multiple files without an active one, or the active one is non-existant
3909 filename
= files
[0].name
3911 if not path
.exists(directory
+filename
):
3912 self
.report({'ERROR'}, filename
+" does not exist!")
3913 return {'CANCELLED'}
3915 without_ext
= '.'.join(filename
.split('.')[:-1])
3917 # if last digit isn't a number, it's not a sequence
3918 if not without_ext
[-1].isdigit():
3919 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3920 return {'CANCELLED'}
3923 extension
= filename
.split('.')[-1]
3924 reverse
= without_ext
[::-1] # reverse string
3927 for char
in reverse
:
3933 without_num
= without_ext
[:count_numbers
*-1]
3935 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3937 num_frames
= len(files
)
3939 nodes_list
= [node
for node
in nodes
]
3941 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3942 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3946 yloc
+= node_mid_pt(node
, 'y')
3947 yloc
= yloc
/len(nodes
)
3952 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3954 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3956 node
.label
= name_with_hashes
3958 img
= bpy
.data
.images
.load(directory
+(without_ext
+'.'+extension
))
3959 img
.source
= 'SEQUENCE'
3960 img
.name
= name_with_hashes
3962 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3963 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
3964 image_user
.frame_duration
= num_frames
3969 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3970 """Add multiple images at once"""
3971 bl_idname
= 'node.nw_add_multiple_images'
3972 bl_label
= 'Open Selected Images'
3973 bl_options
= {'REGISTER', 'UNDO'}
3974 directory
: StringProperty(
3977 files
: CollectionProperty(
3978 type=bpy
.types
.OperatorFileListElement
,
3979 options
={'HIDDEN', 'SKIP_SAVE'}
3982 def execute(self
, context
):
3983 nodes
, links
= get_nodes_links(context
)
3985 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3987 if context
.space_data
.node_tree
.type == 'SHADER':
3988 node_type
= "ShaderNodeTexImage"
3989 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3990 node_type
= "CompositorNodeImage"
3992 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3993 return {'CANCELLED'}
3996 for f
in self
.files
:
3999 node
= nodes
.new(node_type
)
4000 new_nodes
.append(node
)
4003 node
.width_hidden
= 100
4004 node
.location
.x
= xloc
4005 node
.location
.y
= yloc
4008 img
= bpy
.data
.images
.load(self
.directory
+fname
)
4011 # shift new nodes up to center of tree
4012 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
4014 if node
in new_nodes
:
4016 node
.location
.y
+= (list_size
/2)
4022 class NWViewerFocus(bpy
.types
.Operator
):
4023 """Set the viewer tile center to the mouse position"""
4024 bl_idname
= "node.nw_viewer_focus"
4025 bl_label
= "Viewer Focus"
4027 x
: bpy
.props
.IntProperty()
4028 y
: bpy
.props
.IntProperty()
4031 def poll(cls
, context
):
4032 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
4034 def execute(self
, context
):
4037 def invoke(self
, context
, event
):
4038 render
= context
.scene
.render
4039 space
= context
.space_data
4040 percent
= render
.resolution_percentage
*0.01
4042 nodes
, links
= get_nodes_links(context
)
4043 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
4046 mlocx
= event
.mouse_region_x
4047 mlocy
= event
.mouse_region_y
4048 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
4050 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
4051 region_x
= context
.region
.width
4052 region_y
= context
.region
.height
4054 region_center_x
= context
.region
.width
/ 2
4055 region_center_y
= context
.region
.height
/ 2
4057 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
4058 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
4060 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
4061 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
4063 margin_x
= region_center_x
- backdrop_center_x
4064 margin_y
= region_center_y
- backdrop_center_y
4066 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
4067 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
4069 for node
in viewers
:
4070 node
.center_x
= abs_mouse_x
4071 node
.center_y
= abs_mouse_y
4073 return {'PASS_THROUGH'}
4075 return self
.execute(context
)
4078 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
4079 """Save the current viewer node to an image file"""
4080 bl_idname
= "node.nw_save_viewer"
4081 bl_label
= "Save This Image"
4082 filepath
: StringProperty(subtype
="FILE_PATH")
4083 filename_ext
: EnumProperty(
4085 description
="Choose the file format to save to",
4086 items
=(('.bmp', "BMP", ""),
4087 ('.rgb', 'IRIS', ""),
4088 ('.png', 'PNG', ""),
4089 ('.jpg', 'JPEG', ""),
4090 ('.jp2', 'JPEG2000', ""),
4091 ('.tga', 'TARGA', ""),
4092 ('.cin', 'CINEON', ""),
4093 ('.dpx', 'DPX', ""),
4094 ('.exr', 'OPEN_EXR', ""),
4095 ('.hdr', 'HDR', ""),
4096 ('.tif', 'TIFF', "")),
4101 def poll(cls
, context
):
4103 if nw_check(context
):
4104 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4105 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
4106 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
4110 def execute(self
, context
):
4127 basename
, ext
= path
.splitext(fp
)
4128 old_render_format
= context
.scene
.render
.image_settings
.file_format
4129 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
4130 context
.area
.type = "IMAGE_EDITOR"
4131 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
4132 context
.area
.spaces
[0].image
.save_render(fp
)
4133 context
.area
.type = "NODE_EDITOR"
4134 context
.scene
.render
.image_settings
.file_format
= old_render_format
4138 class NWResetNodes(bpy
.types
.Operator
):
4139 """Reset Nodes in Selection"""
4140 bl_idname
= "node.nw_reset_nodes"
4141 bl_label
= "Reset Nodes"
4142 bl_options
= {'REGISTER', 'UNDO'}
4145 def poll(cls
, context
):
4146 space
= context
.space_data
4147 return space
.type == 'NODE_EDITOR'
4149 def execute(self
, context
):
4150 node_active
= context
.active_node
4151 node_selected
= context
.selected_nodes
4152 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4154 # Check if one node is selected at least
4155 if not (len(node_selected
) > 0):
4156 self
.report({'ERROR'}, "1 node must be selected at least")
4157 return {'CANCELLED'}
4159 active_node_name
= node_active
.name
if node_active
.select
else None
4160 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
4162 # Create output lists
4163 selected_node_names
= [n
.name
for n
in node_selected
]
4166 # Reset all valid children in a frame
4167 node_active_is_frame
= False
4168 if len(node_selected
) == 1 and node_active
.type == "FRAME":
4169 node_tree
= node_active
.id_data
4170 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
4172 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
4173 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
4174 node_active_is_frame
= True
4176 # Check if valid nodes in selection
4177 if not (len(valid_nodes
) > 0):
4178 # Check for frames only
4179 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
4180 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
4181 self
.report({'ERROR'}, "Please select only 1 frame to reset")
4183 self
.report({'ERROR'}, "No valid node(s) in selection")
4184 return {'CANCELLED'}
4186 # Report nodes that are not valid
4187 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
4188 valid_node_names
= [n
.name
for n
in valid_nodes
]
4189 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
4190 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
4192 # Deselect all nodes
4193 for i
in node_selected
:
4196 # Run through all valid nodes
4197 for node
in valid_nodes
:
4199 parent
= node
.parent
if node
.parent
else None
4200 node_loc
= [node
.location
.x
, node
.location
.y
]
4202 node_tree
= node
.id_data
4203 props_to_copy
= 'bl_idname name location height width'.split(' ')
4206 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
4207 for i
in (i
for i
in mappings
if i
.is_linked
):
4209 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
4211 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
4213 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
4214 props_to_copy
.pop(0)
4216 for prop
in props_to_copy
:
4217 setattr(new_node
, prop
, props
[prop
])
4219 nodes
= node_tree
.nodes
4221 new_node
.name
= props
['name']
4224 new_node
.parent
= parent
4225 new_node
.location
= node_loc
4227 for str_from
, str_to
in reconnections
:
4228 node_tree
.links
.new(eval(str_from
), eval(str_to
))
4230 new_node
.select
= False
4231 success_names
.append(new_node
.name
)
4233 # Reselect all nodes
4234 if selected_node_names
and node_active_is_frame
is False:
4235 for i
in selected_node_names
:
4236 node_tree
.nodes
[i
].select
= True
4238 if active_node_name
is not None:
4239 node_tree
.nodes
[active_node_name
].select
= True
4240 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
4242 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
4250 def drawlayout(context
, layout
, mode
='non-panel'):
4251 tree_type
= context
.space_data
.tree_type
4253 col
= layout
.column(align
=True)
4254 col
.menu(NWMergeNodesMenu
.bl_idname
)
4257 col
= layout
.column(align
=True)
4258 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
4261 if tree_type
== 'ShaderNodeTree':
4262 col
= layout
.column(align
=True)
4263 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
4264 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
4267 col
= layout
.column(align
=True)
4268 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
4269 col
.operator(NWSwapLinks
.bl_idname
)
4270 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
4273 col
= layout
.column(align
=True)
4274 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
4275 if tree_type
!= 'GeometryNodeTree':
4276 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
4279 col
= layout
.column(align
=True)
4281 row
= col
.row(align
=True)
4282 row
.operator(NWClearLabel
.bl_idname
).option
= True
4283 row
.operator(NWModifyLabels
.bl_idname
)
4285 col
.operator(NWClearLabel
.bl_idname
).option
= True
4286 col
.operator(NWModifyLabels
.bl_idname
)
4287 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
4289 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
4292 col
= layout
.column(align
=True)
4293 if tree_type
== 'CompositorNodeTree':
4294 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
4295 if tree_type
!= 'GeometryNodeTree':
4296 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
4299 col
= layout
.column(align
=True)
4300 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
4303 col
= layout
.column(align
=True)
4304 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
4307 col
= layout
.column(align
=True)
4308 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
4312 class NodeWranglerPanel(Panel
, NWBase
):
4313 bl_idname
= "NODE_PT_nw_node_wrangler"
4314 bl_space_type
= 'NODE_EDITOR'
4315 bl_label
= "Node Wrangler"
4316 bl_region_type
= "UI"
4317 bl_category
= "Node Wrangler"
4319 prepend
: StringProperty(
4322 append
: StringProperty()
4323 remove
: StringProperty()
4325 def draw(self
, context
):
4326 self
.layout
.label(text
="(Quick access: Shift+W)")
4327 drawlayout(context
, self
.layout
, mode
='panel')
4333 class NodeWranglerMenu(Menu
, NWBase
):
4334 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
4335 bl_label
= "Node Wrangler"
4337 def draw(self
, context
):
4338 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
4339 drawlayout(context
, self
.layout
)
4342 class NWMergeNodesMenu(Menu
, NWBase
):
4343 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
4344 bl_label
= "Merge Selected Nodes"
4346 def draw(self
, context
):
4347 type = context
.space_data
.tree_type
4348 layout
= self
.layout
4349 if type == 'ShaderNodeTree':
4350 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
4351 if type == 'GeometryNodeTree':
4352 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
4353 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4355 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
4356 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4357 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
4359 props
.merge_type
= 'ZCOMBINE'
4360 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
4362 props
.merge_type
= 'ALPHAOVER'
4364 class NWMergeGeometryMenu(Menu
, NWBase
):
4365 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
4366 bl_label
= "Merge Selected Nodes using Geometry Nodes"
4367 def draw(self
, context
):
4368 layout
= self
.layout
4369 # The boolean node + Join Geometry node
4370 for type, name
, description
in geo_combine_operations
:
4371 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4373 props
.merge_type
= 'GEOMETRY'
4375 class NWMergeShadersMenu(Menu
, NWBase
):
4376 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
4377 bl_label
= "Merge Selected Nodes using Shaders"
4379 def draw(self
, context
):
4380 layout
= self
.layout
4381 for type in ('MIX', 'ADD'):
4382 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
4384 props
.merge_type
= 'SHADER'
4387 class NWMergeMixMenu(Menu
, NWBase
):
4388 bl_idname
= "NODE_MT_nw_merge_mix_menu"
4389 bl_label
= "Merge Selected Nodes using Mix"
4391 def draw(self
, context
):
4392 layout
= self
.layout
4393 for type, name
, description
in blend_types
:
4394 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4396 props
.merge_type
= 'MIX'
4399 class NWConnectionListOutputs(Menu
, NWBase
):
4400 bl_idname
= "NODE_MT_nw_connection_list_out"
4403 def draw(self
, context
):
4404 layout
= self
.layout
4405 nodes
, links
= get_nodes_links(context
)
4407 n1
= nodes
[context
.scene
.NWLazySource
]
4409 for o
in n1
.outputs
:
4410 # Only show sockets that are exposed.
4412 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
4416 class NWConnectionListInputs(Menu
, NWBase
):
4417 bl_idname
= "NODE_MT_nw_connection_list_in"
4420 def draw(self
, context
):
4421 layout
= self
.layout
4422 nodes
, links
= get_nodes_links(context
)
4424 n2
= nodes
[context
.scene
.NWLazyTarget
]
4428 # Only show sockets that are exposed.
4429 # This prevents, for example, the scale value socket
4430 # of the vector math node being added to the list when
4431 # the mode is not 'SCALE'.
4433 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
4434 op
.from_socket
= context
.scene
.NWSourceSocket
4435 op
.to_socket
= index
4439 class NWMergeMathMenu(Menu
, NWBase
):
4440 bl_idname
= "NODE_MT_nw_merge_math_menu"
4441 bl_label
= "Merge Selected Nodes using Math"
4443 def draw(self
, context
):
4444 layout
= self
.layout
4445 for type, name
, description
in operations
:
4446 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4448 props
.merge_type
= 'MATH'
4451 class NWBatchChangeNodesMenu(Menu
, NWBase
):
4452 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
4453 bl_label
= "Batch Change Selected Nodes"
4455 def draw(self
, context
):
4456 layout
= self
.layout
4457 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
4458 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
4461 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
4462 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
4463 bl_label
= "Batch Change Blend Type"
4465 def draw(self
, context
):
4466 layout
= self
.layout
4467 for type, name
, description
in blend_types
:
4468 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4469 props
.blend_type
= type
4470 props
.operation
= 'CURRENT'
4473 class NWBatchChangeOperationMenu(Menu
, NWBase
):
4474 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
4475 bl_label
= "Batch Change Math Operation"
4477 def draw(self
, context
):
4478 layout
= self
.layout
4479 for type, name
, description
in operations
:
4480 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4481 props
.blend_type
= 'CURRENT'
4482 props
.operation
= type
4485 class NWCopyToSelectedMenu(Menu
, NWBase
):
4486 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4487 bl_label
= "Copy to Selected"
4489 def draw(self
, context
):
4490 layout
= self
.layout
4491 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4492 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4495 class NWCopyLabelMenu(Menu
, NWBase
):
4496 bl_idname
= "NODE_MT_nw_copy_label_menu"
4497 bl_label
= "Copy Label"
4499 def draw(self
, context
):
4500 layout
= self
.layout
4501 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4502 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4503 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4506 class NWAddReroutesMenu(Menu
, NWBase
):
4507 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4508 bl_label
= "Add Reroutes"
4509 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4511 def draw(self
, context
):
4512 layout
= self
.layout
4513 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4514 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4515 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4518 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4519 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4520 bl_label
= "Link Active to Selected"
4522 def draw(self
, context
):
4523 layout
= self
.layout
4524 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4525 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4526 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4529 class NWLinkStandardMenu(Menu
, NWBase
):
4530 bl_idname
= "NODE_MT_nw_link_standard_menu"
4531 bl_label
= "To All Selected"
4533 def draw(self
, context
):
4534 layout
= self
.layout
4535 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4536 props
.replace
= False
4537 props
.use_node_name
= False
4538 props
.use_outputs_names
= False
4539 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4540 props
.replace
= True
4541 props
.use_node_name
= False
4542 props
.use_outputs_names
= False
4545 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4546 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4547 bl_label
= "Use Node Name/Label"
4549 def draw(self
, context
):
4550 layout
= self
.layout
4551 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4552 props
.replace
= False
4553 props
.use_node_name
= True
4554 props
.use_outputs_names
= False
4555 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4556 props
.replace
= True
4557 props
.use_node_name
= True
4558 props
.use_outputs_names
= False
4561 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4562 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4563 bl_label
= "Use Outputs Names"
4565 def draw(self
, context
):
4566 layout
= self
.layout
4567 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4568 props
.replace
= False
4569 props
.use_node_name
= False
4570 props
.use_outputs_names
= True
4571 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4572 props
.replace
= True
4573 props
.use_node_name
= False
4574 props
.use_outputs_names
= True
4577 class NWVertColMenu(bpy
.types
.Menu
):
4578 bl_idname
= "NODE_MT_nw_node_vertex_color_menu"
4579 bl_label
= "Vertex Colors"
4582 def poll(cls
, context
):
4584 if nw_check(context
):
4585 snode
= context
.space_data
4586 valid
= snode
.tree_type
== 'ShaderNodeTree'
4589 def draw(self
, context
):
4591 nodes
, links
= get_nodes_links(context
)
4592 mat
= context
.object.active_material
4595 for obj
in bpy
.data
.objects
:
4596 for slot
in obj
.material_slots
:
4597 if slot
.material
== mat
:
4601 if obj
.data
.vertex_colors
:
4602 for vcol
in obj
.data
.vertex_colors
:
4603 vcols
.append(vcol
.name
)
4604 vcols
= list(set(vcols
)) # get a unique list
4608 l
.operator(NWAddAttrNode
.bl_idname
, text
=vcol
).attr_name
= vcol
4610 l
.label(text
="No Vertex Color layers on objects with this material")
4613 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4614 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4615 bl_label
= "Switch Type to..."
4617 def draw(self
, context
):
4618 layout
= self
.layout
4619 tree
= context
.space_data
.node_tree
4620 if tree
.type == 'SHADER':
4621 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4622 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4623 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4624 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4625 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4626 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4627 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4628 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4629 if tree
.type == 'COMPOSITING':
4630 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4631 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4632 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4633 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4634 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4635 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4636 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4637 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4638 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4639 if tree
.type == 'TEXTURE':
4640 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4641 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4642 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4643 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4644 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4645 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4646 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4647 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4648 if tree
.type == 'GEOMETRY':
4649 categories
= [c
for c
in node_categories_iter(context
)
4650 if c
.name
not in ['Group', 'Script']]
4651 for cat
in categories
:
4652 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4653 if hasattr(bpy
.types
, idname
):
4656 layout
.label(text
="Unable to load altered node lists.")
4657 layout
.label(text
="Please re-enable Node Wrangler.")
4661 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4662 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4665 def draw(self
, context
):
4666 layout
= self
.layout
4667 for ident
, node_type
, rna_name
in shaders_input_nodes_props
:
4668 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4669 props
.to_type
= ident
4672 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4673 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4676 def draw(self
, context
):
4677 layout
= self
.layout
4678 for ident
, node_type
, rna_name
in shaders_output_nodes_props
:
4679 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4680 props
.to_type
= ident
4683 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4684 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4687 def draw(self
, context
):
4688 layout
= self
.layout
4689 for ident
, node_type
, rna_name
in shaders_shader_nodes_props
:
4690 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4691 props
.to_type
= ident
4694 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4695 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4696 bl_label
= "Texture"
4698 def draw(self
, context
):
4699 layout
= self
.layout
4700 for ident
, node_type
, rna_name
in shaders_texture_nodes_props
:
4701 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4702 props
.to_type
= ident
4705 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4706 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4709 def draw(self
, context
):
4710 layout
= self
.layout
4711 for ident
, node_type
, rna_name
in shaders_color_nodes_props
:
4712 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4713 props
.to_type
= ident
4716 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4717 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4720 def draw(self
, context
):
4721 layout
= self
.layout
4722 for ident
, node_type
, rna_name
in shaders_vector_nodes_props
:
4723 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4724 props
.to_type
= ident
4727 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4728 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4729 bl_label
= "Converter"
4731 def draw(self
, context
):
4732 layout
= self
.layout
4733 for ident
, node_type
, rna_name
in shaders_converter_nodes_props
:
4734 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4735 props
.to_type
= ident
4738 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4739 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4742 def draw(self
, context
):
4743 layout
= self
.layout
4744 for ident
, node_type
, rna_name
in shaders_layout_nodes_props
:
4745 if node_type
!= 'FRAME':
4746 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4747 props
.to_type
= ident
4750 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4751 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4754 def draw(self
, context
):
4755 layout
= self
.layout
4756 for ident
, node_type
, rna_name
in compo_input_nodes_props
:
4757 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4758 props
.to_type
= ident
4761 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4762 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4765 def draw(self
, context
):
4766 layout
= self
.layout
4767 for ident
, node_type
, rna_name
in compo_output_nodes_props
:
4768 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4769 props
.to_type
= ident
4772 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4773 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4776 def draw(self
, context
):
4777 layout
= self
.layout
4778 for ident
, node_type
, rna_name
in compo_color_nodes_props
:
4779 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4780 props
.to_type
= ident
4783 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4784 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4785 bl_label
= "Converter"
4787 def draw(self
, context
):
4788 layout
= self
.layout
4789 for ident
, node_type
, rna_name
in compo_converter_nodes_props
:
4790 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4791 props
.to_type
= ident
4794 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4795 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4798 def draw(self
, context
):
4799 layout
= self
.layout
4800 for ident
, node_type
, rna_name
in compo_filter_nodes_props
:
4801 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4802 props
.to_type
= ident
4805 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4806 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4809 def draw(self
, context
):
4810 layout
= self
.layout
4811 for ident
, node_type
, rna_name
in compo_vector_nodes_props
:
4812 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4813 props
.to_type
= ident
4816 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4817 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4820 def draw(self
, context
):
4821 layout
= self
.layout
4822 for ident
, node_type
, rna_name
in compo_matte_nodes_props
:
4823 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4824 props
.to_type
= ident
4827 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4828 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4829 bl_label
= "Distort"
4831 def draw(self
, context
):
4832 layout
= self
.layout
4833 for ident
, node_type
, rna_name
in compo_distort_nodes_props
:
4834 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4835 props
.to_type
= ident
4838 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4839 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4842 def draw(self
, context
):
4843 layout
= self
.layout
4844 for ident
, node_type
, rna_name
in compo_layout_nodes_props
:
4845 if node_type
!= 'FRAME':
4846 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4847 props
.to_type
= ident
4850 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4851 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4854 def draw(self
, context
):
4855 layout
= self
.layout
4856 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4857 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4858 props
.to_type
= ident
4861 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4862 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4865 def draw(self
, context
):
4866 layout
= self
.layout
4867 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4868 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4869 props
.to_type
= ident
4872 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4873 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4876 def draw(self
, context
):
4877 layout
= self
.layout
4878 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4879 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4880 props
.to_type
= ident
4883 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4884 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4887 def draw(self
, context
):
4888 layout
= self
.layout
4889 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4890 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4891 props
.to_type
= ident
4894 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4895 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4896 bl_label
= "Converter"
4898 def draw(self
, context
):
4899 layout
= self
.layout
4900 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4901 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4902 props
.to_type
= ident
4905 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4906 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4909 def draw(self
, context
):
4910 layout
= self
.layout
4911 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4912 if node_type
!= 'FRAME':
4913 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4914 props
.to_type
= ident
4917 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4918 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4921 def draw(self
, context
):
4922 layout
= self
.layout
4923 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4924 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4925 props
.to_type
= ident
4928 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4929 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4932 def draw(self
, context
):
4933 layout
= self
.layout
4934 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4935 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4936 props
.to_type
= ident
4939 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4940 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4943 def draw(self
, context
):
4944 layout
= self
.layout
4945 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4946 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4947 props
.to_type
= ident
4950 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4951 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4952 bl_label
= "Pattern"
4954 def draw(self
, context
):
4955 layout
= self
.layout
4956 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4957 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4958 props
.to_type
= ident
4961 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4962 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4963 bl_label
= "Textures"
4965 def draw(self
, context
):
4966 layout
= self
.layout
4967 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4968 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4969 props
.to_type
= ident
4972 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4973 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4974 bl_label
= "Converter"
4976 def draw(self
, context
):
4977 layout
= self
.layout
4978 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4979 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4980 props
.to_type
= ident
4983 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4984 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4985 bl_label
= "Distort"
4987 def draw(self
, context
):
4988 layout
= self
.layout
4989 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4990 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4991 props
.to_type
= ident
4994 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4995 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4998 def draw(self
, context
):
4999 layout
= self
.layout
5000 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
5001 if node_type
!= 'FRAME':
5002 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
5003 props
.to_type
= ident
5005 def draw_switch_category_submenu(self
, context
):
5006 layout
= self
.layout
5007 if self
.category
.name
== 'Layout':
5008 for node
in self
.category
.items(context
):
5009 if node
.nodetype
!= 'NodeFrame':
5010 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
5011 props
.to_type
= node
.nodetype
5013 for node
in self
.category
.items(context
):
5014 if isinstance(node
, NodeItemCustom
):
5015 node
.draw(self
, layout
, context
)
5017 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
5018 props
.geo_to_type
= node
.nodetype
5021 # APPENDAGES TO EXISTING UI
5025 def select_parent_children_buttons(self
, context
):
5026 layout
= self
.layout
5027 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
5028 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
5031 def attr_nodes_menu_func(self
, context
):
5032 col
= self
.layout
.column(align
=True)
5033 col
.menu("NODE_MT_nw_node_vertex_color_menu")
5037 def multipleimages_menu_func(self
, context
):
5038 col
= self
.layout
.column(align
=True)
5039 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
5040 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
5044 def bgreset_menu_func(self
, context
):
5045 self
.layout
.operator(NWResetBG
.bl_idname
)
5048 def save_viewer_menu_func(self
, context
):
5049 if nw_check(context
):
5050 if context
.space_data
.tree_type
== 'CompositorNodeTree':
5051 if context
.scene
.node_tree
.nodes
.active
:
5052 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
5053 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
5056 def reset_nodes_button(self
, context
):
5057 node_active
= context
.active_node
5058 node_selected
= context
.selected_nodes
5059 node_ignore
= ["FRAME","REROUTE", "GROUP"]
5061 # Check if active node is in the selection and respective type
5062 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
5063 row
= self
.layout
.row()
5064 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
5065 self
.layout
.separator()
5067 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
5068 row
= self
.layout
.row()
5069 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
5070 self
.layout
.separator()
5074 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5076 switch_category_menus
= []
5078 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5079 # props entry: (property name, property value)
5082 # NWMergeNodes with Ctrl (AUTO).
5083 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
5084 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5085 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
5086 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5087 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5088 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5089 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
5090 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5091 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5092 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5093 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
5094 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5095 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5096 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5097 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
5098 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5099 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5100 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5101 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
5102 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5103 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
5104 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5105 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
5106 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5107 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5108 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5109 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5110 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
5111 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5112 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
5113 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5114 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5115 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5116 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
5117 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5118 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5119 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5120 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
5121 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5122 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5123 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5124 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
5125 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5126 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5127 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5128 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
5129 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5130 # NWMergeNodes with Ctrl Shift (MATH)
5131 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5132 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5133 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
5134 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5135 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5136 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5137 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
5138 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5139 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5140 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5141 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
5142 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5143 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5144 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5145 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
5146 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5147 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
5148 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5149 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
5150 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5151 # BATCH CHANGE NODES
5152 # NWBatchChangeNodes with Alt
5153 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
5154 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5155 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
5156 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5157 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5158 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5159 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
5160 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5161 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5162 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5163 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
5164 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5165 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5166 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5167 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
5168 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5169 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5170 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5171 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
5172 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5173 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
5174 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5175 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
5176 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5177 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
5178 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5179 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
5180 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5181 # LINK ACTIVE TO SELECTED
5182 # Don't use names, don't replace links (K)
5183 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
5184 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5185 # Don't use names, replace links (Shift K)
5186 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
5187 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5188 # Use node name, don't replace links (')
5189 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
5190 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5191 # Use node name, replace links (Shift ')
5192 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
5193 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5194 # Don't use names, don't replace links (;)
5195 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
5196 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5197 # Don't use names, replace links (')
5198 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
5199 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5201 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5202 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5203 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5204 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5205 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5206 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5207 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5208 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5209 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5210 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5211 # CLEAR LABEL (Alt L)
5212 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5213 # MODIFY LABEL (Alt Shift L)
5214 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5215 # Copy Label from active to selected
5216 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5217 # DETACH OUTPUTS (Alt Shift D)
5218 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5219 # LINK TO OUTPUT NODE (O)
5220 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5221 # SELECT PARENT/CHILDREN
5223 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5225 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5227 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5228 # Add Principled BSDF Texture Setup
5229 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5231 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5233 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5235 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5237 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5239 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5240 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5242 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
5244 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5246 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5247 # Lazy Connect with Menu
5248 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5249 # Viewer Tile Center
5250 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5252 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5253 # Reset Nodes (Back Space)
5254 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5256 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
5257 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5258 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5259 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
5260 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
5261 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
5266 NWPrincipledPreferences
,
5286 NWAddPrincipledSetup
,
5288 NWLinkActiveToSelected
,
5290 NWSelectParentChildren
,
5296 NWAddMultipleImages
,
5304 NWMergeGeometryMenu
,
5306 NWConnectionListOutputs
,
5307 NWConnectionListInputs
,
5309 NWBatchChangeNodesMenu
,
5310 NWBatchChangeBlendTypeMenu
,
5311 NWBatchChangeOperationMenu
,
5312 NWCopyToSelectedMenu
,
5315 NWLinkActiveToSelectedMenu
,
5317 NWLinkUseNodeNameMenu
,
5318 NWLinkUseOutputsNamesMenu
,
5320 NWSwitchNodeTypeMenu
,
5321 NWSwitchShadersInputSubmenu
,
5322 NWSwitchShadersOutputSubmenu
,
5323 NWSwitchShadersShaderSubmenu
,
5324 NWSwitchShadersTextureSubmenu
,
5325 NWSwitchShadersColorSubmenu
,
5326 NWSwitchShadersVectorSubmenu
,
5327 NWSwitchShadersConverterSubmenu
,
5328 NWSwitchShadersLayoutSubmenu
,
5329 NWSwitchCompoInputSubmenu
,
5330 NWSwitchCompoOutputSubmenu
,
5331 NWSwitchCompoColorSubmenu
,
5332 NWSwitchCompoConverterSubmenu
,
5333 NWSwitchCompoFilterSubmenu
,
5334 NWSwitchCompoVectorSubmenu
,
5335 NWSwitchCompoMatteSubmenu
,
5336 NWSwitchCompoDistortSubmenu
,
5337 NWSwitchCompoLayoutSubmenu
,
5338 NWSwitchMatInputSubmenu
,
5339 NWSwitchMatOutputSubmenu
,
5340 NWSwitchMatColorSubmenu
,
5341 NWSwitchMatVectorSubmenu
,
5342 NWSwitchMatConverterSubmenu
,
5343 NWSwitchMatLayoutSubmenu
,
5344 NWSwitchTexInputSubmenu
,
5345 NWSwitchTexOutputSubmenu
,
5346 NWSwitchTexColorSubmenu
,
5347 NWSwitchTexPatternSubmenu
,
5348 NWSwitchTexTexturesSubmenu
,
5349 NWSwitchTexConverterSubmenu
,
5350 NWSwitchTexDistortSubmenu
,
5351 NWSwitchTexLayoutSubmenu
,
5355 from bpy
.utils
import register_class
5358 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
5359 name
="Busy Drawing!",
5361 description
="An internal property used to store only the first mouse position")
5362 bpy
.types
.Scene
.NWLazySource
= StringProperty(
5363 name
="Lazy Source!",
5365 description
="An internal property used to store the first node in a Lazy Connect operation")
5366 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
5367 name
="Lazy Target!",
5369 description
="An internal property used to store the last node in a Lazy Connect operation")
5370 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
5371 name
="Source Socket!",
5373 description
="An internal property used to store the source socket in a Lazy Connect operation")
5374 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
5377 description
="An internal property used to determine if a socket is generated by the addon"
5384 addon_keymaps
.clear()
5385 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
5387 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
5388 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
5389 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
5391 for prop
, value
in props
:
5392 setattr(kmi
.properties
, prop
, value
)
5393 addon_keymaps
.append((km
, kmi
))
5396 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
5397 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
5398 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
5399 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
5400 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
5401 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
5402 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
5403 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
5406 switch_category_menus
.clear()
5407 for cat
in node_categories_iter(None):
5408 if cat
.name
not in ['Group', 'Script'] and cat
.identifier
.startswith('GEO'):
5409 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
5410 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
5411 "bl_space_type": 'NODE_EDITOR',
5412 "bl_label": cat
.name
,
5415 "draw": draw_switch_category_submenu
,
5418 switch_category_menus
.append(switch_category_type
)
5420 bpy
.utils
.register_class(switch_category_type
)
5424 from bpy
.utils
import unregister_class
5427 del bpy
.types
.Scene
.NWBusyDrawing
5428 del bpy
.types
.Scene
.NWLazySource
5429 del bpy
.types
.Scene
.NWLazyTarget
5430 del bpy
.types
.Scene
.NWSourceSocket
5431 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
5433 for cat_types
in switch_category_menus
:
5434 bpy
.utils
.unregister_class(cat_types
)
5435 switch_category_menus
.clear()
5438 for km
, kmi
in addon_keymaps
:
5439 km
.keymap_items
.remove(kmi
)
5440 addon_keymaps
.clear()
5443 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
5444 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
5445 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
5446 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
5447 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
5448 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
5449 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
5450 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
5453 unregister_class(cls
)
5455 if __name__
== "__main__":