1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
17 from bpy
.types
import Operator
, Panel
, Menu
18 from bpy
.props
import (
27 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
28 from gpu_extras
.batch
import batch_for_shader
29 from mathutils
import Vector
30 from nodeitems_utils
import node_categories_iter
, NodeItemCustom
31 from math
import cos
, sin
, pi
, hypot
35 from itertools
import chain
37 from collections
import namedtuple
41 # list of outputs of Input Render Layer
42 # with attributes determining if pass is used,
43 # and MultiLayer EXR outputs names and corresponding render engines
45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
46 RL_entry
= namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
48 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
49 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
50 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
51 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
52 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
53 RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
54 RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
55 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
56 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
57 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
58 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
59 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
60 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
61 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
62 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
63 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
64 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
65 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
66 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
67 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
68 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
69 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
70 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
71 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
72 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
76 # (rna_type.identifier, type, rna_type.name)
77 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
78 # Keeping things in alphabetical order so we don't need to sort later.
79 shaders_input_nodes_props
= (
80 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
81 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
82 ('ShaderNodeBevel', 'BEVEL', 'Bevel'),
83 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
84 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
85 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
86 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
87 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
88 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
89 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
90 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
91 ('ShaderNodeRGB', 'RGB', 'RGB'),
92 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
93 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
94 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
95 ('ShaderNodeValue', 'VALUE', 'Value'),
96 ('ShaderNodeVertexColor', 'VERTEX_COLOR', 'Vertex Color'),
97 ('ShaderNodeVolumeInfo', 'VOLUME_INFO', 'Volume Info'),
98 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
101 # (rna_type.identifier, type, rna_type.name)
102 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
103 # Keeping things in alphabetical order so we don't need to sort later.
104 shaders_output_nodes_props
= (
105 ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'),
106 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
107 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
108 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
110 # (rna_type.identifier, type, rna_type.name)
111 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
112 # Keeping things in alphabetical order so we don't need to sort later.
113 shaders_shader_nodes_props
= (
114 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
115 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
116 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
117 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
118 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
119 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
120 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
121 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
122 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
123 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
124 ('ShaderNodeBsdfHairPrincipled', 'BSDF_HAIR_PRINCIPLED', 'Principled Hair BSDF'),
125 ('ShaderNodeVolumePrincipled', 'PRINCIPLED_VOLUME', 'Principled Volume'),
126 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
127 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
128 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
129 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
130 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
131 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
132 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
133 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
134 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
136 # (rna_type.identifier, type, rna_type.name)
137 # Keeping things in alphabetical order so we don't need to sort later.
138 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
139 shaders_texture_nodes_props
= (
140 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
141 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
142 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
143 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
144 ('ShaderNodeTexIES', 'TEX_IES', 'IES Texture'),
145 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
146 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
147 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
148 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
149 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
150 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
151 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
152 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
153 ('ShaderNodeTexWhiteNoise', 'TEX_WHITE_NOISE', 'White Noise'),
155 # (rna_type.identifier, type, rna_type.name)
156 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
157 # Keeping things in alphabetical order so we don't need to sort later.
158 shaders_color_nodes_props
= (
159 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
160 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
161 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue Saturation Value'),
162 ('ShaderNodeInvert', 'INVERT', 'Invert'),
163 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
164 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
165 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
167 # (rna_type.identifier, type, rna_type.name)
168 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
169 # Keeping things in alphabetical order so we don't need to sort later.
170 shaders_vector_nodes_props
= (
171 ('ShaderNodeBump', 'BUMP', 'Bump'),
172 ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'),
173 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
174 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
175 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
176 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
177 ('ShaderNodeVectorDisplacement', 'VECTOR_DISPLACEMENT', 'Vector Displacement'),
178 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
180 # (rna_type.identifier, type, rna_type.name)
181 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
182 # Keeping things in alphabetical order so we don't need to sort later.
183 shaders_converter_nodes_props
= (
184 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
185 ('ShaderNodeClamp', 'CLAMP', 'Clamp'),
186 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
187 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
188 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
189 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
190 ('ShaderNodeMapRange', 'MAP_RANGE', 'Map Range'),
191 ('ShaderNodeMath', 'MATH', 'Math'),
192 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
193 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
194 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
195 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
196 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
197 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
199 # (rna_type.identifier, type, rna_type.name)
200 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
201 # Keeping things in alphabetical order so we don't need to sort later.
202 shaders_layout_nodes_props
= (
203 ('NodeFrame', 'FRAME', 'Frame'),
204 ('NodeReroute', 'REROUTE', 'Reroute'),
208 # (rna_type.identifier, type, rna_type.name)
209 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
210 # Keeping things in alphabetical order so we don't need to sort later.
211 compo_input_nodes_props
= (
212 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
213 ('CompositorNodeImage', 'IMAGE', 'Image'),
214 ('CompositorNodeMask', 'MASK', 'Mask'),
215 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
216 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
217 ('CompositorNodeRGB', 'RGB', 'RGB'),
218 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
219 ('CompositorNodeTime', 'TIME', 'Time'),
220 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
221 ('CompositorNodeValue', 'VALUE', 'Value'),
223 # (rna_type.identifier, type, rna_type.name)
224 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
225 # Keeping things in alphabetical order so we don't need to sort later.
226 compo_output_nodes_props
= (
227 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
228 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
229 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
230 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
231 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
233 # (rna_type.identifier, type, rna_type.name)
234 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
235 # Keeping things in alphabetical order so we don't need to sort later.
236 compo_color_nodes_props
= (
237 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
238 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
239 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
240 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
241 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
242 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
243 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
244 ('CompositorNodeInvert', 'INVERT', 'Invert'),
245 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
246 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
247 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
248 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
250 # (rna_type.identifier, type, rna_type.name)
251 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
252 # Keeping things in alphabetical order so we don't need to sort later.
253 compo_converter_nodes_props
= (
254 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
255 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
256 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
257 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
258 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
259 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
260 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
261 ('CompositorNodeMath', 'MATH', 'Math'),
262 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
263 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
264 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
265 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
266 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
267 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
268 ('CompositorNodeSwitchView', 'VIEWSWITCH', 'View Switch'),
270 # (rna_type.identifier, type, rna_type.name)
271 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
272 # Keeping things in alphabetical order so we don't need to sort later.
273 compo_filter_nodes_props
= (
274 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
275 ('CompositorNodeBlur', 'BLUR', 'Blur'),
276 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
277 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
278 ('CompositorNodeDenoise', 'DENOISE', 'Denoise'),
279 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
280 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
281 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
282 ('CompositorNodeFilter', 'FILTER', 'Filter'),
283 ('CompositorNodeGlare', 'GLARE', 'Glare'),
284 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
285 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
286 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
287 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
289 # (rna_type.identifier, type, rna_type.name)
290 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
291 # Keeping things in alphabetical order so we don't need to sort later.
292 compo_vector_nodes_props
= (
293 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
294 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
295 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
296 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
297 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
299 # (rna_type.identifier, type, rna_type.name)
300 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
301 # Keeping things in alphabetical order so we don't need to sort later.
302 compo_matte_nodes_props
= (
303 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
304 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
305 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
306 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
307 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
308 ('CompositorNodeCryptomatte', 'CRYPTOMATTE', 'Cryptomatte'),
309 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
310 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
311 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
312 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
313 ('CompositorNodeKeying', 'KEYING', 'Keying'),
314 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
315 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
317 # (rna_type.identifier, type, rna_type.name)
318 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
319 # Keeping things in alphabetical order so we don't need to sort later.
320 compo_distort_nodes_props
= (
321 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
322 ('CompositorNodeCrop', 'CROP', 'Crop'),
323 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
324 ('CompositorNodeFlip', 'FLIP', 'Flip'),
325 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
326 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
327 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
328 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
329 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
330 ('CompositorNodeScale', 'SCALE', 'Scale'),
331 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
332 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
333 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
335 # (rna_type.identifier, type, rna_type.name)
336 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
337 # Keeping things in alphabetical order so we don't need to sort later.
338 compo_layout_nodes_props
= (
339 ('NodeFrame', 'FRAME', 'Frame'),
340 ('NodeReroute', 'REROUTE', 'Reroute'),
341 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
343 # Blender Render material nodes
344 # (rna_type.identifier, type, rna_type.name)
345 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
346 blender_mat_input_nodes_props
= (
347 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
348 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
349 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
350 ('ShaderNodeValue', 'VALUE', 'Value'),
351 ('ShaderNodeRGB', 'RGB', 'RGB'),
352 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
353 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
354 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
357 # (rna_type.identifier, type, rna_type.name)
358 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
359 blender_mat_output_nodes_props
= (
360 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
363 # (rna_type.identifier, type, rna_type.name)
364 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
365 blender_mat_color_nodes_props
= (
366 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
367 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
368 ('ShaderNodeInvert', 'INVERT', 'Invert'),
369 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue Saturation Value'),
372 # (rna_type.identifier, type, rna_type.name)
373 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
374 blender_mat_vector_nodes_props
= (
375 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
376 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
377 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
380 # (rna_type.identifier, type, rna_type.name)
381 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
382 blender_mat_converter_nodes_props
= (
383 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
384 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
385 ('ShaderNodeMath', 'MATH', 'Math'),
386 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
387 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
388 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
389 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
390 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
391 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
394 # (rna_type.identifier, type, rna_type.name)
395 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
396 blender_mat_layout_nodes_props
= (
397 ('NodeReroute', 'REROUTE', 'Reroute'),
401 # (rna_type.identifier, type, rna_type.name)
402 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
403 texture_input_nodes_props
= (
404 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
405 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
406 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
407 ('TextureNodeImage', 'IMAGE', 'Image'),
410 # (rna_type.identifier, type, rna_type.name)
411 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
412 texture_output_nodes_props
= (
413 ('TextureNodeOutput', 'OUTPUT', 'Output'),
414 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
417 # (rna_type.identifier, type, rna_type.name)
418 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
419 texture_color_nodes_props
= (
420 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
421 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
422 ('TextureNodeInvert', 'INVERT', 'Invert'),
423 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue Saturation Value'),
424 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
425 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
428 # (rna_type.identifier, type, rna_type.name)
429 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
430 texture_pattern_nodes_props
= (
431 ('TextureNodeChecker', 'CHECKER', 'Checker'),
432 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
435 # (rna_type.identifier, type, rna_type.name)
436 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
437 texture_textures_nodes_props
= (
438 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
439 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
440 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
441 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
442 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
443 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
444 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
445 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
446 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
447 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
450 # (rna_type.identifier, type, rna_type.name)
451 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
452 texture_converter_nodes_props
= (
453 ('TextureNodeMath', 'MATH', 'Math'),
454 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
455 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
456 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
457 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
460 # (rna_type.identifier, type, rna_type.name)
461 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
462 texture_distort_nodes_props
= (
463 ('TextureNodeScale', 'SCALE', 'Scale'),
464 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
465 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
466 ('TextureNodeAt', 'AT', 'At'),
469 # (rna_type.identifier, type, rna_type.name)
470 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
471 texture_layout_nodes_props
= (
472 ('NodeReroute', 'REROUTE', 'Reroute'),
475 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
476 # used list, not tuple for easy merging with other lists.
478 ('MIX', 'Mix', 'Mix Mode'),
479 ('ADD', 'Add', 'Add Mode'),
480 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
481 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
482 ('SCREEN', 'Screen', 'Screen Mode'),
483 ('DIVIDE', 'Divide', 'Divide Mode'),
484 ('DIFFERENCE', 'Difference', 'Difference Mode'),
485 ('DARKEN', 'Darken', 'Darken Mode'),
486 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
487 ('OVERLAY', 'Overlay', 'Overlay Mode'),
488 ('DODGE', 'Dodge', 'Dodge Mode'),
489 ('BURN', 'Burn', 'Burn Mode'),
490 ('HUE', 'Hue', 'Hue Mode'),
491 ('SATURATION', 'Saturation', 'Saturation Mode'),
492 ('VALUE', 'Value', 'Value Mode'),
493 ('COLOR', 'Color', 'Color Mode'),
494 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
495 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
498 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
499 # used list, not tuple for easy merging with other lists.
501 ('ADD', 'Add', 'Add Mode'),
502 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
503 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
504 ('DIVIDE', 'Divide', 'Divide Mode'),
505 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
506 ('SINE', 'Sine', 'Sine Mode'),
507 ('COSINE', 'Cosine', 'Cosine Mode'),
508 ('TANGENT', 'Tangent', 'Tangent Mode'),
509 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
510 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
511 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
512 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
513 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
514 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
515 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
516 ('POWER', 'Power', 'Power Mode'),
517 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
518 ('SQRT', 'Square Root', 'Square Root Mode'),
519 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
520 ('EXPONENT', 'Exponent', 'Exponent Mode'),
521 ('MINIMUM', 'Minimum', 'Minimum Mode'),
522 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
523 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
524 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
525 ('SIGN', 'Sign', 'Sign Mode'),
526 ('COMPARE', 'Compare', 'Compare Mode'),
527 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
528 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
529 ('FRACT', 'Fraction', 'Fraction Mode'),
530 ('MODULO', 'Modulo', 'Modulo Mode'),
531 ('SNAP', 'Snap', 'Snap Mode'),
532 ('WRAP', 'Wrap', 'Wrap Mode'),
533 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
534 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
535 ('ROUND', 'Round', 'Round Mode'),
536 ('FLOOR', 'Floor', 'Floor Mode'),
537 ('CEIL', 'Ceil', 'Ceil Mode'),
538 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
539 ('RADIANS', 'To Radians', 'To Radians Mode'),
540 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
543 # Operations used by the geometry boolean node and join geometry node
544 geo_combine_operations
= [
545 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
546 ('INTERSECT', 'Intersect', 'Intersect Mode'),
547 ('UNION', 'Union', 'Union Mode'),
548 ('DIFFERENCE', 'Difference', 'Difference Mode'),
551 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
552 # used list, not tuple for easy merging with other lists.
554 ('CURRENT', 'Current', 'Leave at current state'),
555 ('NEXT', 'Next', 'Next blend type/operation'),
556 ('PREV', 'Prev', 'Previous blend type/operation'),
561 (1.0, 1.0, 1.0, 0.7),
562 (1.0, 0.0, 0.0, 0.7),
566 (0.0, 0.0, 0.0, 1.0),
567 (0.38, 0.77, 0.38, 1.0),
568 (0.38, 0.77, 0.38, 1.0)
571 (0.0, 0.0, 0.0, 1.0),
572 (0.77, 0.77, 0.16, 1.0),
573 (0.77, 0.77, 0.16, 1.0)
576 (0.0, 0.0, 0.0, 1.0),
577 (0.38, 0.38, 0.77, 1.0),
578 (0.38, 0.38, 0.77, 1.0)
581 (0.0, 0.0, 0.0, 1.0),
582 (0.63, 0.63, 0.63, 1.0),
583 (0.63, 0.63, 0.63, 1.0)
586 (1.0, 1.0, 1.0, 0.7),
587 (0.0, 0.0, 0.0, 0.7),
592 viewer_socket_name
= "tmp_viewer"
594 def get_nodes_from_category(category_name
, context
):
595 for category
in node_categories_iter(context
):
596 if category
.name
== category_name
:
597 return sorted(category
.items(context
), key
=lambda node
: node
.label
)
599 def is_visible_socket(socket
):
600 return not socket
.hide
and socket
.enabled
and socket
.type != 'CUSTOM'
602 def nice_hotkey_name(punc
):
603 # convert the ugly string name into the actual character
606 'MIDDLEMOUSE': "MMB",
608 'WHEELUPMOUSE': "Wheel Up",
609 'WHEELDOWNMOUSE': "Wheel Down",
610 'WHEELINMOUSE': "Wheel In",
611 'WHEELOUTMOUSE': "Wheel Out",
624 'LINE_FEED': "Enter",
633 'NUMPAD_1': "Numpad 1",
634 'NUMPAD_2': "Numpad 2",
635 'NUMPAD_3': "Numpad 3",
636 'NUMPAD_4': "Numpad 4",
637 'NUMPAD_5': "Numpad 5",
638 'NUMPAD_6': "Numpad 6",
639 'NUMPAD_7': "Numpad 7",
640 'NUMPAD_8': "Numpad 8",
641 'NUMPAD_9': "Numpad 9",
642 'NUMPAD_0': "Numpad 0",
643 'NUMPAD_PERIOD': "Numpad .",
644 'NUMPAD_SLASH': "Numpad /",
645 'NUMPAD_ASTERIX': "Numpad *",
646 'NUMPAD_MINUS': "Numpad -",
647 'NUMPAD_ENTER': "Numpad Enter",
648 'NUMPAD_PLUS': "Numpad +",
651 return nice_name
[punc
]
653 return punc
.replace("_", " ").title()
656 def force_update(context
):
657 context
.space_data
.node_tree
.update_tag()
661 prefs
= bpy
.context
.preferences
.system
662 return prefs
.dpi
* prefs
.pixel_size
/ 72
665 def node_mid_pt(node
, axis
):
667 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
669 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
675 def autolink(node1
, node2
, links
):
677 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
678 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
679 for outp
in available_outputs
:
680 for inp
in available_inputs
:
681 if not inp
.is_linked
and inp
.name
== outp
.name
:
686 for outp
in available_outputs
:
687 for inp
in available_inputs
:
688 if not inp
.is_linked
and inp
.type == outp
.type:
693 # force some connection even if the type doesn't match
694 if available_outputs
:
695 for inp
in available_inputs
:
696 if not inp
.is_linked
:
698 links
.new(available_outputs
[0], inp
)
701 # even if no sockets are open, force one of matching type
702 for outp
in available_outputs
:
703 for inp
in available_inputs
:
704 if inp
.type == outp
.type:
710 for outp
in available_outputs
:
711 for inp
in available_inputs
:
716 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
719 def abs_node_location(node
):
720 abs_location
= node
.location
721 if node
.parent
is None:
723 return abs_location
+ abs_node_location(node
.parent
)
725 def node_at_pos(nodes
, context
, event
):
726 nodes_under_mouse
= []
729 store_mouse_cursor(context
, event
)
730 x
, y
= context
.space_data
.cursor_location
732 # Make a list of each corner (and middle of border) for each node.
733 # Will be sorted to find nearest point and thus nearest node
734 node_points_with_dist
= []
737 if node
.type != 'FRAME': # no point trying to link to a frame node
738 dimx
= node
.dimensions
.x
/dpifac()
739 dimy
= node
.dimensions
.y
/dpifac()
740 locx
, locy
= abs_node_location(node
)
743 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
744 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
745 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
746 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
748 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
749 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
750 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
751 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
753 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
756 if node
.type != 'FRAME' and skipnode
== False:
757 locx
, locy
= abs_node_location(node
)
758 dimx
= node
.dimensions
.x
/dpifac()
759 dimy
= node
.dimensions
.y
/dpifac()
760 if (locx
<= x
<= locx
+ dimx
) and \
761 (locy
- dimy
<= y
<= locy
):
762 nodes_under_mouse
.append(node
)
764 if len(nodes_under_mouse
) == 1:
765 if nodes_under_mouse
[0] != nearest_node
:
766 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
768 target_node
= nearest_node
# else use the nearest node
770 target_node
= nearest_node
774 def store_mouse_cursor(context
, event
):
775 space
= context
.space_data
776 v2d
= context
.region
.view2d
777 tree
= space
.edit_tree
779 # convert mouse position to the View2D for later node placement
780 if context
.region
.type == 'WINDOW':
781 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
783 space
.cursor_location
= tree
.view_center
785 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
786 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
788 vertices
= ((x1
, y1
), (x2
, y2
))
789 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
790 colour
[1]+(1.0-colour
[1])/4,
791 colour
[2]+(1.0-colour
[2])/4,
792 colour
[3]+(1.0-colour
[3])/4),
795 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
796 bgl
.glLineWidth(size
* dpifac())
802 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
803 radius
= radius
* dpifac()
805 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
806 radius
* sin(i
* 2 * pi
/ sides
) + my
)
807 for i
in range(sides
+ 1)]
809 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
811 shader
.uniform_float("color", colour
)
815 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
816 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
817 bottom_bar
= (16*dpifac()) + 1
819 radius
= radius
*dpifac()
821 nlocx
, nlocy
= abs_node_location(node
)
823 nlocx
= (nlocx
+1)*dpifac()
824 nlocy
= (nlocy
+1)*dpifac()
825 ndimx
= node
.dimensions
.x
826 ndimy
= node
.dimensions
.y
831 if node
.type == 'REROUTE':
839 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
841 for i
in range(sides
+1):
843 if my
> bottom_bar
and mx
< area_width
:
844 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
845 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
846 vertices
.append((cosine
,sine
))
847 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
849 shader
.uniform_float("color", colour
)
853 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
855 for i
in range(sides
+1):
857 if my
> bottom_bar
and mx
< area_width
:
858 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
859 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
860 vertices
.append((cosine
,sine
))
861 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
863 shader
.uniform_float("color", colour
)
867 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
869 for i
in range(sides
+1):
871 if my
> bottom_bar
and mx
< area_width
:
872 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
873 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
874 vertices
.append((cosine
,sine
))
875 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
877 shader
.uniform_float("color", colour
)
880 # Bottom right corner
881 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
883 for i
in range(sides
+1):
885 if my
> bottom_bar
and mx
< area_width
:
886 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
887 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
888 vertices
.append((cosine
,sine
))
889 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
891 shader
.uniform_float("color", colour
)
894 # prepare drawing all edges in one batch
900 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
901 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
902 if m1x
< area_width
and m2x
< area_width
:
903 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
904 (m1x
,m1y
), (m1x
-radius
,m1y
)])
905 indices
.extend([(id_last
, id_last
+1, id_last
+3),
906 (id_last
+3, id_last
+1, id_last
+2)])
910 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
911 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
912 m1x
= min(m1x
, area_width
)
913 m2x
= min(m2x
, area_width
)
914 if m1y
> bottom_bar
and m2y
> bottom_bar
:
915 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
916 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
917 indices
.extend([(id_last
, id_last
+1, id_last
+3),
918 (id_last
+3, id_last
+1, id_last
+2)])
922 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
923 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
924 m1y
= max(m1y
, bottom_bar
)
925 m2y
= max(m2y
, bottom_bar
)
926 if m1x
< area_width
and m2x
< area_width
:
927 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
928 (m1x
+radius
,m1y
), (m1x
,m1y
)])
929 indices
.extend([(id_last
, id_last
+1, id_last
+3),
930 (id_last
+3, id_last
+1, id_last
+2)])
934 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
935 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
936 m1x
= min(m1x
, area_width
)
937 m2x
= min(m2x
, area_width
)
938 if m1y
> bottom_bar
and m2y
> bottom_bar
:
939 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
940 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
941 indices
.extend([(id_last
, id_last
+1, id_last
+3),
942 (id_last
+3, id_last
+1, id_last
+2)])
944 # now draw all edges in one batch
945 if len(vertices
) != 0:
946 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
948 shader
.uniform_float("color", colour
)
951 def draw_callback_nodeoutline(self
, context
, mode
):
955 bgl
.glEnable(bgl
.GL_BLEND
)
956 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
957 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
959 nodes
, links
= get_nodes_links(context
)
961 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
964 col_outer
= (1.0, 0.2, 0.2, 0.4)
965 col_inner
= (0.0, 0.0, 0.0, 0.5)
966 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
967 elif mode
== "LINKMENU":
968 col_outer
= (0.4, 0.6, 1.0, 0.4)
969 col_inner
= (0.0, 0.0, 0.0, 0.5)
970 col_circle_inner
= (0.08, 0.15, .3, 1.0)
972 col_outer
= (0.2, 1.0, 0.2, 0.4)
973 col_inner
= (0.0, 0.0, 0.0, 0.5)
974 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
976 m1x
= self
.mouse_path
[0][0]
977 m1y
= self
.mouse_path
[0][1]
978 m2x
= self
.mouse_path
[-1][0]
979 m2y
= self
.mouse_path
[-1][1]
981 n1
= nodes
[context
.scene
.NWLazySource
]
982 n2
= nodes
[context
.scene
.NWLazyTarget
]
985 col_outer
= (0.4, 0.4, 0.4, 0.4)
986 col_inner
= (0.0, 0.0, 0.0, 0.5)
987 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
989 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
990 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
991 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
992 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
994 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
995 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
998 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
999 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
1002 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
1003 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
1005 bgl
.glDisable(bgl
.GL_BLEND
)
1006 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
1007 def get_active_tree(context
):
1008 tree
= context
.space_data
.node_tree
1010 # Get nodes from currently edited tree.
1011 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1012 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1013 # the same as context.active_node, the user is in a group.
1014 # Check recursively until we find the real active node_tree:
1015 if tree
.nodes
.active
:
1016 while tree
.nodes
.active
!= context
.active_node
:
1017 tree
= tree
.nodes
.active
.node_tree
1021 def get_nodes_links(context
):
1022 tree
, path
= get_active_tree(context
)
1023 return tree
.nodes
, tree
.links
1025 def is_viewer_socket(socket
):
1026 # checks if a internal socket is a valid viewer socket
1027 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
1029 def get_internal_socket(socket
):
1030 #get the internal socket from a socket inside or outside the group
1032 if node
.type == 'GROUP_OUTPUT':
1033 source_iterator
= node
.inputs
1034 iterator
= node
.id_data
.outputs
1035 elif node
.type == 'GROUP_INPUT':
1036 source_iterator
= node
.outputs
1037 iterator
= node
.id_data
.inputs
1038 elif hasattr(node
, "node_tree"):
1039 if socket
.is_output
:
1040 source_iterator
= node
.outputs
1041 iterator
= node
.node_tree
.outputs
1043 source_iterator
= node
.inputs
1044 iterator
= node
.node_tree
.inputs
1048 for i
, s
in enumerate(source_iterator
):
1053 def is_viewer_link(link
, output_node
):
1054 if link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
1056 if link
.to_node
.type == 'GROUP_OUTPUT':
1057 socket
= get_internal_socket(link
.to_socket
)
1058 if is_viewer_socket(socket
):
1062 def get_group_output_node(tree
):
1063 for node
in tree
.nodes
:
1064 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
1067 def get_output_location(tree
):
1068 # get right-most location
1069 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
1070 max_xloc_node
= sorted_by_xloc
[-1]
1072 # get average y location
1074 for node
in tree
.nodes
:
1075 sum_yloc
+= node
.location
.y
1077 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1078 loc_y
= sum_yloc
/ len(tree
.nodes
)
1082 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
1083 base_color
: StringProperty(
1085 default
='diffuse diff albedo base col color',
1086 description
='Naming Components for Base Color maps')
1087 sss_color
: StringProperty(
1088 name
='Subsurface Color',
1089 default
='sss subsurface',
1090 description
='Naming Components for Subsurface Color maps')
1091 metallic
: StringProperty(
1093 default
='metallic metalness metal mtl',
1094 description
='Naming Components for metallness maps')
1095 specular
: StringProperty(
1097 default
='specularity specular spec spc',
1098 description
='Naming Components for Specular maps')
1099 normal
: StringProperty(
1101 default
='normal nor nrm nrml norm',
1102 description
='Naming Components for Normal maps')
1103 bump
: StringProperty(
1106 description
='Naming Components for bump maps')
1107 rough
: StringProperty(
1109 default
='roughness rough rgh',
1110 description
='Naming Components for roughness maps')
1111 gloss
: StringProperty(
1113 default
='gloss glossy glossiness',
1114 description
='Naming Components for glossy maps')
1115 displacement
: StringProperty(
1116 name
='Displacement',
1117 default
='displacement displace disp dsp height heightmap',
1118 description
='Naming Components for displacement maps')
1119 transmission
: StringProperty(
1120 name
='Transmission',
1121 default
='transmission transparency',
1122 description
='Naming Components for transmission maps')
1123 emission
: StringProperty(
1125 default
='emission emissive emit',
1126 description
='Naming Components for emission maps')
1127 alpha
: StringProperty(
1129 default
='alpha opacity',
1130 description
='Naming Components for alpha maps')
1131 ambient_occlusion
: StringProperty(
1132 name
='Ambient Occlusion',
1133 default
='ao ambient occlusion',
1134 description
='Naming Components for AO maps')
1137 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1138 bl_idname
= __name__
1140 merge_hide
: EnumProperty(
1141 name
="Hide Mix nodes",
1143 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1144 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1145 ("NEVER", "Never", "Never collapse the new merge nodes")
1147 default
='NON_SHADER',
1148 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1149 merge_position
: EnumProperty(
1150 name
="Mix Node Position",
1152 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1153 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1156 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1158 show_hotkey_list
: BoolProperty(
1159 name
="Show Hotkey List",
1161 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1163 hotkey_list_filter
: StringProperty(
1164 name
=" Filter by Name",
1166 description
="Show only hotkeys that have this text in their name",
1167 options
={'TEXTEDIT_UPDATE'}
1169 show_principled_lists
: BoolProperty(
1170 name
="Show Principled naming tags",
1172 description
="Expand this box into a list of all naming tags for principled texture setup"
1174 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1176 def draw(self
, context
):
1177 layout
= self
.layout
1178 col
= layout
.column()
1179 col
.prop(self
, "merge_position")
1180 col
.prop(self
, "merge_hide")
1183 col
= box
.column(align
=True)
1184 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1185 if self
.show_principled_lists
:
1186 tags
= self
.principled_tags
1188 col
.prop(tags
, "base_color")
1189 col
.prop(tags
, "sss_color")
1190 col
.prop(tags
, "metallic")
1191 col
.prop(tags
, "specular")
1192 col
.prop(tags
, "rough")
1193 col
.prop(tags
, "gloss")
1194 col
.prop(tags
, "normal")
1195 col
.prop(tags
, "bump")
1196 col
.prop(tags
, "displacement")
1197 col
.prop(tags
, "transmission")
1198 col
.prop(tags
, "emission")
1199 col
.prop(tags
, "alpha")
1200 col
.prop(tags
, "ambient_occlusion")
1203 col
= box
.column(align
=True)
1204 hotkey_button_name
= "Show Hotkey List"
1205 if self
.show_hotkey_list
:
1206 hotkey_button_name
= "Hide Hotkey List"
1207 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1208 if self
.show_hotkey_list
:
1209 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1211 for hotkey
in kmi_defs
:
1213 hotkey_name
= hotkey
[7]
1215 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1216 row
= col
.row(align
=True)
1217 row
.label(text
=hotkey_name
)
1218 keystr
= nice_hotkey_name(hotkey
[1])
1220 keystr
= "Shift " + keystr
1222 keystr
= "Alt " + keystr
1224 keystr
= "Ctrl " + keystr
1225 row
.label(text
=keystr
)
1229 def nw_check(context
):
1230 space
= context
.space_data
1231 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1234 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1241 def poll(cls
, context
):
1242 return nw_check(context
)
1246 class NWLazyMix(Operator
, NWBase
):
1247 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1248 bl_idname
= "node.nw_lazy_mix"
1249 bl_label
= "Mix Nodes"
1250 bl_options
= {'REGISTER', 'UNDO'}
1252 def modal(self
, context
, event
):
1253 context
.area
.tag_redraw()
1254 nodes
, links
= get_nodes_links(context
)
1257 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1260 if not context
.scene
.NWBusyDrawing
:
1261 node1
= node_at_pos(nodes
, context
, event
)
1263 context
.scene
.NWBusyDrawing
= node1
.name
1265 if context
.scene
.NWBusyDrawing
!= 'STOP':
1266 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1268 context
.scene
.NWLazySource
= node1
.name
1269 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1271 if event
.type == 'MOUSEMOVE':
1272 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1274 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1275 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1276 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1279 node2
= node_at_pos(nodes
, context
, event
)
1281 context
.scene
.NWBusyDrawing
= node2
.name
1293 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1295 context
.scene
.NWBusyDrawing
= ""
1298 elif event
.type == 'ESC':
1300 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1301 return {'CANCELLED'}
1303 return {'RUNNING_MODAL'}
1305 def invoke(self
, context
, event
):
1306 if context
.area
.type == 'NODE_EDITOR':
1307 # the arguments we pass the the callback
1308 args
= (self
, context
, 'MIX')
1309 # Add the region OpenGL drawing callback
1310 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1311 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1313 self
.mouse_path
= []
1315 context
.window_manager
.modal_handler_add(self
)
1316 return {'RUNNING_MODAL'}
1318 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1319 return {'CANCELLED'}
1322 class NWLazyConnect(Operator
, NWBase
):
1323 """Connect two nodes without clicking a specific socket (automatically determined"""
1324 bl_idname
= "node.nw_lazy_connect"
1325 bl_label
= "Lazy Connect"
1326 bl_options
= {'REGISTER', 'UNDO'}
1327 with_menu
: BoolProperty()
1329 def modal(self
, context
, event
):
1330 context
.area
.tag_redraw()
1331 nodes
, links
= get_nodes_links(context
)
1334 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1337 if not context
.scene
.NWBusyDrawing
:
1338 node1
= node_at_pos(nodes
, context
, event
)
1340 context
.scene
.NWBusyDrawing
= node1
.name
1342 if context
.scene
.NWBusyDrawing
!= 'STOP':
1343 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1345 context
.scene
.NWLazySource
= node1
.name
1346 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1348 if event
.type == 'MOUSEMOVE':
1349 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1351 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1352 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1353 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1356 node2
= node_at_pos(nodes
, context
, event
)
1358 context
.scene
.NWBusyDrawing
= node2
.name
1363 link_success
= False
1369 if node
.select
== True:
1371 original_sel
.append(node
)
1373 original_unsel
.append(node
)
1377 #link_success = autolink(node1, node2, links)
1379 if len(node1
.outputs
) > 1 and node2
.inputs
:
1380 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1381 elif len(node1
.outputs
) == 1:
1382 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1384 link_success
= autolink(node1
, node2
, links
)
1386 for node
in original_sel
:
1388 for node
in original_unsel
:
1392 force_update(context
)
1393 context
.scene
.NWBusyDrawing
= ""
1396 elif event
.type == 'ESC':
1397 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1398 return {'CANCELLED'}
1400 return {'RUNNING_MODAL'}
1402 def invoke(self
, context
, event
):
1403 if context
.area
.type == 'NODE_EDITOR':
1404 nodes
, links
= get_nodes_links(context
)
1405 node
= node_at_pos(nodes
, context
, event
)
1407 context
.scene
.NWBusyDrawing
= node
.name
1409 # the arguments we pass the the callback
1413 args
= (self
, context
, mode
)
1414 # Add the region OpenGL drawing callback
1415 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1416 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1418 self
.mouse_path
= []
1420 context
.window_manager
.modal_handler_add(self
)
1421 return {'RUNNING_MODAL'}
1423 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1424 return {'CANCELLED'}
1427 class NWDeleteUnused(Operator
, NWBase
):
1428 """Delete all nodes whose output is not used"""
1429 bl_idname
= 'node.nw_del_unused'
1430 bl_label
= 'Delete Unused Nodes'
1431 bl_options
= {'REGISTER', 'UNDO'}
1433 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1434 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1436 def is_unused_node(self
, node
):
1437 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1438 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1439 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1440 if node
.type in end_types
:
1443 for output
in node
.outputs
:
1449 def poll(cls
, context
):
1451 if nw_check(context
):
1452 if context
.space_data
.node_tree
.nodes
:
1456 def execute(self
, context
):
1457 nodes
, links
= get_nodes_links(context
)
1462 if node
.select
== True:
1463 selection
.append(node
.name
)
1469 temp_deleted_nodes
= []
1470 del_unused_iterations
= len(nodes
)
1471 for it
in range(0, del_unused_iterations
):
1472 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1474 if self
.is_unused_node(node
):
1476 deleted_nodes
.append(node
.name
)
1477 bpy
.ops
.node
.delete()
1479 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1482 if self
.delete_frames
:
1490 frames_in_use
.append(node
.parent
)
1492 if node
.type == 'FRAME' and node
not in frames_in_use
:
1495 repeat
= True # repeat for nested frames
1497 if node
not in frames_in_use
:
1499 deleted_nodes
.append(node
.name
)
1500 bpy
.ops
.node
.delete()
1502 if self
.delete_muted
:
1506 deleted_nodes
.append(node
.name
)
1507 bpy
.ops
.node
.delete_reconnect()
1509 # get unique list of deleted nodes (iterations would count the same node more than once)
1510 deleted_nodes
= list(set(deleted_nodes
))
1511 for n
in deleted_nodes
:
1512 self
.report({'INFO'}, "Node " + n
+ " deleted")
1513 num_deleted
= len(deleted_nodes
)
1518 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1520 self
.report({'INFO'}, "Nothing deleted")
1523 nodes
, links
= get_nodes_links(context
)
1525 if node
.name
in selection
:
1529 def invoke(self
, context
, event
):
1530 return context
.window_manager
.invoke_confirm(self
, event
)
1533 class NWSwapLinks(Operator
, NWBase
):
1534 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1535 bl_idname
= 'node.nw_swap_links'
1536 bl_label
= 'Swap Links'
1537 bl_options
= {'REGISTER', 'UNDO'}
1540 def poll(cls
, context
):
1542 if nw_check(context
):
1543 if context
.selected_nodes
:
1544 valid
= len(context
.selected_nodes
) <= 2
1547 def execute(self
, context
):
1548 nodes
, links
= get_nodes_links(context
)
1549 selected_nodes
= context
.selected_nodes
1550 n1
= selected_nodes
[0]
1553 if len(selected_nodes
) == 2:
1554 n2
= selected_nodes
[1]
1555 if n1
.outputs
and n2
.outputs
:
1560 for output
in n1
.outputs
:
1562 for link
in output
.links
:
1563 n1_outputs
.append([out_index
, link
.to_socket
])
1568 for output
in n2
.outputs
:
1570 for link
in output
.links
:
1571 n2_outputs
.append([out_index
, link
.to_socket
])
1575 for connection
in n1_outputs
:
1577 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1579 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1580 for connection
in n2_outputs
:
1582 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1584 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1586 if n1
.outputs
or n2
.outputs
:
1587 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1589 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1592 elif len(selected_nodes
) == 1:
1593 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1594 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1599 for i1
in n1
.inputs
:
1600 if i1
.is_linked
and not i1
.is_multi_input
:
1602 for i2
in n1
.inputs
:
1603 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1605 types
.append ([i1
, similar_types
, i
])
1607 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1612 for i2
in n1
.inputs
:
1613 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1615 i1f
= pair
[0].links
[0].from_socket
1616 i1t
= pair
[0].links
[0].to_socket
1617 i2f
= pair
[1].links
[0].from_socket
1618 i2t
= pair
[1].links
[0].to_socket
1623 fs
= t
[0].links
[0].from_socket
1625 links
.remove(t
[0].links
[0])
1626 if i
+1 == len(n1
.inputs
):
1629 while n1
.inputs
[i
].is_linked
:
1631 links
.new(fs
, n1
.inputs
[i
])
1632 elif len(types
) == 2:
1633 i1f
= types
[0][0].links
[0].from_socket
1634 i1t
= types
[0][0].links
[0].to_socket
1635 i2f
= types
[1][0].links
[0].from_socket
1636 i2t
= types
[1][0].links
[0].to_socket
1641 self
.report({'WARNING'}, "This node has no input connections to swap!")
1643 self
.report({'WARNING'}, "This node has no inputs to swap!")
1645 force_update(context
)
1649 class NWResetBG(Operator
, NWBase
):
1650 """Reset the zoom and position of the background image"""
1651 bl_idname
= 'node.nw_bg_reset'
1652 bl_label
= 'Reset Backdrop'
1653 bl_options
= {'REGISTER', 'UNDO'}
1656 def poll(cls
, context
):
1658 if nw_check(context
):
1659 snode
= context
.space_data
1660 valid
= snode
.tree_type
== 'CompositorNodeTree'
1663 def execute(self
, context
):
1664 context
.space_data
.backdrop_zoom
= 1
1665 context
.space_data
.backdrop_offset
[0] = 0
1666 context
.space_data
.backdrop_offset
[1] = 0
1670 class NWAddAttrNode(Operator
, NWBase
):
1671 """Add an Attribute node with this name"""
1672 bl_idname
= 'node.nw_add_attr_node'
1673 bl_label
= 'Add UV map'
1674 bl_options
= {'REGISTER', 'UNDO'}
1676 attr_name
: StringProperty()
1678 def execute(self
, context
):
1679 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1680 nodes
, links
= get_nodes_links(context
)
1681 nodes
.active
.attribute_name
= self
.attr_name
1684 class NWPreviewNode(Operator
, NWBase
):
1685 bl_idname
= "node.nw_preview_node"
1686 bl_label
= "Preview Node"
1687 bl_description
= "Connect active node to the Node Group output or the Material Output"
1688 bl_options
= {'REGISTER', 'UNDO'}
1690 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1691 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1692 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1693 run_in_geometry_nodes
: BoolProperty(default
=True)
1696 self
.shader_output_type
= ""
1697 self
.shader_output_ident
= ""
1700 def poll(cls
, context
):
1701 if nw_check(context
):
1702 space
= context
.space_data
1703 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1704 if context
.active_node
:
1705 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1711 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1712 #check if a viewer output already exists in a node group otherwise create
1713 if hasattr(node
, "node_tree"):
1715 if len(node
.node_tree
.outputs
):
1717 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1718 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1719 #if viewer output is already used but leads to the same socket we can still use it
1720 is_used
= self
.is_socket_used_other_mats(socket
)
1722 if connect_socket
== None:
1724 groupout
= get_group_output_node(node
.node_tree
)
1725 groupout_input
= groupout
.inputs
[i
]
1726 links
= groupout_input
.links
1727 if connect_socket
not in [link
.from_socket
for link
in links
]:
1733 if not index
and free_socket
:
1737 #create viewer socket
1738 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1739 index
= len(node
.node_tree
.outputs
) - 1
1740 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1743 def init_shader_variables(self
, space
, shader_type
):
1744 if shader_type
== 'OBJECT':
1745 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1746 self
.shader_output_type
= "OUTPUT_MATERIAL"
1747 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1749 self
.shader_output_type
= "OUTPUT_LIGHT"
1750 self
.shader_output_ident
= "ShaderNodeOutputLight"
1752 elif shader_type
== 'WORLD':
1753 self
.shader_output_type
= "OUTPUT_WORLD"
1754 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1756 def get_shader_output_node(self
, tree
):
1757 for node
in tree
.nodes
:
1758 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1762 def ensure_group_output(cls
, tree
):
1763 #check if a group output node exists otherwise create
1764 groupout
= get_group_output_node(tree
)
1766 groupout
= tree
.nodes
.new('NodeGroupOutput')
1767 loc_x
, loc_y
= get_output_location(tree
)
1768 groupout
.location
.x
= loc_x
1769 groupout
.location
.y
= loc_y
1770 groupout
.select
= False
1771 # So that we don't keep on adding new group outputs
1772 groupout
.is_active_output
= True
1776 def search_sockets(cls
, node
, sockets
, index
=None):
1777 # recursively scan nodes for viewer sockets and store in list
1778 for i
, input_socket
in enumerate(node
.inputs
):
1779 if index
and i
!= index
:
1781 if len(input_socket
.links
):
1782 link
= input_socket
.links
[0]
1783 next_node
= link
.from_node
1784 external_socket
= link
.from_socket
1785 if hasattr(next_node
, "node_tree"):
1786 for socket_index
, s
in enumerate(next_node
.outputs
):
1787 if s
== external_socket
:
1789 socket
= next_node
.node_tree
.outputs
[socket_index
]
1790 if is_viewer_socket(socket
) and socket
not in sockets
:
1791 sockets
.append(socket
)
1792 #continue search inside of node group but restrict socket to where we came from
1793 groupout
= get_group_output_node(next_node
.node_tree
)
1794 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1797 def scan_nodes(cls
, tree
, sockets
):
1798 # get all viewer sockets in a material tree
1799 for node
in tree
.nodes
:
1800 if hasattr(node
, "node_tree"):
1801 for socket
in node
.node_tree
.outputs
:
1802 if is_viewer_socket(socket
) and (socket
not in sockets
):
1803 sockets
.append(socket
)
1804 cls
.scan_nodes(node
.node_tree
, sockets
)
1806 def link_leads_to_used_socket(self
, link
):
1807 #return True if link leads to a socket that is already used in this material
1808 socket
= get_internal_socket(link
.to_socket
)
1809 return (socket
and self
.is_socket_used_active_mat(socket
))
1811 def is_socket_used_active_mat(self
, socket
):
1812 #ensure used sockets in active material is calculated and check given socket
1813 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1814 self
.used_viewer_sockets_active_mat
= []
1815 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1817 self
.search_sockets(materialout
, self
.used_viewer_sockets_active_mat
)
1818 return socket
in self
.used_viewer_sockets_active_mat
1820 def is_socket_used_other_mats(self
, socket
):
1821 #ensure used sockets in other materials are calculated and check given socket
1822 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1823 self
.used_viewer_sockets_other_mats
= []
1824 for mat
in bpy
.data
.materials
:
1825 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1828 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1830 self
.search_sockets(materialout
, self
.used_viewer_sockets_other_mats
)
1831 return socket
in self
.used_viewer_sockets_other_mats
1833 def invoke(self
, context
, event
):
1834 space
= context
.space_data
1835 # Ignore operator when running in wrong context.
1836 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1837 return {'PASS_THROUGH'}
1839 shader_type
= space
.shader_type
1840 self
.init_shader_variables(space
, shader_type
)
1841 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1842 mlocx
= event
.mouse_region_x
1843 mlocy
= event
.mouse_region_y
1844 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
1845 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1846 active_tree
, path_to_tree
= get_active_tree(context
)
1847 nodes
, links
= active_tree
.nodes
, active_tree
.links
1848 base_node_tree
= space
.node_tree
1849 active
= nodes
.active
1851 # For geometry node trees we just connect to the group output
1852 if space
.tree_type
== "GeometryNodeTree":
1855 for out
in active
.outputs
:
1856 if is_visible_socket(out
):
1865 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1866 self
.scan_nodes(base_node_tree
, delete_sockets
)
1868 # Find (or create if needed) the output of this node tree
1869 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1871 # Analyze outputs, make links
1874 for i
, out
in enumerate(active
.outputs
):
1875 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1876 valid_outputs
.append(i
)
1878 out_i
= valid_outputs
[0] # Start index of node's outputs
1879 for i
, valid_i
in enumerate(valid_outputs
):
1880 for out_link
in active
.outputs
[valid_i
].links
:
1881 if is_viewer_link(out_link
, geometryoutput
):
1882 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1883 if i
< len(valid_outputs
) - 1:
1884 out_i
= valid_outputs
[i
+ 1]
1886 out_i
= valid_outputs
[0]
1888 make_links
= [] # store sockets for new links
1890 # If there is no 'GEOMETRY' output type - We can't preview the node
1893 socket_type
= 'GEOMETRY'
1894 # Find an input socket of the output of type geometry
1895 geometryoutindex
= None
1896 for i
,inp
in enumerate(geometryoutput
.inputs
):
1897 if inp
.type == socket_type
:
1898 geometryoutindex
= i
1900 if geometryoutindex
is None:
1901 # Create geometry socket
1902 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1903 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1905 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1906 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1907 for li_from
, li_to
in make_links
:
1908 base_node_tree
.links
.new(li_from
, li_to
)
1909 tree
= base_node_tree
1910 link_end
= output_socket
1911 while tree
.nodes
.active
!= active
:
1912 node
= tree
.nodes
.active
1913 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1914 link_start
= node
.outputs
[index
]
1915 node_socket
= node
.node_tree
.outputs
[index
]
1916 if node_socket
in delete_sockets
:
1917 delete_sockets
.remove(node_socket
)
1918 tree
.links
.new(link_start
, link_end
)
1920 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1921 tree
= tree
.nodes
.active
.node_tree
1922 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1925 for socket
in delete_sockets
:
1926 tree
= socket
.id_data
1927 tree
.outputs
.remove(socket
)
1929 nodes
.active
= active
1930 active
.select
= True
1931 force_update(context
)
1935 # What follows is code for the shader editor
1936 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1939 if active
.type not in output_types
:
1940 for out
in active
.outputs
:
1941 if is_visible_socket(out
):
1945 # get material_output node
1946 materialout
= None # placeholder node
1949 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1950 self
.scan_nodes(base_node_tree
, delete_sockets
)
1952 materialout
= self
.get_shader_output_node(base_node_tree
)
1954 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1955 materialout
.location
= get_output_location(base_node_tree
)
1956 materialout
.select
= False
1960 for i
, out
in enumerate(active
.outputs
):
1961 if is_visible_socket(out
):
1962 valid_outputs
.append(i
)
1964 out_i
= valid_outputs
[0] # Start index of node's outputs
1965 for i
, valid_i
in enumerate(valid_outputs
):
1966 for out_link
in active
.outputs
[valid_i
].links
:
1967 if is_viewer_link(out_link
, materialout
):
1968 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1969 if i
< len(valid_outputs
) - 1:
1970 out_i
= valid_outputs
[i
+ 1]
1972 out_i
= valid_outputs
[0]
1974 make_links
= [] # store sockets for new links
1976 socket_type
= 'NodeSocketShader'
1977 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
1978 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
1979 output_socket
= materialout
.inputs
[materialout_index
]
1980 for li_from
, li_to
in make_links
:
1981 base_node_tree
.links
.new(li_from
, li_to
)
1983 # Create links through node groups until we reach the active node
1984 tree
= base_node_tree
1985 link_end
= output_socket
1986 while tree
.nodes
.active
!= active
:
1987 node
= tree
.nodes
.active
1988 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1989 link_start
= node
.outputs
[index
]
1990 node_socket
= node
.node_tree
.outputs
[index
]
1991 if node_socket
in delete_sockets
:
1992 delete_sockets
.remove(node_socket
)
1993 tree
.links
.new(link_start
, link_end
)
1995 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1996 tree
= tree
.nodes
.active
.node_tree
1997 tree
.links
.new(active
.outputs
[out_i
], link_end
)
2000 for socket
in delete_sockets
:
2001 if not self
.is_socket_used_other_mats(socket
):
2002 tree
= socket
.id_data
2003 tree
.outputs
.remove(socket
)
2005 nodes
.active
= active
2006 active
.select
= True
2008 force_update(context
)
2012 return {'CANCELLED'}
2015 class NWFrameSelected(Operator
, NWBase
):
2016 bl_idname
= "node.nw_frame_selected"
2017 bl_label
= "Frame Selected"
2018 bl_description
= "Add a frame node and parent the selected nodes to it"
2019 bl_options
= {'REGISTER', 'UNDO'}
2021 label_prop
: StringProperty(
2023 description
='The visual name of the frame node',
2026 use_custom_color_prop
: BoolProperty(
2027 name
="Custom Color",
2028 description
="Use custom color for the frame node",
2031 color_prop
: FloatVectorProperty(
2033 description
="The color of the frame node",
2034 default
=(0.604, 0.604, 0.604),
2035 min=0, max=1, step
=1, precision
=3,
2036 subtype
='COLOR_GAMMA', size
=3
2039 def draw(self
, context
):
2040 layout
= self
.layout
2041 layout
.prop(self
, 'label_prop')
2042 layout
.prop(self
, 'use_custom_color_prop')
2043 col
= layout
.column()
2044 col
.active
= self
.use_custom_color_prop
2045 col
.prop(self
, 'color_prop', text
="")
2047 def execute(self
, context
):
2048 nodes
, links
= get_nodes_links(context
)
2051 if node
.select
== True:
2052 selected
.append(node
)
2054 bpy
.ops
.node
.add_node(type='NodeFrame')
2056 frm
.label
= self
.label_prop
2057 frm
.use_custom_color
= self
.use_custom_color_prop
2058 frm
.color
= self
.color_prop
2060 for node
in selected
:
2066 class NWReloadImages(Operator
):
2067 bl_idname
= "node.nw_reload_images"
2068 bl_label
= "Reload Images"
2069 bl_description
= "Update all the image nodes to match their files on disk"
2072 def poll(cls
, context
):
2074 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
2075 if context
.active_node
is not None:
2076 for out
in context
.active_node
.outputs
:
2077 if is_visible_socket(out
):
2082 def execute(self
, context
):
2083 nodes
, links
= get_nodes_links(context
)
2084 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2087 if node
.type in image_types
:
2088 if node
.type == "TEXTURE":
2089 if node
.texture
: # node has texture assigned
2090 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2091 if node
.texture
.image
: # texture has image assigned
2092 node
.texture
.image
.reload()
2100 self
.report({'INFO'}, "Reloaded images")
2101 print("Reloaded " + str(num_reloaded
) + " images")
2102 force_update(context
)
2105 self
.report({'WARNING'}, "No images found to reload in this node tree")
2106 return {'CANCELLED'}
2109 class NWSwitchNodeType(Operator
, NWBase
):
2110 """Switch type of selected nodes """
2111 bl_idname
= "node.nw_swtch_node_type"
2112 bl_label
= "Switch Node Type"
2113 bl_options
= {'REGISTER', 'UNDO'}
2115 to_type
: EnumProperty(
2116 name
="Switch to type",
2117 items
=list(shaders_input_nodes_props
) +
2118 list(shaders_output_nodes_props
) +
2119 list(shaders_shader_nodes_props
) +
2120 list(shaders_texture_nodes_props
) +
2121 list(shaders_color_nodes_props
) +
2122 list(shaders_vector_nodes_props
) +
2123 list(shaders_converter_nodes_props
) +
2124 list(shaders_layout_nodes_props
) +
2125 list(compo_input_nodes_props
) +
2126 list(compo_output_nodes_props
) +
2127 list(compo_color_nodes_props
) +
2128 list(compo_converter_nodes_props
) +
2129 list(compo_filter_nodes_props
) +
2130 list(compo_vector_nodes_props
) +
2131 list(compo_matte_nodes_props
) +
2132 list(compo_distort_nodes_props
) +
2133 list(compo_layout_nodes_props
) +
2134 list(blender_mat_input_nodes_props
) +
2135 list(blender_mat_output_nodes_props
) +
2136 list(blender_mat_color_nodes_props
) +
2137 list(blender_mat_vector_nodes_props
) +
2138 list(blender_mat_converter_nodes_props
) +
2139 list(blender_mat_layout_nodes_props
) +
2140 list(texture_input_nodes_props
) +
2141 list(texture_output_nodes_props
) +
2142 list(texture_color_nodes_props
) +
2143 list(texture_pattern_nodes_props
) +
2144 list(texture_textures_nodes_props
) +
2145 list(texture_converter_nodes_props
) +
2146 list(texture_distort_nodes_props
) +
2147 list(texture_layout_nodes_props
)
2150 geo_to_type
: StringProperty(
2151 name
="Switch to type",
2155 def execute(self
, context
):
2156 nodes
, links
= get_nodes_links(context
)
2157 to_type
= self
.to_type
2158 if self
.geo_to_type
!= '':
2159 to_type
= self
.geo_to_type
2160 # Those types of nodes will not swap.
2161 src_excludes
= ('NodeFrame')
2162 # Those attributes of nodes will be copied if possible
2163 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
2164 'show_options', 'show_preview', 'show_texture',
2165 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2167 selected
= [n
for n
in nodes
if n
.select
]
2169 for node
in [n
for n
in selected
if
2170 n
.rna_type
.identifier
not in src_excludes
and
2171 n
.rna_type
.identifier
!= to_type
]:
2172 new_node
= nodes
.new(to_type
)
2173 for attr
in attrs_to_pass
:
2174 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
2175 setattr(new_node
, attr
, getattr(node
, attr
))
2176 # set image datablock of dst to image of src
2177 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
2179 new_node
.image
= node
.image
2181 if new_node
.type == 'SWITCH':
2182 new_node
.hide
= True
2183 # Dictionaries: src_sockets and dst_sockets:
2184 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2185 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2186 # in 'INPUTS' and 'OUTPUTS':
2187 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2189 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2191 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2192 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2195 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2196 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2198 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2199 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2200 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2201 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
2202 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2203 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
2204 # enumerate in inputs, then in outputs
2205 # find name, default value and links of socket
2206 for i
, socket
in enumerate(in_out
):
2207 the_name
= socket
.name
2209 # Not every socket, especially in outputs has "default_value"
2210 if hasattr(socket
, 'default_value'):
2211 dval
= socket
.default_value
2213 for lnk
in socket
.links
:
2214 socket_links
.append(lnk
)
2215 # check type of socket to fill proper keys.
2216 for the_type
in types_order_one
:
2217 if socket
.type == the_type
:
2218 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2219 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2220 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
2221 # Check which of the types in inputs/outputs is considered to be "main".
2222 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2223 for type_check
in types_order_one
:
2224 if sockets
[in_out_name
][type_check
]:
2225 sockets
[in_out_name
]['MAIN'] = type_check
2229 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2230 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2233 for inout
, soctype
in (
2234 ('INPUTS', 'MAIN',),
2235 ('INPUTS', 'SHADER',),
2236 ('INPUTS', 'RGBA',),
2237 ('INPUTS', 'VECTOR',),
2238 ('INPUTS', 'VALUE',),
2239 ('OUTPUTS', 'MAIN',),
2240 ('OUTPUTS', 'SHADER',),
2241 ('OUTPUTS', 'RGBA',),
2242 ('OUTPUTS', 'VECTOR',),
2243 ('OUTPUTS', 'VALUE',),
2245 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
2246 if soctype
== 'MAIN':
2247 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
2248 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
2250 sc
= src_sockets
[inout
][soctype
]
2251 dt
= dst_sockets
[inout
][soctype
]
2252 # start with 'dt' to determine number of possibilities.
2253 for i
, soc
in enumerate(dt
):
2254 # if src main has enough entries - match them with dst main sockets by indexes.
2256 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
2257 # add 'VALUE_NAME' criterion to inputs.
2258 if inout
== 'INPUTS' and soctype
== 'VALUE':
2260 if s
[2] == soc
[2]: # if names match
2261 # append src (index, dval), dst (index, dval)
2262 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
2264 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2265 # This creates better links when relinking textures.
2266 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
2267 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
2269 # Pass default values and RELINK:
2270 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2271 # INPUTS: Base on matches in proper order.
2272 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
2274 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
2275 new_node
.inputs
[dst_i
].default_value
= src_dval
2276 # Special case: switch to math
2277 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2278 new_node
.type == 'MATH' and\
2280 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
2281 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
2282 if node
.type == 'MIX_RGB':
2283 if node
.blend_type
in [o
[0] for o
in operations
]:
2284 new_node
.operation
= node
.blend_type
2285 # Special case: switch from math to some types
2286 if node
.type == 'MATH' and\
2287 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2290 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
2291 if new_node
.type == 'MIX_RGB':
2292 if node
.operation
in [t
[0] for t
in blend_types
]:
2293 new_node
.blend_type
= node
.operation
2294 # Set Fac of MIX_RGB to 1.0
2295 new_node
.inputs
[0].default_value
= 1.0
2296 # make link only when dst matching input is not linked already.
2297 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
2298 in_src_link
= node
.inputs
[src_i
].links
[0]
2299 in_dst_socket
= new_node
.inputs
[dst_i
]
2300 links
.new(in_src_link
.from_socket
, in_dst_socket
)
2301 links
.remove(in_src_link
)
2302 # OUTPUTS: Base on matches in proper order.
2303 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
2304 for out_src_link
in node
.outputs
[src_i
].links
:
2305 out_dst_socket
= new_node
.outputs
[dst_i
]
2306 links
.new(out_dst_socket
, out_src_link
.to_socket
)
2307 # relink rest inputs if possible, no criteria
2308 for src_inp
in node
.inputs
:
2309 for dst_inp
in new_node
.inputs
:
2310 if src_inp
.links
and not dst_inp
.links
:
2311 src_link
= src_inp
.links
[0]
2312 links
.new(src_link
.from_socket
, dst_inp
)
2313 links
.remove(src_link
)
2314 # relink rest outputs if possible, base on node kind if any left.
2315 for src_o
in node
.outputs
:
2316 for out_src_link
in src_o
.links
:
2317 for dst_o
in new_node
.outputs
:
2318 if src_o
.type == dst_o
.type:
2319 links
.new(dst_o
, out_src_link
.to_socket
)
2320 # relink rest outputs no criteria if any left. Link all from first output.
2321 for src_o
in node
.outputs
:
2322 for out_src_link
in src_o
.links
:
2323 if new_node
.outputs
:
2324 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2326 force_update(context
)
2330 class NWMergeNodes(Operator
, NWBase
):
2331 bl_idname
= "node.nw_merge_nodes"
2332 bl_label
= "Merge Nodes"
2333 bl_description
= "Merge Selected Nodes"
2334 bl_options
= {'REGISTER', 'UNDO'}
2338 description
="All possible blend types, boolean operations and math operations",
2339 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
],
2341 merge_type
: EnumProperty(
2343 description
="Type of Merge to be used",
2345 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2346 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2347 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2348 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2349 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2350 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2351 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2355 # Check if the link connects to a node that is in selected_nodes
2356 # If not, then check recursively for each link in the nodes outputs.
2357 # If yes, return True. If the recursion stops without finding a node
2358 # in selected_nodes, it returns False. The depth is used to prevent
2359 # getting stuck in a loop because of an already present cycle.
2361 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
2363 # We're stuck in a cycle, but that cycle was already present,
2364 # so we return False.
2365 # NOTE: The number 255 is arbitrary, but seems to work well.
2368 if node
in selected_nodes
:
2370 if not node
.outputs
:
2372 for output
in node
.outputs
:
2373 if output
.is_linked
:
2374 for olink
in output
.links
:
2375 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
2377 # None of the outputs found a node in selected_nodes, so there is no cycle.
2380 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2381 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2382 # be connected. The last one is assumed to be a multi input socket.
2383 # For convenience the node is returned.
2385 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
2386 # The y-location of the last node
2387 loc_y
= nodes_list
[-1][2]
2388 if merge_position
== 'CENTER':
2389 # Average the y-location
2390 for i
in range(len(nodes_list
)-1):
2391 loc_y
+= nodes_list
[i
][2]
2392 loc_y
= loc_y
/len(nodes_list
)
2393 new_node
= nodes
.new(node_name
)
2394 new_node
.hide
= do_hide
2395 new_node
.location
.x
= loc_x
2396 new_node
.location
.y
= loc_y
2397 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
2399 outputs_for_multi_input
= []
2400 for i
,node
in enumerate(selected_nodes
):
2402 # Search for the first node which had output links that do not create
2403 # a cycle, which we can then reconnect afterwards.
2404 if prev_links
== [] and node
.outputs
[0].is_linked
:
2405 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
2406 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2407 # To get the placement to look right we need to reverse the order in which we connect the
2408 # outputs to the multi input socket.
2409 if i
< len(socket_indices
) - 1:
2410 ind
= socket_indices
[i
]
2411 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
2413 outputs_for_multi_input
.insert(0, node
.outputs
[0])
2414 if outputs_for_multi_input
!= []:
2415 ind
= socket_indices
[-1]
2416 for output
in outputs_for_multi_input
:
2417 links
.new(output
, new_node
.inputs
[ind
])
2418 if prev_links
!= []:
2419 for link
in prev_links
:
2420 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
2423 def execute(self
, context
):
2424 settings
= context
.preferences
.addons
[__name__
].preferences
2425 merge_hide
= settings
.merge_hide
2426 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2429 do_hide_shader
= False
2430 if merge_hide
== 'ALWAYS':
2432 do_hide_shader
= True
2433 elif merge_hide
== 'NON_SHADER':
2436 tree_type
= context
.space_data
.node_tree
.type
2437 if tree_type
== 'GEOMETRY':
2438 node_type
= 'GeometryNode'
2439 if tree_type
== 'COMPOSITING':
2440 node_type
= 'CompositorNode'
2441 elif tree_type
== 'SHADER':
2442 node_type
= 'ShaderNode'
2443 elif tree_type
== 'TEXTURE':
2444 node_type
= 'TextureNode'
2445 nodes
, links
= get_nodes_links(context
)
2447 merge_type
= self
.merge_type
2448 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2449 # 'ZCOMBINE' works only if mode == 'MIX'
2450 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2451 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2454 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2456 # The math nodes used for geometry nodes are of type 'ShaderNode'
2457 if merge_type
== 'MATH' and tree_type
== 'GEOMETRY':
2458 node_type
= 'ShaderNode'
2459 selected_mix
= [] # entry = [index, loc]
2460 selected_shader
= [] # entry = [index, loc]
2461 selected_geometry
= [] # entry = [index, loc]
2462 selected_math
= [] # entry = [index, loc]
2463 selected_vector
= [] # entry = [index, loc]
2464 selected_z
= [] # entry = [index, loc]
2465 selected_alphaover
= [] # entry = [index, loc]
2467 for i
, node
in enumerate(nodes
):
2468 if node
.select
and node
.outputs
:
2469 if merge_type
== 'AUTO':
2470 for (type, types_list
, dst
) in (
2471 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2472 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2473 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2474 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2475 ('VECTOR', [], selected_vector
),
2477 output_type
= node
.outputs
[0].type
2478 valid_mode
= mode
in types_list
2479 # When mode is 'MIX' we have to cheat since the mix node is not used in
2481 if tree_type
== 'GEOMETRY':
2483 if output_type
== 'VALUE' and type == 'VALUE':
2485 elif output_type
== 'VECTOR' and type == 'VECTOR':
2487 elif type == 'GEOMETRY':
2489 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2490 # Cheat that output type is 'RGBA',
2491 # and that 'MIX' exists in math operations list.
2492 # This way when selected_mix list is analyzed:
2493 # Node data will be appended even though it doesn't meet requirements.
2494 elif output_type
!= 'SHADER' and mode
== 'MIX':
2495 output_type
= 'RGBA'
2497 if output_type
== type and valid_mode
:
2498 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2500 for (type, types_list
, dst
) in (
2501 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2502 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2503 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2504 ('MATH', [t
[0] for t
in operations
], selected_math
),
2505 ('ZCOMBINE', ('MIX', ), selected_z
),
2506 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2508 if merge_type
== type and mode
in types_list
:
2509 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2510 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2511 # use only 'Mix' nodes for merging.
2512 # For that we add selected_math list to selected_mix list and clear selected_math.
2513 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2514 selected_mix
+= selected_math
2516 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2519 count_before
= len(nodes
)
2520 # sort list by loc_x - reversed
2521 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2523 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2524 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2526 # Change the node type for math nodes in a geometry node tree.
2527 if tree_type
== 'GEOMETRY':
2528 if nodes_list
is selected_math
or nodes_list
is selected_vector
:
2529 node_type
= 'ShaderNode'
2533 node_type
= 'GeometryNode'
2534 if merge_position
== 'CENTER':
2535 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)
2536 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2542 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2546 if nodes_list
== selected_shader
and not do_hide_shader
:
2548 the_range
= len(nodes_list
) - 1
2549 if len(nodes_list
) == 1:
2552 for i
in range(the_range
):
2553 if nodes_list
== selected_mix
:
2554 add_type
= node_type
+ 'MixRGB'
2555 add
= nodes
.new(add_type
)
2556 add
.blend_type
= mode
2558 add
.inputs
[0].default_value
= 1.0
2559 add
.show_preview
= False
2565 add
.width_hidden
= 100.0
2566 elif nodes_list
== selected_math
:
2567 add_type
= node_type
+ 'Math'
2568 add
= nodes
.new(add_type
)
2569 add
.operation
= mode
2575 add
.width_hidden
= 100.0
2576 elif nodes_list
== selected_shader
:
2578 add_type
= node_type
+ 'MixShader'
2579 add
= nodes
.new(add_type
)
2580 add
.hide
= do_hide_shader
2585 add
.width_hidden
= 100.0
2587 add_type
= node_type
+ 'AddShader'
2588 add
= nodes
.new(add_type
)
2589 add
.hide
= do_hide_shader
2594 add
.width_hidden
= 100.0
2595 elif nodes_list
== selected_geometry
:
2596 if mode
in ('JOIN', 'MIX'):
2597 add_type
= node_type
+ 'JoinGeometry'
2598 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2600 add_type
= node_type
+ 'Boolean'
2601 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2602 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2603 add
.operation
= mode
2606 elif nodes_list
== selected_vector
:
2607 add_type
= node_type
+ 'VectorMath'
2608 add
= nodes
.new(add_type
)
2609 add
.operation
= mode
2615 add
.width_hidden
= 100.0
2616 elif nodes_list
== selected_z
:
2617 add
= nodes
.new('CompositorNodeZcombine')
2618 add
.show_preview
= False
2624 add
.width_hidden
= 100.0
2625 elif nodes_list
== selected_alphaover
:
2626 add
= nodes
.new('CompositorNodeAlphaOver')
2627 add
.show_preview
= False
2633 add
.width_hidden
= 100.0
2634 add
.location
= loc_x
, loc_y
2638 # This has already been handled separately
2642 count_after
= len(nodes
)
2643 index
= count_after
- 1
2644 first_selected
= nodes
[nodes_list
[0][0]]
2645 # "last" node has been added as first, so its index is count_before.
2646 last_add
= nodes
[count_before
]
2647 # Create list of invalid indexes.
2648 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2651 # Two nodes were selected and first selected has no output links, second selected has output links.
2652 # Then add links from last add to all links 'to_socket' of out links of second selected.
2653 if len(nodes_list
) == 2:
2654 if not first_selected
.outputs
[0].links
:
2655 second_selected
= nodes
[nodes_list
[1][0]]
2656 for ss_link
in second_selected
.outputs
[0].links
:
2657 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2658 # Link only if "to_node" index not in invalid indexes list.
2659 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2660 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2661 # add links from last_add to all links 'to_socket' of out links of first selected.
2662 for fs_link
in first_selected
.outputs
[0].links
:
2663 # Link only if "to_node" index not in invalid indexes list.
2664 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2665 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2666 # add link from "first" selected and "first" add node
2667 node_to
= nodes
[count_after
- 1]
2668 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2669 if node_to
.type == 'ZCOMBINE':
2670 for fs_out
in first_selected
.outputs
:
2671 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2672 links
.new(fs_out
, node_to
.inputs
[1])
2674 # add links between added ADD nodes and between selected and ADD nodes
2675 for i
in range(count_adds
):
2676 if i
< count_adds
- 1:
2677 node_from
= nodes
[index
]
2678 node_to
= nodes
[index
- 1]
2679 node_to_input_i
= first
2680 node_to_z_i
= 1 # if z combine - link z to first z input
2681 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2682 if node_to
.type == 'ZCOMBINE':
2683 for from_out
in node_from
.outputs
:
2684 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2685 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2686 if len(nodes_list
) > 1:
2687 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2688 node_to
= nodes
[index
]
2689 node_to_input_i
= second
2690 node_to_z_i
= 3 # if z combine - link z to second z input
2691 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2692 if node_to
.type == 'ZCOMBINE':
2693 for from_out
in node_from
.outputs
:
2694 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2695 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2697 # set "last" of added nodes as active
2698 nodes
.active
= last_add
2699 for i
, x
, y
, dx
, h
in nodes_list
:
2700 nodes
[i
].select
= False
2705 class NWBatchChangeNodes(Operator
, NWBase
):
2706 bl_idname
= "node.nw_batch_change"
2707 bl_label
= "Batch Change"
2708 bl_description
= "Batch Change Blend Type and Math Operation"
2709 bl_options
= {'REGISTER', 'UNDO'}
2711 blend_type
: EnumProperty(
2713 items
=blend_types
+ navs
,
2715 operation
: EnumProperty(
2717 items
=operations
+ navs
,
2720 def execute(self
, context
):
2721 blend_type
= self
.blend_type
2722 operation
= self
.operation
2723 for node
in context
.selected_nodes
:
2724 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2725 if not blend_type
in [nav
[0] for nav
in navs
]:
2726 node
.blend_type
= blend_type
2728 if blend_type
== 'NEXT':
2729 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2730 #index = blend_types.index(node.blend_type)
2731 if index
== len(blend_types
) - 1:
2732 node
.blend_type
= blend_types
[0][0]
2734 node
.blend_type
= blend_types
[index
+ 1][0]
2736 if blend_type
== 'PREV':
2737 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2739 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2741 node
.blend_type
= blend_types
[index
- 1][0]
2743 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2744 if not operation
in [nav
[0] for nav
in navs
]:
2745 node
.operation
= operation
2747 if operation
== 'NEXT':
2748 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2749 #index = operations.index(node.operation)
2750 if index
== len(operations
) - 1:
2751 node
.operation
= operations
[0][0]
2753 node
.operation
= operations
[index
+ 1][0]
2755 if operation
== 'PREV':
2756 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2757 #index = operations.index(node.operation)
2759 node
.operation
= operations
[len(operations
) - 1][0]
2761 node
.operation
= operations
[index
- 1][0]
2766 class NWChangeMixFactor(Operator
, NWBase
):
2767 bl_idname
= "node.nw_factor"
2768 bl_label
= "Change Factor"
2769 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2770 bl_options
= {'REGISTER', 'UNDO'}
2772 # option: Change factor.
2773 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2774 # Else - change factor by option value.
2775 option
: FloatProperty()
2777 def execute(self
, context
):
2778 nodes
, links
= get_nodes_links(context
)
2779 option
= self
.option
2780 selected
= [] # entry = index
2781 for si
, node
in enumerate(nodes
):
2783 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2787 fac
= nodes
[si
].inputs
[0]
2788 nodes
[si
].hide
= False
2789 if option
in {0.0, 1.0}:
2790 fac
.default_value
= option
2792 fac
.default_value
+= option
2797 class NWCopySettings(Operator
, NWBase
):
2798 bl_idname
= "node.nw_copy_settings"
2799 bl_label
= "Copy Settings"
2800 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2801 bl_options
= {'REGISTER', 'UNDO'}
2804 def poll(cls
, context
):
2806 if nw_check(context
):
2808 context
.active_node
is not None and
2809 context
.active_node
.type != 'FRAME'
2814 def execute(self
, context
):
2815 node_active
= context
.active_node
2816 node_selected
= context
.selected_nodes
2819 if not (len(node_selected
) > 1):
2820 self
.report({'ERROR'}, "2 nodes must be selected at least")
2821 return {'CANCELLED'}
2823 # Check if active node is in the selection
2824 selected_node_names
= [n
.name
for n
in node_selected
]
2825 if node_active
.name
not in selected_node_names
:
2826 self
.report({'ERROR'}, "No active node")
2827 return {'CANCELLED'}
2829 # Get nodes in selection by type
2830 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2832 if not (len(valid_nodes
) > 1) and node_active
:
2833 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2834 return {'CANCELLED'}
2836 if len(valid_nodes
) != len(node_selected
):
2837 # Report nodes that are not valid
2838 valid_node_names
= [n
.name
for n
in valid_nodes
]
2839 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2840 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2842 # Reference original
2844 #node_selected_names = [n.name for n in node_selected]
2849 # Deselect all nodes
2850 for i
in node_selected
:
2853 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2854 # Run through all other nodes
2855 for node
in valid_nodes
[1:]:
2857 # Check for frame node
2858 parent
= node
.parent
if node
.parent
else None
2859 node_loc
= [node
.location
.x
, node
.location
.y
]
2861 # Select original to duplicate
2864 # Duplicate selected node
2865 bpy
.ops
.node
.duplicate()
2866 new_node
= context
.selected_nodes
[0]
2869 new_node
.select
= False
2871 # Properties to copy
2872 node_tree
= node
.id_data
2873 props_to_copy
= 'bl_idname name location height width'.split(' ')
2877 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2878 for i
in (i
for i
in mappings
if i
.is_linked
):
2880 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2883 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2884 props_to_copy
.pop(0)
2886 for prop
in props_to_copy
:
2887 setattr(new_node
, prop
, props
[prop
])
2889 # Get the node tree to remove the old node
2890 nodes
= node_tree
.nodes
2892 new_node
.name
= props
['name']
2895 new_node
.parent
= parent
2896 new_node
.location
= node_loc
2898 for str_from
, str_to
in reconnections
:
2899 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2901 success_names
.append(new_node
.name
)
2904 node_tree
.nodes
.active
= orig
2905 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2909 class NWCopyLabel(Operator
, NWBase
):
2910 bl_idname
= "node.nw_copy_label"
2911 bl_label
= "Copy Label"
2912 bl_options
= {'REGISTER', 'UNDO'}
2914 option
: EnumProperty(
2916 description
="Source of name of label",
2918 ('FROM_ACTIVE', 'from active', 'from active node',),
2919 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2920 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2924 def execute(self
, context
):
2925 nodes
, links
= get_nodes_links(context
)
2926 option
= self
.option
2927 active
= nodes
.active
2928 if option
== 'FROM_ACTIVE':
2930 src_label
= active
.label
2931 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2932 node
.label
= src_label
2933 elif option
== 'FROM_NODE':
2934 selected
= [n
for n
in nodes
if n
.select
]
2935 for node
in selected
:
2936 for input in node
.inputs
:
2938 src
= input.links
[0].from_node
2939 node
.label
= src
.label
2941 elif option
== 'FROM_SOCKET':
2942 selected
= [n
for n
in nodes
if n
.select
]
2943 for node
in selected
:
2944 for input in node
.inputs
:
2946 src
= input.links
[0].from_socket
2947 node
.label
= src
.name
2953 class NWClearLabel(Operator
, NWBase
):
2954 bl_idname
= "node.nw_clear_label"
2955 bl_label
= "Clear Label"
2956 bl_options
= {'REGISTER', 'UNDO'}
2958 option
: BoolProperty()
2960 def execute(self
, context
):
2961 nodes
, links
= get_nodes_links(context
)
2962 for node
in [n
for n
in nodes
if n
.select
]:
2967 def invoke(self
, context
, event
):
2969 return self
.execute(context
)
2971 return context
.window_manager
.invoke_confirm(self
, event
)
2974 class NWModifyLabels(Operator
, NWBase
):
2975 """Modify Labels of all selected nodes"""
2976 bl_idname
= "node.nw_modify_labels"
2977 bl_label
= "Modify Labels"
2978 bl_options
= {'REGISTER', 'UNDO'}
2980 prepend
: StringProperty(
2981 name
="Add to Beginning"
2983 append
: StringProperty(
2986 replace_from
: StringProperty(
2987 name
="Text to Replace"
2989 replace_to
: StringProperty(
2993 def execute(self
, context
):
2994 nodes
, links
= get_nodes_links(context
)
2995 for node
in [n
for n
in nodes
if n
.select
]:
2996 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
3000 def invoke(self
, context
, event
):
3004 return context
.window_manager
.invoke_props_dialog(self
)
3007 class NWAddTextureSetup(Operator
, NWBase
):
3008 bl_idname
= "node.nw_add_texture"
3009 bl_label
= "Texture Setup"
3010 bl_description
= "Add Texture Node Setup to Selected Shaders"
3011 bl_options
= {'REGISTER', 'UNDO'}
3013 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
3016 def poll(cls
, context
):
3018 if nw_check(context
):
3019 space
= context
.space_data
3020 if space
.tree_type
== 'ShaderNodeTree':
3024 def execute(self
, context
):
3025 nodes
, links
= get_nodes_links(context
)
3026 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3027 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
3028 selected_nodes
= [n
for n
in nodes
if n
.select
]
3029 for t_node
in selected_nodes
:
3033 for index
, i
in enumerate(t_node
.inputs
):
3039 locx
= t_node
.location
.x
3040 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
3042 xoffset
= [500, 700]
3044 if t_node
.type in texture_types
+ ['MAPPING']:
3045 xoffset
= [290, 500]
3049 image_type
= 'ShaderNodeTexImage'
3051 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
3052 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
3053 if t_node
.type == 'BACKGROUND':
3054 image_type
= 'ShaderNodeTexEnvironment'
3057 tex
= nodes
.new(image_type
)
3058 tex
.location
= [locx
- 200, locy
+ 112]
3060 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
3062 t_node
.select
= False
3063 if self
.add_mapping
or is_texture
:
3064 if t_node
.type != 'MAPPING':
3065 m
= nodes
.new('ShaderNodeMapping')
3066 m
.location
= [locx
- xoffset
[0], locy
+ 141]
3070 coord
= nodes
.new('ShaderNodeTexCoord')
3071 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
3074 links
.new(m
.outputs
[0], tex
.inputs
[0])
3075 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3078 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
3079 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3081 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
3085 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
3086 bl_idname
= "node.nw_add_textures_for_principled"
3087 bl_label
= "Principled Texture Setup"
3088 bl_description
= "Add Texture Node Setup for Principled BSDF"
3089 bl_options
= {'REGISTER', 'UNDO'}
3091 directory
: StringProperty(
3095 description
='Folder to search in for image files'
3097 files
: CollectionProperty(
3098 type=bpy
.types
.OperatorFileListElement
,
3099 options
={'HIDDEN', 'SKIP_SAVE'}
3102 relative_path
: BoolProperty(
3103 name
='Relative Path',
3104 description
='Set the file path relative to the blend file, when possible',
3113 def draw(self
, context
):
3114 layout
= self
.layout
3115 layout
.alignment
= 'LEFT'
3117 layout
.prop(self
, 'relative_path')
3120 def poll(cls
, context
):
3122 if nw_check(context
):
3123 space
= context
.space_data
3124 if space
.tree_type
== 'ShaderNodeTree':
3128 def execute(self
, context
):
3129 # Check if everything is ok
3130 if not self
.directory
:
3131 self
.report({'INFO'}, 'No Folder Selected')
3132 return {'CANCELLED'}
3133 if not self
.files
[:]:
3134 self
.report({'INFO'}, 'No Files Selected')
3135 return {'CANCELLED'}
3137 nodes
, links
= get_nodes_links(context
)
3138 active_node
= nodes
.active
3139 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
3140 self
.report({'INFO'}, 'Select Principled BSDF')
3141 return {'CANCELLED'}
3144 def split_into__components(fname
):
3145 # Split filename into components
3146 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3148 fname
= path
.splitext(fname
)[0]
3150 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
3151 # Separate CamelCase by space
3152 fname
= re
.sub(r
"([a-z])([A-Z])", r
"\g<1> \g<2>",fname
)
3153 # Replace common separators with SPACE
3154 separators
= ['_', '.', '-', '__', '--', '#']
3155 for sep
in separators
:
3156 fname
= fname
.replace(sep
, ' ')
3158 components
= fname
.split(' ')
3159 components
= [c
.lower() for c
in components
]
3162 # Filter textures names for texturetypes in filenames
3163 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3164 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
3165 normal_abbr
= tags
.normal
.split(' ')
3166 bump_abbr
= tags
.bump
.split(' ')
3167 gloss_abbr
= tags
.gloss
.split(' ')
3168 rough_abbr
= tags
.rough
.split(' ')
3170 ['Displacement', tags
.displacement
.split(' '), None],
3171 ['Base Color', tags
.base_color
.split(' '), None],
3172 ['Subsurface Color', tags
.sss_color
.split(' '), None],
3173 ['Metallic', tags
.metallic
.split(' '), None],
3174 ['Specular', tags
.specular
.split(' '), None],
3175 ['Roughness', rough_abbr
+ gloss_abbr
, None],
3176 ['Normal', normal_abbr
+ bump_abbr
, None],
3177 ['Transmission', tags
.transmission
.split(' '), None],
3178 ['Emission', tags
.emission
.split(' '), None],
3179 ['Alpha', tags
.alpha
.split(' '), None],
3180 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
3183 # Look through texture_types and set value as filename of first matched file
3184 def match_files_to_socket_names():
3185 for sname
in socketnames
:
3186 for file in self
.files
:
3188 filenamecomponents
= split_into__components(fname
)
3189 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
3190 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3195 match_files_to_socket_names()
3196 # Remove socketnames without found files
3197 socketnames
= [s
for s
in socketnames
if s
[2]
3198 and path
.exists(self
.directory
+s
[2])]
3200 self
.report({'INFO'}, 'No matching images found')
3201 print('No matching images found')
3202 return {'CANCELLED'}
3204 # Don't override path earlier as os.path is used to check the absolute path
3205 import_path
= self
.directory
3206 if self
.relative_path
:
3207 if bpy
.data
.filepath
:
3209 import_path
= bpy
.path
.relpath(self
.directory
)
3214 print('\nMatched Textures:')
3219 roughness_node
= None
3220 for i
, sname
in enumerate(socketnames
):
3221 print(i
, sname
[0], sname
[2])
3223 # DISPLACEMENT NODES
3224 if sname
[0] == 'Displacement':
3225 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
3226 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3227 disp_texture
.image
= img
3228 disp_texture
.label
= 'Displacement'
3229 if disp_texture
.image
:
3230 disp_texture
.image
.colorspace_settings
.is_data
= True
3232 # Add displacement offset nodes
3233 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
3234 # Align the Displacement node under the active Principled BSDF node
3235 disp_node
.location
= active_node
.location
+ Vector((100, -700))
3236 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
3238 # TODO Turn on true displacement in the material
3239 # Too complicated for now
3242 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
3244 if not output_node
[0].inputs
[2].is_linked
:
3245 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
3249 # AMBIENT OCCLUSION TEXTURE
3250 if sname
[0] == 'Ambient Occlusion':
3251 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
3252 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3253 ao_texture
.image
= img
3254 ao_texture
.label
= sname
[0]
3255 if ao_texture
.image
:
3256 ao_texture
.image
.colorspace_settings
.is_data
= True
3260 if not active_node
.inputs
[sname
[0]].is_linked
:
3261 # No texture node connected -> add texture node with new image
3262 texture_node
= nodes
.new(type='ShaderNodeTexImage')
3263 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3264 texture_node
.image
= img
3267 if sname
[0] == 'Normal':
3268 # Test if new texture node is normal or bump map
3269 fname_components
= split_into__components(sname
[2])
3270 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
3271 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
3273 # If Normal add normal node in between
3274 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
3275 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
3277 # If Bump add bump node in between
3278 normal_node
= nodes
.new(type='ShaderNodeBump')
3279 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
3281 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
3282 normal_node_texture
= texture_node
3284 elif sname
[0] == 'Roughness':
3285 # Test if glossy or roughness map
3286 fname_components
= split_into__components(sname
[2])
3287 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
3288 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
3291 # If Roughness nothing to to
3292 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3295 # If Gloss Map add invert node
3296 invert_node
= nodes
.new(type='ShaderNodeInvert')
3297 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
3299 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
3300 roughness_node
= texture_node
3303 # This is a simple connection Texture --> Input slot
3304 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3306 # Use non-color for all but 'Base Color' Textures
3307 if not sname
[0] in ['Base Color', 'Emission'] and texture_node
.image
:
3308 texture_node
.image
.colorspace_settings
.is_data
= True
3311 # If already texture connected. add to node list for alignment
3312 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
3314 # This are all connected texture nodes
3315 texture_nodes
.append(texture_node
)
3316 texture_node
.label
= sname
[0]
3319 texture_nodes
.append(disp_texture
)
3322 # We want the ambient occlusion texture to be the top most texture node
3323 texture_nodes
.insert(0, ao_texture
)
3326 for i
, texture_node
in enumerate(texture_nodes
):
3327 offset
= Vector((-550, (i
* -280) + 200))
3328 texture_node
.location
= active_node
.location
+ offset
3331 # Extra alignment if normal node was added
3332 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
3335 # Alignment of invert node if glossy map
3336 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
3338 # Add texture input + mapping
3339 mapping
= nodes
.new(type='ShaderNodeMapping')
3340 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
3341 if len(texture_nodes
) > 1:
3342 # If more than one texture add reroute node in between
3343 reroute
= nodes
.new(type='NodeReroute')
3344 texture_nodes
.append(reroute
)
3345 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
3346 reroute
.location
= tex_coords
+ Vector((-50, -120))
3347 for texture_node
in texture_nodes
:
3348 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
3349 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
3351 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
3353 # Connect texture_coordiantes to mapping node
3354 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
3355 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
3356 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
3358 # Create frame around tex coords and mapping
3359 frame
= nodes
.new(type='NodeFrame')
3360 frame
.label
= 'Mapping'
3361 mapping
.parent
= frame
3362 texture_input
.parent
= frame
3365 # Create frame around texture nodes
3366 frame
= nodes
.new(type='NodeFrame')
3367 frame
.label
= 'Textures'
3368 for tnode
in texture_nodes
:
3369 tnode
.parent
= frame
3373 active_node
.select
= False
3376 force_update(context
)
3380 class NWAddReroutes(Operator
, NWBase
):
3381 """Add Reroute Nodes and link them to outputs of selected nodes"""
3382 bl_idname
= "node.nw_add_reroutes"
3383 bl_label
= "Add Reroutes"
3384 bl_description
= "Add Reroutes to Outputs"
3385 bl_options
= {'REGISTER', 'UNDO'}
3387 option
: EnumProperty(
3390 ('ALL', 'to all', 'Add to all outputs'),
3391 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3392 ('LINKED', 'to linked', 'Add only to linked outputs'),
3396 def execute(self
, context
):
3397 tree_type
= context
.space_data
.node_tree
.type
3398 option
= self
.option
3399 nodes
, links
= get_nodes_links(context
)
3400 # output valid when option is 'all' or when 'loose' output has no links
3402 post_select
= [] # nodes to be selected after execution
3403 # create reroutes and recreate links
3404 for node
in [n
for n
in nodes
if n
.select
]:
3409 # unhide 'REROUTE' nodes to avoid issues with location.y
3410 if node
.type == 'REROUTE':
3412 # When node is hidden - width_hidden not usable.
3413 # Hack needed to calculate real width
3415 bpy
.ops
.node
.select_all(action
='DESELECT')
3416 helper
= nodes
.new('NodeReroute')
3417 helper
.select
= True
3419 # resize node and helper to zero. Then check locations to calculate width
3420 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
3421 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
3422 # restore node location
3423 node
.location
= x
, y
3426 # only helper is selected now
3427 bpy
.ops
.node
.delete()
3428 x
= node
.location
.x
+ width
+ 20.0
3429 if node
.type != 'REROUTE':
3433 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3434 for out_i
, output
in enumerate(node
.outputs
):
3435 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3436 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3437 if node
.type != 'R_LAYERS':
3439 else: # if 'R_LAYERS' check if output represent used render pass
3440 node_scene
= node
.scene
3441 node_layer
= node
.layer
3442 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3443 if output
.name
== 'Alpha':
3446 # check entries in global 'rl_outputs' variable
3447 for rlo
in rl_outputs
:
3448 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3449 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3452 valid
= ((option
== 'ALL') or
3453 (option
== 'LOOSE' and not output
.links
) or
3454 (option
== 'LINKED' and output
.links
))
3455 # Add reroutes only if valid, but offset location in all cases.
3457 n
= nodes
.new('NodeReroute')
3459 for link
in output
.links
:
3460 links
.new(n
.outputs
[0], link
.to_socket
)
3461 links
.new(output
, n
.inputs
[0])
3463 post_select
.append(n
)
3467 # disselect the node so that after execution of script only newly created nodes are selected
3469 # nicer reroutes distribution along y when node.hide
3471 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3472 for reroute
in [r
for r
in nodes
if r
.select
]:
3473 reroute
.location
.y
-= y_translate
3474 for node
in post_select
:
3480 class NWLinkActiveToSelected(Operator
, NWBase
):
3481 """Link active node to selected nodes basing on various criteria"""
3482 bl_idname
= "node.nw_link_active_to_selected"
3483 bl_label
= "Link Active Node to Selected"
3484 bl_options
= {'REGISTER', 'UNDO'}
3486 replace
: BoolProperty()
3487 use_node_name
: BoolProperty()
3488 use_outputs_names
: BoolProperty()
3491 def poll(cls
, context
):
3493 if nw_check(context
):
3494 if context
.active_node
is not None:
3495 if context
.active_node
.select
:
3499 def execute(self
, context
):
3500 nodes
, links
= get_nodes_links(context
)
3501 replace
= self
.replace
3502 use_node_name
= self
.use_node_name
3503 use_outputs_names
= self
.use_outputs_names
3504 active
= nodes
.active
3505 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3506 outputs
= [] # Only usable outputs of active nodes will be stored here.
3507 for out
in active
.outputs
:
3508 if active
.type != 'R_LAYERS':
3511 # 'R_LAYERS' node type needs special handling.
3512 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3513 # Only outputs that represent used passes should be taken into account
3514 # Check if pass represented by output is used.
3515 # global 'rl_outputs' list will be used for that
3516 for rlo
in rl_outputs
:
3517 pass_used
= False # initial value. Will be set to True if pass is used
3518 if out
.name
== 'Alpha':
3519 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3521 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3522 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3523 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3527 doit
= True # Will be changed to False when links successfully added to previous output.
3530 for node
in selected
:
3531 dst_name
= node
.name
# Will be compared with src_name if needed.
3532 # When node has label - use it as dst_name
3534 dst_name
= node
.label
3535 valid
= True # Initial value. Will be changed to False if names don't match.
3536 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
3538 # Set src_name to source node name or label
3539 src_name
= active
.name
3541 src_name
= active
.label
3542 elif use_outputs_names
:
3543 src_name
= (out
.name
, )
3544 for rlo
in rl_outputs
:
3545 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3546 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3547 if dst_name
not in src_name
:
3550 for input in node
.inputs
:
3551 if input.type == out
.type or node
.type == 'REROUTE':
3552 if replace
or not input.is_linked
:
3553 links
.new(out
, input)
3554 if not use_node_name
and not use_outputs_names
:
3561 class NWAlignNodes(Operator
, NWBase
):
3562 '''Align the selected nodes neatly in a row/column'''
3563 bl_idname
= "node.nw_align_nodes"
3564 bl_label
= "Align Nodes"
3565 bl_options
= {'REGISTER', 'UNDO'}
3566 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3568 def execute(self
, context
):
3569 nodes
, links
= get_nodes_links(context
)
3570 margin
= self
.margin
3574 if node
.select
and node
.type != 'FRAME':
3575 selection
.append(node
)
3577 # If no nodes are selected, align all nodes
3581 elif nodes
.active
in selection
:
3582 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3584 # Check if nodes should be laid out horizontally or vertically
3585 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3586 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3587 x_range
= max(x_locs
) - min(x_locs
)
3588 y_range
= max(y_locs
) - min(y_locs
)
3589 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3590 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3591 horizontal
= x_range
> y_range
3593 # Sort selection by location of node mid-point
3595 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3597 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3601 for node
in selection
:
3602 current_margin
= margin
3603 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3606 node
.location
.x
= current_pos
3607 current_pos
+= current_margin
+ node
.dimensions
.x
3608 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3610 node
.location
.y
= current_pos
3611 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3612 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3614 # If active node is selected, center nodes around it
3615 if active_loc
is not None:
3616 active_loc_diff
= active_loc
- nodes
.active
.location
3617 for node
in selection
:
3618 node
.location
+= active_loc_diff
3619 else: # Position nodes centered around where they used to be
3620 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
])
3621 new_mid
= (max(locs
) + min(locs
)) / 2
3622 for node
in selection
:
3624 node
.location
.x
+= (mid_x
- new_mid
)
3626 node
.location
.y
+= (mid_y
- new_mid
)
3631 class NWSelectParentChildren(Operator
, NWBase
):
3632 bl_idname
= "node.nw_select_parent_child"
3633 bl_label
= "Select Parent or Children"
3634 bl_options
= {'REGISTER', 'UNDO'}
3636 option
: EnumProperty(
3639 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3640 ('CHILD', 'Select Children', 'Select members of selected frame'),
3644 def execute(self
, context
):
3645 nodes
, links
= get_nodes_links(context
)
3646 option
= self
.option
3647 selected
= [node
for node
in nodes
if node
.select
]
3648 if option
== 'PARENT':
3649 for sel
in selected
:
3652 parent
.select
= True
3653 else: # option == 'CHILD'
3654 for sel
in selected
:
3655 children
= [node
for node
in nodes
if node
.parent
== sel
]
3656 for kid
in children
:
3662 class NWDetachOutputs(Operator
, NWBase
):
3663 """Detach outputs of selected node leaving inputs linked"""
3664 bl_idname
= "node.nw_detach_outputs"
3665 bl_label
= "Detach Outputs"
3666 bl_options
= {'REGISTER', 'UNDO'}
3668 def execute(self
, context
):
3669 nodes
, links
= get_nodes_links(context
)
3670 selected
= context
.selected_nodes
3671 bpy
.ops
.node
.duplicate_move_keep_inputs()
3672 new_nodes
= context
.selected_nodes
3673 bpy
.ops
.node
.select_all(action
="DESELECT")
3674 for node
in selected
:
3676 bpy
.ops
.node
.delete_reconnect()
3677 for new_node
in new_nodes
:
3678 new_node
.select
= True
3679 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3684 class NWLinkToOutputNode(Operator
):
3685 """Link to Composite node or Material Output node"""
3686 bl_idname
= "node.nw_link_out"
3687 bl_label
= "Connect to Output"
3688 bl_options
= {'REGISTER', 'UNDO'}
3691 def poll(cls
, context
):
3693 if nw_check(context
):
3694 if context
.active_node
is not None:
3695 for out
in context
.active_node
.outputs
:
3696 if is_visible_socket(out
):
3701 def execute(self
, context
):
3702 nodes
, links
= get_nodes_links(context
)
3703 active
= nodes
.active
3706 tree_type
= context
.space_data
.tree_type
3707 if tree_type
== 'ShaderNodeTree':
3708 output_types
= [x
[1] for x
in shaders_output_nodes_props
] + ['OUTPUT']
3709 elif tree_type
== 'CompositorNodeTree':
3710 output_types
= ['COMPOSITE']
3711 elif tree_type
== 'TextureNodeTree':
3712 output_types
= ['OUTPUT']
3713 elif tree_type
== 'GeometryNodeTree':
3714 output_types
= ['GROUP_OUTPUT']
3716 if node
.type in output_types
:
3720 bpy
.ops
.node
.select_all(action
="DESELECT")
3721 if tree_type
== 'ShaderNodeTree':
3722 if context
.space_data
.shader_type
== 'OBJECT':
3723 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3724 elif context
.space_data
.shader_type
== 'WORLD':
3725 output_node
= nodes
.new('ShaderNodeOutputWorld')
3726 elif tree_type
== 'CompositorNodeTree':
3727 output_node
= nodes
.new('CompositorNodeComposite')
3728 elif tree_type
== 'TextureNodeTree':
3729 output_node
= nodes
.new('TextureNodeOutput')
3730 elif tree_type
== 'GeometryNodeTree':
3731 output_node
= nodes
.new('NodeGroupOutput')
3732 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3733 output_node
.location
.y
= active
.location
.y
3734 if (output_node
and active
.outputs
):
3735 for i
, output
in enumerate(active
.outputs
):
3736 if is_visible_socket(output
):
3739 for i
, output
in enumerate(active
.outputs
):
3740 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3745 if tree_type
== 'ShaderNodeTree':
3746 if active
.outputs
[output_index
].name
== 'Volume':
3748 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3750 elif tree_type
== 'GeometryNodeTree':
3751 if active
.outputs
[output_index
].type != 'GEOMETRY':
3752 return {'CANCELLED'}
3753 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3755 force_update(context
) # viewport render does not update
3760 class NWMakeLink(Operator
, NWBase
):
3761 """Make a link from one socket to another"""
3762 bl_idname
= 'node.nw_make_link'
3763 bl_label
= 'Make Link'
3764 bl_options
= {'REGISTER', 'UNDO'}
3765 from_socket
: IntProperty()
3766 to_socket
: IntProperty()
3768 def execute(self
, context
):
3769 nodes
, links
= get_nodes_links(context
)
3771 n1
= nodes
[context
.scene
.NWLazySource
]
3772 n2
= nodes
[context
.scene
.NWLazyTarget
]
3774 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3776 force_update(context
)
3781 class NWCallInputsMenu(Operator
, NWBase
):
3782 """Link from this output"""
3783 bl_idname
= 'node.nw_call_inputs_menu'
3784 bl_label
= 'Make Link'
3785 bl_options
= {'REGISTER', 'UNDO'}
3786 from_socket
: IntProperty()
3788 def execute(self
, context
):
3789 nodes
, links
= get_nodes_links(context
)
3791 context
.scene
.NWSourceSocket
= self
.from_socket
3793 n1
= nodes
[context
.scene
.NWLazySource
]
3794 n2
= nodes
[context
.scene
.NWLazyTarget
]
3795 if len(n2
.inputs
) > 1:
3796 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3797 elif len(n2
.inputs
) == 1:
3798 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3802 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3803 """Add an Image Sequence"""
3804 bl_idname
= 'node.nw_add_sequence'
3805 bl_label
= 'Import Image Sequence'
3806 bl_options
= {'REGISTER', 'UNDO'}
3808 directory
: StringProperty(
3811 filename
: StringProperty(
3814 files
: CollectionProperty(
3815 type=bpy
.types
.OperatorFileListElement
,
3816 options
={'HIDDEN', 'SKIP_SAVE'}
3818 relative_path
: BoolProperty(
3819 name
='Relative Path',
3820 description
='Set the file path relative to the blend file, when possible',
3824 def draw(self
, context
):
3825 layout
= self
.layout
3826 layout
.alignment
= 'LEFT'
3828 layout
.prop(self
, 'relative_path')
3830 def execute(self
, context
):
3831 nodes
, links
= get_nodes_links(context
)
3832 directory
= self
.directory
3833 filename
= self
.filename
3835 tree
= context
.space_data
.node_tree
3838 # print ("\nDIR:", directory)
3839 # print ("FN:", filename)
3840 # print ("Fs:", list(f.name for f in files), '\n')
3842 if tree
.type == 'SHADER':
3843 node_type
= "ShaderNodeTexImage"
3844 elif tree
.type == 'COMPOSITING':
3845 node_type
= "CompositorNodeImage"
3847 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3848 return {'CANCELLED'}
3850 if not files
[0].name
and not filename
:
3851 self
.report({'ERROR'}, "No file chosen")
3852 return {'CANCELLED'}
3853 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3854 # User has selected multiple files without an active one, or the active one is non-existant
3855 filename
= files
[0].name
3857 if not path
.exists(directory
+filename
):
3858 self
.report({'ERROR'}, filename
+" does not exist!")
3859 return {'CANCELLED'}
3861 without_ext
= '.'.join(filename
.split('.')[:-1])
3863 # if last digit isn't a number, it's not a sequence
3864 if not without_ext
[-1].isdigit():
3865 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3866 return {'CANCELLED'}
3869 extension
= filename
.split('.')[-1]
3870 reverse
= without_ext
[::-1] # reverse string
3873 for char
in reverse
:
3879 without_num
= without_ext
[:count_numbers
*-1]
3881 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3883 num_frames
= len(files
)
3885 nodes_list
= [node
for node
in nodes
]
3887 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3888 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3892 yloc
+= node_mid_pt(node
, 'y')
3893 yloc
= yloc
/len(nodes
)
3898 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3900 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3902 node
.label
= name_with_hashes
3904 filepath
= directory
+(without_ext
+'.'+extension
)
3905 if self
.relative_path
:
3906 if bpy
.data
.filepath
:
3908 filepath
= bpy
.path
.relpath(filepath
)
3912 img
= bpy
.data
.images
.load(filepath
)
3913 img
.source
= 'SEQUENCE'
3914 img
.name
= name_with_hashes
3916 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3917 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
3918 image_user
.frame_duration
= num_frames
3923 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3924 """Add multiple images at once"""
3925 bl_idname
= 'node.nw_add_multiple_images'
3926 bl_label
= 'Open Selected Images'
3927 bl_options
= {'REGISTER', 'UNDO'}
3928 directory
: StringProperty(
3931 files
: CollectionProperty(
3932 type=bpy
.types
.OperatorFileListElement
,
3933 options
={'HIDDEN', 'SKIP_SAVE'}
3936 def execute(self
, context
):
3937 nodes
, links
= get_nodes_links(context
)
3939 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3941 if context
.space_data
.node_tree
.type == 'SHADER':
3942 node_type
= "ShaderNodeTexImage"
3943 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3944 node_type
= "CompositorNodeImage"
3946 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3947 return {'CANCELLED'}
3950 for f
in self
.files
:
3953 node
= nodes
.new(node_type
)
3954 new_nodes
.append(node
)
3957 node
.width_hidden
= 100
3958 node
.location
.x
= xloc
3959 node
.location
.y
= yloc
3962 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3965 # shift new nodes up to center of tree
3966 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3968 if node
in new_nodes
:
3970 node
.location
.y
+= (list_size
/2)
3976 class NWViewerFocus(bpy
.types
.Operator
):
3977 """Set the viewer tile center to the mouse position"""
3978 bl_idname
= "node.nw_viewer_focus"
3979 bl_label
= "Viewer Focus"
3981 x
: bpy
.props
.IntProperty()
3982 y
: bpy
.props
.IntProperty()
3985 def poll(cls
, context
):
3986 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
3988 def execute(self
, context
):
3991 def invoke(self
, context
, event
):
3992 render
= context
.scene
.render
3993 space
= context
.space_data
3994 percent
= render
.resolution_percentage
*0.01
3996 nodes
, links
= get_nodes_links(context
)
3997 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
4000 mlocx
= event
.mouse_region_x
4001 mlocy
= event
.mouse_region_y
4002 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
4004 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
4005 region_x
= context
.region
.width
4006 region_y
= context
.region
.height
4008 region_center_x
= context
.region
.width
/ 2
4009 region_center_y
= context
.region
.height
/ 2
4011 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
4012 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
4014 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
4015 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
4017 margin_x
= region_center_x
- backdrop_center_x
4018 margin_y
= region_center_y
- backdrop_center_y
4020 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
4021 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
4023 for node
in viewers
:
4024 node
.center_x
= abs_mouse_x
4025 node
.center_y
= abs_mouse_y
4027 return {'PASS_THROUGH'}
4029 return self
.execute(context
)
4032 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
4033 """Save the current viewer node to an image file"""
4034 bl_idname
= "node.nw_save_viewer"
4035 bl_label
= "Save This Image"
4036 filepath
: StringProperty(subtype
="FILE_PATH")
4037 filename_ext
: EnumProperty(
4039 description
="Choose the file format to save to",
4040 items
=(('.bmp', "BMP", ""),
4041 ('.rgb', 'IRIS', ""),
4042 ('.png', 'PNG', ""),
4043 ('.jpg', 'JPEG', ""),
4044 ('.jp2', 'JPEG2000', ""),
4045 ('.tga', 'TARGA', ""),
4046 ('.cin', 'CINEON', ""),
4047 ('.dpx', 'DPX', ""),
4048 ('.exr', 'OPEN_EXR', ""),
4049 ('.hdr', 'HDR', ""),
4050 ('.tif', 'TIFF', "")),
4055 def poll(cls
, context
):
4057 if nw_check(context
):
4058 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4059 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
4060 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
4064 def execute(self
, context
):
4081 basename
, ext
= path
.splitext(fp
)
4082 old_render_format
= context
.scene
.render
.image_settings
.file_format
4083 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
4084 context
.area
.type = "IMAGE_EDITOR"
4085 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
4086 context
.area
.spaces
[0].image
.save_render(fp
)
4087 context
.area
.type = "NODE_EDITOR"
4088 context
.scene
.render
.image_settings
.file_format
= old_render_format
4092 class NWResetNodes(bpy
.types
.Operator
):
4093 """Reset Nodes in Selection"""
4094 bl_idname
= "node.nw_reset_nodes"
4095 bl_label
= "Reset Nodes"
4096 bl_options
= {'REGISTER', 'UNDO'}
4099 def poll(cls
, context
):
4100 space
= context
.space_data
4101 return space
.type == 'NODE_EDITOR'
4103 def execute(self
, context
):
4104 node_active
= context
.active_node
4105 node_selected
= context
.selected_nodes
4106 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4108 # Check if one node is selected at least
4109 if not (len(node_selected
) > 0):
4110 self
.report({'ERROR'}, "1 node must be selected at least")
4111 return {'CANCELLED'}
4113 active_node_name
= node_active
.name
if node_active
.select
else None
4114 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
4116 # Create output lists
4117 selected_node_names
= [n
.name
for n
in node_selected
]
4120 # Reset all valid children in a frame
4121 node_active_is_frame
= False
4122 if len(node_selected
) == 1 and node_active
.type == "FRAME":
4123 node_tree
= node_active
.id_data
4124 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
4126 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
4127 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
4128 node_active_is_frame
= True
4130 # Check if valid nodes in selection
4131 if not (len(valid_nodes
) > 0):
4132 # Check for frames only
4133 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
4134 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
4135 self
.report({'ERROR'}, "Please select only 1 frame to reset")
4137 self
.report({'ERROR'}, "No valid node(s) in selection")
4138 return {'CANCELLED'}
4140 # Report nodes that are not valid
4141 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
4142 valid_node_names
= [n
.name
for n
in valid_nodes
]
4143 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
4144 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
4146 # Deselect all nodes
4147 for i
in node_selected
:
4150 # Run through all valid nodes
4151 for node
in valid_nodes
:
4153 parent
= node
.parent
if node
.parent
else None
4154 node_loc
= [node
.location
.x
, node
.location
.y
]
4156 node_tree
= node
.id_data
4157 props_to_copy
= 'bl_idname name location height width'.split(' ')
4160 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
4161 for i
in (i
for i
in mappings
if i
.is_linked
):
4163 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
4165 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
4167 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
4168 props_to_copy
.pop(0)
4170 for prop
in props_to_copy
:
4171 setattr(new_node
, prop
, props
[prop
])
4173 nodes
= node_tree
.nodes
4175 new_node
.name
= props
['name']
4178 new_node
.parent
= parent
4179 new_node
.location
= node_loc
4181 for str_from
, str_to
in reconnections
:
4182 node_tree
.links
.new(eval(str_from
), eval(str_to
))
4184 new_node
.select
= False
4185 success_names
.append(new_node
.name
)
4187 # Reselect all nodes
4188 if selected_node_names
and node_active_is_frame
is False:
4189 for i
in selected_node_names
:
4190 node_tree
.nodes
[i
].select
= True
4192 if active_node_name
is not None:
4193 node_tree
.nodes
[active_node_name
].select
= True
4194 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
4196 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
4204 def drawlayout(context
, layout
, mode
='non-panel'):
4205 tree_type
= context
.space_data
.tree_type
4207 col
= layout
.column(align
=True)
4208 col
.menu(NWMergeNodesMenu
.bl_idname
)
4211 col
= layout
.column(align
=True)
4212 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
4215 if tree_type
== 'ShaderNodeTree':
4216 col
= layout
.column(align
=True)
4217 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
4218 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
4221 col
= layout
.column(align
=True)
4222 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
4223 col
.operator(NWSwapLinks
.bl_idname
)
4224 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
4227 col
= layout
.column(align
=True)
4228 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
4229 if tree_type
!= 'GeometryNodeTree':
4230 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
4233 col
= layout
.column(align
=True)
4235 row
= col
.row(align
=True)
4236 row
.operator(NWClearLabel
.bl_idname
).option
= True
4237 row
.operator(NWModifyLabels
.bl_idname
)
4239 col
.operator(NWClearLabel
.bl_idname
).option
= True
4240 col
.operator(NWModifyLabels
.bl_idname
)
4241 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
4243 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
4246 col
= layout
.column(align
=True)
4247 if tree_type
== 'CompositorNodeTree':
4248 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
4249 if tree_type
!= 'GeometryNodeTree':
4250 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
4253 col
= layout
.column(align
=True)
4254 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
4257 col
= layout
.column(align
=True)
4258 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
4261 col
= layout
.column(align
=True)
4262 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
4266 class NodeWranglerPanel(Panel
, NWBase
):
4267 bl_idname
= "NODE_PT_nw_node_wrangler"
4268 bl_space_type
= 'NODE_EDITOR'
4269 bl_label
= "Node Wrangler"
4270 bl_region_type
= "UI"
4271 bl_category
= "Node Wrangler"
4273 prepend
: StringProperty(
4276 append
: StringProperty()
4277 remove
: StringProperty()
4279 def draw(self
, context
):
4280 self
.layout
.label(text
="(Quick access: Shift+W)")
4281 drawlayout(context
, self
.layout
, mode
='panel')
4287 class NodeWranglerMenu(Menu
, NWBase
):
4288 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
4289 bl_label
= "Node Wrangler"
4291 def draw(self
, context
):
4292 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
4293 drawlayout(context
, self
.layout
)
4296 class NWMergeNodesMenu(Menu
, NWBase
):
4297 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
4298 bl_label
= "Merge Selected Nodes"
4300 def draw(self
, context
):
4301 type = context
.space_data
.tree_type
4302 layout
= self
.layout
4303 if type == 'ShaderNodeTree':
4304 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
4305 if type == 'GeometryNodeTree':
4306 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
4307 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4309 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
4310 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4311 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
4313 props
.merge_type
= 'ZCOMBINE'
4314 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
4316 props
.merge_type
= 'ALPHAOVER'
4318 class NWMergeGeometryMenu(Menu
, NWBase
):
4319 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
4320 bl_label
= "Merge Selected Nodes using Geometry Nodes"
4321 def draw(self
, context
):
4322 layout
= self
.layout
4323 # The boolean node + Join Geometry node
4324 for type, name
, description
in geo_combine_operations
:
4325 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4327 props
.merge_type
= 'GEOMETRY'
4329 class NWMergeShadersMenu(Menu
, NWBase
):
4330 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
4331 bl_label
= "Merge Selected Nodes using Shaders"
4333 def draw(self
, context
):
4334 layout
= self
.layout
4335 for type in ('MIX', 'ADD'):
4336 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
4338 props
.merge_type
= 'SHADER'
4341 class NWMergeMixMenu(Menu
, NWBase
):
4342 bl_idname
= "NODE_MT_nw_merge_mix_menu"
4343 bl_label
= "Merge Selected Nodes using Mix"
4345 def draw(self
, context
):
4346 layout
= self
.layout
4347 for type, name
, description
in blend_types
:
4348 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4350 props
.merge_type
= 'MIX'
4353 class NWConnectionListOutputs(Menu
, NWBase
):
4354 bl_idname
= "NODE_MT_nw_connection_list_out"
4357 def draw(self
, context
):
4358 layout
= self
.layout
4359 nodes
, links
= get_nodes_links(context
)
4361 n1
= nodes
[context
.scene
.NWLazySource
]
4362 for index
, output
in enumerate(n1
.outputs
):
4363 # Only show sockets that are exposed.
4365 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=output
.name
, icon
="RADIOBUT_OFF").from_socket
=index
4368 class NWConnectionListInputs(Menu
, NWBase
):
4369 bl_idname
= "NODE_MT_nw_connection_list_in"
4372 def draw(self
, context
):
4373 layout
= self
.layout
4374 nodes
, links
= get_nodes_links(context
)
4376 n2
= nodes
[context
.scene
.NWLazyTarget
]
4378 for index
, input in enumerate(n2
.inputs
):
4379 # Only show sockets that are exposed.
4380 # This prevents, for example, the scale value socket
4381 # of the vector math node being added to the list when
4382 # the mode is not 'SCALE'.
4384 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=input.name
, icon
="FORWARD")
4385 op
.from_socket
= context
.scene
.NWSourceSocket
4386 op
.to_socket
= index
4389 class NWMergeMathMenu(Menu
, NWBase
):
4390 bl_idname
= "NODE_MT_nw_merge_math_menu"
4391 bl_label
= "Merge Selected Nodes using Math"
4393 def draw(self
, context
):
4394 layout
= self
.layout
4395 for type, name
, description
in operations
:
4396 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4398 props
.merge_type
= 'MATH'
4401 class NWBatchChangeNodesMenu(Menu
, NWBase
):
4402 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
4403 bl_label
= "Batch Change Selected Nodes"
4405 def draw(self
, context
):
4406 layout
= self
.layout
4407 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
4408 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
4411 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
4412 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
4413 bl_label
= "Batch Change Blend Type"
4415 def draw(self
, context
):
4416 layout
= self
.layout
4417 for type, name
, description
in blend_types
:
4418 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4419 props
.blend_type
= type
4420 props
.operation
= 'CURRENT'
4423 class NWBatchChangeOperationMenu(Menu
, NWBase
):
4424 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
4425 bl_label
= "Batch Change Math Operation"
4427 def draw(self
, context
):
4428 layout
= self
.layout
4429 for type, name
, description
in operations
:
4430 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4431 props
.blend_type
= 'CURRENT'
4432 props
.operation
= type
4435 class NWCopyToSelectedMenu(Menu
, NWBase
):
4436 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4437 bl_label
= "Copy to Selected"
4439 def draw(self
, context
):
4440 layout
= self
.layout
4441 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4442 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4445 class NWCopyLabelMenu(Menu
, NWBase
):
4446 bl_idname
= "NODE_MT_nw_copy_label_menu"
4447 bl_label
= "Copy Label"
4449 def draw(self
, context
):
4450 layout
= self
.layout
4451 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4452 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4453 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4456 class NWAddReroutesMenu(Menu
, NWBase
):
4457 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4458 bl_label
= "Add Reroutes"
4459 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4461 def draw(self
, context
):
4462 layout
= self
.layout
4463 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4464 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4465 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4468 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4469 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4470 bl_label
= "Link Active to Selected"
4472 def draw(self
, context
):
4473 layout
= self
.layout
4474 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4475 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4476 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4479 class NWLinkStandardMenu(Menu
, NWBase
):
4480 bl_idname
= "NODE_MT_nw_link_standard_menu"
4481 bl_label
= "To All Selected"
4483 def draw(self
, context
):
4484 layout
= self
.layout
4485 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4486 props
.replace
= False
4487 props
.use_node_name
= False
4488 props
.use_outputs_names
= False
4489 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4490 props
.replace
= True
4491 props
.use_node_name
= False
4492 props
.use_outputs_names
= False
4495 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4496 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4497 bl_label
= "Use Node Name/Label"
4499 def draw(self
, context
):
4500 layout
= self
.layout
4501 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4502 props
.replace
= False
4503 props
.use_node_name
= True
4504 props
.use_outputs_names
= False
4505 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4506 props
.replace
= True
4507 props
.use_node_name
= True
4508 props
.use_outputs_names
= False
4511 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4512 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4513 bl_label
= "Use Outputs Names"
4515 def draw(self
, context
):
4516 layout
= self
.layout
4517 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4518 props
.replace
= False
4519 props
.use_node_name
= False
4520 props
.use_outputs_names
= True
4521 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4522 props
.replace
= True
4523 props
.use_node_name
= False
4524 props
.use_outputs_names
= True
4527 class NWAttributeMenu(bpy
.types
.Menu
):
4528 bl_idname
= "NODE_MT_nw_node_attribute_menu"
4529 bl_label
= "Attributes"
4532 def poll(cls
, context
):
4534 if nw_check(context
):
4535 snode
= context
.space_data
4536 valid
= snode
.tree_type
== 'ShaderNodeTree'
4539 def draw(self
, context
):
4541 nodes
, links
= get_nodes_links(context
)
4542 mat
= context
.object.active_material
4545 for obj
in bpy
.data
.objects
:
4546 for slot
in obj
.material_slots
:
4547 if slot
.material
== mat
:
4551 if obj
.data
.attributes
:
4552 for attr
in obj
.data
.attributes
:
4553 attrs
.append(attr
.name
)
4554 attrs
= list(set(attrs
)) # get a unique list
4558 l
.operator(NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
4560 l
.label(text
="No attributes on objects with this material")
4563 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4564 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4565 bl_label
= "Switch Type to..."
4567 def draw(self
, context
):
4568 layout
= self
.layout
4569 tree
= context
.space_data
.node_tree
4570 if tree
.type == 'SHADER':
4571 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4572 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4573 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4574 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4575 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4576 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4577 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4578 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4579 if tree
.type == 'COMPOSITING':
4580 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4581 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4582 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4583 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4584 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4585 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4586 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4587 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4588 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4589 if tree
.type == 'TEXTURE':
4590 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4591 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4592 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4593 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4594 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4595 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4596 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4597 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4598 if tree
.type == 'GEOMETRY':
4599 categories
= [c
for c
in node_categories_iter(context
)
4600 if c
.name
not in ['Group', 'Script']]
4601 for cat
in categories
:
4602 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4603 if hasattr(bpy
.types
, idname
):
4606 layout
.label(text
="Unable to load altered node lists.")
4607 layout
.label(text
="Please re-enable Node Wrangler.")
4611 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4612 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4615 def draw(self
, context
):
4616 layout
= self
.layout
4617 for ident
, node_type
, rna_name
in shaders_input_nodes_props
:
4618 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4619 props
.to_type
= ident
4622 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4623 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4626 def draw(self
, context
):
4627 layout
= self
.layout
4628 for ident
, node_type
, rna_name
in shaders_output_nodes_props
:
4629 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4630 props
.to_type
= ident
4633 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4634 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4637 def draw(self
, context
):
4638 layout
= self
.layout
4639 for ident
, node_type
, rna_name
in shaders_shader_nodes_props
:
4640 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4641 props
.to_type
= ident
4644 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4645 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4646 bl_label
= "Texture"
4648 def draw(self
, context
):
4649 layout
= self
.layout
4650 for ident
, node_type
, rna_name
in shaders_texture_nodes_props
:
4651 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4652 props
.to_type
= ident
4655 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4656 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4659 def draw(self
, context
):
4660 layout
= self
.layout
4661 for ident
, node_type
, rna_name
in shaders_color_nodes_props
:
4662 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4663 props
.to_type
= ident
4666 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4667 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4670 def draw(self
, context
):
4671 layout
= self
.layout
4672 for ident
, node_type
, rna_name
in shaders_vector_nodes_props
:
4673 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4674 props
.to_type
= ident
4677 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4678 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4679 bl_label
= "Converter"
4681 def draw(self
, context
):
4682 layout
= self
.layout
4683 for ident
, node_type
, rna_name
in shaders_converter_nodes_props
:
4684 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4685 props
.to_type
= ident
4688 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4689 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4692 def draw(self
, context
):
4693 layout
= self
.layout
4694 for ident
, node_type
, rna_name
in shaders_layout_nodes_props
:
4695 if node_type
!= 'FRAME':
4696 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4697 props
.to_type
= ident
4700 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4701 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4704 def draw(self
, context
):
4705 layout
= self
.layout
4706 for ident
, node_type
, rna_name
in compo_input_nodes_props
:
4707 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4708 props
.to_type
= ident
4711 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4712 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4715 def draw(self
, context
):
4716 layout
= self
.layout
4717 for ident
, node_type
, rna_name
in compo_output_nodes_props
:
4718 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4719 props
.to_type
= ident
4722 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4723 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4726 def draw(self
, context
):
4727 layout
= self
.layout
4728 for ident
, node_type
, rna_name
in compo_color_nodes_props
:
4729 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4730 props
.to_type
= ident
4733 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4734 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4735 bl_label
= "Converter"
4737 def draw(self
, context
):
4738 layout
= self
.layout
4739 for ident
, node_type
, rna_name
in compo_converter_nodes_props
:
4740 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4741 props
.to_type
= ident
4744 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4745 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4748 def draw(self
, context
):
4749 layout
= self
.layout
4750 for ident
, node_type
, rna_name
in compo_filter_nodes_props
:
4751 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4752 props
.to_type
= ident
4755 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4756 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4759 def draw(self
, context
):
4760 layout
= self
.layout
4761 for ident
, node_type
, rna_name
in compo_vector_nodes_props
:
4762 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4763 props
.to_type
= ident
4766 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4767 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4770 def draw(self
, context
):
4771 layout
= self
.layout
4772 for ident
, node_type
, rna_name
in compo_matte_nodes_props
:
4773 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4774 props
.to_type
= ident
4777 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4778 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4779 bl_label
= "Distort"
4781 def draw(self
, context
):
4782 layout
= self
.layout
4783 for ident
, node_type
, rna_name
in compo_distort_nodes_props
:
4784 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4785 props
.to_type
= ident
4788 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4789 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4792 def draw(self
, context
):
4793 layout
= self
.layout
4794 for ident
, node_type
, rna_name
in compo_layout_nodes_props
:
4795 if node_type
!= 'FRAME':
4796 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4797 props
.to_type
= ident
4800 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4801 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4804 def draw(self
, context
):
4805 layout
= self
.layout
4806 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4807 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4808 props
.to_type
= ident
4811 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4812 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4815 def draw(self
, context
):
4816 layout
= self
.layout
4817 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4818 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4819 props
.to_type
= ident
4822 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4823 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4826 def draw(self
, context
):
4827 layout
= self
.layout
4828 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4829 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4830 props
.to_type
= ident
4833 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4834 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4837 def draw(self
, context
):
4838 layout
= self
.layout
4839 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4840 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4841 props
.to_type
= ident
4844 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4845 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4846 bl_label
= "Converter"
4848 def draw(self
, context
):
4849 layout
= self
.layout
4850 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4851 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4852 props
.to_type
= ident
4855 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4856 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4859 def draw(self
, context
):
4860 layout
= self
.layout
4861 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4862 if node_type
!= 'FRAME':
4863 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4864 props
.to_type
= ident
4867 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4868 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4871 def draw(self
, context
):
4872 layout
= self
.layout
4873 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4874 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4875 props
.to_type
= ident
4878 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4879 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4882 def draw(self
, context
):
4883 layout
= self
.layout
4884 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4885 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4886 props
.to_type
= ident
4889 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4890 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4893 def draw(self
, context
):
4894 layout
= self
.layout
4895 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4896 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4897 props
.to_type
= ident
4900 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4901 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4902 bl_label
= "Pattern"
4904 def draw(self
, context
):
4905 layout
= self
.layout
4906 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4907 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4908 props
.to_type
= ident
4911 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4912 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4913 bl_label
= "Textures"
4915 def draw(self
, context
):
4916 layout
= self
.layout
4917 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4918 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4919 props
.to_type
= ident
4922 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4923 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4924 bl_label
= "Converter"
4926 def draw(self
, context
):
4927 layout
= self
.layout
4928 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4929 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4930 props
.to_type
= ident
4933 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4934 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4935 bl_label
= "Distort"
4937 def draw(self
, context
):
4938 layout
= self
.layout
4939 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4940 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4941 props
.to_type
= ident
4944 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4945 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4948 def draw(self
, context
):
4949 layout
= self
.layout
4950 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
4951 if node_type
!= 'FRAME':
4952 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4953 props
.to_type
= ident
4955 def draw_switch_category_submenu(self
, context
):
4956 layout
= self
.layout
4957 if self
.category
.name
== 'Layout':
4958 for node
in self
.category
.items(context
):
4959 if node
.nodetype
!= 'NodeFrame':
4960 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4961 props
.to_type
= node
.nodetype
4963 for node
in self
.category
.items(context
):
4964 if isinstance(node
, NodeItemCustom
):
4965 node
.draw(self
, layout
, context
)
4967 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4968 props
.geo_to_type
= node
.nodetype
4971 # APPENDAGES TO EXISTING UI
4975 def select_parent_children_buttons(self
, context
):
4976 layout
= self
.layout
4977 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
4978 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
4981 def attr_nodes_menu_func(self
, context
):
4982 col
= self
.layout
.column(align
=True)
4983 col
.menu("NODE_MT_nw_node_attribute_menu")
4987 def multipleimages_menu_func(self
, context
):
4988 col
= self
.layout
.column(align
=True)
4989 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
4990 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
4994 def bgreset_menu_func(self
, context
):
4995 self
.layout
.operator(NWResetBG
.bl_idname
)
4998 def save_viewer_menu_func(self
, context
):
4999 if nw_check(context
):
5000 if context
.space_data
.tree_type
== 'CompositorNodeTree':
5001 if context
.scene
.node_tree
.nodes
.active
:
5002 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
5003 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
5006 def reset_nodes_button(self
, context
):
5007 node_active
= context
.active_node
5008 node_selected
= context
.selected_nodes
5009 node_ignore
= ["FRAME","REROUTE", "GROUP"]
5011 # Check if active node is in the selection and respective type
5012 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
5013 row
= self
.layout
.row()
5014 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
5015 self
.layout
.separator()
5017 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
5018 row
= self
.layout
.row()
5019 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
5020 self
.layout
.separator()
5024 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5026 switch_category_menus
= []
5028 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5029 # props entry: (property name, property value)
5032 # NWMergeNodes with Ctrl (AUTO).
5033 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
5034 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5035 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
5036 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5037 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5038 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5039 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
5040 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5041 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5042 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5043 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
5044 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5045 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5046 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5047 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
5048 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5049 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5050 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5051 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
5052 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5053 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
5054 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5055 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
5056 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5057 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5058 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5059 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5060 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
5061 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5062 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
5063 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5064 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5065 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5066 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
5067 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5068 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5069 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5070 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
5071 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5072 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5073 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5074 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
5075 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5076 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5077 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5078 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
5079 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5080 # NWMergeNodes with Ctrl Shift (MATH)
5081 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5082 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5083 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
5084 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5085 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5086 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5087 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
5088 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5089 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5090 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5091 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
5092 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5093 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5094 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5095 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
5096 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5097 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
5098 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5099 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
5100 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5101 # BATCH CHANGE NODES
5102 # NWBatchChangeNodes with Alt
5103 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
5104 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5105 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
5106 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5107 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5108 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5109 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
5110 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5111 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5112 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5113 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
5114 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5115 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5116 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5117 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
5118 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5119 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5120 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5121 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
5122 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5123 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
5124 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5125 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
5126 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5127 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
5128 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5129 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
5130 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5131 # LINK ACTIVE TO SELECTED
5132 # Don't use names, don't replace links (K)
5133 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
5134 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5135 # Don't use names, replace links (Shift K)
5136 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
5137 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5138 # Use node name, don't replace links (')
5139 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
5140 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5141 # Use node name, replace links (Shift ')
5142 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
5143 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5144 # Don't use names, don't replace links (;)
5145 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
5146 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5147 # Don't use names, replace links (')
5148 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
5149 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5151 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5152 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5153 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5154 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5155 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5156 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5157 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5158 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5159 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5160 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5161 # CLEAR LABEL (Alt L)
5162 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5163 # MODIFY LABEL (Alt Shift L)
5164 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5165 # Copy Label from active to selected
5166 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5167 # DETACH OUTPUTS (Alt Shift D)
5168 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5169 # LINK TO OUTPUT NODE (O)
5170 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5171 # SELECT PARENT/CHILDREN
5173 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5175 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5177 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5178 # Add Principled BSDF Texture Setup
5179 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5181 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5183 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5185 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5187 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5189 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5190 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5192 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
5194 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5196 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5197 # Lazy Connect with Menu
5198 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5199 # Viewer Tile Center
5200 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5202 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5203 # Reset Nodes (Back Space)
5204 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5206 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
5207 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5208 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5209 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
5210 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
5211 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
5216 NWPrincipledPreferences
,
5236 NWAddPrincipledSetup
,
5238 NWLinkActiveToSelected
,
5240 NWSelectParentChildren
,
5246 NWAddMultipleImages
,
5254 NWMergeGeometryMenu
,
5256 NWConnectionListOutputs
,
5257 NWConnectionListInputs
,
5259 NWBatchChangeNodesMenu
,
5260 NWBatchChangeBlendTypeMenu
,
5261 NWBatchChangeOperationMenu
,
5262 NWCopyToSelectedMenu
,
5265 NWLinkActiveToSelectedMenu
,
5267 NWLinkUseNodeNameMenu
,
5268 NWLinkUseOutputsNamesMenu
,
5270 NWSwitchNodeTypeMenu
,
5271 NWSwitchShadersInputSubmenu
,
5272 NWSwitchShadersOutputSubmenu
,
5273 NWSwitchShadersShaderSubmenu
,
5274 NWSwitchShadersTextureSubmenu
,
5275 NWSwitchShadersColorSubmenu
,
5276 NWSwitchShadersVectorSubmenu
,
5277 NWSwitchShadersConverterSubmenu
,
5278 NWSwitchShadersLayoutSubmenu
,
5279 NWSwitchCompoInputSubmenu
,
5280 NWSwitchCompoOutputSubmenu
,
5281 NWSwitchCompoColorSubmenu
,
5282 NWSwitchCompoConverterSubmenu
,
5283 NWSwitchCompoFilterSubmenu
,
5284 NWSwitchCompoVectorSubmenu
,
5285 NWSwitchCompoMatteSubmenu
,
5286 NWSwitchCompoDistortSubmenu
,
5287 NWSwitchCompoLayoutSubmenu
,
5288 NWSwitchMatInputSubmenu
,
5289 NWSwitchMatOutputSubmenu
,
5290 NWSwitchMatColorSubmenu
,
5291 NWSwitchMatVectorSubmenu
,
5292 NWSwitchMatConverterSubmenu
,
5293 NWSwitchMatLayoutSubmenu
,
5294 NWSwitchTexInputSubmenu
,
5295 NWSwitchTexOutputSubmenu
,
5296 NWSwitchTexColorSubmenu
,
5297 NWSwitchTexPatternSubmenu
,
5298 NWSwitchTexTexturesSubmenu
,
5299 NWSwitchTexConverterSubmenu
,
5300 NWSwitchTexDistortSubmenu
,
5301 NWSwitchTexLayoutSubmenu
,
5305 from bpy
.utils
import register_class
5308 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
5309 name
="Busy Drawing!",
5311 description
="An internal property used to store only the first mouse position")
5312 bpy
.types
.Scene
.NWLazySource
= StringProperty(
5313 name
="Lazy Source!",
5315 description
="An internal property used to store the first node in a Lazy Connect operation")
5316 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
5317 name
="Lazy Target!",
5319 description
="An internal property used to store the last node in a Lazy Connect operation")
5320 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
5321 name
="Source Socket!",
5323 description
="An internal property used to store the source socket in a Lazy Connect operation")
5324 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
5327 description
="An internal property used to determine if a socket is generated by the addon"
5334 addon_keymaps
.clear()
5335 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
5337 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
5338 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
5339 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
5341 for prop
, value
in props
:
5342 setattr(kmi
.properties
, prop
, value
)
5343 addon_keymaps
.append((km
, kmi
))
5346 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
5347 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
5348 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
5349 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
5350 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
5351 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
5352 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
5353 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
5356 switch_category_menus
.clear()
5357 for cat
in node_categories_iter(None):
5358 if cat
.name
not in ['Group', 'Script'] and cat
.identifier
.startswith('GEO'):
5359 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
5360 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
5361 "bl_space_type": 'NODE_EDITOR',
5362 "bl_label": cat
.name
,
5365 "draw": draw_switch_category_submenu
,
5368 switch_category_menus
.append(switch_category_type
)
5370 bpy
.utils
.register_class(switch_category_type
)
5374 from bpy
.utils
import unregister_class
5377 del bpy
.types
.Scene
.NWBusyDrawing
5378 del bpy
.types
.Scene
.NWLazySource
5379 del bpy
.types
.Scene
.NWLazyTarget
5380 del bpy
.types
.Scene
.NWSourceSocket
5381 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
5383 for cat_types
in switch_category_menus
:
5384 bpy
.utils
.unregister_class(cat_types
)
5385 switch_category_menus
.clear()
5388 for km
, kmi
in addon_keymaps
:
5389 km
.keymap_items
.remove(kmi
)
5390 addon_keymaps
.clear()
5393 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
5394 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
5395 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
5396 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
5397 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
5398 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
5399 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
5400 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
5403 unregister_class(cls
)
5405 if __name__
== "__main__":