Newer
Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "Node Wrangler (aka Nodes Efficiency Tools)",
"author": "Bartek Skorupa, Greg Zaal",
"location": "Node Editor Properties Panel or Ctrl-Space",
"description": "Various tools to enhance and speed up node-based workflow",
"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
"Scripts/Nodes/Nodes_Efficiency_Tools",
"category": "Node",
import bpy, blf, bgl
from bpy.types import Operator, Panel, Menu
from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty, FloatVectorProperty, CollectionProperty
from bpy_extras.io_utils import ImportHelper
from math import cos, sin, pi, hypot
#################
# rl_outputs:
# list of outputs of Input Render Layer
# with attributes determinig if pass is used,
# and MultiLayer EXR outputs names and corresponding render engines
#
# rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles)
rl_outputs = (
('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
('use_pass_color', 'Color', 'Color', True, False),
('use_pass_combined', 'Image', 'Combined', True, True),
('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False),
('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
('use_pass_emit', 'Emit', 'Emit', True, False),
('use_pass_environment', 'Environment', 'Env', True, False),
('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
('use_pass_indirect', 'Indirect', 'Indirect', True, False),
('use_pass_material_index', 'IndexMA', 'IndexMA', True, True),
('use_pass_mist', 'Mist', 'Mist', True, False),
('use_pass_normal', 'Normal', 'Normal', True, True),
('use_pass_object_index', 'IndexOB', 'IndexOB', True, True),
('use_pass_reflection', 'Reflect', 'Reflect', True, False),
('use_pass_refraction', 'Refract', 'Refract', True, False),
('use_pass_shadow', 'Shadow', 'Shadow', True, True),
('use_pass_specular', 'Specular', 'Spec', True, False),
('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', False, True),
('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', False, True),
('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
('use_pass_uv', 'UV', 'UV', True, True),
('use_pass_vector', 'Speed', 'Vector', True, True),
('use_pass_z', 'Z', 'Depth', True, True),
)
# shader nodes
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_input_nodes_props = (
('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
('ShaderNodeRGB', 'RGB', 'RGB'),
('ShaderNodeValue', 'VALUE', 'Value'),
('ShaderNodeTangent', 'TANGENT', 'Tangent'),
('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_output_nodes_props = (
('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
('ShaderNodeOutputLamp', 'OUTPUT_LAMP', 'Lamp Output'),
('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_shader_nodes_props = (
('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
('ShaderNodeEmission', 'EMISSION', 'Emission'),
('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
('ShaderNodeBackground', 'BACKGROUND', 'Background'),
('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_texture_nodes_props = (
('ShaderNodeTexImage', 'TEX_IMAGE', 'Image'),
('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment'),
('ShaderNodeTexSky', 'TEX_SKY', 'Sky'),
('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise'),
('ShaderNodeTexWave', 'TEX_WAVE', 'Wave'),
('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient'),
('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic'),
('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker'),
('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick')
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_color_nodes_props = (
('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
('ShaderNodeInvert', 'INVERT', 'Invert'),
('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
('ShaderNodeGamma', 'GAMMA', 'Gamma'),
('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_vector_nodes_props = (
('ShaderNodeMapping', 'MAPPING', 'Mapping'),
('ShaderNodeBump', 'BUMP', 'Bump'),
('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
('ShaderNodeNormal', 'NORMAL', 'Normal'),
('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_converter_nodes_props = (
('ShaderNodeMath', 'MATH', 'Math'),
('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
shaders_layout_nodes_props = (
('NodeFrame', 'FRAME', 'Frame'),
('NodeReroute', 'REROUTE', 'Reroute'),
)
# compositing nodes
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_input_nodes_props = (
('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
('CompositorNodeImage', 'IMAGE', 'Image'),
('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
('CompositorNodeMask', 'MASK', 'Mask'),
('CompositorNodeRGB', 'RGB', 'RGB'),
('CompositorNodeValue', 'VALUE', 'Value'),
('CompositorNodeTexture', 'TEXTURE', 'Texture'),
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
('CompositorNodeTime', 'TIME', 'Time'),
('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_output_nodes_props = (
('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
('CompositorNodeViewer', 'VIEWER', 'Viewer'),
('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
('CompositorNodeLevels', 'LEVELS', 'Levels'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_color_nodes_props = (
('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
('CompositorNodeInvert', 'INVERT', 'Invert'),
('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
('CompositorNodeGamma', 'GAMMA', 'Gamma'),
('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_converter_nodes_props = (
('CompositorNodeMath', 'MATH', 'Math'),
('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_filter_nodes_props = (
('CompositorNodeBlur', 'BLUR', 'Blur'),
('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
('CompositorNodeFilter', 'FILTER', 'Filter'),
('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
('CompositorNodeGlare', 'GLARE', 'Glare'),
('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_vector_nodes_props = (
('CompositorNodeNormal', 'NORMAL', 'Normal'),
('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_matte_nodes_props = (
('CompositorNodeKeying', 'KEYING', 'Keying'),
('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_distort_nodes_props = (
('CompositorNodeScale', 'SCALE', 'Scale'),
('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
('CompositorNodeRotate', 'ROTATE', 'Rotate'),
('CompositorNodeFlip', 'FLIP', 'Flip'),
('CompositorNodeCrop', 'CROP', 'Crop'),
('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
)
# (rna_type.identifier, type, rna_type.name)
# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
compo_layout_nodes_props = (
('NodeFrame', 'FRAME', 'Frame'),
('NodeReroute', 'REROUTE', 'Reroute'),
('CompositorNodeSwitch', 'SWITCH', 'Switch'),
)
# list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
# used list, not tuple for easy merging with other lists.
blend_types = [
('MIX', 'Mix', 'Mix Mode'),
('ADD', 'Add', 'Add Mode'),
('MULTIPLY', 'Multiply', 'Multiply Mode'),
('SUBTRACT', 'Subtract', 'Subtract Mode'),
('SCREEN', 'Screen', 'Screen Mode'),
('DIVIDE', 'Divide', 'Divide Mode'),
('DIFFERENCE', 'Difference', 'Difference Mode'),
('DARKEN', 'Darken', 'Darken Mode'),
('LIGHTEN', 'Lighten', 'Lighten Mode'),
('OVERLAY', 'Overlay', 'Overlay Mode'),
('DODGE', 'Dodge', 'Dodge Mode'),
('BURN', 'Burn', 'Burn Mode'),
('HUE', 'Hue', 'Hue Mode'),
('SATURATION', 'Saturation', 'Saturation Mode'),
('VALUE', 'Value', 'Value Mode'),
('COLOR', 'Color', 'Color Mode'),
('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
# list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
# used list, not tuple for easy merging with other lists.
operations = [
('ADD', 'Add', 'Add Mode'),
('SUBTRACT', 'Subtract', 'Subtract Mode'),
('MULTIPLY', 'Multiply', 'Multiply Mode'),
('DIVIDE', 'Divide', 'Divide Mode'),
('SINE', 'Sine', 'Sine Mode'),
('COSINE', 'Cosine', 'Cosine Mode'),
('TANGENT', 'Tangent', 'Tangent Mode'),
('ARCSINE', 'Arcsine', 'Arcsine Mode'),
('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
('POWER', 'Power', 'Power Mode'),
('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
('MINIMUM', 'Minimum', 'Minimum Mode'),
('MAXIMUM', 'Maximum', 'Maximum Mode'),
('ROUND', 'Round', 'Round Mode'),
('LESS_THAN', 'Less Than', 'Less Than Mode'),
('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
('MODULO', 'Modulo', 'Modulo Mode'),
('ABSOLUTE', 'Absolute', 'Absolute Mode'),
]
# in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
# used list, not tuple for easy merging with other lists.
navs = [
('CURRENT', 'Current', 'Leave at current state'),
('NEXT', 'Next', 'Next blend type/operation'),
('PREV', 'Prev', 'Previous blend type/operation'),
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
]
draw_color_sets = {
"red_white": (
(1.0, 1.0, 1.0, 0.7),
(1.0, 0.0, 0.0, 0.7),
(0.8, 0.2, 0.2, 1.0)
),
"green": (
(0.0, 0.0, 0.0, 1.0),
(0.38, 0.77, 0.38, 1.0),
(0.38, 0.77, 0.38, 1.0)
),
"yellow": (
(0.0, 0.0, 0.0, 1.0),
(0.77, 0.77, 0.16, 1.0),
(0.77, 0.77, 0.16, 1.0)
),
"purple": (
(0.0, 0.0, 0.0, 1.0),
(0.38, 0.38, 0.77, 1.0),
(0.38, 0.38, 0.77, 1.0)
),
"grey": (
(0.0, 0.0, 0.0, 1.0),
(0.63, 0.63, 0.63, 1.0),
(0.63, 0.63, 0.63, 1.0)
),
"black": (
(1.0, 1.0, 1.0, 0.7),
(0.0, 0.0, 0.0, 0.7),
(0.2, 0.2, 0.2, 1.0)
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
}
def nice_hotkey_name(punc):
# convert the ugly string name into the actual character
pairs = (
('LEFTMOUSE', "LMB"),
('MIDDLEMOUSE', "MMB"),
('RIGHTMOUSE', "RMB"),
('SELECTMOUSE', "Select"),
('WHEELUPMOUSE', "Wheel Up"),
('WHEELDOWNMOUSE', "Wheel Down"),
('WHEELINMOUSE', "Wheel In"),
('WHEELOUTMOUSE', "Wheel Out"),
('ZERO', "0"),
('ONE', "1"),
('TWO', "2"),
('THREE', "3"),
('FOUR', "4"),
('FIVE', "5"),
('SIX', "6"),
('SEVEN', "7"),
('EIGHT', "8"),
('NINE', "9"),
('OSKEY', "Super"),
('RET', "Enter"),
('LINE_FEED', "Enter"),
('SEMI_COLON', ";"),
('PERIOD', "."),
('COMMA', ","),
('QUOTE', '"'),
('MINUS', "-"),
('SLASH', "/"),
('BACK_SLASH', "\\"),
('EQUAL', "="),
('NUMPAD_1', "Numpad 1"),
('NUMPAD_2', "Numpad 2"),
('NUMPAD_3', "Numpad 3"),
('NUMPAD_4', "Numpad 4"),
('NUMPAD_5', "Numpad 5"),
('NUMPAD_6', "Numpad 6"),
('NUMPAD_7', "Numpad 7"),
('NUMPAD_8', "Numpad 8"),
('NUMPAD_9', "Numpad 9"),
('NUMPAD_0', "Numpad 0"),
('NUMPAD_PERIOD', "Numpad ."),
('NUMPAD_SLASH', "Numpad /"),
('NUMPAD_ASTERIX', "Numpad *"),
('NUMPAD_MINUS', "Numpad -"),
('NUMPAD_ENTER', "Numpad Enter"),
('NUMPAD_PLUS', "Numpad +"),
Bartek Skorupa
committed
)
nice_punc = False
for (ugly, nice) in pairs:
if punc == ugly:
nice_punc = nice
break
if not nice_punc:
nice_punc = punc.replace("_", " ").title()
return nice_punc
def hack_force_update(context, nodes):
if context.space_data.tree_type == "ShaderNodeTree":
node = nodes.new('ShaderNodeMath')
node.inputs[0].default_value = 0.0
nodes.remove(node)
elif context.space_data.tree_type == "CompositorNodeTree":
node = nodes.new('CompositorNodeMath')
node.inputs[0].default_value = 0.0
nodes.remove(node)
return False
def dpifac():
return bpy.context.user_preferences.system.dpi/72
def is_end_node(node):
bool = True
for output in node.outputs:
if output.links:
bool = False
break
return bool
def node_mid_pt(node, axis):
if axis == 'x':
d = node.location.x + (node.dimensions.x / 2)
elif axis == 'y':
d = node.location.y - (node.dimensions.y / 2)
else:
d = 0
return d
def autolink(node1, node2, links):
link_made = False
for outp in node1.outputs:
for inp in node2.inputs:
if not inp.is_linked and inp.name == outp.name:
link_made = True
links.new(outp, inp)
return True
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
for outp in node1.outputs:
for inp in node2.inputs:
if not inp.is_linked and inp.type == outp.type:
link_made = True
links.new(outp, inp)
return True
# force some connection even if the type doesn't match
for outp in node1.outputs:
for inp in node2.inputs:
if not inp.is_linked:
link_made = True
links.new(outp, inp)
return True
# even if no sockets are open, force one of matching type
for outp in node1.outputs:
for inp in node2.inputs:
if inp.type == outp.type:
link_made = True
links.new(outp, inp)
return True
# do something!
for outp in node1.outputs:
for inp in node2.inputs:
link_made = True
links.new(outp, inp)
return True
print("Could not make a link from " + node1.name + " to " + node2.name)
return link_made
def node_at_pos(nodes, context, event):
nodes_near_mouse = []
nodes_under_mouse = []
target_node = None
store_mouse_cursor(context, event)
x, y = context.space_data.cursor_location
x = x
y = y
# Make a list of each corner (and middle of border) for each node.
# Will be sorted to find nearest point and thus nearest node
node_points_with_dist = []
for node in nodes:
skipnode = False
if node.type != 'FRAME': # no point trying to link to a frame node
locx = node.location.x
locy = node.location.y
dimx = node.dimensions.x/dpifac()
dimy = node.dimensions.y/dpifac()
if node.parent:
locx += node.parent.location.x
locy += node.parent.location.y
if node.parent.parent:
locx += node.parent.parent.location.x
locy += node.parent.parent.location.y
if node.parent.parent.parent:
locx += node.parent.parent.parent.location.x
locy += node.parent.parent.parent.location.y
if node.parent.parent.parent.parent:
# Support three levels or parenting
# There's got to be a better way to do this...
skipnode = True
if not skipnode:
node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
for node in nodes:
if node.type != 'FRAME' and skipnode == False:
locx = node.location.x
locy = node.location.y
dimx = node.dimensions.x/dpifac()
dimy = node.dimensions.y/dpifac()
if node.parent:
locx += node.parent.location.x
locy += node.parent.location.y
if (locx <= x <= locx + dimx) and \
(locy - dimy <= y <= locy):
nodes_under_mouse.append(node)
if len(nodes_under_mouse) == 1:
if nodes_under_mouse[0] != nearest_node:
target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
else:
target_node = nearest_node # else use the nearest node
return target_node
def store_mouse_cursor(context, event):
space = context.space_data
v2d = context.region.view2d
tree = space.edit_tree
# convert mouse position to the View2D for later node placement
if context.region.type == 'WINDOW':
space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
else:
space.cursor_location = tree.view_center
def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]):
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(size)
bgl.glShadeModel(bgl.GL_SMOOTH)
bgl.glBegin(bgl.GL_LINE_STRIP)
try:
bgl.glColor4f(colour[0]+(1.0-colour[0])/4, colour[1]+(1.0-colour[1])/4, colour[2]+(1.0-colour[2])/4, colour[3]+(1.0-colour[3])/4)
bgl.glVertex2f(x1, y1)
bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
bgl.glVertex2f(x2, y2)
except:
pass
bgl.glEnd()
bgl.glShadeModel(bgl.GL_FLAT)
def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]):
bgl.glBegin(bgl.GL_TRIANGLE_FAN)
bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
radius = radius
sides = 32
for i in range(sides + 1):
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
bgl.glVertex2f(cosine, sine)
bgl.glEnd()
def draw_rounded_node_border(node, radius=8, colour=[1.0, 1.0, 1.0, 0.7]):
bgl.glEnable(bgl.GL_BLEND)
settings = bpy.context.user_preferences.addons[__name__].preferences
if settings.bgl_antialiasing:
bgl.glEnable(bgl.GL_LINE_SMOOTH)
area_width = bpy.context.area.width - (16*dpifac()) - 1
bottom_bar = (16*dpifac()) + 1
bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
nlocx = (node.location.x+1)*dpifac()
nlocy = (node.location.y+1)*dpifac()
ndimx = node.dimensions.x
ndimy = node.dimensions.y
# This is a stupid way to do this... TODO use while loop
if node.parent:
nlocx += node.parent.location.x
nlocy += node.parent.location.y
if node.parent.parent:
nlocx += node.parent.parent.location.x
nlocy += node.parent.parent.location.y
if node.parent.parent.parent:
nlocx += node.parent.parent.parent.location.x
nlocy += node.parent.parent.parent.location.y
if node.hide:
nlocx += -1
nlocy += 5
if node.type == 'REROUTE':
#nlocx += 1
nlocy -= 1
ndimx = 0
ndimy = 0
radius += 6
# Top left corner
bgl.glBegin(bgl.GL_TRIANGLE_FAN)
mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
bgl.glVertex2f(mx,my)
for i in range(sides+1):
if (4<=i<=8):
if my > bottom_bar and mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
bgl.glVertex2f(cosine, sine)
bgl.glEnd()
bgl.glBegin(bgl.GL_TRIANGLE_FAN)
mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
bgl.glVertex2f(mx,my)
for i in range(sides+1):
if (0<=i<=4):
if my > bottom_bar and mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
bgl.glVertex2f(cosine, sine)
bgl.glEnd()
bgl.glBegin(bgl.GL_TRIANGLE_FAN)
mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
bgl.glVertex2f(mx,my)
for i in range(sides+1):
if (8<=i<=12):
if my > bottom_bar and mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
bgl.glVertex2f(cosine, sine)
bgl.glEnd()
bgl.glBegin(bgl.GL_TRIANGLE_FAN)
mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
bgl.glVertex2f(mx,my)
for i in range(sides+1):
if (12<=i<=16):
if my > bottom_bar and mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
bgl.glVertex2f(cosine, sine)
bgl.glEnd()
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
m1y = max(m1y, bottom_bar)
m2y = max(m2y, bottom_bar)
if m1x < area_width and m2x < area_width:
bgl.glVertex2f(m2x-radius,m2y) # draw order is important, start with bottom left and go anti-clockwise
bgl.glVertex2f(m2x,m2y)
bgl.glVertex2f(m1x,m1y)
bgl.glVertex2f(m1x-radius,m1y)
bgl.glEnd()
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
m1x = min(m1x, area_width)
m2x = min(m2x, area_width)
if m1y > bottom_bar and m2y > bottom_bar:
bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
bgl.glVertex2f(m2x,m2y)
bgl.glVertex2f(m2x,m1y+radius)
bgl.glVertex2f(m1x,m1y+radius)
bgl.glEnd()
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
m1y = max(m1y, bottom_bar)
m2y = max(m2y, bottom_bar)
if m1x < area_width and m2x < area_width:
bgl.glVertex2f(m2x,m2y) # draw order is important, start with bottom left and go anti-clockwise
bgl.glVertex2f(m2x+radius,m2y)
bgl.glVertex2f(m1x+radius,m1y)
bgl.glVertex2f(m1x,m1y)
bgl.glEnd()
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
m1x = min(m1x, area_width)
m2x = min(m2x, area_width)
if m1y > bottom_bar and m2y > bottom_bar:
bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
bgl.glVertex2f(m2x,m2y)
bgl.glVertex2f(m2x,m1y-radius)
bgl.glVertex2f(m1x,m1y-radius)
bgl.glEnd()
bgl.glDisable(bgl.GL_BLEND)
if settings.bgl_antialiasing:
bgl.glDisable(bgl.GL_LINE_SMOOTH)
def draw_callback_mixnodes(self, context, mode):
if self.mouse_path:
nodes = context.space_data.node_tree.nodes
settings = context.user_preferences.addons[__name__].preferences
if settings.bgl_antialiasing:
bgl.glEnable(bgl.GL_LINE_SMOOTH)
if mode == "LINK":
col_outer = [1.0, 0.2, 0.2, 0.4]
col_inner = [0.0, 0.0, 0.0, 0.5]
col_circle_inner = [0.3, 0.05, 0.05, 1.0]
if mode == "LINKMENU":
col_outer = [0.4, 0.6, 1.0, 0.4]
col_inner = [0.0, 0.0, 0.0, 0.5]
col_circle_inner = [0.08, 0.15, .3, 1.0]
elif mode == "MIX":
col_outer = [0.2, 1.0, 0.2, 0.4]
col_inner = [0.0, 0.0, 0.0, 0.5]
col_circle_inner = [0.05, 0.3, 0.05, 1.0]
m1x = self.mouse_path[0][0]
m1y = self.mouse_path[0][1]
m2x = self.mouse_path[-1][0]
m2y = self.mouse_path[-1][1]
n1 = nodes[context.scene.NWLazySource]
n2 = nodes[context.scene.NWLazyTarget]
if n1 != n2:
draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
draw_line(m1x, m1y, m2x, m2y, 4, col_outer) # line outline
draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
# circle outline
draw_circle(m1x, m1y, 6, col_outer)
draw_circle(m2x, m2y, 6, col_outer)
# circle inner
draw_circle(m1x, m1y, 5, col_circle_inner)
draw_circle(m2x, m2y, 5, col_circle_inner)
# restore opengl defaults
bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_BLEND)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
if settings.bgl_antialiasing:
bgl.glDisable(bgl.GL_LINE_SMOOTH)
def get_nodes_links(context):
space = context.space_data
tree = space.node_tree
nodes = tree.nodes
links = tree.links
active = nodes.active
context_active = context.active_node
# check if we are working on regular node tree or node group is currently edited.
# if group is edited - active node of space_tree is the group
# if context.active_node != space active node - it means that the group is being edited.
# in such case we set "nodes" to be nodes of this group, "links" to be links of this group
# if context.active_node == space.active_node it means that we are not currently editing group
is_main_tree = True
if active:
is_main_tree = context_active == active
if not is_main_tree: # if group is currently edited
tree = active.node_tree
nodes = tree.nodes
links = tree.links
return nodes, links
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
# Addon prefs
class NWNodeWrangler(bpy.types.AddonPreferences):
bl_idname = __name__
merge_hide = EnumProperty(
name="Hide Mix nodes",
items=(
("ALWAYS", "Always", "Always collapse the new merge nodes"),
("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
("NEVER", "Never", "Never collapse the new merge nodes")
),
default='NON_SHADER',
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded")
merge_position = EnumProperty(
name="Mix Node Position",
items=(
("CENTER", "Center", "Place the Mix node between the two nodes"),
("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
),
default='CENTER',
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes")
bgl_antialiasing = BoolProperty(
name="Line Antialiasing",
default=False,
description="Remove aliasing artifacts on lines drawn in interactive modes such as Lazy Connect (Alt+LMB) and Lazy Merge (Alt+RMB) - this may cause issues on some systems"
)
show_hotkey_list = BoolProperty(
name="Show Hotkey List",
default=False,
description="Expand this box into a list of all the hotkeys for functions in this addon"
)
hotkey_list_filter = StringProperty(
name=" Filter by Name",
default="",
description="Show only hotkeys that have this text in their name"
)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "merge_position")
col.prop(self, "merge_hide")
col.prop(self, "bgl_antialiasing")
box = col.box()
col = box.column(align=True)
hotkey_button_name = "Show Hotkey List"
if self.show_hotkey_list:
hotkey_button_name = "Hide Hotkey List"
col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
if self.show_hotkey_list:
col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
col.separator()
for hotkey in kmi_defs:
if hotkey[6]:
hotkey_name = hotkey[6]
if self.hotkey_list_filter.lower() in hotkey_name.lower():
row = col.row(align=True)
row.label(hotkey_name)
keystr = nice_hotkey_name(hotkey[1])
if hotkey[3]:
keystr = "Shift " + keystr
if hotkey[4]:
keystr = "Alt " + keystr
if hotkey[2]:
keystr = "Ctrl " + keystr
row.label(keystr)
class NWBase:
@classmethod
def poll(cls, context):
space = context.space_data
return space.type == 'NODE_EDITOR' and space.node_tree is not None
# OPERATORS
class NWLazyMix(Operator, NWBase):
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
bl_idname = "node.nw_lazy_mix"
bl_label = "Mix Nodes"
bl_options = {'REGISTER', 'UNDO'}
def modal(self, context, event):
context.area.tag_redraw()
nodes, links = get_nodes_links(context)
cont = True
start_pos = [event.mouse_region_x, event.mouse_region_y]
node1 = None
if not context.scene.NWBusyDrawing:
node1 = node_at_pos(nodes, context, event)
if node1:
context.scene.NWBusyDrawing = node1.name
else:
if context.scene.NWBusyDrawing != 'STOP':
node1 = nodes[context.scene.NWBusyDrawing]
context.scene.NWLazySource = node1.name
context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
if event.type == 'MOUSEMOVE':
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
elif event.type == 'RIGHTMOUSE':
end_pos = [event.mouse_region_x, event.mouse_region_y]
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
node2 = None
node2 = node_at_pos(nodes, context, event)
if node2:
context.scene.NWBusyDrawing = node2.name
if node1 == node2:
cont = False
if cont:
if node1 and node2:
for node in nodes:
node.select = False
node1.select = True
node2.select = True
bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
context.scene.NWBusyDrawing = ""
return {'FINISHED'}
elif event.type == 'ESC':
print('cancelled')
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'NODE_EDITOR':
# the arguments we pass the the callback
args = (self, context, 'MIX')
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
self.mouse_path = []
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
class NWLazyConnect(Operator, NWBase):
"""Connect two nodes without clicking a specific socket (automatically determined"""
bl_idname = "node.nw_lazy_connect"
bl_label = "Lazy Connect"
bl_options = {'REGISTER', 'UNDO'}
with_menu = BoolProperty()
def modal(self, context, event):
context.area.tag_redraw()
nodes, links = get_nodes_links(context)
cont = True
start_pos = [event.mouse_region_x, event.mouse_region_y]
node1 = None
if not context.scene.NWBusyDrawing:
node1 = node_at_pos(nodes, context, event)
if node1:
context.scene.NWBusyDrawing = node1.name
else:
if context.scene.NWBusyDrawing != 'STOP':
node1 = nodes[context.scene.NWBusyDrawing]
context.scene.NWLazySource = node1.name
context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
if event.type == 'MOUSEMOVE':
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
elif event.type == 'RIGHTMOUSE':
end_pos = [event.mouse_region_x, event.mouse_region_y]
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
node2 = None
node2 = node_at_pos(nodes, context, event)
if node2:
context.scene.NWBusyDrawing = node2.name
if node1 == node2:
cont = False
link_success = False
if cont:
if node1 and node2:
original_sel = []
original_unsel = []
for node in nodes:
if node.select == True:
node.select = False
original_sel.append(node)
else:
original_unsel.append(node)
node1.select = True
node2.select = True
#link_success = autolink(node1, node2, links)
if self.with_menu:
if len(node1.outputs) > 1 and node2.inputs:
bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
elif len(node1.outputs) == 1:
bpy.ops.node.nw_call_inputs_menu(from_socket=0)
else:
link_success = autolink(node1, node2, links)
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
for node in original_sel:
node.select = True
for node in original_unsel:
node.select = False
if link_success:
hack_force_update(context, nodes)
context.scene.NWBusyDrawing = ""
return {'FINISHED'}
elif event.type == 'ESC':
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'NODE_EDITOR':
nodes, links = get_nodes_links(context)
node = node_at_pos(nodes, context, event)
if node:
context.scene.NWBusyDrawing = node.name
# the arguments we pass the the callback
mode = "LINK"
if self.with_menu:
mode = "LINKMENU"
args = (self, context, mode)
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
self.mouse_path = []
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
class NWDeleteUnused(Operator, NWBase):
"""Delete all nodes whose output is not used"""
bl_idname = 'node.nw_del_unused'
bl_label = 'Delete Unused Nodes'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
valid = False
if context.space_data:
if context.space_data.node_tree:
if context.space_data.node_tree.nodes:
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
end_types = 'OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LAMP', \
'OUTPUT_WORLD', 'GROUP', 'GROUP_INPUT', 'GROUP_OUTPUT'
# Store selection
selection = []
for node in nodes:
if node.select == True:
selection.append(node.name)
deleted_nodes = []
temp_deleted_nodes = []
del_unused_iterations = len(nodes)
for it in range(0, del_unused_iterations):
temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
for node in nodes:
node.select = False
for node in nodes:
if is_end_node(node) and not node.type in end_types and node.type != 'FRAME':
node.select = True
deleted_nodes.append(node.name)
bpy.ops.node.delete()
if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
break
# get unique list of deleted nodes (iterations would count the same node more than once)
deleted_nodes = list(set(deleted_nodes))
for n in deleted_nodes:
self.report({'INFO'}, "Node " + n + " deleted")
num_deleted = len(deleted_nodes)
n = ' node'
if num_deleted > 1:
n += 's'
if num_deleted:
self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
else:
self.report({'INFO'}, "Nothing deleted")
# Restore selection
nodes, links = get_nodes_links(context)
for node in nodes:
if node.name in selection:
node.select = True
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class NWSwapLinks(Operator, NWBase):
"""Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
bl_idname = 'node.nw_swap_links'
bl_label = 'Swap Links'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
snode = context.space_data
if context.selected_nodes:
return len(context.selected_nodes) <= 2
else:
return False
def execute(self, context):
nodes, links = get_nodes_links(context)
selected_nodes = context.selected_nodes
n1 = selected_nodes[0]
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
# Swap outputs
if len(selected_nodes) == 2:
n2 = selected_nodes[1]
if n1.outputs and n2.outputs:
n1_outputs = []
n2_outputs = []
out_index = 0
for output in n1.outputs:
if output.links:
for link in output.links:
n1_outputs.append([out_index, link.to_socket])
links.remove(link)
out_index += 1
out_index = 0
for output in n2.outputs:
if output.links:
for link in output.links:
n2_outputs.append([out_index, link.to_socket])
links.remove(link)
out_index += 1
for connection in n1_outputs:
try:
links.new(n2.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
for connection in n2_outputs:
try:
links.new(n1.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
else:
if n1.outputs or n2.outputs:
self.report({'WARNING'}, "One of the nodes has no outputs!")
else:
self.report({'WARNING'}, "Neither of the nodes have outputs!")
# Swap Inputs
elif len(selected_nodes) == 1:
if n1.inputs:
types = []
i=0
for i1 in n1.inputs:
if i1.is_linked:
similar_types = 0
for i2 in n1.inputs:
if i1.type == i2.type and i2.is_linked:
similar_types += 1
types.append ([i1, similar_types, i])
i += 1
types.sort(key=lambda k: k[1], reverse=True)
if types:
t = types[0]
if t[1] == 2:
for i2 in n1.inputs:
if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
pair = [t[0], i2]
i1f = pair[0].links[0].from_socket
i1t = pair[0].links[0].to_socket
i2f = pair[1].links[0].from_socket
i2t = pair[1].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
if t[1] == 1:
if len(types) == 1:
fs = t[0].links[0].from_socket
i = t[2]
links.remove(t[0].links[0])
if i+1 == len(n1.inputs):
i = -1
i += 1
while n1.inputs[i].is_linked:
i += 1
links.new(fs, n1.inputs[i])
elif len(types) == 2:
i1f = types[0][0].links[0].from_socket
i1t = types[0][0].links[0].to_socket
i2f = types[1][0].links[0].from_socket
i2t = types[1][0].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
else:
self.report({'WARNING'}, "This node has no input connections to swap!")
else:
self.report({'WARNING'}, "This node has no inputs to swap!")
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
hack_force_update(context, nodes)
return {'FINISHED'}
class NWResetBG(Operator, NWBase):
"""Reset the zoom and position of the background image"""
bl_idname = 'node.nw_bg_reset'
bl_label = 'Reset Backdrop'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
snode = context.space_data
return snode.tree_type == 'CompositorNodeTree'
def execute(self, context):
context.space_data.backdrop_zoom = 1
context.space_data.backdrop_x = 0
context.space_data.backdrop_y = 0
return {'FINISHED'}
class NWAddAttrNode(Operator, NWBase):
"""Add an Attribute node with this name"""
bl_idname = 'node.nw_add_attr_node'
bl_label = 'Add UV map'
attr_name = StringProperty()
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
nodes, links = get_nodes_links(context)
nodes.active.attribute_name = self.attr_name
return {'FINISHED'}
class NWEmissionViewer(Operator, NWBase):
bl_idname = "node.nw_emission_viewer"
bl_label = "Emission Viewer"
bl_description = "Connect active node to Emission Shader for shadeless previews"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
valid = False
if space.type == 'NODE_EDITOR':
if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None and (context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD"):
valid = True
return valid
def invoke(self, context, event):
shader_type = context.space_data.shader_type
if shader_type == 'OBJECT':
shader_output_type = "OUTPUT_MATERIAL"
shader_output_ident = "ShaderNodeOutputMaterial"
shader_viewer_ident = "ShaderNodeEmission"
elif shader_type == 'WORLD':
shader_output_type = "OUTPUT_WORLD"
shader_output_ident = "ShaderNodeOutputWorld"
shader_viewer_ident = "ShaderNodeBackground"
shader_types = [x[1] for x in shaders_shader_nodes_props]
mlocx = event.mouse_region_x
mlocy = event.mouse_region_y
select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
if 'FINISHED' in select_node: # only run if mouse click is on a node
nodes, links = get_nodes_links(context)
in_group = context.active_node != context.space_data.node_tree.nodes.active
active = nodes.active
output_types = [x[1] for x in shaders_output_nodes_props]
if active:
if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
# get material_output node, store selection, deselect all
materialout_exists = False
materialout = None # placeholder node
for node in nodes:
if node.type == shader_output_type:
materialout_exists = True
materialout = node
if node.select:
selection.append(node.name)
node.select = False
if not materialout:
materialout = nodes.new(shader_output_ident)
sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
max_xloc_node = sorted_by_xloc[-1]
if max_xloc_node.name == 'Emission Viewer':
max_xloc_node = sorted_by_xloc[-2]
materialout.location.x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
sum_yloc = 0
for node in nodes:
sum_yloc += node.location.y
# put material output at average y location
materialout.location.y = sum_yloc / len(nodes)
materialout.select = False
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
# Analyze outputs, add "Emission Viewer" if needed, make links
out_i = 0 # Start index of node's outputs
for i, out in enumerate(active.outputs):
for out_link in out.links:
linked_to_out = False
if "Emission Viewer" in out_link.to_node.name or out_link.to_node == materialout:
linked_to_out = True
if linked_to_out:
out_i = i + 1
# If last output is linked to Viewer or materialout - go to first.
if out_i == len(active.outputs):
out_i = 0
make_links = [] # store sockets for new links
if active.outputs:
# If output type not 'SHADER' - "Emission Viewer" needed
if active.outputs[out_i].type != 'SHADER':
# get Emission Viewer node
emission_exists = False
emission_placeholder = nodes[0]
for node in nodes:
if "Emission Viewer" in node.name:
emission_exists = True
emission_placeholder = node
if not emission_exists:
emission = nodes.new(shader_viewer_ident)
emission.hide = True
emission.location = [materialout.location.x, (materialout.location.y + 40)]
emission.label = "Viewer"
emission.name = "Emission Viewer"
emission.use_custom_color = True
emission.color = (0.6, 0.5, 0.4)
emission.select = False
else:
emission = emission_placeholder
make_links.append((active.outputs[out_i], emission.inputs[0]))
make_links.append((emission.outputs[0], materialout.inputs[0]))
else:
# Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
for node in nodes:
if node.name == 'Emission Viewer':
node.select = True
bpy.ops.node.delete()
for li_from, li_to in make_links:
links.new(li_from, li_to)
# Restore selection
nodes.active = active
for node in nodes:
if node.name in selection:
node.select = True
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
return {'FINISHED'}
else:
return {'CANCELLED'}
class NWFrameSelected(Operator, NWBase):
bl_idname = "node.nw_frame_selected"
bl_label = "Frame Selected"
bl_description = "Add a frame node and parent the selected nodes to it"
bl_options = {'REGISTER', 'UNDO'}
label_prop = StringProperty(name='Label', default=' ', description='The visual name of the frame node')
color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6),
min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3)
@classmethod
def poll(cls, context):
space = context.space_data
valid = False
if space.type == 'NODE_EDITOR':
if space.node_tree is not None:
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = []
for node in nodes:
if node.select == True:
selected.append(node)
bpy.ops.node.add_node(type='NodeFrame')
frm = nodes.active
frm.label = self.label_prop
frm.use_custom_color = True
frm.color = self.color_prop
for node in selected:
node.parent = frm
return {'FINISHED'}
class NWReloadImages(Operator, NWBase):
bl_idname = "node.nw_reload_images"
bl_label = "Reload Images"
bl_description = "Update all the image nodes to match their files on disk"
@classmethod
def poll(cls, context):
space = context.space_data
valid = False
if space.type == 'NODE_EDITOR':
if space.node_tree is not None:
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
num_reloaded = 0
for node in nodes:
if node.type in image_types:
if node.type == "TEXTURE":
if node.texture: # node has texture assigned
if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
if node.texture.image: # texture has image assigned
node.texture.image.reload()
num_reloaded += 1
else:
if node.image:
node.image.reload()
num_reloaded += 1
if num_reloaded:
self.report({'INFO'}, "Reloaded images")
print("Reloaded " + str(num_reloaded) + " images")
hack_force_update(context, nodes)
return {'FINISHED'}
else:
self.report({'WARNING'}, "No images found to reload in this node tree")
return {'CANCELLED'}
class NWSwitchNodeType(Operator, NWBase):
"""Switch type of selected nodes """
bl_idname = "node.nw_swtch_node_type"
bl_label = "Switch Node Type"
bl_options = {'REGISTER', 'UNDO'}
to_type = EnumProperty(
name="Switch to type",
items=list(shaders_input_nodes_props) +
list(shaders_output_nodes_props) +
list(shaders_shader_nodes_props) +
list(shaders_texture_nodes_props) +
list(shaders_color_nodes_props) +
list(shaders_vector_nodes_props) +
list(shaders_converter_nodes_props) +
list(shaders_layout_nodes_props) +
list(compo_input_nodes_props) +
list(compo_output_nodes_props) +
list(compo_color_nodes_props) +
list(compo_converter_nodes_props) +
list(compo_filter_nodes_props) +
list(compo_vector_nodes_props) +
list(compo_matte_nodes_props) +
list(compo_distort_nodes_props) +
list(compo_layout_nodes_props),
)
def execute(self, context):
nodes, links = get_nodes_links(context)
to_type = self.to_type
# Those types of nodes will not swap.
src_excludes = ('NodeFrame')
# Those attributes of nodes will be copied if possible
attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
'show_options', 'show_preview', 'show_texture',
'use_alpha', 'use_clamp', 'use_custom_color', 'location'
)
selected = [n for n in nodes if n.select]
reselect = []
for node in [n for n in selected if
n.rna_type.identifier not in src_excludes and
n.rna_type.identifier != to_type]:
new_node = nodes.new(to_type)
for attr in attrs_to_pass:
if hasattr(node, attr) and hasattr(new_node, attr):
setattr(new_node, attr, getattr(node, attr))
# set image datablock of dst to image of src
if hasattr(node, 'image') and hasattr(new_node, 'image'):
if node.image:
new_node.image = node.image
# Special cases
if new_node.type == 'SWITCH':
new_node.hide = True
# Dictionaries: src_sockets and dst_sockets:
# 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
# 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
# in 'INPUTS' and 'OUTPUTS':
# 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
# socket entry:
# (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
src_sockets = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
}
dst_sockets = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
}
types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
# check src node to set src_sockets values and dst node to set dst_sockets dict values
for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
# Check node's inputs and outputs and fill proper entries in "sockets" dict
for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
# enumerate in inputs, then in outputs
# find name, default value and links of socket
for i, socket in enumerate(in_out):
the_name = socket.name
dval = None
# Not every socket, especially in outputs has "default_value"
if hasattr(socket, 'default_value'):
dval = socket.default_value
socket_links = []
for lnk in socket.links:
socket_links.append(lnk)
# check type of socket to fill proper keys.
for the_type in types_order_one:
if socket.type == the_type:
# create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
# entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
# Check which of the types in inputs/outputs is considered to be "main".
# Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
for type_check in types_order_one:
if sockets[in_out_name][type_check]:
sockets[in_out_name]['MAIN'] = type_check
break
matches = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
}
for inout, soctype in (
('INPUTS', 'MAIN',),
('INPUTS', 'SHADER',),
('INPUTS', 'RGBA',),
('INPUTS', 'VECTOR',),
('INPUTS', 'VALUE',),
('OUTPUTS', 'MAIN',),
('OUTPUTS', 'SHADER',),
('OUTPUTS', 'RGBA',),
('OUTPUTS', 'VECTOR',),
('OUTPUTS', 'VALUE',),
):
if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
if soctype == 'MAIN':
sc = src_sockets[inout][src_sockets[inout]['MAIN']]
dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
else:
sc = src_sockets[inout][soctype]
dt = dst_sockets[inout][soctype]
# start with 'dt' to determine number of possibilities.
for i, soc in enumerate(dt):
# if src main has enough entries - match them with dst main sockets by indexes.
if len(sc) > i:
matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
# add 'VALUE_NAME' criterion to inputs.
if inout == 'INPUTS' and soctype == 'VALUE':
for s in sc:
if s[2] == soc[2]: # if names match
# append src (index, dval), dst (index, dval)
matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
# When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
# This creates better links when relinking textures.
if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
# Pass default values and RELINK:
for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
# INPUTS: Base on matches in proper order.
for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
# pass dvals
if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
new_node.inputs[dst_i].default_value = src_dval
# Special case: switch to math
if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
new_node.type == 'MATH' and\
tp == 'MAIN':
new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
new_node.inputs[dst_i].default_value = new_dst_dval
if node.type == 'MIX_RGB':
if node.blend_type in [o[0] for o in operations]:
new_node.operation = node.blend_type
# Special case: switch from math to some types
if node.type == 'MATH' and\
new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
tp == 'MAIN':
for i in range(3):
new_node.inputs[dst_i].default_value[i] = src_dval
if new_node.type == 'MIX_RGB':
if node.operation in [t[0] for t in blend_types]:
new_node.blend_type = node.operation
# Set Fac of MIX_RGB to 1.0
new_node.inputs[0].default_value = 1.0
# make link only when dst matching input is not linked already.
if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
in_src_link = node.inputs[src_i].links[0]
in_dst_socket = new_node.inputs[dst_i]
links.new(in_src_link.from_socket, in_dst_socket)
links.remove(in_src_link)
# OUTPUTS: Base on matches in proper order.
for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
for out_src_link in node.outputs[src_i].links:
out_dst_socket = new_node.outputs[dst_i]
links.new(out_dst_socket, out_src_link.to_socket)
# relink rest inputs if possible, no criteria
for src_inp in node.inputs:
for dst_inp in new_node.inputs:
if src_inp.links and not dst_inp.links:
src_link = src_inp.links[0]
links.new(src_link.from_socket, dst_inp)
links.remove(src_link)
# relink rest outputs if possible, base on node kind if any left.
for src_o in node.outputs:
for out_src_link in src_o.links:
for dst_o in new_node.outputs:
if src_o.type == dst_o.type:
links.new(dst_o, out_src_link.to_socket)
# relink rest outputs no criteria if any left. Link all from first output.
for src_o in node.outputs:
for out_src_link in src_o.links:
if new_node.outputs:
links.new(new_node.outputs[0], out_src_link.to_socket)
nodes.remove(node)
return {'FINISHED'}
class NWMergeNodes(Operator, NWBase):
bl_idname = "node.nw_merge_nodes"
bl_label = "Merge Nodes"
bl_description = "Merge Selected Nodes"
bl_options = {'REGISTER', 'UNDO'}
mode = EnumProperty(
name="mode",
description="All possible blend types and math operations",
items=blend_types + [op for op in operations if op not in blend_types],
)
merge_type = EnumProperty(
name="merge type",
description="Type of Merge to be used",
items=(
('AUTO', 'Auto', 'Automatic Output Type Detection'),
('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
('MIX', 'Mix Node', 'Merge using Mix Nodes'),
('MATH', 'Math Node', 'Merge using Math Nodes'),
('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
def execute(self, context):
settings = context.user_preferences.addons[__name__].preferences
merge_hide = settings.merge_hide
merge_position = settings.merge_position # 'center' or 'bottom'
do_hide = False
do_hide_shader = False
if merge_hide == 'ALWAYS':
do_hide = True
do_hide_shader = True
elif merge_hide == 'NON_SHADER':
do_hide = True
tree_type = context.space_data.node_tree.type
if tree_type == 'COMPOSITING':
node_type = 'CompositorNode'
elif tree_type == 'SHADER':
node_type = 'ShaderNode'
nodes, links = get_nodes_links(context)
mode = self.mode
merge_type = self.merge_type
# Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
# 'ZCOMBINE' works only if mode == 'MIX'
# Setting mode to None prevents trying to add 'ZCOMBINE' node.
if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
merge_type = 'MIX'
mode = 'MIX'
selected_mix = [] # entry = [index, loc]
selected_shader = [] # entry = [index, loc]
selected_math = [] # entry = [index, loc]
selected_z = [] # entry = [index, loc]
selected_alphaover = [] # entry = [index, loc]
for i, node in enumerate(nodes):
if node.select and node.outputs:
if merge_type == 'AUTO':
for (type, types_list, dst) in (
('SHADER', ('MIX', 'ADD'), selected_shader),
('RGBA', [t[0] for t in blend_types], selected_mix),
('VALUE', [t[0] for t in operations], selected_math),
output_type = node.outputs[0].type
valid_mode = mode in types_list
# When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
# Cheat that output type is 'RGBA',
# and that 'MIX' exists in math operations list.
# This way when selected_mix list is analyzed:
# Node data will be appended even though it doesn't meet requirements.
if output_type != 'SHADER' and mode == 'MIX':
output_type = 'RGBA'
valid_mode = True
if output_type == type and valid_mode:
dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
else:
for (type, types_list, dst) in (
('SHADER', ('MIX', 'ADD'), selected_shader),
('MIX', [t[0] for t in blend_types], selected_mix),
('MATH', [t[0] for t in operations], selected_math),
('ZCOMBINE', ('MIX', ), selected_z),
('ALPHAOVER', ('MIX', ), selected_alphaover),
if merge_type == type and mode in types_list:
dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
# When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
# use only 'Mix' nodes for merging.
# For that we add selected_math list to selected_mix list and clear selected_math.
if selected_mix and selected_math and merge_type == 'AUTO':
selected_mix += selected_math
selected_math = []
for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
if nodes_list:
count_before = len(nodes)
# sort list by loc_x - reversed
nodes_list.sort(key=lambda k: k[1], reverse=True)
# get maximum loc_x
loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
nodes_list.sort(key=lambda k: k[2], reverse=True)
if merge_position == 'CENTER':
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)
if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
if do_hide:
loc_y += 40
else:
loc_y += 80
else:
loc_y = nodes_list[len(nodes_list) - 1][2]
offset_y = 100
if not do_hide:
offset_y = 200
if nodes_list == selected_shader and not do_hide_shader:
offset_y = 150.0
the_range = len(nodes_list) - 1
if len(nodes_list) == 1:
the_range = 1
for i in range(the_range):
if nodes_list == selected_mix:
add_type = node_type + 'MixRGB'
add = nodes.new(add_type)
add.blend_type = mode
if mode != 'MIX':
add.inputs[0].default_value = 1.0
add.show_preview = False
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 1
second = 2
add.width_hidden = 100.0
elif nodes_list == selected_math:
add_type = node_type + 'Math'
add = nodes.new(add_type)
add.operation = mode
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_shader:
if mode == 'MIX':
add_type = node_type + 'MixShader'
add = nodes.new(add_type)
add.hide = do_hide_shader
if do_hide_shader:
loc_y = loc_y - 50
first = 1
second = 2
add.width_hidden = 100.0
elif mode == 'ADD':
add_type = node_type + 'AddShader'
add = nodes.new(add_type)
add.hide = do_hide_shader
if do_hide_shader:
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_z:
add = nodes.new('CompositorNodeZcombine')
add.show_preview = False
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 0
second = 2
add.width_hidden = 100.0
elif nodes_list == selected_alphaover:
add = nodes.new('CompositorNodeAlphaOver')
add.show_preview = False
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 1
second = 2
add.width_hidden = 100.0
add.location = loc_x, loc_y
loc_y += offset_y
add.select = True
count_adds = i + 1
count_after = len(nodes)
index = count_after - 1
Bartek Skorupa
committed
first_selected = nodes[nodes_list[0][0]]
# "last" node has been added as first, so its index is count_before.
last_add = nodes[count_before]
# add links from last_add to all links 'to_socket' of out links of first selected.
for fs_link in first_selected.outputs[0].links:
Bartek Skorupa
committed
# Prevent cyclic dependencies when nodes to be marged are linked to one another.
# Create list of invalid indexes.
invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
Bartek Skorupa
committed
# Link only if "to_node" index not in invalid indexes list.
if fs_link.to_node not in [nodes[i] for i in invalid_i]:
links.new(last_add.outputs[0], fs_link.to_socket)
# add link from "first" selected and "first" add node
node_to = nodes[count_after - 1]
links.new(first_selected.outputs[0], node_to.inputs[first])
if node_to.type == 'ZCOMBINE':
for fs_out in first_selected.outputs:
if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
links.new(fs_out, node_to.inputs[1])
break
# add links between added ADD nodes and between selected and ADD nodes
for i in range(count_adds):
if i < count_adds - 1:
node_from = nodes[index]
node_to = nodes[index - 1]
node_to_input_i = first
node_to_z_i = 1 # if z combine - link z to first z input
links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
if node_to.type == 'ZCOMBINE':
for from_out in node_from.outputs:
if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
links.new(from_out, node_to.inputs[node_to_z_i])
node_from = nodes[nodes_list[i + 1][0]]
node_to = nodes[index]
node_to_input_i = second
node_to_z_i = 3 # if z combine - link z to second z input
links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
if node_to.type == 'ZCOMBINE':
for from_out in node_from.outputs:
if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
links.new(from_out, node_to.inputs[node_to_z_i])
index -= 1
# set "last" of added nodes as active
Bartek Skorupa
committed
nodes.active = last_add
nodes[i].select = False
return {'FINISHED'}
class NWBatchChangeNodes(Operator, NWBase):
bl_idname = "node.nw_batch_change"
bl_label = "Batch Change"
bl_description = "Batch Change Blend Type and Math Operation"
bl_options = {'REGISTER', 'UNDO'}
blend_type = EnumProperty(
name="Blend Type",
items=blend_types + navs,
)
name="Operation",
items=operations + navs,
)
def execute(self, context):
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
nodes, links = get_nodes_links(context)
blend_type = self.blend_type
operation = self.operation
for node in context.selected_nodes:
if node.type == 'MIX_RGB':
if not blend_type in [nav[0] for nav in navs]:
node.blend_type = blend_type
else:
if blend_type == 'NEXT':
index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
#index = blend_types.index(node.blend_type)
if index == len(blend_types) - 1:
node.blend_type = blend_types[0][0]
else:
node.blend_type = blend_types[index + 1][0]
if blend_type == 'PREV':
index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
if index == 0:
node.blend_type = blend_types[len(blend_types) - 1][0]
else:
node.blend_type = blend_types[index - 1][0]
if node.type == 'MATH':
if not operation in [nav[0] for nav in navs]:
node.operation = operation
else:
if operation == 'NEXT':
index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
#index = operations.index(node.operation)
if index == len(operations) - 1:
node.operation = operations[0][0]
else:
node.operation = operations[index + 1][0]
if operation == 'PREV':
index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
#index = operations.index(node.operation)
if index == 0:
node.operation = operations[len(operations) - 1][0]
else:
node.operation = operations[index - 1][0]
return {'FINISHED'}
class NWChangeMixFactor(Operator, NWBase):
bl_idname = "node.nw_factor"
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
bl_label = "Change Factor"
bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
bl_options = {'REGISTER', 'UNDO'}
# option: Change factor.
# If option is 1.0 or 0.0 - set to 1.0 or 0.0
# Else - change factor by option value.
option = FloatProperty()
def execute(self, context):
nodes, links = get_nodes_links(context)
option = self.option
selected = [] # entry = index
for si, node in enumerate(nodes):
if node.select:
if node.type in {'MIX_RGB', 'MIX_SHADER'}:
selected.append(si)
for si in selected:
fac = nodes[si].inputs[0]
nodes[si].hide = False
if option in {0.0, 1.0}:
fac.default_value = option
else:
fac.default_value += option
return {'FINISHED'}
class NWCopySettings(Operator, NWBase):
bl_idname = "node.nw_copy_settings"
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
bl_label = "Copy Settings"
bl_description = "Copy Settings of Active Node to Selected Nodes"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
valid = False
if (space.type == 'NODE_EDITOR' and
space.node_tree is not None and
context.active_node is not None and
context.active_node.type is not 'FRAME'
):
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = [n for n in nodes if n.select]
reselect = [] # duplicated nodes will be selected after execution
active = nodes.active
if active.select:
reselect.append(active)
for node in selected:
if node.type == active.type and node != active:
# duplicate active, relink links as in 'node', append copy to 'reselect', delete node
bpy.ops.node.select_all(action='DESELECT')
nodes.active = active
active.select = True
bpy.ops.node.duplicate()
copied = nodes.active
# Copied active should however inherit some properties from 'node'
attributes = (
'hide', 'show_preview', 'mute', 'label',
'use_custom_color', 'color', 'width', 'width_hidden',
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
for attr in attributes:
setattr(copied, attr, getattr(node, attr))
# Handle scenario when 'node' is in frame. 'copied' is in same frame then.
if copied.parent:
bpy.ops.node.parent_clear()
locx = node.location.x
locy = node.location.y
# get absolute node location
parent = node.parent
while parent:
locx += parent.location.x
locy += parent.location.y
parent = parent.parent
copied.location = [locx, locy]
# reconnect links from node to copied
for i, input in enumerate(node.inputs):
if input.links:
link = input.links[0]
links.new(link.from_socket, copied.inputs[i])
for out, output in enumerate(node.outputs):
if output.links:
out_links = output.links
for link in out_links:
links.new(copied.outputs[out], link.to_socket)
bpy.ops.node.select_all(action='DESELECT')
node.select = True
bpy.ops.node.delete()
reselect.append(copied)
else: # If selected wasn't copied, need to reselect it afterwards.
reselect.append(node)
# clean up
bpy.ops.node.select_all(action='DESELECT')
for node in reselect:
node.select = True
nodes.active = active
return {'FINISHED'}
class NWCopyLabel(Operator, NWBase):
bl_idname = "node.nw_copy_label"
bl_label = "Copy Label"
bl_options = {'REGISTER', 'UNDO'}
option = EnumProperty(
name="option",
description="Source of name of label",
items=(
('FROM_ACTIVE', 'from active', 'from active node',),
('FROM_NODE', 'from node', 'from node linked to selected node'),
('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
)
)
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
def execute(self, context):
nodes, links = get_nodes_links(context)
option = self.option
active = nodes.active
if option == 'FROM_ACTIVE':
if active:
src_label = active.label
for node in [n for n in nodes if n.select and nodes.active != n]:
node.label = src_label
elif option == 'FROM_NODE':
selected = [n for n in nodes if n.select]
for node in selected:
for input in node.inputs:
if input.links:
src = input.links[0].from_node
node.label = src.label
break
elif option == 'FROM_SOCKET':
selected = [n for n in nodes if n.select]
for node in selected:
for input in node.inputs:
if input.links:
src = input.links[0].from_socket
node.label = src.name
break
return {'FINISHED'}
class NWClearLabel(Operator, NWBase):
bl_idname = "node.nw_clear_label"
bl_label = "Clear Label"
bl_options = {'REGISTER', 'UNDO'}
option = BoolProperty()
def execute(self, context):
Loading
Loading full blame…