1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
17 from bpy
.types
import Operator
, Panel
, Menu
18 from bpy
.props
import (
27 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
28 from gpu_extras
.batch
import batch_for_shader
29 from mathutils
import Vector
30 from nodeitems_utils
import node_categories_iter
, NodeItemCustom
31 from math
import cos
, sin
, pi
, hypot
35 from itertools
import chain
37 from collections
import namedtuple
41 # list of outputs of Input Render Layer
42 # with attributes determining if pass is used,
43 # and MultiLayer EXR outputs names and corresponding render engines
45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
46 RL_entry
= namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
48 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
49 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
50 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
51 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
52 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
53 RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
54 RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
55 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
56 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
57 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
58 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
59 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
60 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
61 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
62 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
63 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
64 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
65 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
66 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
67 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
68 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
69 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
70 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
71 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
72 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
76 # (rna_type.identifier, type, rna_type.name)
77 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
78 # Keeping things in alphabetical order so we don't need to sort later.
79 shaders_input_nodes_props
= (
80 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
81 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
82 ('ShaderNodeBevel', 'BEVEL', 'Bevel'),
83 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
84 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
85 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
86 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
87 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
88 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
89 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
90 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
91 ('ShaderNodeRGB', 'RGB', 'RGB'),
92 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
93 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
94 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
95 ('ShaderNodeValue', 'VALUE', 'Value'),
96 ('ShaderNodeVertexColor', 'VERTEX_COLOR', 'Vertex Color'),
97 ('ShaderNodeVolumeInfo', 'VOLUME_INFO', 'Volume Info'),
98 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
101 # (rna_type.identifier, type, rna_type.name)
102 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
103 # Keeping things in alphabetical order so we don't need to sort later.
104 shaders_output_nodes_props
= (
105 ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'),
106 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
107 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
108 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
110 # (rna_type.identifier, type, rna_type.name)
111 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
112 # Keeping things in alphabetical order so we don't need to sort later.
113 shaders_shader_nodes_props
= (
114 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
115 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
116 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
117 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
118 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
119 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
120 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
121 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
122 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
123 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
124 ('ShaderNodeBsdfHairPrincipled', 'BSDF_HAIR_PRINCIPLED', 'Principled Hair BSDF'),
125 ('ShaderNodeVolumePrincipled', 'PRINCIPLED_VOLUME', 'Principled Volume'),
126 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
127 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
128 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
129 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
130 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
131 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
132 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
133 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
134 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
136 # (rna_type.identifier, type, rna_type.name)
137 # Keeping things in alphabetical order so we don't need to sort later.
138 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
139 shaders_texture_nodes_props
= (
140 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
141 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
142 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
143 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
144 ('ShaderNodeTexIES', 'TEX_IES', 'IES Texture'),
145 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
146 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
147 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
148 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
149 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
150 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
151 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
152 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
153 ('ShaderNodeTexWhiteNoise', 'TEX_WHITE_NOISE', 'White Noise'),
155 # (rna_type.identifier, type, rna_type.name)
156 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
157 # Keeping things in alphabetical order so we don't need to sort later.
158 shaders_color_nodes_props
= (
159 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
160 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
161 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
162 ('ShaderNodeInvert', 'INVERT', 'Invert'),
163 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
164 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
165 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
167 # (rna_type.identifier, type, rna_type.name)
168 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
169 # Keeping things in alphabetical order so we don't need to sort later.
170 shaders_vector_nodes_props
= (
171 ('ShaderNodeBump', 'BUMP', 'Bump'),
172 ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'),
173 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
174 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
175 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
176 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
177 ('ShaderNodeVectorDisplacement', 'VECTOR_DISPLACEMENT', 'Vector Displacement'),
178 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
180 # (rna_type.identifier, type, rna_type.name)
181 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
182 # Keeping things in alphabetical order so we don't need to sort later.
183 shaders_converter_nodes_props
= (
184 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
185 ('ShaderNodeClamp', 'CLAMP', 'Clamp'),
186 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
187 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
188 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
189 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
190 ('ShaderNodeMapRange', 'MAP_RANGE', 'Map Range'),
191 ('ShaderNodeMath', 'MATH', 'Math'),
192 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
193 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
194 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
195 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
196 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
197 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
199 # (rna_type.identifier, type, rna_type.name)
200 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
201 # Keeping things in alphabetical order so we don't need to sort later.
202 shaders_layout_nodes_props
= (
203 ('NodeFrame', 'FRAME', 'Frame'),
204 ('NodeReroute', 'REROUTE', 'Reroute'),
208 # (rna_type.identifier, type, rna_type.name)
209 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
210 # Keeping things in alphabetical order so we don't need to sort later.
211 compo_input_nodes_props
= (
212 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
213 ('CompositorNodeImage', 'IMAGE', 'Image'),
214 ('CompositorNodeMask', 'MASK', 'Mask'),
215 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
216 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
217 ('CompositorNodeRGB', 'RGB', 'RGB'),
218 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
219 ('CompositorNodeTime', 'TIME', 'Time'),
220 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
221 ('CompositorNodeValue', 'VALUE', 'Value'),
223 # (rna_type.identifier, type, rna_type.name)
224 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
225 # Keeping things in alphabetical order so we don't need to sort later.
226 compo_output_nodes_props
= (
227 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
228 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
229 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
230 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
231 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
233 # (rna_type.identifier, type, rna_type.name)
234 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
235 # Keeping things in alphabetical order so we don't need to sort later.
236 compo_color_nodes_props
= (
237 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
238 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
239 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
240 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
241 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
242 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
243 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
244 ('CompositorNodeInvert', 'INVERT', 'Invert'),
245 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
246 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
247 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
248 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
250 # (rna_type.identifier, type, rna_type.name)
251 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
252 # Keeping things in alphabetical order so we don't need to sort later.
253 compo_converter_nodes_props
= (
254 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
255 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
256 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
257 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
258 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
259 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
260 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
261 ('CompositorNodeMath', 'MATH', 'Math'),
262 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
263 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
264 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
265 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
266 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
267 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
268 ('CompositorNodeSwitchView', 'VIEWSWITCH', 'View Switch'),
270 # (rna_type.identifier, type, rna_type.name)
271 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
272 # Keeping things in alphabetical order so we don't need to sort later.
273 compo_filter_nodes_props
= (
274 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
275 ('CompositorNodeBlur', 'BLUR', 'Blur'),
276 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
277 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
278 ('CompositorNodeDenoise', 'DENOISE', 'Denoise'),
279 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
280 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
281 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
282 ('CompositorNodeFilter', 'FILTER', 'Filter'),
283 ('CompositorNodeGlare', 'GLARE', 'Glare'),
284 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
285 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
286 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
287 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
289 # (rna_type.identifier, type, rna_type.name)
290 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
291 # Keeping things in alphabetical order so we don't need to sort later.
292 compo_vector_nodes_props
= (
293 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
294 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
295 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
296 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
297 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
299 # (rna_type.identifier, type, rna_type.name)
300 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
301 # Keeping things in alphabetical order so we don't need to sort later.
302 compo_matte_nodes_props
= (
303 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
304 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
305 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
306 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
307 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
308 ('CompositorNodeCryptomatte', 'CRYPTOMATTE', 'Cryptomatte'),
309 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
310 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
311 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
312 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
313 ('CompositorNodeKeying', 'KEYING', 'Keying'),
314 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
315 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
317 # (rna_type.identifier, type, rna_type.name)
318 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
319 # Keeping things in alphabetical order so we don't need to sort later.
320 compo_distort_nodes_props
= (
321 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
322 ('CompositorNodeCrop', 'CROP', 'Crop'),
323 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
324 ('CompositorNodeFlip', 'FLIP', 'Flip'),
325 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
326 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
327 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
328 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
329 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
330 ('CompositorNodeScale', 'SCALE', 'Scale'),
331 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
332 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
333 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
335 # (rna_type.identifier, type, rna_type.name)
336 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
337 # Keeping things in alphabetical order so we don't need to sort later.
338 compo_layout_nodes_props
= (
339 ('NodeFrame', 'FRAME', 'Frame'),
340 ('NodeReroute', 'REROUTE', 'Reroute'),
341 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
343 # Blender Render material nodes
344 # (rna_type.identifier, type, rna_type.name)
345 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
346 blender_mat_input_nodes_props
= (
347 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
348 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
349 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
350 ('ShaderNodeValue', 'VALUE', 'Value'),
351 ('ShaderNodeRGB', 'RGB', 'RGB'),
352 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
353 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
354 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
357 # (rna_type.identifier, type, rna_type.name)
358 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
359 blender_mat_output_nodes_props
= (
360 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
363 # (rna_type.identifier, type, rna_type.name)
364 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
365 blender_mat_color_nodes_props
= (
366 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
367 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
368 ('ShaderNodeInvert', 'INVERT', 'Invert'),
369 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
372 # (rna_type.identifier, type, rna_type.name)
373 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
374 blender_mat_vector_nodes_props
= (
375 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
376 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
377 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
380 # (rna_type.identifier, type, rna_type.name)
381 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
382 blender_mat_converter_nodes_props
= (
383 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
384 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
385 ('ShaderNodeMath', 'MATH', 'Math'),
386 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
387 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
388 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
389 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
390 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
391 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
394 # (rna_type.identifier, type, rna_type.name)
395 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
396 blender_mat_layout_nodes_props
= (
397 ('NodeReroute', 'REROUTE', 'Reroute'),
401 # (rna_type.identifier, type, rna_type.name)
402 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
403 texture_input_nodes_props
= (
404 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
405 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
406 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
407 ('TextureNodeImage', 'IMAGE', 'Image'),
410 # (rna_type.identifier, type, rna_type.name)
411 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
412 texture_output_nodes_props
= (
413 ('TextureNodeOutput', 'OUTPUT', 'Output'),
414 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
417 # (rna_type.identifier, type, rna_type.name)
418 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
419 texture_color_nodes_props
= (
420 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
421 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
422 ('TextureNodeInvert', 'INVERT', 'Invert'),
423 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
424 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
425 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
428 # (rna_type.identifier, type, rna_type.name)
429 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
430 texture_pattern_nodes_props
= (
431 ('TextureNodeChecker', 'CHECKER', 'Checker'),
432 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
435 # (rna_type.identifier, type, rna_type.name)
436 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
437 texture_textures_nodes_props
= (
438 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
439 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
440 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
441 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
442 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
443 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
444 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
445 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
446 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
447 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
450 # (rna_type.identifier, type, rna_type.name)
451 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
452 texture_converter_nodes_props
= (
453 ('TextureNodeMath', 'MATH', 'Math'),
454 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
455 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
456 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
457 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
460 # (rna_type.identifier, type, rna_type.name)
461 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
462 texture_distort_nodes_props
= (
463 ('TextureNodeScale', 'SCALE', 'Scale'),
464 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
465 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
466 ('TextureNodeAt', 'AT', 'At'),
469 # (rna_type.identifier, type, rna_type.name)
470 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
471 texture_layout_nodes_props
= (
472 ('NodeReroute', 'REROUTE', 'Reroute'),
475 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
476 # used list, not tuple for easy merging with other lists.
478 ('MIX', 'Mix', 'Mix Mode'),
479 ('ADD', 'Add', 'Add Mode'),
480 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
481 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
482 ('SCREEN', 'Screen', 'Screen Mode'),
483 ('DIVIDE', 'Divide', 'Divide Mode'),
484 ('DIFFERENCE', 'Difference', 'Difference Mode'),
485 ('DARKEN', 'Darken', 'Darken Mode'),
486 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
487 ('OVERLAY', 'Overlay', 'Overlay Mode'),
488 ('DODGE', 'Dodge', 'Dodge Mode'),
489 ('BURN', 'Burn', 'Burn Mode'),
490 ('HUE', 'Hue', 'Hue Mode'),
491 ('SATURATION', 'Saturation', 'Saturation Mode'),
492 ('VALUE', 'Value', 'Value Mode'),
493 ('COLOR', 'Color', 'Color Mode'),
494 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
495 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
498 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
499 # used list, not tuple for easy merging with other lists.
501 ('ADD', 'Add', 'Add Mode'),
502 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
503 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
504 ('DIVIDE', 'Divide', 'Divide Mode'),
505 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
506 ('SINE', 'Sine', 'Sine Mode'),
507 ('COSINE', 'Cosine', 'Cosine Mode'),
508 ('TANGENT', 'Tangent', 'Tangent Mode'),
509 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
510 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
511 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
512 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
513 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
514 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
515 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
516 ('POWER', 'Power', 'Power Mode'),
517 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
518 ('SQRT', 'Square Root', 'Square Root Mode'),
519 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
520 ('EXPONENT', 'Exponent', 'Exponent Mode'),
521 ('MINIMUM', 'Minimum', 'Minimum Mode'),
522 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
523 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
524 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
525 ('SIGN', 'Sign', 'Sign Mode'),
526 ('COMPARE', 'Compare', 'Compare Mode'),
527 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
528 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
529 ('FRACT', 'Fraction', 'Fraction Mode'),
530 ('MODULO', 'Modulo', 'Modulo Mode'),
531 ('SNAP', 'Snap', 'Snap Mode'),
532 ('WRAP', 'Wrap', 'Wrap Mode'),
533 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
534 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
535 ('ROUND', 'Round', 'Round Mode'),
536 ('FLOOR', 'Floor', 'Floor Mode'),
537 ('CEIL', 'Ceil', 'Ceil Mode'),
538 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
539 ('RADIANS', 'To Radians', 'To Radians Mode'),
540 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
543 # Operations used by the geometry boolean node and join geometry node
544 geo_combine_operations
= [
545 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
546 ('INTERSECT', 'Intersect', 'Intersect Mode'),
547 ('UNION', 'Union', 'Union Mode'),
548 ('DIFFERENCE', 'Difference', 'Difference Mode'),
551 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
552 # used list, not tuple for easy merging with other lists.
554 ('CURRENT', 'Current', 'Leave at current state'),
555 ('NEXT', 'Next', 'Next blend type/operation'),
556 ('PREV', 'Prev', 'Previous blend type/operation'),
561 (1.0, 1.0, 1.0, 0.7),
562 (1.0, 0.0, 0.0, 0.7),
566 (0.0, 0.0, 0.0, 1.0),
567 (0.38, 0.77, 0.38, 1.0),
568 (0.38, 0.77, 0.38, 1.0)
571 (0.0, 0.0, 0.0, 1.0),
572 (0.77, 0.77, 0.16, 1.0),
573 (0.77, 0.77, 0.16, 1.0)
576 (0.0, 0.0, 0.0, 1.0),
577 (0.38, 0.38, 0.77, 1.0),
578 (0.38, 0.38, 0.77, 1.0)
581 (0.0, 0.0, 0.0, 1.0),
582 (0.63, 0.63, 0.63, 1.0),
583 (0.63, 0.63, 0.63, 1.0)
586 (1.0, 1.0, 1.0, 0.7),
587 (0.0, 0.0, 0.0, 0.7),
592 viewer_socket_name
= "tmp_viewer"
594 def get_nodes_from_category(category_name
, context
):
595 for category
in node_categories_iter(context
):
596 if category
.name
== category_name
:
597 return sorted(category
.items(context
), key
=lambda node
: node
.label
)
599 def is_visible_socket(socket
):
600 return not socket
.hide
and socket
.enabled
and socket
.type != 'CUSTOM'
602 def nice_hotkey_name(punc
):
603 # convert the ugly string name into the actual character
606 'MIDDLEMOUSE': "MMB",
608 'WHEELUPMOUSE': "Wheel Up",
609 'WHEELDOWNMOUSE': "Wheel Down",
610 'WHEELINMOUSE': "Wheel In",
611 'WHEELOUTMOUSE': "Wheel Out",
624 'LINE_FEED': "Enter",
633 'NUMPAD_1': "Numpad 1",
634 'NUMPAD_2': "Numpad 2",
635 'NUMPAD_3': "Numpad 3",
636 'NUMPAD_4': "Numpad 4",
637 'NUMPAD_5': "Numpad 5",
638 'NUMPAD_6': "Numpad 6",
639 'NUMPAD_7': "Numpad 7",
640 'NUMPAD_8': "Numpad 8",
641 'NUMPAD_9': "Numpad 9",
642 'NUMPAD_0': "Numpad 0",
643 'NUMPAD_PERIOD': "Numpad .",
644 'NUMPAD_SLASH': "Numpad /",
645 'NUMPAD_ASTERIX': "Numpad *",
646 'NUMPAD_MINUS': "Numpad -",
647 'NUMPAD_ENTER': "Numpad Enter",
648 'NUMPAD_PLUS': "Numpad +",
651 return nice_name
[punc
]
653 return punc
.replace("_", " ").title()
656 def force_update(context
):
657 context
.space_data
.node_tree
.update_tag()
661 prefs
= bpy
.context
.preferences
.system
662 return prefs
.dpi
* prefs
.pixel_size
/ 72
665 def node_mid_pt(node
, axis
):
667 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
669 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
675 def autolink(node1
, node2
, links
):
677 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
678 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
679 for outp
in available_outputs
:
680 for inp
in available_inputs
:
681 if not inp
.is_linked
and inp
.name
== outp
.name
:
686 for outp
in available_outputs
:
687 for inp
in available_inputs
:
688 if not inp
.is_linked
and inp
.type == outp
.type:
693 # force some connection even if the type doesn't match
694 if available_outputs
:
695 for inp
in available_inputs
:
696 if not inp
.is_linked
:
698 links
.new(available_outputs
[0], inp
)
701 # even if no sockets are open, force one of matching type
702 for outp
in available_outputs
:
703 for inp
in available_inputs
:
704 if inp
.type == outp
.type:
710 for outp
in available_outputs
:
711 for inp
in available_inputs
:
716 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
720 def node_at_pos(nodes
, context
, event
):
721 nodes_under_mouse
= []
724 store_mouse_cursor(context
, event
)
725 x
, y
= context
.space_data
.cursor_location
727 # Make a list of each corner (and middle of border) for each node.
728 # Will be sorted to find nearest point and thus nearest node
729 node_points_with_dist
= []
732 if node
.type != 'FRAME': # no point trying to link to a frame node
733 locx
= node
.location
.x
734 locy
= node
.location
.y
735 dimx
= node
.dimensions
.x
/dpifac()
736 dimy
= node
.dimensions
.y
/dpifac()
738 locx
+= node
.parent
.location
.x
739 locy
+= node
.parent
.location
.y
740 if node
.parent
.parent
:
741 locx
+= node
.parent
.parent
.location
.x
742 locy
+= node
.parent
.parent
.location
.y
743 if node
.parent
.parent
.parent
:
744 locx
+= node
.parent
.parent
.parent
.location
.x
745 locy
+= node
.parent
.parent
.parent
.location
.y
746 if node
.parent
.parent
.parent
.parent
:
747 # Support three levels or parenting
748 # There's got to be a better way to do this...
751 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
752 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
753 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
754 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
756 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
757 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
758 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
759 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
761 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
764 if node
.type != 'FRAME' and skipnode
== False:
765 locx
= node
.location
.x
766 locy
= node
.location
.y
767 dimx
= node
.dimensions
.x
/dpifac()
768 dimy
= node
.dimensions
.y
/dpifac()
770 locx
+= node
.parent
.location
.x
771 locy
+= node
.parent
.location
.y
772 if (locx
<= x
<= locx
+ dimx
) and \
773 (locy
- dimy
<= y
<= locy
):
774 nodes_under_mouse
.append(node
)
776 if len(nodes_under_mouse
) == 1:
777 if nodes_under_mouse
[0] != nearest_node
:
778 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
780 target_node
= nearest_node
# else use the nearest node
782 target_node
= nearest_node
786 def store_mouse_cursor(context
, event
):
787 space
= context
.space_data
788 v2d
= context
.region
.view2d
789 tree
= space
.edit_tree
791 # convert mouse position to the View2D for later node placement
792 if context
.region
.type == 'WINDOW':
793 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
795 space
.cursor_location
= tree
.view_center
797 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
798 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
800 vertices
= ((x1
, y1
), (x2
, y2
))
801 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
802 colour
[1]+(1.0-colour
[1])/4,
803 colour
[2]+(1.0-colour
[2])/4,
804 colour
[3]+(1.0-colour
[3])/4),
807 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
808 bgl
.glLineWidth(size
* dpifac())
814 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
815 radius
= radius
* dpifac()
817 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
818 radius
* sin(i
* 2 * pi
/ sides
) + my
)
819 for i
in range(sides
+ 1)]
821 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
823 shader
.uniform_float("color", colour
)
826 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
827 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
828 bottom_bar
= (16*dpifac()) + 1
830 radius
= radius
*dpifac()
832 nlocx
= (node
.location
.x
+1)*dpifac()
833 nlocy
= (node
.location
.y
+1)*dpifac()
834 ndimx
= node
.dimensions
.x
835 ndimy
= node
.dimensions
.y
836 # This is a stupid way to do this... TODO use while loop
838 nlocx
+= node
.parent
.location
.x
839 nlocy
+= node
.parent
.location
.y
840 if node
.parent
.parent
:
841 nlocx
+= node
.parent
.parent
.location
.x
842 nlocy
+= node
.parent
.parent
.location
.y
843 if node
.parent
.parent
.parent
:
844 nlocx
+= node
.parent
.parent
.parent
.location
.x
845 nlocy
+= node
.parent
.parent
.parent
.location
.y
850 if node
.type == 'REROUTE':
858 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
860 for i
in range(sides
+1):
862 if my
> bottom_bar
and mx
< area_width
:
863 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
864 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
865 vertices
.append((cosine
,sine
))
866 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
868 shader
.uniform_float("color", colour
)
872 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
874 for i
in range(sides
+1):
876 if my
> bottom_bar
and mx
< area_width
:
877 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
878 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
879 vertices
.append((cosine
,sine
))
880 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
882 shader
.uniform_float("color", colour
)
886 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
888 for i
in range(sides
+1):
890 if my
> bottom_bar
and mx
< area_width
:
891 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
892 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
893 vertices
.append((cosine
,sine
))
894 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
896 shader
.uniform_float("color", colour
)
899 # Bottom right corner
900 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
902 for i
in range(sides
+1):
904 if my
> bottom_bar
and mx
< area_width
:
905 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
906 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
907 vertices
.append((cosine
,sine
))
908 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
910 shader
.uniform_float("color", colour
)
913 # prepare drawing all edges in one batch
919 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
920 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
921 if m1x
< area_width
and m2x
< area_width
:
922 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
923 (m1x
,m1y
), (m1x
-radius
,m1y
)])
924 indices
.extend([(id_last
, id_last
+1, id_last
+3),
925 (id_last
+3, id_last
+1, id_last
+2)])
929 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
930 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
931 m1x
= min(m1x
, area_width
)
932 m2x
= min(m2x
, area_width
)
933 if m1y
> bottom_bar
and m2y
> bottom_bar
:
934 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
935 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
936 indices
.extend([(id_last
, id_last
+1, id_last
+3),
937 (id_last
+3, id_last
+1, id_last
+2)])
941 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
942 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
943 m1y
= max(m1y
, bottom_bar
)
944 m2y
= max(m2y
, bottom_bar
)
945 if m1x
< area_width
and m2x
< area_width
:
946 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
947 (m1x
+radius
,m1y
), (m1x
,m1y
)])
948 indices
.extend([(id_last
, id_last
+1, id_last
+3),
949 (id_last
+3, id_last
+1, id_last
+2)])
953 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
954 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
955 m1x
= min(m1x
, area_width
)
956 m2x
= min(m2x
, area_width
)
957 if m1y
> bottom_bar
and m2y
> bottom_bar
:
958 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
959 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
960 indices
.extend([(id_last
, id_last
+1, id_last
+3),
961 (id_last
+3, id_last
+1, id_last
+2)])
963 # now draw all edges in one batch
964 if len(vertices
) != 0:
965 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
967 shader
.uniform_float("color", colour
)
970 def draw_callback_nodeoutline(self
, context
, mode
):
974 bgl
.glEnable(bgl
.GL_BLEND
)
975 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
976 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
978 nodes
, links
= get_nodes_links(context
)
980 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
983 col_outer
= (1.0, 0.2, 0.2, 0.4)
984 col_inner
= (0.0, 0.0, 0.0, 0.5)
985 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
986 elif mode
== "LINKMENU":
987 col_outer
= (0.4, 0.6, 1.0, 0.4)
988 col_inner
= (0.0, 0.0, 0.0, 0.5)
989 col_circle_inner
= (0.08, 0.15, .3, 1.0)
991 col_outer
= (0.2, 1.0, 0.2, 0.4)
992 col_inner
= (0.0, 0.0, 0.0, 0.5)
993 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
995 m1x
= self
.mouse_path
[0][0]
996 m1y
= self
.mouse_path
[0][1]
997 m2x
= self
.mouse_path
[-1][0]
998 m2y
= self
.mouse_path
[-1][1]
1000 n1
= nodes
[context
.scene
.NWLazySource
]
1001 n2
= nodes
[context
.scene
.NWLazyTarget
]
1004 col_outer
= (0.4, 0.4, 0.4, 0.4)
1005 col_inner
= (0.0, 0.0, 0.0, 0.5)
1006 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
1008 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
1009 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
1010 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
1011 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
1013 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
1014 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
1017 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
1018 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
1021 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
1022 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
1024 bgl
.glDisable(bgl
.GL_BLEND
)
1025 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
1026 def get_active_tree(context
):
1027 tree
= context
.space_data
.node_tree
1029 # Get nodes from currently edited tree.
1030 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1031 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1032 # the same as context.active_node, the user is in a group.
1033 # Check recursively until we find the real active node_tree:
1034 if tree
.nodes
.active
:
1035 while tree
.nodes
.active
!= context
.active_node
:
1036 tree
= tree
.nodes
.active
.node_tree
1040 def get_nodes_links(context
):
1041 tree
, path
= get_active_tree(context
)
1042 return tree
.nodes
, tree
.links
1044 def is_viewer_socket(socket
):
1045 # checks if a internal socket is a valid viewer socket
1046 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
1048 def get_internal_socket(socket
):
1049 #get the internal socket from a socket inside or outside the group
1051 if node
.type == 'GROUP_OUTPUT':
1052 source_iterator
= node
.inputs
1053 iterator
= node
.id_data
.outputs
1054 elif node
.type == 'GROUP_INPUT':
1055 source_iterator
= node
.outputs
1056 iterator
= node
.id_data
.inputs
1057 elif hasattr(node
, "node_tree"):
1058 if socket
.is_output
:
1059 source_iterator
= node
.outputs
1060 iterator
= node
.node_tree
.outputs
1062 source_iterator
= node
.inputs
1063 iterator
= node
.node_tree
.inputs
1067 for i
, s
in enumerate(source_iterator
):
1072 def is_viewer_link(link
, output_node
):
1073 if "Emission Viewer" in link
.to_node
.name
or link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
1075 if link
.to_node
.type == 'GROUP_OUTPUT':
1076 socket
= get_internal_socket(link
.to_socket
)
1077 if is_viewer_socket(socket
):
1081 def get_group_output_node(tree
):
1082 for node
in tree
.nodes
:
1083 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
1086 def get_output_location(tree
):
1087 # get right-most location
1088 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
1089 max_xloc_node
= sorted_by_xloc
[-1]
1090 if max_xloc_node
.name
== 'Emission Viewer':
1091 max_xloc_node
= sorted_by_xloc
[-2]
1093 # get average y location
1095 for node
in tree
.nodes
:
1096 sum_yloc
+= node
.location
.y
1098 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1099 loc_y
= sum_yloc
/ len(tree
.nodes
)
1103 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
1104 base_color
: StringProperty(
1106 default
='diffuse diff albedo base col color',
1107 description
='Naming Components for Base Color maps')
1108 sss_color
: StringProperty(
1109 name
='Subsurface Color',
1110 default
='sss subsurface',
1111 description
='Naming Components for Subsurface Color maps')
1112 metallic
: StringProperty(
1114 default
='metallic metalness metal mtl',
1115 description
='Naming Components for metallness maps')
1116 specular
: StringProperty(
1118 default
='specularity specular spec spc',
1119 description
='Naming Components for Specular maps')
1120 normal
: StringProperty(
1122 default
='normal nor nrm nrml norm',
1123 description
='Naming Components for Normal maps')
1124 bump
: StringProperty(
1127 description
='Naming Components for bump maps')
1128 rough
: StringProperty(
1130 default
='roughness rough rgh',
1131 description
='Naming Components for roughness maps')
1132 gloss
: StringProperty(
1134 default
='gloss glossy glossiness',
1135 description
='Naming Components for glossy maps')
1136 displacement
: StringProperty(
1137 name
='Displacement',
1138 default
='displacement displace disp dsp height heightmap',
1139 description
='Naming Components for displacement maps')
1142 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1143 bl_idname
= __name__
1145 merge_hide
: EnumProperty(
1146 name
="Hide Mix nodes",
1148 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1149 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1150 ("NEVER", "Never", "Never collapse the new merge nodes")
1152 default
='NON_SHADER',
1153 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1154 merge_position
: EnumProperty(
1155 name
="Mix Node Position",
1157 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1158 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1161 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1163 show_hotkey_list
: BoolProperty(
1164 name
="Show Hotkey List",
1166 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1168 hotkey_list_filter
: StringProperty(
1169 name
=" Filter by Name",
1171 description
="Show only hotkeys that have this text in their name"
1173 show_principled_lists
: BoolProperty(
1174 name
="Show Principled naming tags",
1176 description
="Expand this box into a list of all naming tags for principled texture setup"
1178 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1180 def draw(self
, context
):
1181 layout
= self
.layout
1182 col
= layout
.column()
1183 col
.prop(self
, "merge_position")
1184 col
.prop(self
, "merge_hide")
1187 col
= box
.column(align
=True)
1188 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1189 if self
.show_principled_lists
:
1190 tags
= self
.principled_tags
1192 col
.prop(tags
, "base_color")
1193 col
.prop(tags
, "sss_color")
1194 col
.prop(tags
, "metallic")
1195 col
.prop(tags
, "specular")
1196 col
.prop(tags
, "rough")
1197 col
.prop(tags
, "gloss")
1198 col
.prop(tags
, "normal")
1199 col
.prop(tags
, "bump")
1200 col
.prop(tags
, "displacement")
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 Emission Shader for shadeless previews, or to the geometry node tree's 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
= ""
1698 self
.shader_viewer_ident
= ""
1701 def poll(cls
, context
):
1702 if nw_check(context
):
1703 space
= context
.space_data
1704 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1705 if context
.active_node
:
1706 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1712 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1713 #check if a viewer output already exists in a node group otherwise create
1714 if hasattr(node
, "node_tree"):
1716 if len(node
.node_tree
.outputs
):
1718 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1719 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1720 #if viewer output is already used but leads to the same socket we can still use it
1721 is_used
= self
.is_socket_used_other_mats(socket
)
1723 if connect_socket
== None:
1725 groupout
= get_group_output_node(node
.node_tree
)
1726 groupout_input
= groupout
.inputs
[i
]
1727 links
= groupout_input
.links
1728 if connect_socket
not in [link
.from_socket
for link
in links
]:
1734 if not index
and free_socket
:
1738 #create viewer socket
1739 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1740 index
= len(node
.node_tree
.outputs
) - 1
1741 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1744 def init_shader_variables(self
, space
, shader_type
):
1745 if shader_type
== 'OBJECT':
1746 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1747 self
.shader_output_type
= "OUTPUT_MATERIAL"
1748 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1749 self
.shader_viewer_ident
= "ShaderNodeEmission"
1751 self
.shader_output_type
= "OUTPUT_LIGHT"
1752 self
.shader_output_ident
= "ShaderNodeOutputLight"
1753 self
.shader_viewer_ident
= "ShaderNodeEmission"
1755 elif shader_type
== 'WORLD':
1756 self
.shader_output_type
= "OUTPUT_WORLD"
1757 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1758 self
.shader_viewer_ident
= "ShaderNodeBackground"
1760 def get_shader_output_node(self
, tree
):
1761 for node
in tree
.nodes
:
1762 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1766 def ensure_group_output(cls
, tree
):
1767 #check if a group output node exists otherwise create
1768 groupout
= get_group_output_node(tree
)
1770 groupout
= tree
.nodes
.new('NodeGroupOutput')
1771 loc_x
, loc_y
= get_output_location(tree
)
1772 groupout
.location
.x
= loc_x
1773 groupout
.location
.y
= loc_y
1774 groupout
.select
= False
1775 # So that we don't keep on adding new group outputs
1776 groupout
.is_active_output
= True
1780 def search_sockets(cls
, node
, sockets
, index
=None):
1781 # recursively scan nodes for viewer sockets and store in list
1782 for i
, input_socket
in enumerate(node
.inputs
):
1783 if index
and i
!= index
:
1785 if len(input_socket
.links
):
1786 link
= input_socket
.links
[0]
1787 next_node
= link
.from_node
1788 external_socket
= link
.from_socket
1789 if hasattr(next_node
, "node_tree"):
1790 for socket_index
, s
in enumerate(next_node
.outputs
):
1791 if s
== external_socket
:
1793 socket
= next_node
.node_tree
.outputs
[socket_index
]
1794 if is_viewer_socket(socket
) and socket
not in sockets
:
1795 sockets
.append(socket
)
1796 #continue search inside of node group but restrict socket to where we came from
1797 groupout
= get_group_output_node(next_node
.node_tree
)
1798 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1801 def scan_nodes(cls
, tree
, sockets
):
1802 # get all viewer sockets in a material tree
1803 for node
in tree
.nodes
:
1804 if hasattr(node
, "node_tree"):
1805 for socket
in node
.node_tree
.outputs
:
1806 if is_viewer_socket(socket
) and (socket
not in sockets
):
1807 sockets
.append(socket
)
1808 cls
.scan_nodes(node
.node_tree
, sockets
)
1810 def link_leads_to_used_socket(self
, link
):
1811 #return True if link leads to a socket that is already used in this material
1812 socket
= get_internal_socket(link
.to_socket
)
1813 return (socket
and self
.is_socket_used_active_mat(socket
))
1815 def is_socket_used_active_mat(self
, socket
):
1816 #ensure used sockets in active material is calculated and check given socket
1817 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1818 self
.used_viewer_sockets_active_mat
= []
1819 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1821 emission
= self
.get_viewer_node(materialout
)
1822 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_active_mat
)
1823 return socket
in self
.used_viewer_sockets_active_mat
1825 def is_socket_used_other_mats(self
, socket
):
1826 #ensure used sockets in other materials are calculated and check given socket
1827 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1828 self
.used_viewer_sockets_other_mats
= []
1829 for mat
in bpy
.data
.materials
:
1830 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1833 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1835 emission
= self
.get_viewer_node(materialout
)
1836 self
.search_sockets((emission
if emission
else materialout
), self
.used_viewer_sockets_other_mats
)
1837 return socket
in self
.used_viewer_sockets_other_mats
1840 def get_viewer_node(materialout
):
1841 input_socket
= materialout
.inputs
[0]
1842 if len(input_socket
.links
) > 0:
1843 node
= input_socket
.links
[0].from_node
1844 if node
.type == 'EMISSION' and node
.name
== "Emission Viewer":
1847 def invoke(self
, context
, event
):
1848 space
= context
.space_data
1849 # Ignore operator when running in wrong context.
1850 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1851 return {'PASS_THROUGH'}
1853 shader_type
= space
.shader_type
1854 self
.init_shader_variables(space
, shader_type
)
1855 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1856 mlocx
= event
.mouse_region_x
1857 mlocy
= event
.mouse_region_y
1858 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
1859 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1860 active_tree
, path_to_tree
= get_active_tree(context
)
1861 nodes
, links
= active_tree
.nodes
, active_tree
.links
1862 base_node_tree
= space
.node_tree
1863 active
= nodes
.active
1865 # For geometry node trees we just connect to the group output,
1866 # because there is no "viewer node" yet.
1867 if space
.tree_type
== "GeometryNodeTree":
1870 for out
in active
.outputs
:
1871 if is_visible_socket(out
):
1880 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1881 self
.scan_nodes(base_node_tree
, delete_sockets
)
1883 # Find (or create if needed) the output of this node tree
1884 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1886 # Analyze outputs, make links
1889 for i
, out
in enumerate(active
.outputs
):
1890 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1891 valid_outputs
.append(i
)
1893 out_i
= valid_outputs
[0] # Start index of node's outputs
1894 for i
, valid_i
in enumerate(valid_outputs
):
1895 for out_link
in active
.outputs
[valid_i
].links
:
1896 if is_viewer_link(out_link
, geometryoutput
):
1897 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1898 if i
< len(valid_outputs
) - 1:
1899 out_i
= valid_outputs
[i
+ 1]
1901 out_i
= valid_outputs
[0]
1903 make_links
= [] # store sockets for new links
1904 delete_nodes
= [] # store unused nodes to delete in the end
1906 # If there is no 'GEOMETRY' output type - We can't preview the node
1909 socket_type
= 'GEOMETRY'
1910 # Find an input socket of the output of type geometry
1911 geometryoutindex
= None
1912 for i
,inp
in enumerate(geometryoutput
.inputs
):
1913 if inp
.type == socket_type
:
1914 geometryoutindex
= i
1916 if geometryoutindex
is None:
1917 # Create geometry socket
1918 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1919 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1921 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1922 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1923 for li_from
, li_to
in make_links
:
1924 base_node_tree
.links
.new(li_from
, li_to
)
1925 tree
= base_node_tree
1926 link_end
= output_socket
1927 while tree
.nodes
.active
!= active
:
1928 node
= tree
.nodes
.active
1929 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1930 link_start
= node
.outputs
[index
]
1931 node_socket
= node
.node_tree
.outputs
[index
]
1932 if node_socket
in delete_sockets
:
1933 delete_sockets
.remove(node_socket
)
1934 tree
.links
.new(link_start
, link_end
)
1936 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1937 tree
= tree
.nodes
.active
.node_tree
1938 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1941 for socket
in delete_sockets
:
1942 tree
= socket
.id_data
1943 tree
.outputs
.remove(socket
)
1946 for tree
, node
in delete_nodes
:
1947 tree
.nodes
.remove(node
)
1949 nodes
.active
= active
1950 active
.select
= True
1951 force_update(context
)
1955 # What follows is code for the shader editor
1956 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1959 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
):
1960 for out
in active
.outputs
:
1961 if is_visible_socket(out
):
1965 # get material_output node
1966 materialout
= None # placeholder node
1969 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1970 self
.scan_nodes(base_node_tree
, delete_sockets
)
1972 materialout
= self
.get_shader_output_node(base_node_tree
)
1974 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1975 materialout
.location
= get_output_location(base_node_tree
)
1976 materialout
.select
= False
1977 # Analyze outputs, add "Emission Viewer" if needed, make links
1980 for i
, out
in enumerate(active
.outputs
):
1981 if is_visible_socket(out
):
1982 valid_outputs
.append(i
)
1984 out_i
= valid_outputs
[0] # Start index of node's outputs
1985 for i
, valid_i
in enumerate(valid_outputs
):
1986 for out_link
in active
.outputs
[valid_i
].links
:
1987 if is_viewer_link(out_link
, materialout
):
1988 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1989 if i
< len(valid_outputs
) - 1:
1990 out_i
= valid_outputs
[i
+ 1]
1992 out_i
= valid_outputs
[0]
1994 make_links
= [] # store sockets for new links
1995 delete_nodes
= [] # store unused nodes to delete in the end
1997 # If output type not 'SHADER' - "Emission Viewer" needed
1998 if active
.outputs
[out_i
].type != 'SHADER':
1999 socket_type
= 'NodeSocketColor'
2000 # get Emission Viewer node
2001 emission_exists
= False
2002 emission_placeholder
= base_node_tree
.nodes
[0]
2003 for node
in base_node_tree
.nodes
:
2004 if "Emission Viewer" in node
.name
:
2005 emission_exists
= True
2006 emission_placeholder
= node
2007 if not emission_exists
:
2008 emission
= base_node_tree
.nodes
.new(self
.shader_viewer_ident
)
2009 emission
.hide
= True
2010 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
2011 emission
.label
= "Viewer"
2012 emission
.name
= "Emission Viewer"
2013 emission
.use_custom_color
= True
2014 emission
.color
= (0.6, 0.5, 0.4)
2015 emission
.select
= False
2017 emission
= emission_placeholder
2018 output_socket
= emission
.inputs
[0]
2020 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2021 if emission
.outputs
[0].links
.__len
__() > 0:
2022 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
2023 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2025 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
2027 # Set brightness of viewer to compensate for Film and CM exposure
2028 if context
.scene
.render
.engine
== 'CYCLES' and hasattr(context
.scene
, 'cycles'):
2029 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
2033 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
2034 emission
.inputs
[1].default_value
= intensity
2037 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2038 socket_type
= 'NodeSocketShader'
2039 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
2040 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
2041 output_socket
= materialout
.inputs
[materialout_index
]
2042 for node
in base_node_tree
.nodes
:
2043 if node
.name
== 'Emission Viewer':
2044 delete_nodes
.append((base_node_tree
, node
))
2045 for li_from
, li_to
in make_links
:
2046 base_node_tree
.links
.new(li_from
, li_to
)
2048 # Create links through node groups until we reach the active node
2049 tree
= base_node_tree
2050 link_end
= output_socket
2051 while tree
.nodes
.active
!= active
:
2052 node
= tree
.nodes
.active
2053 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
2054 link_start
= node
.outputs
[index
]
2055 node_socket
= node
.node_tree
.outputs
[index
]
2056 if node_socket
in delete_sockets
:
2057 delete_sockets
.remove(node_socket
)
2058 tree
.links
.new(link_start
, link_end
)
2060 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
2061 tree
= tree
.nodes
.active
.node_tree
2062 tree
.links
.new(active
.outputs
[out_i
], link_end
)
2065 for socket
in delete_sockets
:
2066 if not self
.is_socket_used_other_mats(socket
):
2067 tree
= socket
.id_data
2068 tree
.outputs
.remove(socket
)
2071 for tree
, node
in delete_nodes
:
2072 tree
.nodes
.remove(node
)
2074 nodes
.active
= active
2075 active
.select
= True
2077 force_update(context
)
2081 return {'CANCELLED'}
2084 class NWFrameSelected(Operator
, NWBase
):
2085 bl_idname
= "node.nw_frame_selected"
2086 bl_label
= "Frame Selected"
2087 bl_description
= "Add a frame node and parent the selected nodes to it"
2088 bl_options
= {'REGISTER', 'UNDO'}
2090 label_prop
: StringProperty(
2092 description
='The visual name of the frame node',
2095 color_prop
: FloatVectorProperty(
2097 description
="The color of the frame node",
2098 default
=(0.6, 0.6, 0.6),
2099 min=0, max=1, step
=1, precision
=3,
2100 subtype
='COLOR_GAMMA', size
=3
2103 def execute(self
, context
):
2104 nodes
, links
= get_nodes_links(context
)
2107 if node
.select
== True:
2108 selected
.append(node
)
2110 bpy
.ops
.node
.add_node(type='NodeFrame')
2112 frm
.label
= self
.label_prop
2113 frm
.use_custom_color
= True
2114 frm
.color
= self
.color_prop
2116 for node
in selected
:
2122 class NWReloadImages(Operator
):
2123 bl_idname
= "node.nw_reload_images"
2124 bl_label
= "Reload Images"
2125 bl_description
= "Update all the image nodes to match their files on disk"
2128 def poll(cls
, context
):
2130 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
2131 if context
.active_node
is not None:
2132 for out
in context
.active_node
.outputs
:
2133 if is_visible_socket(out
):
2138 def execute(self
, context
):
2139 nodes
, links
= get_nodes_links(context
)
2140 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2143 if node
.type in image_types
:
2144 if node
.type == "TEXTURE":
2145 if node
.texture
: # node has texture assigned
2146 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2147 if node
.texture
.image
: # texture has image assigned
2148 node
.texture
.image
.reload()
2156 self
.report({'INFO'}, "Reloaded images")
2157 print("Reloaded " + str(num_reloaded
) + " images")
2158 force_update(context
)
2161 self
.report({'WARNING'}, "No images found to reload in this node tree")
2162 return {'CANCELLED'}
2165 class NWSwitchNodeType(Operator
, NWBase
):
2166 """Switch type of selected nodes """
2167 bl_idname
= "node.nw_swtch_node_type"
2168 bl_label
= "Switch Node Type"
2169 bl_options
= {'REGISTER', 'UNDO'}
2171 to_type
: EnumProperty(
2172 name
="Switch to type",
2173 items
=list(shaders_input_nodes_props
) +
2174 list(shaders_output_nodes_props
) +
2175 list(shaders_shader_nodes_props
) +
2176 list(shaders_texture_nodes_props
) +
2177 list(shaders_color_nodes_props
) +
2178 list(shaders_vector_nodes_props
) +
2179 list(shaders_converter_nodes_props
) +
2180 list(shaders_layout_nodes_props
) +
2181 list(compo_input_nodes_props
) +
2182 list(compo_output_nodes_props
) +
2183 list(compo_color_nodes_props
) +
2184 list(compo_converter_nodes_props
) +
2185 list(compo_filter_nodes_props
) +
2186 list(compo_vector_nodes_props
) +
2187 list(compo_matte_nodes_props
) +
2188 list(compo_distort_nodes_props
) +
2189 list(compo_layout_nodes_props
) +
2190 list(blender_mat_input_nodes_props
) +
2191 list(blender_mat_output_nodes_props
) +
2192 list(blender_mat_color_nodes_props
) +
2193 list(blender_mat_vector_nodes_props
) +
2194 list(blender_mat_converter_nodes_props
) +
2195 list(blender_mat_layout_nodes_props
) +
2196 list(texture_input_nodes_props
) +
2197 list(texture_output_nodes_props
) +
2198 list(texture_color_nodes_props
) +
2199 list(texture_pattern_nodes_props
) +
2200 list(texture_textures_nodes_props
) +
2201 list(texture_converter_nodes_props
) +
2202 list(texture_distort_nodes_props
) +
2203 list(texture_layout_nodes_props
)
2206 geo_to_type
: StringProperty(
2207 name
="Switch to type",
2211 def execute(self
, context
):
2212 nodes
, links
= get_nodes_links(context
)
2213 to_type
= self
.to_type
2214 if self
.geo_to_type
!= '':
2215 to_type
= self
.geo_to_type
2216 # Those types of nodes will not swap.
2217 src_excludes
= ('NodeFrame')
2218 # Those attributes of nodes will be copied if possible
2219 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
2220 'show_options', 'show_preview', 'show_texture',
2221 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2223 selected
= [n
for n
in nodes
if n
.select
]
2225 for node
in [n
for n
in selected
if
2226 n
.rna_type
.identifier
not in src_excludes
and
2227 n
.rna_type
.identifier
!= to_type
]:
2228 new_node
= nodes
.new(to_type
)
2229 for attr
in attrs_to_pass
:
2230 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
2231 setattr(new_node
, attr
, getattr(node
, attr
))
2232 # set image datablock of dst to image of src
2233 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
2235 new_node
.image
= node
.image
2237 if new_node
.type == 'SWITCH':
2238 new_node
.hide
= True
2239 # Dictionaries: src_sockets and dst_sockets:
2240 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2241 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2242 # in 'INPUTS' and 'OUTPUTS':
2243 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2245 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2247 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2248 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2251 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2252 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2254 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2255 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2256 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2257 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
2258 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2259 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
2260 # enumerate in inputs, then in outputs
2261 # find name, default value and links of socket
2262 for i
, socket
in enumerate(in_out
):
2263 the_name
= socket
.name
2265 # Not every socket, especially in outputs has "default_value"
2266 if hasattr(socket
, 'default_value'):
2267 dval
= socket
.default_value
2269 for lnk
in socket
.links
:
2270 socket_links
.append(lnk
)
2271 # check type of socket to fill proper keys.
2272 for the_type
in types_order_one
:
2273 if socket
.type == the_type
:
2274 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2275 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2276 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
2277 # Check which of the types in inputs/outputs is considered to be "main".
2278 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2279 for type_check
in types_order_one
:
2280 if sockets
[in_out_name
][type_check
]:
2281 sockets
[in_out_name
]['MAIN'] = type_check
2285 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2286 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2289 for inout
, soctype
in (
2290 ('INPUTS', 'MAIN',),
2291 ('INPUTS', 'SHADER',),
2292 ('INPUTS', 'RGBA',),
2293 ('INPUTS', 'VECTOR',),
2294 ('INPUTS', 'VALUE',),
2295 ('OUTPUTS', 'MAIN',),
2296 ('OUTPUTS', 'SHADER',),
2297 ('OUTPUTS', 'RGBA',),
2298 ('OUTPUTS', 'VECTOR',),
2299 ('OUTPUTS', 'VALUE',),
2301 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
2302 if soctype
== 'MAIN':
2303 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
2304 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
2306 sc
= src_sockets
[inout
][soctype
]
2307 dt
= dst_sockets
[inout
][soctype
]
2308 # start with 'dt' to determine number of possibilities.
2309 for i
, soc
in enumerate(dt
):
2310 # if src main has enough entries - match them with dst main sockets by indexes.
2312 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
2313 # add 'VALUE_NAME' criterion to inputs.
2314 if inout
== 'INPUTS' and soctype
== 'VALUE':
2316 if s
[2] == soc
[2]: # if names match
2317 # append src (index, dval), dst (index, dval)
2318 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
2320 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2321 # This creates better links when relinking textures.
2322 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
2323 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
2325 # Pass default values and RELINK:
2326 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2327 # INPUTS: Base on matches in proper order.
2328 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
2330 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
2331 new_node
.inputs
[dst_i
].default_value
= src_dval
2332 # Special case: switch to math
2333 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2334 new_node
.type == 'MATH' and\
2336 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
2337 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
2338 if node
.type == 'MIX_RGB':
2339 if node
.blend_type
in [o
[0] for o
in operations
]:
2340 new_node
.operation
= node
.blend_type
2341 # Special case: switch from math to some types
2342 if node
.type == 'MATH' and\
2343 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2346 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
2347 if new_node
.type == 'MIX_RGB':
2348 if node
.operation
in [t
[0] for t
in blend_types
]:
2349 new_node
.blend_type
= node
.operation
2350 # Set Fac of MIX_RGB to 1.0
2351 new_node
.inputs
[0].default_value
= 1.0
2352 # make link only when dst matching input is not linked already.
2353 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
2354 in_src_link
= node
.inputs
[src_i
].links
[0]
2355 in_dst_socket
= new_node
.inputs
[dst_i
]
2356 links
.new(in_src_link
.from_socket
, in_dst_socket
)
2357 links
.remove(in_src_link
)
2358 # OUTPUTS: Base on matches in proper order.
2359 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
2360 for out_src_link
in node
.outputs
[src_i
].links
:
2361 out_dst_socket
= new_node
.outputs
[dst_i
]
2362 links
.new(out_dst_socket
, out_src_link
.to_socket
)
2363 # relink rest inputs if possible, no criteria
2364 for src_inp
in node
.inputs
:
2365 for dst_inp
in new_node
.inputs
:
2366 if src_inp
.links
and not dst_inp
.links
:
2367 src_link
= src_inp
.links
[0]
2368 links
.new(src_link
.from_socket
, dst_inp
)
2369 links
.remove(src_link
)
2370 # relink rest outputs if possible, base on node kind if any left.
2371 for src_o
in node
.outputs
:
2372 for out_src_link
in src_o
.links
:
2373 for dst_o
in new_node
.outputs
:
2374 if src_o
.type == dst_o
.type:
2375 links
.new(dst_o
, out_src_link
.to_socket
)
2376 # relink rest outputs no criteria if any left. Link all from first output.
2377 for src_o
in node
.outputs
:
2378 for out_src_link
in src_o
.links
:
2379 if new_node
.outputs
:
2380 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2382 force_update(context
)
2386 class NWMergeNodes(Operator
, NWBase
):
2387 bl_idname
= "node.nw_merge_nodes"
2388 bl_label
= "Merge Nodes"
2389 bl_description
= "Merge Selected Nodes"
2390 bl_options
= {'REGISTER', 'UNDO'}
2394 description
="All possible blend types, boolean operations and math operations",
2395 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
],
2397 merge_type
: EnumProperty(
2399 description
="Type of Merge to be used",
2401 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2402 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2403 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2404 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2405 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2406 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2407 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2411 # Check if the link connects to a node that is in selected_nodes
2412 # If not, then check recursively for each link in the nodes outputs.
2413 # If yes, return True. If the recursion stops without finding a node
2414 # in selected_nodes, it returns False. The depth is used to prevent
2415 # getting stuck in a loop because of an already present cycle.
2417 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
2419 # We're stuck in a cycle, but that cycle was already present,
2420 # so we return False.
2421 # NOTE: The number 255 is arbitrary, but seems to work well.
2424 if node
in selected_nodes
:
2426 if not node
.outputs
:
2428 for output
in node
.outputs
:
2429 if output
.is_linked
:
2430 for olink
in output
.links
:
2431 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
2433 # None of the outputs found a node in selected_nodes, so there is no cycle.
2436 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2437 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2438 # be connected. The last one is assumed to be a multi input socket.
2439 # For convenience the node is returned.
2441 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
2442 # The y-location of the last node
2443 loc_y
= nodes_list
[-1][2]
2444 if merge_position
== 'CENTER':
2445 # Average the y-location
2446 for i
in range(len(nodes_list
)-1):
2447 loc_y
+= nodes_list
[i
][2]
2448 loc_y
= loc_y
/len(nodes_list
)
2449 new_node
= nodes
.new(node_name
)
2450 new_node
.hide
= do_hide
2451 new_node
.location
.x
= loc_x
2452 new_node
.location
.y
= loc_y
2453 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
2455 outputs_for_multi_input
= []
2456 for i
,node
in enumerate(selected_nodes
):
2458 # Search for the first node which had output links that do not create
2459 # a cycle, which we can then reconnect afterwards.
2460 if prev_links
== [] and node
.outputs
[0].is_linked
:
2461 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
2462 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2463 # To get the placement to look right we need to reverse the order in which we connect the
2464 # outputs to the multi input socket.
2465 if i
< len(socket_indices
) - 1:
2466 ind
= socket_indices
[i
]
2467 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
2469 outputs_for_multi_input
.insert(0, node
.outputs
[0])
2470 if outputs_for_multi_input
!= []:
2471 ind
= socket_indices
[-1]
2472 for output
in outputs_for_multi_input
:
2473 links
.new(output
, new_node
.inputs
[ind
])
2474 if prev_links
!= []:
2475 for link
in prev_links
:
2476 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
2479 def execute(self
, context
):
2480 settings
= context
.preferences
.addons
[__name__
].preferences
2481 merge_hide
= settings
.merge_hide
2482 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2485 do_hide_shader
= False
2486 if merge_hide
== 'ALWAYS':
2488 do_hide_shader
= True
2489 elif merge_hide
== 'NON_SHADER':
2492 tree_type
= context
.space_data
.node_tree
.type
2493 if tree_type
== 'GEOMETRY':
2494 node_type
= 'GeometryNode'
2495 if tree_type
== 'COMPOSITING':
2496 node_type
= 'CompositorNode'
2497 elif tree_type
== 'SHADER':
2498 node_type
= 'ShaderNode'
2499 elif tree_type
== 'TEXTURE':
2500 node_type
= 'TextureNode'
2501 nodes
, links
= get_nodes_links(context
)
2503 merge_type
= self
.merge_type
2504 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2505 # 'ZCOMBINE' works only if mode == 'MIX'
2506 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2507 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2510 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2512 # The math nodes used for geometry nodes are of type 'ShaderNode'
2513 if merge_type
== 'MATH' and tree_type
== 'GEOMETRY':
2514 node_type
= 'ShaderNode'
2515 selected_mix
= [] # entry = [index, loc]
2516 selected_shader
= [] # entry = [index, loc]
2517 selected_geometry
= [] # entry = [index, loc]
2518 selected_math
= [] # entry = [index, loc]
2519 selected_vector
= [] # entry = [index, loc]
2520 selected_z
= [] # entry = [index, loc]
2521 selected_alphaover
= [] # entry = [index, loc]
2523 for i
, node
in enumerate(nodes
):
2524 if node
.select
and node
.outputs
:
2525 if merge_type
== 'AUTO':
2526 for (type, types_list
, dst
) in (
2527 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2528 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2529 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2530 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2531 ('VECTOR', [], selected_vector
),
2533 output_type
= node
.outputs
[0].type
2534 valid_mode
= mode
in types_list
2535 # When mode is 'MIX' we have to cheat since the mix node is not used in
2537 if tree_type
== 'GEOMETRY':
2539 if output_type
== 'VALUE' and type == 'VALUE':
2541 elif output_type
== 'VECTOR' and type == 'VECTOR':
2543 elif type == 'GEOMETRY':
2545 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2546 # Cheat that output type is 'RGBA',
2547 # and that 'MIX' exists in math operations list.
2548 # This way when selected_mix list is analyzed:
2549 # Node data will be appended even though it doesn't meet requirements.
2550 elif output_type
!= 'SHADER' and mode
== 'MIX':
2551 output_type
= 'RGBA'
2553 if output_type
== type and valid_mode
:
2554 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2556 for (type, types_list
, dst
) in (
2557 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2558 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2559 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2560 ('MATH', [t
[0] for t
in operations
], selected_math
),
2561 ('ZCOMBINE', ('MIX', ), selected_z
),
2562 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2564 if merge_type
== type and mode
in types_list
:
2565 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2566 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2567 # use only 'Mix' nodes for merging.
2568 # For that we add selected_math list to selected_mix list and clear selected_math.
2569 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2570 selected_mix
+= selected_math
2572 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2575 count_before
= len(nodes
)
2576 # sort list by loc_x - reversed
2577 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2579 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2580 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2582 # Change the node type for math nodes in a geometry node tree.
2583 if tree_type
== 'GEOMETRY':
2584 if nodes_list
is selected_math
or nodes_list
is selected_vector
:
2585 node_type
= 'ShaderNode'
2589 node_type
= 'GeometryNode'
2590 if merge_position
== 'CENTER':
2591 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)
2592 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2598 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2602 if nodes_list
== selected_shader
and not do_hide_shader
:
2604 the_range
= len(nodes_list
) - 1
2605 if len(nodes_list
) == 1:
2608 for i
in range(the_range
):
2609 if nodes_list
== selected_mix
:
2610 add_type
= node_type
+ 'MixRGB'
2611 add
= nodes
.new(add_type
)
2612 add
.blend_type
= mode
2614 add
.inputs
[0].default_value
= 1.0
2615 add
.show_preview
= False
2621 add
.width_hidden
= 100.0
2622 elif nodes_list
== selected_math
:
2623 add_type
= node_type
+ 'Math'
2624 add
= nodes
.new(add_type
)
2625 add
.operation
= mode
2631 add
.width_hidden
= 100.0
2632 elif nodes_list
== selected_shader
:
2634 add_type
= node_type
+ 'MixShader'
2635 add
= nodes
.new(add_type
)
2636 add
.hide
= do_hide_shader
2641 add
.width_hidden
= 100.0
2643 add_type
= node_type
+ 'AddShader'
2644 add
= nodes
.new(add_type
)
2645 add
.hide
= do_hide_shader
2650 add
.width_hidden
= 100.0
2651 elif nodes_list
== selected_geometry
:
2652 if mode
in ('JOIN', 'MIX'):
2653 add_type
= node_type
+ 'JoinGeometry'
2654 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2656 add_type
= node_type
+ 'Boolean'
2657 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2658 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2659 add
.operation
= mode
2662 elif nodes_list
== selected_vector
:
2663 add_type
= node_type
+ 'VectorMath'
2664 add
= nodes
.new(add_type
)
2665 add
.operation
= mode
2671 add
.width_hidden
= 100.0
2672 elif nodes_list
== selected_z
:
2673 add
= nodes
.new('CompositorNodeZcombine')
2674 add
.show_preview
= False
2680 add
.width_hidden
= 100.0
2681 elif nodes_list
== selected_alphaover
:
2682 add
= nodes
.new('CompositorNodeAlphaOver')
2683 add
.show_preview
= False
2689 add
.width_hidden
= 100.0
2690 add
.location
= loc_x
, loc_y
2694 # This has already been handled separately
2698 count_after
= len(nodes
)
2699 index
= count_after
- 1
2700 first_selected
= nodes
[nodes_list
[0][0]]
2701 # "last" node has been added as first, so its index is count_before.
2702 last_add
= nodes
[count_before
]
2703 # Create list of invalid indexes.
2704 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2707 # Two nodes were selected and first selected has no output links, second selected has output links.
2708 # Then add links from last add to all links 'to_socket' of out links of second selected.
2709 if len(nodes_list
) == 2:
2710 if not first_selected
.outputs
[0].links
:
2711 second_selected
= nodes
[nodes_list
[1][0]]
2712 for ss_link
in second_selected
.outputs
[0].links
:
2713 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2714 # Link only if "to_node" index not in invalid indexes list.
2715 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2716 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2717 # add links from last_add to all links 'to_socket' of out links of first selected.
2718 for fs_link
in first_selected
.outputs
[0].links
:
2719 # Link only if "to_node" index not in invalid indexes list.
2720 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2721 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2722 # add link from "first" selected and "first" add node
2723 node_to
= nodes
[count_after
- 1]
2724 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2725 if node_to
.type == 'ZCOMBINE':
2726 for fs_out
in first_selected
.outputs
:
2727 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2728 links
.new(fs_out
, node_to
.inputs
[1])
2730 # add links between added ADD nodes and between selected and ADD nodes
2731 for i
in range(count_adds
):
2732 if i
< count_adds
- 1:
2733 node_from
= nodes
[index
]
2734 node_to
= nodes
[index
- 1]
2735 node_to_input_i
= first
2736 node_to_z_i
= 1 # if z combine - link z to first z input
2737 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2738 if node_to
.type == 'ZCOMBINE':
2739 for from_out
in node_from
.outputs
:
2740 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2741 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2742 if len(nodes_list
) > 1:
2743 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2744 node_to
= nodes
[index
]
2745 node_to_input_i
= second
2746 node_to_z_i
= 3 # if z combine - link z to second z input
2747 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2748 if node_to
.type == 'ZCOMBINE':
2749 for from_out
in node_from
.outputs
:
2750 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2751 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2753 # set "last" of added nodes as active
2754 nodes
.active
= last_add
2755 for i
, x
, y
, dx
, h
in nodes_list
:
2756 nodes
[i
].select
= False
2761 class NWBatchChangeNodes(Operator
, NWBase
):
2762 bl_idname
= "node.nw_batch_change"
2763 bl_label
= "Batch Change"
2764 bl_description
= "Batch Change Blend Type and Math Operation"
2765 bl_options
= {'REGISTER', 'UNDO'}
2767 blend_type
: EnumProperty(
2769 items
=blend_types
+ navs
,
2771 operation
: EnumProperty(
2773 items
=operations
+ navs
,
2776 def execute(self
, context
):
2777 blend_type
= self
.blend_type
2778 operation
= self
.operation
2779 for node
in context
.selected_nodes
:
2780 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2781 if not blend_type
in [nav
[0] for nav
in navs
]:
2782 node
.blend_type
= blend_type
2784 if blend_type
== 'NEXT':
2785 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2786 #index = blend_types.index(node.blend_type)
2787 if index
== len(blend_types
) - 1:
2788 node
.blend_type
= blend_types
[0][0]
2790 node
.blend_type
= blend_types
[index
+ 1][0]
2792 if blend_type
== 'PREV':
2793 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2795 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2797 node
.blend_type
= blend_types
[index
- 1][0]
2799 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2800 if not operation
in [nav
[0] for nav
in navs
]:
2801 node
.operation
= operation
2803 if operation
== 'NEXT':
2804 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2805 #index = operations.index(node.operation)
2806 if index
== len(operations
) - 1:
2807 node
.operation
= operations
[0][0]
2809 node
.operation
= operations
[index
+ 1][0]
2811 if operation
== 'PREV':
2812 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2813 #index = operations.index(node.operation)
2815 node
.operation
= operations
[len(operations
) - 1][0]
2817 node
.operation
= operations
[index
- 1][0]
2822 class NWChangeMixFactor(Operator
, NWBase
):
2823 bl_idname
= "node.nw_factor"
2824 bl_label
= "Change Factor"
2825 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2826 bl_options
= {'REGISTER', 'UNDO'}
2828 # option: Change factor.
2829 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2830 # Else - change factor by option value.
2831 option
: FloatProperty()
2833 def execute(self
, context
):
2834 nodes
, links
= get_nodes_links(context
)
2835 option
= self
.option
2836 selected
= [] # entry = index
2837 for si
, node
in enumerate(nodes
):
2839 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2843 fac
= nodes
[si
].inputs
[0]
2844 nodes
[si
].hide
= False
2845 if option
in {0.0, 1.0}:
2846 fac
.default_value
= option
2848 fac
.default_value
+= option
2853 class NWCopySettings(Operator
, NWBase
):
2854 bl_idname
= "node.nw_copy_settings"
2855 bl_label
= "Copy Settings"
2856 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2857 bl_options
= {'REGISTER', 'UNDO'}
2860 def poll(cls
, context
):
2862 if nw_check(context
):
2864 context
.active_node
is not None and
2865 context
.active_node
.type != 'FRAME'
2870 def execute(self
, context
):
2871 node_active
= context
.active_node
2872 node_selected
= context
.selected_nodes
2875 if not (len(node_selected
) > 1):
2876 self
.report({'ERROR'}, "2 nodes must be selected at least")
2877 return {'CANCELLED'}
2879 # Check if active node is in the selection
2880 selected_node_names
= [n
.name
for n
in node_selected
]
2881 if node_active
.name
not in selected_node_names
:
2882 self
.report({'ERROR'}, "No active node")
2883 return {'CANCELLED'}
2885 # Get nodes in selection by type
2886 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2888 if not (len(valid_nodes
) > 1) and node_active
:
2889 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2890 return {'CANCELLED'}
2892 if len(valid_nodes
) != len(node_selected
):
2893 # Report nodes that are not valid
2894 valid_node_names
= [n
.name
for n
in valid_nodes
]
2895 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2896 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2898 # Reference original
2900 #node_selected_names = [n.name for n in node_selected]
2905 # Deselect all nodes
2906 for i
in node_selected
:
2909 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2910 # Run through all other nodes
2911 for node
in valid_nodes
[1:]:
2913 # Check for frame node
2914 parent
= node
.parent
if node
.parent
else None
2915 node_loc
= [node
.location
.x
, node
.location
.y
]
2917 # Select original to duplicate
2920 # Duplicate selected node
2921 bpy
.ops
.node
.duplicate()
2922 new_node
= context
.selected_nodes
[0]
2925 new_node
.select
= False
2927 # Properties to copy
2928 node_tree
= node
.id_data
2929 props_to_copy
= 'bl_idname name location height width'.split(' ')
2933 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2934 for i
in (i
for i
in mappings
if i
.is_linked
):
2936 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2939 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2940 props_to_copy
.pop(0)
2942 for prop
in props_to_copy
:
2943 setattr(new_node
, prop
, props
[prop
])
2945 # Get the node tree to remove the old node
2946 nodes
= node_tree
.nodes
2948 new_node
.name
= props
['name']
2951 new_node
.parent
= parent
2952 new_node
.location
= node_loc
2954 for str_from
, str_to
in reconnections
:
2955 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2957 success_names
.append(new_node
.name
)
2960 node_tree
.nodes
.active
= orig
2961 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2965 class NWCopyLabel(Operator
, NWBase
):
2966 bl_idname
= "node.nw_copy_label"
2967 bl_label
= "Copy Label"
2968 bl_options
= {'REGISTER', 'UNDO'}
2970 option
: EnumProperty(
2972 description
="Source of name of label",
2974 ('FROM_ACTIVE', 'from active', 'from active node',),
2975 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2976 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2980 def execute(self
, context
):
2981 nodes
, links
= get_nodes_links(context
)
2982 option
= self
.option
2983 active
= nodes
.active
2984 if option
== 'FROM_ACTIVE':
2986 src_label
= active
.label
2987 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2988 node
.label
= src_label
2989 elif option
== 'FROM_NODE':
2990 selected
= [n
for n
in nodes
if n
.select
]
2991 for node
in selected
:
2992 for input in node
.inputs
:
2994 src
= input.links
[0].from_node
2995 node
.label
= src
.label
2997 elif option
== 'FROM_SOCKET':
2998 selected
= [n
for n
in nodes
if n
.select
]
2999 for node
in selected
:
3000 for input in node
.inputs
:
3002 src
= input.links
[0].from_socket
3003 node
.label
= src
.name
3009 class NWClearLabel(Operator
, NWBase
):
3010 bl_idname
= "node.nw_clear_label"
3011 bl_label
= "Clear Label"
3012 bl_options
= {'REGISTER', 'UNDO'}
3014 option
: BoolProperty()
3016 def execute(self
, context
):
3017 nodes
, links
= get_nodes_links(context
)
3018 for node
in [n
for n
in nodes
if n
.select
]:
3023 def invoke(self
, context
, event
):
3025 return self
.execute(context
)
3027 return context
.window_manager
.invoke_confirm(self
, event
)
3030 class NWModifyLabels(Operator
, NWBase
):
3031 """Modify Labels of all selected nodes"""
3032 bl_idname
= "node.nw_modify_labels"
3033 bl_label
= "Modify Labels"
3034 bl_options
= {'REGISTER', 'UNDO'}
3036 prepend
: StringProperty(
3037 name
="Add to Beginning"
3039 append
: StringProperty(
3042 replace_from
: StringProperty(
3043 name
="Text to Replace"
3045 replace_to
: StringProperty(
3049 def execute(self
, context
):
3050 nodes
, links
= get_nodes_links(context
)
3051 for node
in [n
for n
in nodes
if n
.select
]:
3052 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
3056 def invoke(self
, context
, event
):
3060 return context
.window_manager
.invoke_props_dialog(self
)
3063 class NWAddTextureSetup(Operator
, NWBase
):
3064 bl_idname
= "node.nw_add_texture"
3065 bl_label
= "Texture Setup"
3066 bl_description
= "Add Texture Node Setup to Selected Shaders"
3067 bl_options
= {'REGISTER', 'UNDO'}
3069 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
3072 def poll(cls
, context
):
3074 if nw_check(context
):
3075 space
= context
.space_data
3076 if space
.tree_type
== 'ShaderNodeTree':
3080 def execute(self
, context
):
3081 nodes
, links
= get_nodes_links(context
)
3082 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3083 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
3084 selected_nodes
= [n
for n
in nodes
if n
.select
]
3085 for t_node
in selected_nodes
:
3089 for index
, i
in enumerate(t_node
.inputs
):
3095 locx
= t_node
.location
.x
3096 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
3098 xoffset
= [500, 700]
3100 if t_node
.type in texture_types
+ ['MAPPING']:
3101 xoffset
= [290, 500]
3105 image_type
= 'ShaderNodeTexImage'
3107 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
3108 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
3109 if t_node
.type == 'BACKGROUND':
3110 image_type
= 'ShaderNodeTexEnvironment'
3113 tex
= nodes
.new(image_type
)
3114 tex
.location
= [locx
- 200, locy
+ 112]
3116 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
3118 t_node
.select
= False
3119 if self
.add_mapping
or is_texture
:
3120 if t_node
.type != 'MAPPING':
3121 m
= nodes
.new('ShaderNodeMapping')
3122 m
.location
= [locx
- xoffset
[0], locy
+ 141]
3126 coord
= nodes
.new('ShaderNodeTexCoord')
3127 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
3130 links
.new(m
.outputs
[0], tex
.inputs
[0])
3131 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3134 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
3135 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
3137 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
3141 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
3142 bl_idname
= "node.nw_add_textures_for_principled"
3143 bl_label
= "Principled Texture Setup"
3144 bl_description
= "Add Texture Node Setup for Principled BSDF"
3145 bl_options
= {'REGISTER', 'UNDO'}
3147 directory
: StringProperty(
3151 description
='Folder to search in for image files'
3153 files
: CollectionProperty(
3154 type=bpy
.types
.OperatorFileListElement
,
3155 options
={'HIDDEN', 'SKIP_SAVE'}
3158 relative_path
: BoolProperty(
3159 name
='Relative Path',
3160 description
='Set the file path relative to the blend file, when possible',
3169 def draw(self
, context
):
3170 layout
= self
.layout
3171 layout
.alignment
= 'LEFT'
3173 layout
.prop(self
, 'relative_path')
3176 def poll(cls
, context
):
3178 if nw_check(context
):
3179 space
= context
.space_data
3180 if space
.tree_type
== 'ShaderNodeTree':
3184 def execute(self
, context
):
3185 # Check if everything is ok
3186 if not self
.directory
:
3187 self
.report({'INFO'}, 'No Folder Selected')
3188 return {'CANCELLED'}
3189 if not self
.files
[:]:
3190 self
.report({'INFO'}, 'No Files Selected')
3191 return {'CANCELLED'}
3193 nodes
, links
= get_nodes_links(context
)
3194 active_node
= nodes
.active
3195 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
3196 self
.report({'INFO'}, 'Select Principled BSDF')
3197 return {'CANCELLED'}
3200 def split_into__components(fname
):
3201 # Split filename into components
3202 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3204 fname
= path
.splitext(fname
)[0]
3206 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
3207 # Separate CamelCase by space
3208 fname
= re
.sub(r
"([a-z])([A-Z])", r
"\g<1> \g<2>",fname
)
3209 # Replace common separators with SPACE
3210 separators
= ['_', '.', '-', '__', '--', '#']
3211 for sep
in separators
:
3212 fname
= fname
.replace(sep
, ' ')
3214 components
= fname
.split(' ')
3215 components
= [c
.lower() for c
in components
]
3218 # Filter textures names for texturetypes in filenames
3219 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3220 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
3221 normal_abbr
= tags
.normal
.split(' ')
3222 bump_abbr
= tags
.bump
.split(' ')
3223 gloss_abbr
= tags
.gloss
.split(' ')
3224 rough_abbr
= tags
.rough
.split(' ')
3226 ['Displacement', tags
.displacement
.split(' '), None],
3227 ['Base Color', tags
.base_color
.split(' '), None],
3228 ['Subsurface Color', tags
.sss_color
.split(' '), None],
3229 ['Metallic', tags
.metallic
.split(' '), None],
3230 ['Specular', tags
.specular
.split(' '), None],
3231 ['Roughness', rough_abbr
+ gloss_abbr
, None],
3232 ['Normal', normal_abbr
+ bump_abbr
, None],
3235 # Look through texture_types and set value as filename of first matched file
3236 def match_files_to_socket_names():
3237 for sname
in socketnames
:
3238 for file in self
.files
:
3240 filenamecomponents
= split_into__components(fname
)
3241 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
3242 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3247 match_files_to_socket_names()
3248 # Remove socketnames without found files
3249 socketnames
= [s
for s
in socketnames
if s
[2]
3250 and path
.exists(self
.directory
+s
[2])]
3252 self
.report({'INFO'}, 'No matching images found')
3253 print('No matching images found')
3254 return {'CANCELLED'}
3256 # Don't override path earlier as os.path is used to check the absolute path
3257 import_path
= self
.directory
3258 if self
.relative_path
:
3259 if bpy
.data
.filepath
:
3261 import_path
= bpy
.path
.relpath(self
.directory
)
3266 print('\nMatched Textures:')
3270 roughness_node
= None
3271 for i
, sname
in enumerate(socketnames
):
3272 print(i
, sname
[0], sname
[2])
3274 # DISPLACEMENT NODES
3275 if sname
[0] == 'Displacement':
3276 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
3277 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3278 disp_texture
.image
= img
3279 disp_texture
.label
= 'Displacement'
3280 if disp_texture
.image
:
3281 disp_texture
.image
.colorspace_settings
.is_data
= True
3283 # Add displacement offset nodes
3284 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
3285 disp_node
.location
= active_node
.location
+ Vector((0, -560))
3286 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
3288 # TODO Turn on true displacement in the material
3289 # Too complicated for now
3292 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
3294 if not output_node
[0].inputs
[2].is_linked
:
3295 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
3299 if not active_node
.inputs
[sname
[0]].is_linked
:
3300 # No texture node connected -> add texture node with new image
3301 texture_node
= nodes
.new(type='ShaderNodeTexImage')
3302 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
3303 texture_node
.image
= img
3306 if sname
[0] == 'Normal':
3307 # Test if new texture node is normal or bump map
3308 fname_components
= split_into__components(sname
[2])
3309 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
3310 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
3312 # If Normal add normal node in between
3313 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
3314 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
3316 # If Bump add bump node in between
3317 normal_node
= nodes
.new(type='ShaderNodeBump')
3318 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
3320 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
3321 normal_node_texture
= texture_node
3323 elif sname
[0] == 'Roughness':
3324 # Test if glossy or roughness map
3325 fname_components
= split_into__components(sname
[2])
3326 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
3327 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
3330 # If Roughness nothing to to
3331 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3334 # If Gloss Map add invert node
3335 invert_node
= nodes
.new(type='ShaderNodeInvert')
3336 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
3338 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
3339 roughness_node
= texture_node
3342 # This is a simple connection Texture --> Input slot
3343 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
3345 # Use non-color for all but 'Base Color' Textures
3346 if not sname
[0] in ['Base Color'] and texture_node
.image
:
3347 texture_node
.image
.colorspace_settings
.is_data
= True
3350 # If already texture connected. add to node list for alignment
3351 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
3353 # This are all connected texture nodes
3354 texture_nodes
.append(texture_node
)
3355 texture_node
.label
= sname
[0]
3358 texture_nodes
.append(disp_texture
)
3361 for i
, texture_node
in enumerate(texture_nodes
):
3362 offset
= Vector((-550, (i
* -280) + 200))
3363 texture_node
.location
= active_node
.location
+ offset
3366 # Extra alignment if normal node was added
3367 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
3370 # Alignment of invert node if glossy map
3371 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
3373 # Add texture input + mapping
3374 mapping
= nodes
.new(type='ShaderNodeMapping')
3375 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
3376 if len(texture_nodes
) > 1:
3377 # If more than one texture add reroute node in between
3378 reroute
= nodes
.new(type='NodeReroute')
3379 texture_nodes
.append(reroute
)
3380 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
3381 reroute
.location
= tex_coords
+ Vector((-50, -120))
3382 for texture_node
in texture_nodes
:
3383 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
3384 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
3386 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
3388 # Connect texture_coordiantes to mapping node
3389 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
3390 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
3391 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
3393 # Create frame around tex coords and mapping
3394 frame
= nodes
.new(type='NodeFrame')
3395 frame
.label
= 'Mapping'
3396 mapping
.parent
= frame
3397 texture_input
.parent
= frame
3400 # Create frame around texture nodes
3401 frame
= nodes
.new(type='NodeFrame')
3402 frame
.label
= 'Textures'
3403 for tnode
in texture_nodes
:
3404 tnode
.parent
= frame
3408 active_node
.select
= False
3411 force_update(context
)
3415 class NWAddReroutes(Operator
, NWBase
):
3416 """Add Reroute Nodes and link them to outputs of selected nodes"""
3417 bl_idname
= "node.nw_add_reroutes"
3418 bl_label
= "Add Reroutes"
3419 bl_description
= "Add Reroutes to Outputs"
3420 bl_options
= {'REGISTER', 'UNDO'}
3422 option
: EnumProperty(
3425 ('ALL', 'to all', 'Add to all outputs'),
3426 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3427 ('LINKED', 'to linked', 'Add only to linked outputs'),
3431 def execute(self
, context
):
3432 tree_type
= context
.space_data
.node_tree
.type
3433 option
= self
.option
3434 nodes
, links
= get_nodes_links(context
)
3435 # output valid when option is 'all' or when 'loose' output has no links
3437 post_select
= [] # nodes to be selected after execution
3438 # create reroutes and recreate links
3439 for node
in [n
for n
in nodes
if n
.select
]:
3444 # unhide 'REROUTE' nodes to avoid issues with location.y
3445 if node
.type == 'REROUTE':
3447 # When node is hidden - width_hidden not usable.
3448 # Hack needed to calculate real width
3450 bpy
.ops
.node
.select_all(action
='DESELECT')
3451 helper
= nodes
.new('NodeReroute')
3452 helper
.select
= True
3454 # resize node and helper to zero. Then check locations to calculate width
3455 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
3456 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
3457 # restore node location
3458 node
.location
= x
, y
3461 # only helper is selected now
3462 bpy
.ops
.node
.delete()
3463 x
= node
.location
.x
+ width
+ 20.0
3464 if node
.type != 'REROUTE':
3468 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3469 for out_i
, output
in enumerate(node
.outputs
):
3470 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3471 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3472 if node
.type != 'R_LAYERS':
3474 else: # if 'R_LAYERS' check if output represent used render pass
3475 node_scene
= node
.scene
3476 node_layer
= node
.layer
3477 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3478 if output
.name
== 'Alpha':
3481 # check entries in global 'rl_outputs' variable
3482 for rlo
in rl_outputs
:
3483 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3484 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3487 valid
= ((option
== 'ALL') or
3488 (option
== 'LOOSE' and not output
.links
) or
3489 (option
== 'LINKED' and output
.links
))
3490 # Add reroutes only if valid, but offset location in all cases.
3492 n
= nodes
.new('NodeReroute')
3494 for link
in output
.links
:
3495 links
.new(n
.outputs
[0], link
.to_socket
)
3496 links
.new(output
, n
.inputs
[0])
3498 post_select
.append(n
)
3502 # disselect the node so that after execution of script only newly created nodes are selected
3504 # nicer reroutes distribution along y when node.hide
3506 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3507 for reroute
in [r
for r
in nodes
if r
.select
]:
3508 reroute
.location
.y
-= y_translate
3509 for node
in post_select
:
3515 class NWLinkActiveToSelected(Operator
, NWBase
):
3516 """Link active node to selected nodes basing on various criteria"""
3517 bl_idname
= "node.nw_link_active_to_selected"
3518 bl_label
= "Link Active Node to Selected"
3519 bl_options
= {'REGISTER', 'UNDO'}
3521 replace
: BoolProperty()
3522 use_node_name
: BoolProperty()
3523 use_outputs_names
: BoolProperty()
3526 def poll(cls
, context
):
3528 if nw_check(context
):
3529 if context
.active_node
is not None:
3530 if context
.active_node
.select
:
3534 def execute(self
, context
):
3535 nodes
, links
= get_nodes_links(context
)
3536 replace
= self
.replace
3537 use_node_name
= self
.use_node_name
3538 use_outputs_names
= self
.use_outputs_names
3539 active
= nodes
.active
3540 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3541 outputs
= [] # Only usable outputs of active nodes will be stored here.
3542 for out
in active
.outputs
:
3543 if active
.type != 'R_LAYERS':
3546 # 'R_LAYERS' node type needs special handling.
3547 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3548 # Only outputs that represent used passes should be taken into account
3549 # Check if pass represented by output is used.
3550 # global 'rl_outputs' list will be used for that
3551 for rlo
in rl_outputs
:
3552 pass_used
= False # initial value. Will be set to True if pass is used
3553 if out
.name
== 'Alpha':
3554 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3556 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3557 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3558 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3562 doit
= True # Will be changed to False when links successfully added to previous output.
3565 for node
in selected
:
3566 dst_name
= node
.name
# Will be compared with src_name if needed.
3567 # When node has label - use it as dst_name
3569 dst_name
= node
.label
3570 valid
= True # Initial value. Will be changed to False if names don't match.
3571 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
3573 # Set src_name to source node name or label
3574 src_name
= active
.name
3576 src_name
= active
.label
3577 elif use_outputs_names
:
3578 src_name
= (out
.name
, )
3579 for rlo
in rl_outputs
:
3580 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3581 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3582 if dst_name
not in src_name
:
3585 for input in node
.inputs
:
3586 if input.type == out
.type or node
.type == 'REROUTE':
3587 if replace
or not input.is_linked
:
3588 links
.new(out
, input)
3589 if not use_node_name
and not use_outputs_names
:
3596 class NWAlignNodes(Operator
, NWBase
):
3597 '''Align the selected nodes neatly in a row/column'''
3598 bl_idname
= "node.nw_align_nodes"
3599 bl_label
= "Align Nodes"
3600 bl_options
= {'REGISTER', 'UNDO'}
3601 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3603 def execute(self
, context
):
3604 nodes
, links
= get_nodes_links(context
)
3605 margin
= self
.margin
3609 if node
.select
and node
.type != 'FRAME':
3610 selection
.append(node
)
3612 # If no nodes are selected, align all nodes
3616 elif nodes
.active
in selection
:
3617 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3619 # Check if nodes should be laid out horizontally or vertically
3620 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3621 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3622 x_range
= max(x_locs
) - min(x_locs
)
3623 y_range
= max(y_locs
) - min(y_locs
)
3624 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3625 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3626 horizontal
= x_range
> y_range
3628 # Sort selection by location of node mid-point
3630 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3632 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3636 for node
in selection
:
3637 current_margin
= margin
3638 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3641 node
.location
.x
= current_pos
3642 current_pos
+= current_margin
+ node
.dimensions
.x
3643 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3645 node
.location
.y
= current_pos
3646 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3647 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3649 # If active node is selected, center nodes around it
3650 if active_loc
is not None:
3651 active_loc_diff
= active_loc
- nodes
.active
.location
3652 for node
in selection
:
3653 node
.location
+= active_loc_diff
3654 else: # Position nodes centered around where they used to be
3655 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
])
3656 new_mid
= (max(locs
) + min(locs
)) / 2
3657 for node
in selection
:
3659 node
.location
.x
+= (mid_x
- new_mid
)
3661 node
.location
.y
+= (mid_y
- new_mid
)
3666 class NWSelectParentChildren(Operator
, NWBase
):
3667 bl_idname
= "node.nw_select_parent_child"
3668 bl_label
= "Select Parent or Children"
3669 bl_options
= {'REGISTER', 'UNDO'}
3671 option
: EnumProperty(
3674 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3675 ('CHILD', 'Select Children', 'Select members of selected frame'),
3679 def execute(self
, context
):
3680 nodes
, links
= get_nodes_links(context
)
3681 option
= self
.option
3682 selected
= [node
for node
in nodes
if node
.select
]
3683 if option
== 'PARENT':
3684 for sel
in selected
:
3687 parent
.select
= True
3688 else: # option == 'CHILD'
3689 for sel
in selected
:
3690 children
= [node
for node
in nodes
if node
.parent
== sel
]
3691 for kid
in children
:
3697 class NWDetachOutputs(Operator
, NWBase
):
3698 """Detach outputs of selected node leaving inputs linked"""
3699 bl_idname
= "node.nw_detach_outputs"
3700 bl_label
= "Detach Outputs"
3701 bl_options
= {'REGISTER', 'UNDO'}
3703 def execute(self
, context
):
3704 nodes
, links
= get_nodes_links(context
)
3705 selected
= context
.selected_nodes
3706 bpy
.ops
.node
.duplicate_move_keep_inputs()
3707 new_nodes
= context
.selected_nodes
3708 bpy
.ops
.node
.select_all(action
="DESELECT")
3709 for node
in selected
:
3711 bpy
.ops
.node
.delete_reconnect()
3712 for new_node
in new_nodes
:
3713 new_node
.select
= True
3714 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3719 class NWLinkToOutputNode(Operator
):
3720 """Link to Composite node or Material Output node"""
3721 bl_idname
= "node.nw_link_out"
3722 bl_label
= "Connect to Output"
3723 bl_options
= {'REGISTER', 'UNDO'}
3726 def poll(cls
, context
):
3728 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
3729 if context
.active_node
is not None:
3730 for out
in context
.active_node
.outputs
:
3731 if is_visible_socket(out
):
3736 def execute(self
, context
):
3737 nodes
, links
= get_nodes_links(context
)
3738 active
= nodes
.active
3741 tree_type
= context
.space_data
.tree_type
3742 output_types_shaders
= [x
[1] for x
in shaders_output_nodes_props
]
3743 output_types_compo
= ['COMPOSITE']
3744 output_types_blender_mat
= ['OUTPUT']
3745 output_types_textures
= ['OUTPUT']
3746 output_types
= output_types_shaders
+ output_types_compo
+ output_types_blender_mat
3748 if node
.type in output_types
:
3752 bpy
.ops
.node
.select_all(action
="DESELECT")
3753 if tree_type
== 'ShaderNodeTree':
3754 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3755 elif tree_type
== 'CompositorNodeTree':
3756 output_node
= nodes
.new('CompositorNodeComposite')
3757 elif tree_type
== 'TextureNodeTree':
3758 output_node
= nodes
.new('TextureNodeOutput')
3759 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3760 output_node
.location
.y
= active
.location
.y
3761 if (output_node
and active
.outputs
):
3762 for i
, output
in enumerate(active
.outputs
):
3763 if is_visible_socket(output
):
3766 for i
, output
in enumerate(active
.outputs
):
3767 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3772 if tree_type
== 'ShaderNodeTree':
3773 if active
.outputs
[output_index
].name
== 'Volume':
3775 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3777 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3779 force_update(context
) # viewport render does not update
3784 class NWMakeLink(Operator
, NWBase
):
3785 """Make a link from one socket to another"""
3786 bl_idname
= 'node.nw_make_link'
3787 bl_label
= 'Make Link'
3788 bl_options
= {'REGISTER', 'UNDO'}
3789 from_socket
: IntProperty()
3790 to_socket
: IntProperty()
3792 def execute(self
, context
):
3793 nodes
, links
= get_nodes_links(context
)
3795 n1
= nodes
[context
.scene
.NWLazySource
]
3796 n2
= nodes
[context
.scene
.NWLazyTarget
]
3798 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3800 force_update(context
)
3805 class NWCallInputsMenu(Operator
, NWBase
):
3806 """Link from this output"""
3807 bl_idname
= 'node.nw_call_inputs_menu'
3808 bl_label
= 'Make Link'
3809 bl_options
= {'REGISTER', 'UNDO'}
3810 from_socket
: IntProperty()
3812 def execute(self
, context
):
3813 nodes
, links
= get_nodes_links(context
)
3815 context
.scene
.NWSourceSocket
= self
.from_socket
3817 n1
= nodes
[context
.scene
.NWLazySource
]
3818 n2
= nodes
[context
.scene
.NWLazyTarget
]
3819 if len(n2
.inputs
) > 1:
3820 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3821 elif len(n2
.inputs
) == 1:
3822 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3826 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3827 """Add an Image Sequence"""
3828 bl_idname
= 'node.nw_add_sequence'
3829 bl_label
= 'Import Image Sequence'
3830 bl_options
= {'REGISTER', 'UNDO'}
3832 directory
: StringProperty(
3835 filename
: StringProperty(
3838 files
: CollectionProperty(
3839 type=bpy
.types
.OperatorFileListElement
,
3840 options
={'HIDDEN', 'SKIP_SAVE'}
3843 def execute(self
, context
):
3844 nodes
, links
= get_nodes_links(context
)
3845 directory
= self
.directory
3846 filename
= self
.filename
3848 tree
= context
.space_data
.node_tree
3851 # print ("\nDIR:", directory)
3852 # print ("FN:", filename)
3853 # print ("Fs:", list(f.name for f in files), '\n')
3855 if tree
.type == 'SHADER':
3856 node_type
= "ShaderNodeTexImage"
3857 elif tree
.type == 'COMPOSITING':
3858 node_type
= "CompositorNodeImage"
3860 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3861 return {'CANCELLED'}
3863 if not files
[0].name
and not filename
:
3864 self
.report({'ERROR'}, "No file chosen")
3865 return {'CANCELLED'}
3866 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3867 # User has selected multiple files without an active one, or the active one is non-existant
3868 filename
= files
[0].name
3870 if not path
.exists(directory
+filename
):
3871 self
.report({'ERROR'}, filename
+" does not exist!")
3872 return {'CANCELLED'}
3874 without_ext
= '.'.join(filename
.split('.')[:-1])
3876 # if last digit isn't a number, it's not a sequence
3877 if not without_ext
[-1].isdigit():
3878 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3879 return {'CANCELLED'}
3882 extension
= filename
.split('.')[-1]
3883 reverse
= without_ext
[::-1] # reverse string
3886 for char
in reverse
:
3892 without_num
= without_ext
[:count_numbers
*-1]
3894 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3896 num_frames
= len(files
)
3898 nodes_list
= [node
for node
in nodes
]
3900 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3901 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3905 yloc
+= node_mid_pt(node
, 'y')
3906 yloc
= yloc
/len(nodes
)
3911 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3913 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3915 node
.label
= name_with_hashes
3917 img
= bpy
.data
.images
.load(directory
+(without_ext
+'.'+extension
))
3918 img
.source
= 'SEQUENCE'
3919 img
.name
= name_with_hashes
3921 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3922 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
3923 image_user
.frame_duration
= num_frames
3928 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3929 """Add multiple images at once"""
3930 bl_idname
= 'node.nw_add_multiple_images'
3931 bl_label
= 'Open Selected Images'
3932 bl_options
= {'REGISTER', 'UNDO'}
3933 directory
: StringProperty(
3936 files
: CollectionProperty(
3937 type=bpy
.types
.OperatorFileListElement
,
3938 options
={'HIDDEN', 'SKIP_SAVE'}
3941 def execute(self
, context
):
3942 nodes
, links
= get_nodes_links(context
)
3944 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3946 if context
.space_data
.node_tree
.type == 'SHADER':
3947 node_type
= "ShaderNodeTexImage"
3948 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3949 node_type
= "CompositorNodeImage"
3951 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3952 return {'CANCELLED'}
3955 for f
in self
.files
:
3958 node
= nodes
.new(node_type
)
3959 new_nodes
.append(node
)
3962 node
.width_hidden
= 100
3963 node
.location
.x
= xloc
3964 node
.location
.y
= yloc
3967 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3970 # shift new nodes up to center of tree
3971 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3973 if node
in new_nodes
:
3975 node
.location
.y
+= (list_size
/2)
3981 class NWViewerFocus(bpy
.types
.Operator
):
3982 """Set the viewer tile center to the mouse position"""
3983 bl_idname
= "node.nw_viewer_focus"
3984 bl_label
= "Viewer Focus"
3986 x
: bpy
.props
.IntProperty()
3987 y
: bpy
.props
.IntProperty()
3990 def poll(cls
, context
):
3991 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
3993 def execute(self
, context
):
3996 def invoke(self
, context
, event
):
3997 render
= context
.scene
.render
3998 space
= context
.space_data
3999 percent
= render
.resolution_percentage
*0.01
4001 nodes
, links
= get_nodes_links(context
)
4002 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
4005 mlocx
= event
.mouse_region_x
4006 mlocy
= event
.mouse_region_y
4007 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
4009 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
4010 region_x
= context
.region
.width
4011 region_y
= context
.region
.height
4013 region_center_x
= context
.region
.width
/ 2
4014 region_center_y
= context
.region
.height
/ 2
4016 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
4017 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
4019 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
4020 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
4022 margin_x
= region_center_x
- backdrop_center_x
4023 margin_y
= region_center_y
- backdrop_center_y
4025 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
4026 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
4028 for node
in viewers
:
4029 node
.center_x
= abs_mouse_x
4030 node
.center_y
= abs_mouse_y
4032 return {'PASS_THROUGH'}
4034 return self
.execute(context
)
4037 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
4038 """Save the current viewer node to an image file"""
4039 bl_idname
= "node.nw_save_viewer"
4040 bl_label
= "Save This Image"
4041 filepath
: StringProperty(subtype
="FILE_PATH")
4042 filename_ext
: EnumProperty(
4044 description
="Choose the file format to save to",
4045 items
=(('.bmp', "BMP", ""),
4046 ('.rgb', 'IRIS', ""),
4047 ('.png', 'PNG', ""),
4048 ('.jpg', 'JPEG', ""),
4049 ('.jp2', 'JPEG2000', ""),
4050 ('.tga', 'TARGA', ""),
4051 ('.cin', 'CINEON', ""),
4052 ('.dpx', 'DPX', ""),
4053 ('.exr', 'OPEN_EXR', ""),
4054 ('.hdr', 'HDR', ""),
4055 ('.tif', 'TIFF', "")),
4060 def poll(cls
, context
):
4062 if nw_check(context
):
4063 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4064 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
4065 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
4069 def execute(self
, context
):
4086 basename
, ext
= path
.splitext(fp
)
4087 old_render_format
= context
.scene
.render
.image_settings
.file_format
4088 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
4089 context
.area
.type = "IMAGE_EDITOR"
4090 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
4091 context
.area
.spaces
[0].image
.save_render(fp
)
4092 context
.area
.type = "NODE_EDITOR"
4093 context
.scene
.render
.image_settings
.file_format
= old_render_format
4097 class NWResetNodes(bpy
.types
.Operator
):
4098 """Reset Nodes in Selection"""
4099 bl_idname
= "node.nw_reset_nodes"
4100 bl_label
= "Reset Nodes"
4101 bl_options
= {'REGISTER', 'UNDO'}
4104 def poll(cls
, context
):
4105 space
= context
.space_data
4106 return space
.type == 'NODE_EDITOR'
4108 def execute(self
, context
):
4109 node_active
= context
.active_node
4110 node_selected
= context
.selected_nodes
4111 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4113 # Check if one node is selected at least
4114 if not (len(node_selected
) > 0):
4115 self
.report({'ERROR'}, "1 node must be selected at least")
4116 return {'CANCELLED'}
4118 active_node_name
= node_active
.name
if node_active
.select
else None
4119 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
4121 # Create output lists
4122 selected_node_names
= [n
.name
for n
in node_selected
]
4125 # Reset all valid children in a frame
4126 node_active_is_frame
= False
4127 if len(node_selected
) == 1 and node_active
.type == "FRAME":
4128 node_tree
= node_active
.id_data
4129 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
4131 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
4132 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
4133 node_active_is_frame
= True
4135 # Check if valid nodes in selection
4136 if not (len(valid_nodes
) > 0):
4137 # Check for frames only
4138 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
4139 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
4140 self
.report({'ERROR'}, "Please select only 1 frame to reset")
4142 self
.report({'ERROR'}, "No valid node(s) in selection")
4143 return {'CANCELLED'}
4145 # Report nodes that are not valid
4146 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
4147 valid_node_names
= [n
.name
for n
in valid_nodes
]
4148 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
4149 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
4151 # Deselect all nodes
4152 for i
in node_selected
:
4155 # Run through all valid nodes
4156 for node
in valid_nodes
:
4158 parent
= node
.parent
if node
.parent
else None
4159 node_loc
= [node
.location
.x
, node
.location
.y
]
4161 node_tree
= node
.id_data
4162 props_to_copy
= 'bl_idname name location height width'.split(' ')
4165 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
4166 for i
in (i
for i
in mappings
if i
.is_linked
):
4168 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
4170 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
4172 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
4173 props_to_copy
.pop(0)
4175 for prop
in props_to_copy
:
4176 setattr(new_node
, prop
, props
[prop
])
4178 nodes
= node_tree
.nodes
4180 new_node
.name
= props
['name']
4183 new_node
.parent
= parent
4184 new_node
.location
= node_loc
4186 for str_from
, str_to
in reconnections
:
4187 node_tree
.links
.new(eval(str_from
), eval(str_to
))
4189 new_node
.select
= False
4190 success_names
.append(new_node
.name
)
4192 # Reselect all nodes
4193 if selected_node_names
and node_active_is_frame
is False:
4194 for i
in selected_node_names
:
4195 node_tree
.nodes
[i
].select
= True
4197 if active_node_name
is not None:
4198 node_tree
.nodes
[active_node_name
].select
= True
4199 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
4201 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
4209 def drawlayout(context
, layout
, mode
='non-panel'):
4210 tree_type
= context
.space_data
.tree_type
4212 col
= layout
.column(align
=True)
4213 col
.menu(NWMergeNodesMenu
.bl_idname
)
4216 col
= layout
.column(align
=True)
4217 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
4220 if tree_type
== 'ShaderNodeTree':
4221 col
= layout
.column(align
=True)
4222 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
4223 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
4226 col
= layout
.column(align
=True)
4227 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
4228 col
.operator(NWSwapLinks
.bl_idname
)
4229 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
4232 col
= layout
.column(align
=True)
4233 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
4234 if tree_type
!= 'GeometryNodeTree':
4235 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
4238 col
= layout
.column(align
=True)
4240 row
= col
.row(align
=True)
4241 row
.operator(NWClearLabel
.bl_idname
).option
= True
4242 row
.operator(NWModifyLabels
.bl_idname
)
4244 col
.operator(NWClearLabel
.bl_idname
).option
= True
4245 col
.operator(NWModifyLabels
.bl_idname
)
4246 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
4248 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
4251 col
= layout
.column(align
=True)
4252 if tree_type
== 'CompositorNodeTree':
4253 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
4254 if tree_type
!= 'GeometryNodeTree':
4255 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
4258 col
= layout
.column(align
=True)
4259 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
4262 col
= layout
.column(align
=True)
4263 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
4266 col
= layout
.column(align
=True)
4267 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
4271 class NodeWranglerPanel(Panel
, NWBase
):
4272 bl_idname
= "NODE_PT_nw_node_wrangler"
4273 bl_space_type
= 'NODE_EDITOR'
4274 bl_label
= "Node Wrangler"
4275 bl_region_type
= "UI"
4276 bl_category
= "Node Wrangler"
4278 prepend
: StringProperty(
4281 append
: StringProperty()
4282 remove
: StringProperty()
4284 def draw(self
, context
):
4285 self
.layout
.label(text
="(Quick access: Shift+W)")
4286 drawlayout(context
, self
.layout
, mode
='panel')
4292 class NodeWranglerMenu(Menu
, NWBase
):
4293 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
4294 bl_label
= "Node Wrangler"
4296 def draw(self
, context
):
4297 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
4298 drawlayout(context
, self
.layout
)
4301 class NWMergeNodesMenu(Menu
, NWBase
):
4302 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
4303 bl_label
= "Merge Selected Nodes"
4305 def draw(self
, context
):
4306 type = context
.space_data
.tree_type
4307 layout
= self
.layout
4308 if type == 'ShaderNodeTree':
4309 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
4310 if type == 'GeometryNodeTree':
4311 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
4312 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4314 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
4315 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
4316 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
4318 props
.merge_type
= 'ZCOMBINE'
4319 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
4321 props
.merge_type
= 'ALPHAOVER'
4323 class NWMergeGeometryMenu(Menu
, NWBase
):
4324 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
4325 bl_label
= "Merge Selected Nodes using Geometry Nodes"
4326 def draw(self
, context
):
4327 layout
= self
.layout
4328 # The boolean node + Join Geometry node
4329 for type, name
, description
in geo_combine_operations
:
4330 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4332 props
.merge_type
= 'GEOMETRY'
4334 class NWMergeShadersMenu(Menu
, NWBase
):
4335 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
4336 bl_label
= "Merge Selected Nodes using Shaders"
4338 def draw(self
, context
):
4339 layout
= self
.layout
4340 for type in ('MIX', 'ADD'):
4341 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
4343 props
.merge_type
= 'SHADER'
4346 class NWMergeMixMenu(Menu
, NWBase
):
4347 bl_idname
= "NODE_MT_nw_merge_mix_menu"
4348 bl_label
= "Merge Selected Nodes using Mix"
4350 def draw(self
, context
):
4351 layout
= self
.layout
4352 for type, name
, description
in blend_types
:
4353 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4355 props
.merge_type
= 'MIX'
4358 class NWConnectionListOutputs(Menu
, NWBase
):
4359 bl_idname
= "NODE_MT_nw_connection_list_out"
4362 def draw(self
, context
):
4363 layout
= self
.layout
4364 nodes
, links
= get_nodes_links(context
)
4366 n1
= nodes
[context
.scene
.NWLazySource
]
4368 for o
in n1
.outputs
:
4369 # Only show sockets that are exposed.
4371 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
4375 class NWConnectionListInputs(Menu
, NWBase
):
4376 bl_idname
= "NODE_MT_nw_connection_list_in"
4379 def draw(self
, context
):
4380 layout
= self
.layout
4381 nodes
, links
= get_nodes_links(context
)
4383 n2
= nodes
[context
.scene
.NWLazyTarget
]
4387 # Only show sockets that are exposed.
4388 # This prevents, for example, the scale value socket
4389 # of the vector math node being added to the list when
4390 # the mode is not 'SCALE'.
4392 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
4393 op
.from_socket
= context
.scene
.NWSourceSocket
4394 op
.to_socket
= index
4398 class NWMergeMathMenu(Menu
, NWBase
):
4399 bl_idname
= "NODE_MT_nw_merge_math_menu"
4400 bl_label
= "Merge Selected Nodes using Math"
4402 def draw(self
, context
):
4403 layout
= self
.layout
4404 for type, name
, description
in operations
:
4405 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
4407 props
.merge_type
= 'MATH'
4410 class NWBatchChangeNodesMenu(Menu
, NWBase
):
4411 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
4412 bl_label
= "Batch Change Selected Nodes"
4414 def draw(self
, context
):
4415 layout
= self
.layout
4416 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
4417 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
4420 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
4421 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
4422 bl_label
= "Batch Change Blend Type"
4424 def draw(self
, context
):
4425 layout
= self
.layout
4426 for type, name
, description
in blend_types
:
4427 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4428 props
.blend_type
= type
4429 props
.operation
= 'CURRENT'
4432 class NWBatchChangeOperationMenu(Menu
, NWBase
):
4433 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
4434 bl_label
= "Batch Change Math Operation"
4436 def draw(self
, context
):
4437 layout
= self
.layout
4438 for type, name
, description
in operations
:
4439 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
4440 props
.blend_type
= 'CURRENT'
4441 props
.operation
= type
4444 class NWCopyToSelectedMenu(Menu
, NWBase
):
4445 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4446 bl_label
= "Copy to Selected"
4448 def draw(self
, context
):
4449 layout
= self
.layout
4450 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4451 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4454 class NWCopyLabelMenu(Menu
, NWBase
):
4455 bl_idname
= "NODE_MT_nw_copy_label_menu"
4456 bl_label
= "Copy Label"
4458 def draw(self
, context
):
4459 layout
= self
.layout
4460 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4461 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4462 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4465 class NWAddReroutesMenu(Menu
, NWBase
):
4466 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4467 bl_label
= "Add Reroutes"
4468 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4470 def draw(self
, context
):
4471 layout
= self
.layout
4472 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4473 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4474 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4477 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4478 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4479 bl_label
= "Link Active to Selected"
4481 def draw(self
, context
):
4482 layout
= self
.layout
4483 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4484 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4485 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4488 class NWLinkStandardMenu(Menu
, NWBase
):
4489 bl_idname
= "NODE_MT_nw_link_standard_menu"
4490 bl_label
= "To All Selected"
4492 def draw(self
, context
):
4493 layout
= self
.layout
4494 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4495 props
.replace
= False
4496 props
.use_node_name
= False
4497 props
.use_outputs_names
= False
4498 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4499 props
.replace
= True
4500 props
.use_node_name
= False
4501 props
.use_outputs_names
= False
4504 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4505 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4506 bl_label
= "Use Node Name/Label"
4508 def draw(self
, context
):
4509 layout
= self
.layout
4510 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4511 props
.replace
= False
4512 props
.use_node_name
= True
4513 props
.use_outputs_names
= False
4514 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4515 props
.replace
= True
4516 props
.use_node_name
= True
4517 props
.use_outputs_names
= False
4520 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4521 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4522 bl_label
= "Use Outputs Names"
4524 def draw(self
, context
):
4525 layout
= self
.layout
4526 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4527 props
.replace
= False
4528 props
.use_node_name
= False
4529 props
.use_outputs_names
= True
4530 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4531 props
.replace
= True
4532 props
.use_node_name
= False
4533 props
.use_outputs_names
= True
4536 class NWVertColMenu(bpy
.types
.Menu
):
4537 bl_idname
= "NODE_MT_nw_node_vertex_color_menu"
4538 bl_label
= "Vertex Colors"
4541 def poll(cls
, context
):
4543 if nw_check(context
):
4544 snode
= context
.space_data
4545 valid
= snode
.tree_type
== 'ShaderNodeTree'
4548 def draw(self
, context
):
4550 nodes
, links
= get_nodes_links(context
)
4551 mat
= context
.object.active_material
4554 for obj
in bpy
.data
.objects
:
4555 for slot
in obj
.material_slots
:
4556 if slot
.material
== mat
:
4560 if obj
.data
.vertex_colors
:
4561 for vcol
in obj
.data
.vertex_colors
:
4562 vcols
.append(vcol
.name
)
4563 vcols
= list(set(vcols
)) # get a unique list
4567 l
.operator(NWAddAttrNode
.bl_idname
, text
=vcol
).attr_name
= vcol
4569 l
.label(text
="No Vertex Color layers on objects with this material")
4572 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4573 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4574 bl_label
= "Switch Type to..."
4576 def draw(self
, context
):
4577 layout
= self
.layout
4578 tree
= context
.space_data
.node_tree
4579 if tree
.type == 'SHADER':
4580 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4581 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4582 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4583 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4584 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4585 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4586 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4587 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4588 if tree
.type == 'COMPOSITING':
4589 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4590 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4591 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4592 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4593 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4594 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4595 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4596 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4597 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4598 if tree
.type == 'TEXTURE':
4599 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4600 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4601 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4602 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4603 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4604 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4605 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4606 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4607 if tree
.type == 'GEOMETRY':
4608 categories
= [c
for c
in node_categories_iter(context
)
4609 if c
.name
not in ['Group', 'Script']]
4610 for cat
in categories
:
4611 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4612 if hasattr(bpy
.types
, idname
):
4615 layout
.label(text
="Unable to load altered node lists.")
4616 layout
.label(text
="Please re-enable Node Wrangler.")
4620 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4621 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4624 def draw(self
, context
):
4625 layout
= self
.layout
4626 for ident
, node_type
, rna_name
in shaders_input_nodes_props
:
4627 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4628 props
.to_type
= ident
4631 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4632 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4635 def draw(self
, context
):
4636 layout
= self
.layout
4637 for ident
, node_type
, rna_name
in shaders_output_nodes_props
:
4638 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4639 props
.to_type
= ident
4642 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4643 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4646 def draw(self
, context
):
4647 layout
= self
.layout
4648 for ident
, node_type
, rna_name
in shaders_shader_nodes_props
:
4649 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4650 props
.to_type
= ident
4653 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4654 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4655 bl_label
= "Texture"
4657 def draw(self
, context
):
4658 layout
= self
.layout
4659 for ident
, node_type
, rna_name
in shaders_texture_nodes_props
:
4660 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4661 props
.to_type
= ident
4664 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4665 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4668 def draw(self
, context
):
4669 layout
= self
.layout
4670 for ident
, node_type
, rna_name
in shaders_color_nodes_props
:
4671 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4672 props
.to_type
= ident
4675 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4676 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4679 def draw(self
, context
):
4680 layout
= self
.layout
4681 for ident
, node_type
, rna_name
in shaders_vector_nodes_props
:
4682 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4683 props
.to_type
= ident
4686 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4687 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4688 bl_label
= "Converter"
4690 def draw(self
, context
):
4691 layout
= self
.layout
4692 for ident
, node_type
, rna_name
in shaders_converter_nodes_props
:
4693 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4694 props
.to_type
= ident
4697 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4698 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4701 def draw(self
, context
):
4702 layout
= self
.layout
4703 for ident
, node_type
, rna_name
in shaders_layout_nodes_props
:
4704 if node_type
!= 'FRAME':
4705 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4706 props
.to_type
= ident
4709 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4710 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4713 def draw(self
, context
):
4714 layout
= self
.layout
4715 for ident
, node_type
, rna_name
in compo_input_nodes_props
:
4716 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4717 props
.to_type
= ident
4720 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4721 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4724 def draw(self
, context
):
4725 layout
= self
.layout
4726 for ident
, node_type
, rna_name
in compo_output_nodes_props
:
4727 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4728 props
.to_type
= ident
4731 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4732 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4735 def draw(self
, context
):
4736 layout
= self
.layout
4737 for ident
, node_type
, rna_name
in compo_color_nodes_props
:
4738 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4739 props
.to_type
= ident
4742 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4743 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4744 bl_label
= "Converter"
4746 def draw(self
, context
):
4747 layout
= self
.layout
4748 for ident
, node_type
, rna_name
in compo_converter_nodes_props
:
4749 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4750 props
.to_type
= ident
4753 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4754 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4757 def draw(self
, context
):
4758 layout
= self
.layout
4759 for ident
, node_type
, rna_name
in compo_filter_nodes_props
:
4760 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4761 props
.to_type
= ident
4764 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4765 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4768 def draw(self
, context
):
4769 layout
= self
.layout
4770 for ident
, node_type
, rna_name
in compo_vector_nodes_props
:
4771 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4772 props
.to_type
= ident
4775 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4776 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4779 def draw(self
, context
):
4780 layout
= self
.layout
4781 for ident
, node_type
, rna_name
in compo_matte_nodes_props
:
4782 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4783 props
.to_type
= ident
4786 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4787 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4788 bl_label
= "Distort"
4790 def draw(self
, context
):
4791 layout
= self
.layout
4792 for ident
, node_type
, rna_name
in compo_distort_nodes_props
:
4793 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4794 props
.to_type
= ident
4797 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4798 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4801 def draw(self
, context
):
4802 layout
= self
.layout
4803 for ident
, node_type
, rna_name
in compo_layout_nodes_props
:
4804 if node_type
!= 'FRAME':
4805 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4806 props
.to_type
= ident
4809 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4810 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4813 def draw(self
, context
):
4814 layout
= self
.layout
4815 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4816 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4817 props
.to_type
= ident
4820 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4821 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4824 def draw(self
, context
):
4825 layout
= self
.layout
4826 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4827 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4828 props
.to_type
= ident
4831 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4832 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4835 def draw(self
, context
):
4836 layout
= self
.layout
4837 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4838 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4839 props
.to_type
= ident
4842 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4843 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4846 def draw(self
, context
):
4847 layout
= self
.layout
4848 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4849 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4850 props
.to_type
= ident
4853 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4854 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4855 bl_label
= "Converter"
4857 def draw(self
, context
):
4858 layout
= self
.layout
4859 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4860 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4861 props
.to_type
= ident
4864 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4865 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4868 def draw(self
, context
):
4869 layout
= self
.layout
4870 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4871 if node_type
!= 'FRAME':
4872 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4873 props
.to_type
= ident
4876 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4877 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4880 def draw(self
, context
):
4881 layout
= self
.layout
4882 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4883 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4884 props
.to_type
= ident
4887 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4888 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4891 def draw(self
, context
):
4892 layout
= self
.layout
4893 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4894 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4895 props
.to_type
= ident
4898 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4899 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4902 def draw(self
, context
):
4903 layout
= self
.layout
4904 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4905 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4906 props
.to_type
= ident
4909 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4910 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4911 bl_label
= "Pattern"
4913 def draw(self
, context
):
4914 layout
= self
.layout
4915 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4916 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4917 props
.to_type
= ident
4920 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4921 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4922 bl_label
= "Textures"
4924 def draw(self
, context
):
4925 layout
= self
.layout
4926 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4927 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4928 props
.to_type
= ident
4931 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4932 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4933 bl_label
= "Converter"
4935 def draw(self
, context
):
4936 layout
= self
.layout
4937 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4938 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4939 props
.to_type
= ident
4942 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4943 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4944 bl_label
= "Distort"
4946 def draw(self
, context
):
4947 layout
= self
.layout
4948 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4949 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4950 props
.to_type
= ident
4953 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4954 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4957 def draw(self
, context
):
4958 layout
= self
.layout
4959 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
4960 if node_type
!= 'FRAME':
4961 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4962 props
.to_type
= ident
4964 def draw_switch_category_submenu(self
, context
):
4965 layout
= self
.layout
4966 if self
.category
.name
== 'Layout':
4967 for node
in self
.category
.items(context
):
4968 if node
.nodetype
!= 'NodeFrame':
4969 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4970 props
.to_type
= node
.nodetype
4972 for node
in self
.category
.items(context
):
4973 if isinstance(node
, NodeItemCustom
):
4974 node
.draw(self
, layout
, context
)
4976 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4977 props
.geo_to_type
= node
.nodetype
4980 # APPENDAGES TO EXISTING UI
4984 def select_parent_children_buttons(self
, context
):
4985 layout
= self
.layout
4986 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
4987 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
4990 def attr_nodes_menu_func(self
, context
):
4991 col
= self
.layout
.column(align
=True)
4992 col
.menu("NODE_MT_nw_node_vertex_color_menu")
4996 def multipleimages_menu_func(self
, context
):
4997 col
= self
.layout
.column(align
=True)
4998 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
4999 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
5003 def bgreset_menu_func(self
, context
):
5004 self
.layout
.operator(NWResetBG
.bl_idname
)
5007 def save_viewer_menu_func(self
, context
):
5008 if nw_check(context
):
5009 if context
.space_data
.tree_type
== 'CompositorNodeTree':
5010 if context
.scene
.node_tree
.nodes
.active
:
5011 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
5012 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
5015 def reset_nodes_button(self
, context
):
5016 node_active
= context
.active_node
5017 node_selected
= context
.selected_nodes
5018 node_ignore
= ["FRAME","REROUTE", "GROUP"]
5020 # Check if active node is in the selection and respective type
5021 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
5022 row
= self
.layout
.row()
5023 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
5024 self
.layout
.separator()
5026 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
5027 row
= self
.layout
.row()
5028 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
5029 self
.layout
.separator()
5033 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5035 switch_category_menus
= []
5037 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5038 # props entry: (property name, property value)
5041 # NWMergeNodes with Ctrl (AUTO).
5042 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
5043 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5044 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
5045 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5046 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5047 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5048 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
5049 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5050 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5051 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5052 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
5053 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5054 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5055 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5056 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
5057 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5058 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5059 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5060 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
5061 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5062 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
5063 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5064 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
5065 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5066 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5067 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5068 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5069 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
5070 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5071 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
5072 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5073 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5074 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5075 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
5076 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5077 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5078 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5079 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
5080 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5081 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5082 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5083 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
5084 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5085 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5086 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5087 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
5088 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5089 # NWMergeNodes with Ctrl Shift (MATH)
5090 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5091 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5092 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
5093 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5094 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5095 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5096 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
5097 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5098 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5099 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5100 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
5101 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5102 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5103 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5104 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
5105 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5106 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
5107 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5108 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
5109 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5110 # BATCH CHANGE NODES
5111 # NWBatchChangeNodes with Alt
5112 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
5113 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5114 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
5115 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5116 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5117 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5118 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
5119 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5120 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5121 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5122 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
5123 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5124 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5125 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5126 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
5127 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5128 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5129 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5130 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
5131 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5132 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
5133 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5134 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
5135 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5136 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
5137 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5138 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
5139 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5140 # LINK ACTIVE TO SELECTED
5141 # Don't use names, don't replace links (K)
5142 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
5143 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5144 # Don't use names, replace links (Shift K)
5145 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
5146 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5147 # Use node name, don't replace links (')
5148 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
5149 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5150 # Use node name, replace links (Shift ')
5151 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
5152 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5153 # Don't use names, don't replace links (;)
5154 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
5155 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5156 # Don't use names, replace links (')
5157 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
5158 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5160 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5161 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5162 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5163 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5164 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5165 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5166 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5167 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5168 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5169 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5170 # CLEAR LABEL (Alt L)
5171 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5172 # MODIFY LABEL (Alt Shift L)
5173 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5174 # Copy Label from active to selected
5175 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5176 # DETACH OUTPUTS (Alt Shift D)
5177 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5178 # LINK TO OUTPUT NODE (O)
5179 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5180 # SELECT PARENT/CHILDREN
5182 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5184 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5186 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5187 # Add Principled BSDF Texture Setup
5188 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5190 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5192 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5194 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5196 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5198 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5199 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5201 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
5203 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5205 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5206 # Lazy Connect with Menu
5207 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5208 # Viewer Tile Center
5209 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5211 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5212 # Reset Nodes (Back Space)
5213 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5215 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
5216 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5217 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
5218 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
5219 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
5220 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
5225 NWPrincipledPreferences
,
5245 NWAddPrincipledSetup
,
5247 NWLinkActiveToSelected
,
5249 NWSelectParentChildren
,
5255 NWAddMultipleImages
,
5263 NWMergeGeometryMenu
,
5265 NWConnectionListOutputs
,
5266 NWConnectionListInputs
,
5268 NWBatchChangeNodesMenu
,
5269 NWBatchChangeBlendTypeMenu
,
5270 NWBatchChangeOperationMenu
,
5271 NWCopyToSelectedMenu
,
5274 NWLinkActiveToSelectedMenu
,
5276 NWLinkUseNodeNameMenu
,
5277 NWLinkUseOutputsNamesMenu
,
5279 NWSwitchNodeTypeMenu
,
5280 NWSwitchShadersInputSubmenu
,
5281 NWSwitchShadersOutputSubmenu
,
5282 NWSwitchShadersShaderSubmenu
,
5283 NWSwitchShadersTextureSubmenu
,
5284 NWSwitchShadersColorSubmenu
,
5285 NWSwitchShadersVectorSubmenu
,
5286 NWSwitchShadersConverterSubmenu
,
5287 NWSwitchShadersLayoutSubmenu
,
5288 NWSwitchCompoInputSubmenu
,
5289 NWSwitchCompoOutputSubmenu
,
5290 NWSwitchCompoColorSubmenu
,
5291 NWSwitchCompoConverterSubmenu
,
5292 NWSwitchCompoFilterSubmenu
,
5293 NWSwitchCompoVectorSubmenu
,
5294 NWSwitchCompoMatteSubmenu
,
5295 NWSwitchCompoDistortSubmenu
,
5296 NWSwitchCompoLayoutSubmenu
,
5297 NWSwitchMatInputSubmenu
,
5298 NWSwitchMatOutputSubmenu
,
5299 NWSwitchMatColorSubmenu
,
5300 NWSwitchMatVectorSubmenu
,
5301 NWSwitchMatConverterSubmenu
,
5302 NWSwitchMatLayoutSubmenu
,
5303 NWSwitchTexInputSubmenu
,
5304 NWSwitchTexOutputSubmenu
,
5305 NWSwitchTexColorSubmenu
,
5306 NWSwitchTexPatternSubmenu
,
5307 NWSwitchTexTexturesSubmenu
,
5308 NWSwitchTexConverterSubmenu
,
5309 NWSwitchTexDistortSubmenu
,
5310 NWSwitchTexLayoutSubmenu
,
5314 from bpy
.utils
import register_class
5317 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
5318 name
="Busy Drawing!",
5320 description
="An internal property used to store only the first mouse position")
5321 bpy
.types
.Scene
.NWLazySource
= StringProperty(
5322 name
="Lazy Source!",
5324 description
="An internal property used to store the first node in a Lazy Connect operation")
5325 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
5326 name
="Lazy Target!",
5328 description
="An internal property used to store the last node in a Lazy Connect operation")
5329 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
5330 name
="Source Socket!",
5332 description
="An internal property used to store the source socket in a Lazy Connect operation")
5333 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
5336 description
="An internal property used to determine if a socket is generated by the addon"
5343 addon_keymaps
.clear()
5344 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
5346 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
5347 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
5348 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
5350 for prop
, value
in props
:
5351 setattr(kmi
.properties
, prop
, value
)
5352 addon_keymaps
.append((km
, kmi
))
5355 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
5356 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
5357 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
5358 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
5359 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
5360 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
5361 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
5362 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
5365 switch_category_menus
.clear()
5366 for cat
in node_categories_iter(None):
5367 if cat
.name
not in ['Group', 'Script'] and cat
.identifier
.startswith('GEO'):
5368 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
5369 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
5370 "bl_space_type": 'NODE_EDITOR',
5371 "bl_label": cat
.name
,
5374 "draw": draw_switch_category_submenu
,
5377 switch_category_menus
.append(switch_category_type
)
5379 bpy
.utils
.register_class(switch_category_type
)
5383 from bpy
.utils
import unregister_class
5386 del bpy
.types
.Scene
.NWBusyDrawing
5387 del bpy
.types
.Scene
.NWLazySource
5388 del bpy
.types
.Scene
.NWLazyTarget
5389 del bpy
.types
.Scene
.NWSourceSocket
5390 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
5392 for cat_types
in switch_category_menus
:
5393 bpy
.utils
.unregister_class(cat_types
)
5394 switch_category_menus
.clear()
5397 for km
, kmi
in addon_keymaps
:
5398 km
.keymap_items
.remove(kmi
)
5399 addon_keymaps
.clear()
5402 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
5403 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
5404 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
5405 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
5406 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
5407 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
5408 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
5409 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
5412 unregister_class(cls
)
5414 if __name__
== "__main__":