Merge branch 'blender-v3.3-release'
[blender-addons.git] / node_wrangler.py
blob290ac082a23a0ca9c0e01cd62ce6568f45b3bc30
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
6 "version": (3, 41),
7 "blender": (2, 93, 0),
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
10 "warning": "",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
12 "category": "Node",
15 import bpy, blf, bgl
16 import gpu
17 from bpy.types import Operator, Panel, Menu
18 from bpy.props import (
19 FloatProperty,
20 EnumProperty,
21 BoolProperty,
22 IntProperty,
23 StringProperty,
24 FloatVectorProperty,
25 CollectionProperty,
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
32 from os import path
33 from glob import glob
34 from copy import copy
35 from itertools import chain
36 import re
37 from collections import namedtuple
39 #################
40 # rl_outputs:
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'])
47 rl_outputs = (
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),
75 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
76 # used list, not tuple for easy merging with other lists.
77 blend_types = [
78 ('MIX', 'Mix', 'Mix Mode'),
79 ('ADD', 'Add', 'Add Mode'),
80 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
81 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
82 ('SCREEN', 'Screen', 'Screen Mode'),
83 ('DIVIDE', 'Divide', 'Divide Mode'),
84 ('DIFFERENCE', 'Difference', 'Difference Mode'),
85 ('DARKEN', 'Darken', 'Darken Mode'),
86 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
87 ('OVERLAY', 'Overlay', 'Overlay Mode'),
88 ('DODGE', 'Dodge', 'Dodge Mode'),
89 ('BURN', 'Burn', 'Burn Mode'),
90 ('HUE', 'Hue', 'Hue Mode'),
91 ('SATURATION', 'Saturation', 'Saturation Mode'),
92 ('VALUE', 'Value', 'Value Mode'),
93 ('COLOR', 'Color', 'Color Mode'),
94 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
95 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
98 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
99 # used list, not tuple for easy merging with other lists.
100 operations = [
101 ('ADD', 'Add', 'Add Mode'),
102 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
103 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
104 ('DIVIDE', 'Divide', 'Divide Mode'),
105 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
106 ('SINE', 'Sine', 'Sine Mode'),
107 ('COSINE', 'Cosine', 'Cosine Mode'),
108 ('TANGENT', 'Tangent', 'Tangent Mode'),
109 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
110 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
111 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
112 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
113 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
114 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
115 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
116 ('POWER', 'Power', 'Power Mode'),
117 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
118 ('SQRT', 'Square Root', 'Square Root Mode'),
119 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
120 ('EXPONENT', 'Exponent', 'Exponent Mode'),
121 ('MINIMUM', 'Minimum', 'Minimum Mode'),
122 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
123 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
124 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
125 ('SIGN', 'Sign', 'Sign Mode'),
126 ('COMPARE', 'Compare', 'Compare Mode'),
127 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
128 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
129 ('FRACT', 'Fraction', 'Fraction Mode'),
130 ('MODULO', 'Modulo', 'Modulo Mode'),
131 ('SNAP', 'Snap', 'Snap Mode'),
132 ('WRAP', 'Wrap', 'Wrap Mode'),
133 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
134 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
135 ('ROUND', 'Round', 'Round Mode'),
136 ('FLOOR', 'Floor', 'Floor Mode'),
137 ('CEIL', 'Ceil', 'Ceil Mode'),
138 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
139 ('RADIANS', 'To Radians', 'To Radians Mode'),
140 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
143 # Operations used by the geometry boolean node and join geometry node
144 geo_combine_operations = [
145 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
146 ('INTERSECT', 'Intersect', 'Intersect Mode'),
147 ('UNION', 'Union', 'Union Mode'),
148 ('DIFFERENCE', 'Difference', 'Difference Mode'),
151 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
152 # used list, not tuple for easy merging with other lists.
153 navs = [
154 ('CURRENT', 'Current', 'Leave at current state'),
155 ('NEXT', 'Next', 'Next blend type/operation'),
156 ('PREV', 'Prev', 'Previous blend type/operation'),
159 draw_color_sets = {
160 "red_white": (
161 (1.0, 1.0, 1.0, 0.7),
162 (1.0, 0.0, 0.0, 0.7),
163 (0.8, 0.2, 0.2, 1.0)
165 "green": (
166 (0.0, 0.0, 0.0, 1.0),
167 (0.38, 0.77, 0.38, 1.0),
168 (0.38, 0.77, 0.38, 1.0)
170 "yellow": (
171 (0.0, 0.0, 0.0, 1.0),
172 (0.77, 0.77, 0.16, 1.0),
173 (0.77, 0.77, 0.16, 1.0)
175 "purple": (
176 (0.0, 0.0, 0.0, 1.0),
177 (0.38, 0.38, 0.77, 1.0),
178 (0.38, 0.38, 0.77, 1.0)
180 "grey": (
181 (0.0, 0.0, 0.0, 1.0),
182 (0.63, 0.63, 0.63, 1.0),
183 (0.63, 0.63, 0.63, 1.0)
185 "black": (
186 (1.0, 1.0, 1.0, 0.7),
187 (0.0, 0.0, 0.0, 0.7),
188 (0.2, 0.2, 0.2, 1.0)
192 viewer_socket_name = "tmp_viewer"
194 def get_nodes_from_category(category_name, context):
195 for category in node_categories_iter(context):
196 if category.name == category_name:
197 return sorted(category.items(context), key=lambda node: node.label)
199 def get_first_enabled_output(node):
200 for output in node.outputs:
201 if output.enabled:
202 return output
203 else:
204 return node.outputs[0]
206 def is_visible_socket(socket):
207 return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
209 def nice_hotkey_name(punc):
210 # convert the ugly string name into the actual character
211 nice_name = {
212 'LEFTMOUSE': "LMB",
213 'MIDDLEMOUSE': "MMB",
214 'RIGHTMOUSE': "RMB",
215 'WHEELUPMOUSE': "Wheel Up",
216 'WHEELDOWNMOUSE': "Wheel Down",
217 'WHEELINMOUSE': "Wheel In",
218 'WHEELOUTMOUSE': "Wheel Out",
219 'ZERO': "0",
220 'ONE': "1",
221 'TWO': "2",
222 'THREE': "3",
223 'FOUR': "4",
224 'FIVE': "5",
225 'SIX': "6",
226 'SEVEN': "7",
227 'EIGHT': "8",
228 'NINE': "9",
229 'OSKEY': "Super",
230 'RET': "Enter",
231 'LINE_FEED': "Enter",
232 'SEMI_COLON': ";",
233 'PERIOD': ".",
234 'COMMA': ",",
235 'QUOTE': '"',
236 'MINUS': "-",
237 'SLASH': "/",
238 'BACK_SLASH': "\\",
239 'EQUAL': "=",
240 'NUMPAD_1': "Numpad 1",
241 'NUMPAD_2': "Numpad 2",
242 'NUMPAD_3': "Numpad 3",
243 'NUMPAD_4': "Numpad 4",
244 'NUMPAD_5': "Numpad 5",
245 'NUMPAD_6': "Numpad 6",
246 'NUMPAD_7': "Numpad 7",
247 'NUMPAD_8': "Numpad 8",
248 'NUMPAD_9': "Numpad 9",
249 'NUMPAD_0': "Numpad 0",
250 'NUMPAD_PERIOD': "Numpad .",
251 'NUMPAD_SLASH': "Numpad /",
252 'NUMPAD_ASTERIX': "Numpad *",
253 'NUMPAD_MINUS': "Numpad -",
254 'NUMPAD_ENTER': "Numpad Enter",
255 'NUMPAD_PLUS': "Numpad +",
257 try:
258 return nice_name[punc]
259 except KeyError:
260 return punc.replace("_", " ").title()
263 def force_update(context):
264 context.space_data.node_tree.update_tag()
267 def dpifac():
268 prefs = bpy.context.preferences.system
269 return prefs.dpi * prefs.pixel_size / 72
272 def node_mid_pt(node, axis):
273 if axis == 'x':
274 d = node.location.x + (node.dimensions.x / 2)
275 elif axis == 'y':
276 d = node.location.y - (node.dimensions.y / 2)
277 else:
278 d = 0
279 return d
282 def autolink(node1, node2, links):
283 link_made = False
284 available_inputs = [inp for inp in node2.inputs if inp.enabled]
285 available_outputs = [outp for outp in node1.outputs if outp.enabled]
286 for outp in available_outputs:
287 for inp in available_inputs:
288 if not inp.is_linked and inp.name == outp.name:
289 link_made = True
290 links.new(outp, inp)
291 return True
293 for outp in available_outputs:
294 for inp in available_inputs:
295 if not inp.is_linked and inp.type == outp.type:
296 link_made = True
297 links.new(outp, inp)
298 return True
300 # force some connection even if the type doesn't match
301 if available_outputs:
302 for inp in available_inputs:
303 if not inp.is_linked:
304 link_made = True
305 links.new(available_outputs[0], inp)
306 return True
308 # even if no sockets are open, force one of matching type
309 for outp in available_outputs:
310 for inp in available_inputs:
311 if inp.type == outp.type:
312 link_made = True
313 links.new(outp, inp)
314 return True
316 # do something!
317 for outp in available_outputs:
318 for inp in available_inputs:
319 link_made = True
320 links.new(outp, inp)
321 return True
323 print("Could not make a link from " + node1.name + " to " + node2.name)
324 return link_made
326 def abs_node_location(node):
327 abs_location = node.location
328 if node.parent is None:
329 return abs_location
330 return abs_location + abs_node_location(node.parent)
332 def node_at_pos(nodes, context, event):
333 nodes_under_mouse = []
334 target_node = None
336 store_mouse_cursor(context, event)
337 x, y = context.space_data.cursor_location
339 # Make a list of each corner (and middle of border) for each node.
340 # Will be sorted to find nearest point and thus nearest node
341 node_points_with_dist = []
342 for node in nodes:
343 skipnode = False
344 if node.type != 'FRAME': # no point trying to link to a frame node
345 dimx = node.dimensions.x/dpifac()
346 dimy = node.dimensions.y/dpifac()
347 locx, locy = abs_node_location(node)
349 if not skipnode:
350 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
351 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
352 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
353 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
355 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
356 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
357 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
358 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
360 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
362 for node in nodes:
363 if node.type != 'FRAME' and skipnode == False:
364 locx, locy = abs_node_location(node)
365 dimx = node.dimensions.x/dpifac()
366 dimy = node.dimensions.y/dpifac()
367 if (locx <= x <= locx + dimx) and \
368 (locy - dimy <= y <= locy):
369 nodes_under_mouse.append(node)
371 if len(nodes_under_mouse) == 1:
372 if nodes_under_mouse[0] != nearest_node:
373 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
374 else:
375 target_node = nearest_node # else use the nearest node
376 else:
377 target_node = nearest_node
378 return target_node
381 def store_mouse_cursor(context, event):
382 space = context.space_data
383 v2d = context.region.view2d
384 tree = space.edit_tree
386 # convert mouse position to the View2D for later node placement
387 if context.region.type == 'WINDOW':
388 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
389 else:
390 space.cursor_location = tree.view_center
392 def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
393 shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
395 vertices = ((x1, y1), (x2, y2))
396 vertex_colors = ((colour[0]+(1.0-colour[0])/4,
397 colour[1]+(1.0-colour[1])/4,
398 colour[2]+(1.0-colour[2])/4,
399 colour[3]+(1.0-colour[3])/4),
400 colour)
402 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
403 bgl.glLineWidth(size * dpifac())
405 shader.bind()
406 batch.draw(shader)
409 def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
410 radius = radius * dpifac()
411 sides = 12
412 vertices = [(radius * cos(i * 2 * pi / sides) + mx,
413 radius * sin(i * 2 * pi / sides) + my)
414 for i in range(sides + 1)]
416 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
417 shader.bind()
418 shader.uniform_float("color", colour)
419 batch.draw(shader)
422 def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
423 area_width = bpy.context.area.width
424 sides = 16
425 radius = radius*dpifac()
427 nlocx, nlocy = abs_node_location(node)
429 nlocx = (nlocx+1)*dpifac()
430 nlocy = (nlocy+1)*dpifac()
431 ndimx = node.dimensions.x
432 ndimy = node.dimensions.y
434 if node.hide:
435 nlocx += -1
436 nlocy += 5
437 if node.type == 'REROUTE':
438 #nlocx += 1
439 nlocy -= 1
440 ndimx = 0
441 ndimy = 0
442 radius += 6
444 # Top left corner
445 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
446 vertices = [(mx,my)]
447 for i in range(sides+1):
448 if (4<=i<=8):
449 if mx < area_width:
450 cosine = radius * cos(i * 2 * pi / sides) + mx
451 sine = radius * sin(i * 2 * pi / sides) + my
452 vertices.append((cosine,sine))
453 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
454 shader.bind()
455 shader.uniform_float("color", colour)
456 batch.draw(shader)
458 # Top right corner
459 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
460 vertices = [(mx,my)]
461 for i in range(sides+1):
462 if (0<=i<=4):
463 if mx < area_width:
464 cosine = radius * cos(i * 2 * pi / sides) + mx
465 sine = radius * sin(i * 2 * pi / sides) + my
466 vertices.append((cosine,sine))
467 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
468 shader.bind()
469 shader.uniform_float("color", colour)
470 batch.draw(shader)
472 # Bottom left corner
473 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
474 vertices = [(mx,my)]
475 for i in range(sides+1):
476 if (8<=i<=12):
477 if mx < area_width:
478 cosine = radius * cos(i * 2 * pi / sides) + mx
479 sine = radius * sin(i * 2 * pi / sides) + my
480 vertices.append((cosine,sine))
481 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
482 shader.bind()
483 shader.uniform_float("color", colour)
484 batch.draw(shader)
486 # Bottom right corner
487 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
488 vertices = [(mx,my)]
489 for i in range(sides+1):
490 if (12<=i<=16):
491 if mx < area_width:
492 cosine = radius * cos(i * 2 * pi / sides) + mx
493 sine = radius * sin(i * 2 * pi / sides) + my
494 vertices.append((cosine,sine))
495 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
496 shader.bind()
497 shader.uniform_float("color", colour)
498 batch.draw(shader)
500 # prepare drawing all edges in one batch
501 vertices = []
502 indices = []
503 id_last = 0
505 # Left edge
506 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
507 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
508 if m1x < area_width and m2x < area_width:
509 vertices.extend([(m2x-radius,m2y), (m2x,m2y),
510 (m1x,m1y), (m1x-radius,m1y)])
511 indices.extend([(id_last, id_last+1, id_last+3),
512 (id_last+3, id_last+1, id_last+2)])
513 id_last += 4
515 # Top edge
516 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
517 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
518 m1x = min(m1x, area_width)
519 m2x = min(m2x, area_width)
520 vertices.extend([(m1x,m1y), (m2x,m1y),
521 (m2x,m1y+radius), (m1x,m1y+radius)])
522 indices.extend([(id_last, id_last+1, id_last+3),
523 (id_last+3, id_last+1, id_last+2)])
524 id_last += 4
526 # Right edge
527 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
528 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
529 if m1x < area_width and m2x < area_width:
530 vertices.extend([(m1x,m2y), (m1x+radius,m2y),
531 (m1x+radius,m1y), (m1x,m1y)])
532 indices.extend([(id_last, id_last+1, id_last+3),
533 (id_last+3, id_last+1, id_last+2)])
534 id_last += 4
536 # Bottom edge
537 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
538 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
539 m1x = min(m1x, area_width)
540 m2x = min(m2x, area_width)
541 vertices.extend([(m1x,m2y), (m2x,m2y),
542 (m2x,m1y-radius), (m1x,m1y-radius)])
543 indices.extend([(id_last, id_last+1, id_last+3),
544 (id_last+3, id_last+1, id_last+2)])
546 # now draw all edges in one batch
547 if len(vertices) != 0:
548 batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
549 shader.bind()
550 shader.uniform_float("color", colour)
551 batch.draw(shader)
553 def draw_callback_nodeoutline(self, context, mode):
554 if self.mouse_path:
556 bgl.glLineWidth(1)
557 bgl.glEnable(bgl.GL_BLEND)
558 bgl.glEnable(bgl.GL_LINE_SMOOTH)
559 bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
561 nodes, links = get_nodes_links(context)
563 shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
565 if mode == "LINK":
566 col_outer = (1.0, 0.2, 0.2, 0.4)
567 col_inner = (0.0, 0.0, 0.0, 0.5)
568 col_circle_inner = (0.3, 0.05, 0.05, 1.0)
569 elif mode == "LINKMENU":
570 col_outer = (0.4, 0.6, 1.0, 0.4)
571 col_inner = (0.0, 0.0, 0.0, 0.5)
572 col_circle_inner = (0.08, 0.15, .3, 1.0)
573 elif mode == "MIX":
574 col_outer = (0.2, 1.0, 0.2, 0.4)
575 col_inner = (0.0, 0.0, 0.0, 0.5)
576 col_circle_inner = (0.05, 0.3, 0.05, 1.0)
578 m1x = self.mouse_path[0][0]
579 m1y = self.mouse_path[0][1]
580 m2x = self.mouse_path[-1][0]
581 m2y = self.mouse_path[-1][1]
583 n1 = nodes[context.scene.NWLazySource]
584 n2 = nodes[context.scene.NWLazyTarget]
586 if n1 == n2:
587 col_outer = (0.4, 0.4, 0.4, 0.4)
588 col_inner = (0.0, 0.0, 0.0, 0.5)
589 col_circle_inner = (0.2, 0.2, 0.2, 1.0)
591 draw_rounded_node_border(shader, n1, radius=6, colour=col_outer) # outline
592 draw_rounded_node_border(shader, n1, radius=5, colour=col_inner) # inner
593 draw_rounded_node_border(shader, n2, radius=6, colour=col_outer) # outline
594 draw_rounded_node_border(shader, n2, radius=5, colour=col_inner) # inner
596 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
597 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
599 # circle outline
600 draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
601 draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
603 # circle inner
604 draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
605 draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
607 bgl.glDisable(bgl.GL_BLEND)
608 bgl.glDisable(bgl.GL_LINE_SMOOTH)
609 def get_active_tree(context):
610 tree = context.space_data.node_tree
611 path = []
612 # Get nodes from currently edited tree.
613 # If user is editing a group, space_data.node_tree is still the base level (outside group).
614 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
615 # the same as context.active_node, the user is in a group.
616 # Check recursively until we find the real active node_tree:
617 if tree.nodes.active:
618 while tree.nodes.active != context.active_node:
619 tree = tree.nodes.active.node_tree
620 path.append(tree)
621 return tree, path
623 def get_nodes_links(context):
624 tree, path = get_active_tree(context)
625 return tree.nodes, tree.links
627 def is_viewer_socket(socket):
628 # checks if a internal socket is a valid viewer socket
629 return socket.name == viewer_socket_name and socket.NWViewerSocket
631 def get_internal_socket(socket):
632 #get the internal socket from a socket inside or outside the group
633 node = socket.node
634 if node.type == 'GROUP_OUTPUT':
635 source_iterator = node.inputs
636 iterator = node.id_data.outputs
637 elif node.type == 'GROUP_INPUT':
638 source_iterator = node.outputs
639 iterator = node.id_data.inputs
640 elif hasattr(node, "node_tree"):
641 if socket.is_output:
642 source_iterator = node.outputs
643 iterator = node.node_tree.outputs
644 else:
645 source_iterator = node.inputs
646 iterator = node.node_tree.inputs
647 else:
648 return None
650 for i, s in enumerate(source_iterator):
651 if s == socket:
652 break
653 return iterator[i]
655 def is_viewer_link(link, output_node):
656 if link.to_node == output_node and link.to_socket == output_node.inputs[0]:
657 return True
658 if link.to_node.type == 'GROUP_OUTPUT':
659 socket = get_internal_socket(link.to_socket)
660 if is_viewer_socket(socket):
661 return True
662 return False
664 def get_group_output_node(tree):
665 for node in tree.nodes:
666 if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
667 return node
669 def get_output_location(tree):
670 # get right-most location
671 sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
672 max_xloc_node = sorted_by_xloc[-1]
674 # get average y location
675 sum_yloc = 0
676 for node in tree.nodes:
677 sum_yloc += node.location.y
679 loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
680 loc_y = sum_yloc / len(tree.nodes)
681 return loc_x, loc_y
683 # Principled prefs
684 class NWPrincipledPreferences(bpy.types.PropertyGroup):
685 base_color: StringProperty(
686 name='Base Color',
687 default='diffuse diff albedo base col color',
688 description='Naming Components for Base Color maps')
689 sss_color: StringProperty(
690 name='Subsurface Color',
691 default='sss subsurface',
692 description='Naming Components for Subsurface Color maps')
693 metallic: StringProperty(
694 name='Metallic',
695 default='metallic metalness metal mtl',
696 description='Naming Components for metallness maps')
697 specular: StringProperty(
698 name='Specular',
699 default='specularity specular spec spc',
700 description='Naming Components for Specular maps')
701 normal: StringProperty(
702 name='Normal',
703 default='normal nor nrm nrml norm',
704 description='Naming Components for Normal maps')
705 bump: StringProperty(
706 name='Bump',
707 default='bump bmp',
708 description='Naming Components for bump maps')
709 rough: StringProperty(
710 name='Roughness',
711 default='roughness rough rgh',
712 description='Naming Components for roughness maps')
713 gloss: StringProperty(
714 name='Gloss',
715 default='gloss glossy glossiness',
716 description='Naming Components for glossy maps')
717 displacement: StringProperty(
718 name='Displacement',
719 default='displacement displace disp dsp height heightmap',
720 description='Naming Components for displacement maps')
721 transmission: StringProperty(
722 name='Transmission',
723 default='transmission transparency',
724 description='Naming Components for transmission maps')
725 emission: StringProperty(
726 name='Emission',
727 default='emission emissive emit',
728 description='Naming Components for emission maps')
729 alpha: StringProperty(
730 name='Alpha',
731 default='alpha opacity',
732 description='Naming Components for alpha maps')
733 ambient_occlusion: StringProperty(
734 name='Ambient Occlusion',
735 default='ao ambient occlusion',
736 description='Naming Components for AO maps')
738 # Addon prefs
739 class NWNodeWrangler(bpy.types.AddonPreferences):
740 bl_idname = __name__
742 merge_hide: EnumProperty(
743 name="Hide Mix nodes",
744 items=(
745 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
746 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
747 ("NEVER", "Never", "Never collapse the new merge nodes")
749 default='NON_SHADER',
750 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
751 merge_position: EnumProperty(
752 name="Mix Node Position",
753 items=(
754 ("CENTER", "Center", "Place the Mix node between the two nodes"),
755 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
757 default='CENTER',
758 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
760 show_hotkey_list: BoolProperty(
761 name="Show Hotkey List",
762 default=False,
763 description="Expand this box into a list of all the hotkeys for functions in this addon"
765 hotkey_list_filter: StringProperty(
766 name=" Filter by Name",
767 default="",
768 description="Show only hotkeys that have this text in their name",
769 options={'TEXTEDIT_UPDATE'}
771 show_principled_lists: BoolProperty(
772 name="Show Principled naming tags",
773 default=False,
774 description="Expand this box into a list of all naming tags for principled texture setup"
776 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
778 def draw(self, context):
779 layout = self.layout
780 col = layout.column()
781 col.prop(self, "merge_position")
782 col.prop(self, "merge_hide")
784 box = layout.box()
785 col = box.column(align=True)
786 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
787 if self.show_principled_lists:
788 tags = self.principled_tags
790 col.prop(tags, "base_color")
791 col.prop(tags, "sss_color")
792 col.prop(tags, "metallic")
793 col.prop(tags, "specular")
794 col.prop(tags, "rough")
795 col.prop(tags, "gloss")
796 col.prop(tags, "normal")
797 col.prop(tags, "bump")
798 col.prop(tags, "displacement")
799 col.prop(tags, "transmission")
800 col.prop(tags, "emission")
801 col.prop(tags, "alpha")
802 col.prop(tags, "ambient_occlusion")
804 box = layout.box()
805 col = box.column(align=True)
806 hotkey_button_name = "Show Hotkey List"
807 if self.show_hotkey_list:
808 hotkey_button_name = "Hide Hotkey List"
809 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
810 if self.show_hotkey_list:
811 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
812 col.separator()
813 for hotkey in kmi_defs:
814 if hotkey[7]:
815 hotkey_name = hotkey[7]
817 if self.hotkey_list_filter.lower() in hotkey_name.lower():
818 row = col.row(align=True)
819 row.label(text=hotkey_name)
820 keystr = nice_hotkey_name(hotkey[1])
821 if hotkey[4]:
822 keystr = "Shift " + keystr
823 if hotkey[5]:
824 keystr = "Alt " + keystr
825 if hotkey[3]:
826 keystr = "Ctrl " + keystr
827 row.label(text=keystr)
831 def nw_check(context):
832 space = context.space_data
833 editor_is_valid = space.type == 'NODE_EDITOR'
835 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
836 tree_is_valid = space.node_tree is not None and space.tree_type in valid_trees
838 return editor_is_valid and tree_is_valid
840 class NWBase:
841 @classmethod
842 def poll(cls, context):
843 return nw_check(context)
846 # OPERATORS
847 class NWLazyMix(Operator, NWBase):
848 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
849 bl_idname = "node.nw_lazy_mix"
850 bl_label = "Mix Nodes"
851 bl_options = {'REGISTER', 'UNDO'}
853 def modal(self, context, event):
854 context.area.tag_redraw()
855 nodes, links = get_nodes_links(context)
856 cont = True
858 start_pos = [event.mouse_region_x, event.mouse_region_y]
860 node1 = None
861 if not context.scene.NWBusyDrawing:
862 node1 = node_at_pos(nodes, context, event)
863 if node1:
864 context.scene.NWBusyDrawing = node1.name
865 else:
866 if context.scene.NWBusyDrawing != 'STOP':
867 node1 = nodes[context.scene.NWBusyDrawing]
869 context.scene.NWLazySource = node1.name
870 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
872 if event.type == 'MOUSEMOVE':
873 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
875 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
876 end_pos = [event.mouse_region_x, event.mouse_region_y]
877 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
879 node2 = None
880 node2 = node_at_pos(nodes, context, event)
881 if node2:
882 context.scene.NWBusyDrawing = node2.name
884 if node1 == node2:
885 cont = False
887 if cont:
888 if node1 and node2:
889 for node in nodes:
890 node.select = False
891 node1.select = True
892 node2.select = True
894 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
896 context.scene.NWBusyDrawing = ""
897 return {'FINISHED'}
899 elif event.type == 'ESC':
900 print('cancelled')
901 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
902 return {'CANCELLED'}
904 return {'RUNNING_MODAL'}
906 def invoke(self, context, event):
907 if context.area.type == 'NODE_EDITOR':
908 # the arguments we pass the the callback
909 args = (self, context, 'MIX')
910 # Add the region OpenGL drawing callback
911 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
912 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
914 self.mouse_path = []
916 context.window_manager.modal_handler_add(self)
917 return {'RUNNING_MODAL'}
918 else:
919 self.report({'WARNING'}, "View3D not found, cannot run operator")
920 return {'CANCELLED'}
923 class NWLazyConnect(Operator, NWBase):
924 """Connect two nodes without clicking a specific socket (automatically determined"""
925 bl_idname = "node.nw_lazy_connect"
926 bl_label = "Lazy Connect"
927 bl_options = {'REGISTER', 'UNDO'}
928 with_menu: BoolProperty()
930 def modal(self, context, event):
931 context.area.tag_redraw()
932 nodes, links = get_nodes_links(context)
933 cont = True
935 start_pos = [event.mouse_region_x, event.mouse_region_y]
937 node1 = None
938 if not context.scene.NWBusyDrawing:
939 node1 = node_at_pos(nodes, context, event)
940 if node1:
941 context.scene.NWBusyDrawing = node1.name
942 else:
943 if context.scene.NWBusyDrawing != 'STOP':
944 node1 = nodes[context.scene.NWBusyDrawing]
946 context.scene.NWLazySource = node1.name
947 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
949 if event.type == 'MOUSEMOVE':
950 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
952 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
953 end_pos = [event.mouse_region_x, event.mouse_region_y]
954 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
956 node2 = None
957 node2 = node_at_pos(nodes, context, event)
958 if node2:
959 context.scene.NWBusyDrawing = node2.name
961 if node1 == node2:
962 cont = False
964 link_success = False
965 if cont:
966 if node1 and node2:
967 original_sel = []
968 original_unsel = []
969 for node in nodes:
970 if node.select == True:
971 node.select = False
972 original_sel.append(node)
973 else:
974 original_unsel.append(node)
975 node1.select = True
976 node2.select = True
978 #link_success = autolink(node1, node2, links)
979 if self.with_menu:
980 if len(node1.outputs) > 1 and node2.inputs:
981 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
982 elif len(node1.outputs) == 1:
983 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
984 else:
985 link_success = autolink(node1, node2, links)
987 for node in original_sel:
988 node.select = True
989 for node in original_unsel:
990 node.select = False
992 if link_success:
993 force_update(context)
994 context.scene.NWBusyDrawing = ""
995 return {'FINISHED'}
997 elif event.type == 'ESC':
998 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
999 return {'CANCELLED'}
1001 return {'RUNNING_MODAL'}
1003 def invoke(self, context, event):
1004 if context.area.type == 'NODE_EDITOR':
1005 nodes, links = get_nodes_links(context)
1006 node = node_at_pos(nodes, context, event)
1007 if node:
1008 context.scene.NWBusyDrawing = node.name
1010 # the arguments we pass the the callback
1011 mode = "LINK"
1012 if self.with_menu:
1013 mode = "LINKMENU"
1014 args = (self, context, mode)
1015 # Add the region OpenGL drawing callback
1016 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1017 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1019 self.mouse_path = []
1021 context.window_manager.modal_handler_add(self)
1022 return {'RUNNING_MODAL'}
1023 else:
1024 self.report({'WARNING'}, "View3D not found, cannot run operator")
1025 return {'CANCELLED'}
1028 class NWDeleteUnused(Operator, NWBase):
1029 """Delete all nodes whose output is not used"""
1030 bl_idname = 'node.nw_del_unused'
1031 bl_label = 'Delete Unused Nodes'
1032 bl_options = {'REGISTER', 'UNDO'}
1034 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1035 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1037 def is_unused_node(self, node):
1038 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1039 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1040 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1041 if node.type in end_types:
1042 return False
1044 for output in node.outputs:
1045 if output.links:
1046 return False
1047 return True
1049 @classmethod
1050 def poll(cls, context):
1051 valid = False
1052 if nw_check(context):
1053 if context.space_data.node_tree.nodes:
1054 valid = True
1055 return valid
1057 def execute(self, context):
1058 nodes, links = get_nodes_links(context)
1060 # Store selection
1061 selection = []
1062 for node in nodes:
1063 if node.select == True:
1064 selection.append(node.name)
1066 for node in nodes:
1067 node.select = False
1069 deleted_nodes = []
1070 temp_deleted_nodes = []
1071 del_unused_iterations = len(nodes)
1072 for it in range(0, del_unused_iterations):
1073 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1074 for node in nodes:
1075 if self.is_unused_node(node):
1076 node.select = True
1077 deleted_nodes.append(node.name)
1078 bpy.ops.node.delete()
1080 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1081 break
1083 if self.delete_frames:
1084 repeat = True
1085 while repeat:
1086 frames_in_use = []
1087 frames = []
1088 repeat = False
1089 for node in nodes:
1090 if node.parent:
1091 frames_in_use.append(node.parent)
1092 for node in nodes:
1093 if node.type == 'FRAME' and node not in frames_in_use:
1094 frames.append(node)
1095 if node.parent:
1096 repeat = True # repeat for nested frames
1097 for node in frames:
1098 if node not in frames_in_use:
1099 node.select = True
1100 deleted_nodes.append(node.name)
1101 bpy.ops.node.delete()
1103 if self.delete_muted:
1104 for node in nodes:
1105 if node.mute:
1106 node.select = True
1107 deleted_nodes.append(node.name)
1108 bpy.ops.node.delete_reconnect()
1110 # get unique list of deleted nodes (iterations would count the same node more than once)
1111 deleted_nodes = list(set(deleted_nodes))
1112 for n in deleted_nodes:
1113 self.report({'INFO'}, "Node " + n + " deleted")
1114 num_deleted = len(deleted_nodes)
1115 n = ' node'
1116 if num_deleted > 1:
1117 n += 's'
1118 if num_deleted:
1119 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1120 else:
1121 self.report({'INFO'}, "Nothing deleted")
1123 # Restore selection
1124 nodes, links = get_nodes_links(context)
1125 for node in nodes:
1126 if node.name in selection:
1127 node.select = True
1128 return {'FINISHED'}
1130 def invoke(self, context, event):
1131 return context.window_manager.invoke_confirm(self, event)
1134 class NWSwapLinks(Operator, NWBase):
1135 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1136 bl_idname = 'node.nw_swap_links'
1137 bl_label = 'Swap Links'
1138 bl_options = {'REGISTER', 'UNDO'}
1140 @classmethod
1141 def poll(cls, context):
1142 valid = False
1143 if nw_check(context):
1144 if context.selected_nodes:
1145 valid = len(context.selected_nodes) <= 2
1146 return valid
1148 def execute(self, context):
1149 nodes, links = get_nodes_links(context)
1150 selected_nodes = context.selected_nodes
1151 n1 = selected_nodes[0]
1153 # Swap outputs
1154 if len(selected_nodes) == 2:
1155 n2 = selected_nodes[1]
1156 if n1.outputs and n2.outputs:
1157 n1_outputs = []
1158 n2_outputs = []
1160 out_index = 0
1161 for output in n1.outputs:
1162 if output.links:
1163 for link in output.links:
1164 n1_outputs.append([out_index, link.to_socket])
1165 links.remove(link)
1166 out_index += 1
1168 out_index = 0
1169 for output in n2.outputs:
1170 if output.links:
1171 for link in output.links:
1172 n2_outputs.append([out_index, link.to_socket])
1173 links.remove(link)
1174 out_index += 1
1176 for connection in n1_outputs:
1177 try:
1178 links.new(n2.outputs[connection[0]], connection[1])
1179 except:
1180 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1181 for connection in n2_outputs:
1182 try:
1183 links.new(n1.outputs[connection[0]], connection[1])
1184 except:
1185 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1186 else:
1187 if n1.outputs or n2.outputs:
1188 self.report({'WARNING'}, "One of the nodes has no outputs!")
1189 else:
1190 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1192 # Swap Inputs
1193 elif len(selected_nodes) == 1:
1194 if n1.inputs and n1.inputs[0].is_multi_input:
1195 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1196 return {'FINISHED'}
1197 if n1.inputs:
1198 types = []
1200 for i1 in n1.inputs:
1201 if i1.is_linked and not i1.is_multi_input:
1202 similar_types = 0
1203 for i2 in n1.inputs:
1204 if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
1205 similar_types += 1
1206 types.append ([i1, similar_types, i])
1207 i += 1
1208 types.sort(key=lambda k: k[1], reverse=True)
1210 if types:
1211 t = types[0]
1212 if t[1] == 2:
1213 for i2 in n1.inputs:
1214 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1215 pair = [t[0], i2]
1216 i1f = pair[0].links[0].from_socket
1217 i1t = pair[0].links[0].to_socket
1218 i2f = pair[1].links[0].from_socket
1219 i2t = pair[1].links[0].to_socket
1220 links.new(i1f, i2t)
1221 links.new(i2f, i1t)
1222 if t[1] == 1:
1223 if len(types) == 1:
1224 fs = t[0].links[0].from_socket
1225 i = t[2]
1226 links.remove(t[0].links[0])
1227 if i+1 == len(n1.inputs):
1228 i = -1
1229 i += 1
1230 while n1.inputs[i].is_linked:
1231 i += 1
1232 links.new(fs, n1.inputs[i])
1233 elif len(types) == 2:
1234 i1f = types[0][0].links[0].from_socket
1235 i1t = types[0][0].links[0].to_socket
1236 i2f = types[1][0].links[0].from_socket
1237 i2t = types[1][0].links[0].to_socket
1238 links.new(i1f, i2t)
1239 links.new(i2f, i1t)
1241 else:
1242 self.report({'WARNING'}, "This node has no input connections to swap!")
1243 else:
1244 self.report({'WARNING'}, "This node has no inputs to swap!")
1246 force_update(context)
1247 return {'FINISHED'}
1250 class NWResetBG(Operator, NWBase):
1251 """Reset the zoom and position of the background image"""
1252 bl_idname = 'node.nw_bg_reset'
1253 bl_label = 'Reset Backdrop'
1254 bl_options = {'REGISTER', 'UNDO'}
1256 @classmethod
1257 def poll(cls, context):
1258 valid = False
1259 if nw_check(context):
1260 snode = context.space_data
1261 valid = snode.tree_type == 'CompositorNodeTree'
1262 return valid
1264 def execute(self, context):
1265 context.space_data.backdrop_zoom = 1
1266 context.space_data.backdrop_offset[0] = 0
1267 context.space_data.backdrop_offset[1] = 0
1268 return {'FINISHED'}
1271 class NWAddAttrNode(Operator, NWBase):
1272 """Add an Attribute node with this name"""
1273 bl_idname = 'node.nw_add_attr_node'
1274 bl_label = 'Add UV map'
1275 bl_options = {'REGISTER', 'UNDO'}
1277 attr_name: StringProperty()
1279 def execute(self, context):
1280 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1281 nodes, links = get_nodes_links(context)
1282 nodes.active.attribute_name = self.attr_name
1283 return {'FINISHED'}
1285 class NWPreviewNode(Operator, NWBase):
1286 bl_idname = "node.nw_preview_node"
1287 bl_label = "Preview Node"
1288 bl_description = "Connect active node to the Node Group output or the Material Output"
1289 bl_options = {'REGISTER', 'UNDO'}
1291 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1292 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1293 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1294 run_in_geometry_nodes: BoolProperty(default=True)
1296 def __init__(self):
1297 self.shader_output_type = ""
1298 self.shader_output_ident = ""
1300 @classmethod
1301 def poll(cls, context):
1302 if nw_check(context):
1303 space = context.space_data
1304 if space.tree_type == 'ShaderNodeTree' or space.tree_type == 'GeometryNodeTree':
1305 if context.active_node:
1306 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1307 return True
1308 else:
1309 return True
1310 return False
1312 def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
1313 #check if a viewer output already exists in a node group otherwise create
1314 if hasattr(node, "node_tree"):
1315 index = None
1316 if len(node.node_tree.outputs):
1317 free_socket = None
1318 for i, socket in enumerate(node.node_tree.outputs):
1319 if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
1320 #if viewer output is already used but leads to the same socket we can still use it
1321 is_used = self.is_socket_used_other_mats(socket)
1322 if is_used:
1323 if connect_socket == None:
1324 continue
1325 groupout = get_group_output_node(node.node_tree)
1326 groupout_input = groupout.inputs[i]
1327 links = groupout_input.links
1328 if connect_socket not in [link.from_socket for link in links]:
1329 continue
1330 index=i
1331 break
1332 if not free_socket:
1333 free_socket = i
1334 if not index and free_socket:
1335 index = free_socket
1337 if not index:
1338 #create viewer socket
1339 node.node_tree.outputs.new(socket_type, viewer_socket_name)
1340 index = len(node.node_tree.outputs) - 1
1341 node.node_tree.outputs[index].NWViewerSocket = True
1342 return index
1344 def init_shader_variables(self, space, shader_type):
1345 if shader_type == 'OBJECT':
1346 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1347 self.shader_output_type = "OUTPUT_MATERIAL"
1348 self.shader_output_ident = "ShaderNodeOutputMaterial"
1349 else:
1350 self.shader_output_type = "OUTPUT_LIGHT"
1351 self.shader_output_ident = "ShaderNodeOutputLight"
1353 elif shader_type == 'WORLD':
1354 self.shader_output_type = "OUTPUT_WORLD"
1355 self.shader_output_ident = "ShaderNodeOutputWorld"
1357 def get_shader_output_node(self, tree):
1358 for node in tree.nodes:
1359 if node.type == self.shader_output_type and node.is_active_output == True:
1360 return node
1362 @classmethod
1363 def ensure_group_output(cls, tree):
1364 #check if a group output node exists otherwise create
1365 groupout = get_group_output_node(tree)
1366 if not groupout:
1367 groupout = tree.nodes.new('NodeGroupOutput')
1368 loc_x, loc_y = get_output_location(tree)
1369 groupout.location.x = loc_x
1370 groupout.location.y = loc_y
1371 groupout.select = False
1372 # So that we don't keep on adding new group outputs
1373 groupout.is_active_output = True
1374 return groupout
1376 @classmethod
1377 def search_sockets(cls, node, sockets, index=None):
1378 # recursively scan nodes for viewer sockets and store in list
1379 for i, input_socket in enumerate(node.inputs):
1380 if index and i != index:
1381 continue
1382 if len(input_socket.links):
1383 link = input_socket.links[0]
1384 next_node = link.from_node
1385 external_socket = link.from_socket
1386 if hasattr(next_node, "node_tree"):
1387 for socket_index, s in enumerate(next_node.outputs):
1388 if s == external_socket:
1389 break
1390 socket = next_node.node_tree.outputs[socket_index]
1391 if is_viewer_socket(socket) and socket not in sockets:
1392 sockets.append(socket)
1393 #continue search inside of node group but restrict socket to where we came from
1394 groupout = get_group_output_node(next_node.node_tree)
1395 cls.search_sockets(groupout, sockets, index=socket_index)
1397 @classmethod
1398 def scan_nodes(cls, tree, sockets):
1399 # get all viewer sockets in a material tree
1400 for node in tree.nodes:
1401 if hasattr(node, "node_tree"):
1402 for socket in node.node_tree.outputs:
1403 if is_viewer_socket(socket) and (socket not in sockets):
1404 sockets.append(socket)
1405 cls.scan_nodes(node.node_tree, sockets)
1407 def link_leads_to_used_socket(self, link):
1408 #return True if link leads to a socket that is already used in this material
1409 socket = get_internal_socket(link.to_socket)
1410 return (socket and self.is_socket_used_active_mat(socket))
1412 def is_socket_used_active_mat(self, socket):
1413 #ensure used sockets in active material is calculated and check given socket
1414 if not hasattr(self, "used_viewer_sockets_active_mat"):
1415 self.used_viewer_sockets_active_mat = []
1416 materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
1417 if materialout:
1418 self.search_sockets(materialout, self.used_viewer_sockets_active_mat)
1419 return socket in self.used_viewer_sockets_active_mat
1421 def is_socket_used_other_mats(self, socket):
1422 #ensure used sockets in other materials are calculated and check given socket
1423 if not hasattr(self, "used_viewer_sockets_other_mats"):
1424 self.used_viewer_sockets_other_mats = []
1425 for mat in bpy.data.materials:
1426 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
1427 continue
1428 # get viewer node
1429 materialout = self.get_shader_output_node(mat.node_tree)
1430 if materialout:
1431 self.search_sockets(materialout, self.used_viewer_sockets_other_mats)
1432 return socket in self.used_viewer_sockets_other_mats
1434 def invoke(self, context, event):
1435 space = context.space_data
1436 # Ignore operator when running in wrong context.
1437 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
1438 return {'PASS_THROUGH'}
1440 shader_type = space.shader_type
1441 self.init_shader_variables(space, shader_type)
1442 mlocx = event.mouse_region_x
1443 mlocy = event.mouse_region_y
1444 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
1445 if 'FINISHED' in select_node: # only run if mouse click is on a node
1446 active_tree, path_to_tree = get_active_tree(context)
1447 nodes, links = active_tree.nodes, active_tree.links
1448 base_node_tree = space.node_tree
1449 active = nodes.active
1451 # For geometry node trees we just connect to the group output
1452 if space.tree_type == "GeometryNodeTree":
1453 valid = False
1454 if active:
1455 for out in active.outputs:
1456 if is_visible_socket(out):
1457 valid = True
1458 break
1459 # Exit early
1460 if not valid:
1461 return {'FINISHED'}
1463 delete_sockets = []
1465 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1466 self.scan_nodes(base_node_tree, delete_sockets)
1468 # Find (or create if needed) the output of this node tree
1469 geometryoutput = self.ensure_group_output(base_node_tree)
1471 # Analyze outputs, make links
1472 out_i = None
1473 valid_outputs = []
1474 for i, out in enumerate(active.outputs):
1475 if is_visible_socket(out) and out.type == 'GEOMETRY':
1476 valid_outputs.append(i)
1477 if valid_outputs:
1478 out_i = valid_outputs[0] # Start index of node's outputs
1479 for i, valid_i in enumerate(valid_outputs):
1480 for out_link in active.outputs[valid_i].links:
1481 if is_viewer_link(out_link, geometryoutput):
1482 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1483 if i < len(valid_outputs) - 1:
1484 out_i = valid_outputs[i + 1]
1485 else:
1486 out_i = valid_outputs[0]
1488 make_links = [] # store sockets for new links
1489 if active.outputs:
1490 # If there is no 'GEOMETRY' output type - We can't preview the node
1491 if out_i is None:
1492 return {'FINISHED'}
1493 socket_type = 'GEOMETRY'
1494 # Find an input socket of the output of type geometry
1495 geometryoutindex = None
1496 for i,inp in enumerate(geometryoutput.inputs):
1497 if inp.type == socket_type:
1498 geometryoutindex = i
1499 break
1500 if geometryoutindex is None:
1501 # Create geometry socket
1502 geometryoutput.inputs.new(socket_type, 'Geometry')
1503 geometryoutindex = len(geometryoutput.inputs) - 1
1505 make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
1506 output_socket = geometryoutput.inputs[geometryoutindex]
1507 for li_from, li_to in make_links:
1508 base_node_tree.links.new(li_from, li_to)
1509 tree = base_node_tree
1510 link_end = output_socket
1511 while tree.nodes.active != active:
1512 node = tree.nodes.active
1513 index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1514 link_start = node.outputs[index]
1515 node_socket = node.node_tree.outputs[index]
1516 if node_socket in delete_sockets:
1517 delete_sockets.remove(node_socket)
1518 tree.links.new(link_start, link_end)
1519 # Iterate
1520 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1521 tree = tree.nodes.active.node_tree
1522 tree.links.new(active.outputs[out_i], link_end)
1524 # Delete sockets
1525 for socket in delete_sockets:
1526 tree = socket.id_data
1527 tree.outputs.remove(socket)
1529 nodes.active = active
1530 active.select = True
1531 force_update(context)
1532 return {'FINISHED'}
1535 # What follows is code for the shader editor
1536 output_types = [x.nodetype for x in
1537 get_nodes_from_category('Output', context)]
1538 valid = False
1539 if active:
1540 if active.rna_type.identifier not in output_types:
1541 for out in active.outputs:
1542 if is_visible_socket(out):
1543 valid = True
1544 break
1545 if valid:
1546 # get material_output node
1547 materialout = None # placeholder node
1548 delete_sockets = []
1550 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1551 self.scan_nodes(base_node_tree, delete_sockets)
1553 materialout = self.get_shader_output_node(base_node_tree)
1554 if not materialout:
1555 materialout = base_node_tree.nodes.new(self.shader_output_ident)
1556 materialout.location = get_output_location(base_node_tree)
1557 materialout.select = False
1558 # Analyze outputs
1559 out_i = None
1560 valid_outputs = []
1561 for i, out in enumerate(active.outputs):
1562 if is_visible_socket(out):
1563 valid_outputs.append(i)
1564 if valid_outputs:
1565 out_i = valid_outputs[0] # Start index of node's outputs
1566 for i, valid_i in enumerate(valid_outputs):
1567 for out_link in active.outputs[valid_i].links:
1568 if is_viewer_link(out_link, materialout):
1569 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1570 if i < len(valid_outputs) - 1:
1571 out_i = valid_outputs[i + 1]
1572 else:
1573 out_i = valid_outputs[0]
1575 make_links = [] # store sockets for new links
1576 if active.outputs:
1577 socket_type = 'NodeSocketShader'
1578 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
1579 make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
1580 output_socket = materialout.inputs[materialout_index]
1581 for li_from, li_to in make_links:
1582 base_node_tree.links.new(li_from, li_to)
1584 # Create links through node groups until we reach the active node
1585 tree = base_node_tree
1586 link_end = output_socket
1587 while tree.nodes.active != active:
1588 node = tree.nodes.active
1589 index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1590 link_start = node.outputs[index]
1591 node_socket = node.node_tree.outputs[index]
1592 if node_socket in delete_sockets:
1593 delete_sockets.remove(node_socket)
1594 tree.links.new(link_start, link_end)
1595 # Iterate
1596 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1597 tree = tree.nodes.active.node_tree
1598 tree.links.new(active.outputs[out_i], link_end)
1600 # Delete sockets
1601 for socket in delete_sockets:
1602 if not self.is_socket_used_other_mats(socket):
1603 tree = socket.id_data
1604 tree.outputs.remove(socket)
1606 nodes.active = active
1607 active.select = True
1609 force_update(context)
1611 return {'FINISHED'}
1612 else:
1613 return {'CANCELLED'}
1616 class NWFrameSelected(Operator, NWBase):
1617 bl_idname = "node.nw_frame_selected"
1618 bl_label = "Frame Selected"
1619 bl_description = "Add a frame node and parent the selected nodes to it"
1620 bl_options = {'REGISTER', 'UNDO'}
1622 label_prop: StringProperty(
1623 name='Label',
1624 description='The visual name of the frame node',
1625 default=' '
1627 use_custom_color_prop: BoolProperty(
1628 name="Custom Color",
1629 description="Use custom color for the frame node",
1630 default=False
1632 color_prop: FloatVectorProperty(
1633 name="Color",
1634 description="The color of the frame node",
1635 default=(0.604, 0.604, 0.604),
1636 min=0, max=1, step=1, precision=3,
1637 subtype='COLOR_GAMMA', size=3
1640 def draw(self, context):
1641 layout = self.layout
1642 layout.prop(self, 'label_prop')
1643 layout.prop(self, 'use_custom_color_prop')
1644 col = layout.column()
1645 col.active = self.use_custom_color_prop
1646 col.prop(self, 'color_prop', text="")
1648 def execute(self, context):
1649 nodes, links = get_nodes_links(context)
1650 selected = []
1651 for node in nodes:
1652 if node.select == True:
1653 selected.append(node)
1655 bpy.ops.node.add_node(type='NodeFrame')
1656 frm = nodes.active
1657 frm.label = self.label_prop
1658 frm.use_custom_color = self.use_custom_color_prop
1659 frm.color = self.color_prop
1661 for node in selected:
1662 node.parent = frm
1664 return {'FINISHED'}
1667 class NWReloadImages(Operator):
1668 bl_idname = "node.nw_reload_images"
1669 bl_label = "Reload Images"
1670 bl_description = "Update all the image nodes to match their files on disk"
1672 @classmethod
1673 def poll(cls, context):
1674 valid = False
1675 if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
1676 if context.active_node is not None:
1677 for out in context.active_node.outputs:
1678 if is_visible_socket(out):
1679 valid = True
1680 break
1681 return valid
1683 def execute(self, context):
1684 nodes, links = get_nodes_links(context)
1685 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1686 num_reloaded = 0
1687 for node in nodes:
1688 if node.type in image_types:
1689 if node.type == "TEXTURE":
1690 if node.texture: # node has texture assigned
1691 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1692 if node.texture.image: # texture has image assigned
1693 node.texture.image.reload()
1694 num_reloaded += 1
1695 else:
1696 if node.image:
1697 node.image.reload()
1698 num_reloaded += 1
1700 if num_reloaded:
1701 self.report({'INFO'}, "Reloaded images")
1702 print("Reloaded " + str(num_reloaded) + " images")
1703 force_update(context)
1704 return {'FINISHED'}
1705 else:
1706 self.report({'WARNING'}, "No images found to reload in this node tree")
1707 return {'CANCELLED'}
1710 class NWSwitchNodeType(Operator, NWBase):
1711 """Switch type of selected nodes """
1712 bl_idname = "node.nw_swtch_node_type"
1713 bl_label = "Switch Node Type"
1714 bl_options = {'REGISTER', 'UNDO'}
1716 to_type: StringProperty(
1717 name="Switch to type",
1718 default = '',
1721 def execute(self, context):
1722 to_type = self.to_type
1723 if len(to_type) == 0:
1724 return {'CANCELLED'}
1726 nodes, links = get_nodes_links(context)
1727 # Those types of nodes will not swap.
1728 src_excludes = ('NodeFrame')
1729 # Those attributes of nodes will be copied if possible
1730 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1731 'show_options', 'show_preview', 'show_texture',
1732 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1734 selected = [n for n in nodes if n.select]
1735 reselect = []
1736 for node in [n for n in selected if
1737 n.rna_type.identifier not in src_excludes and
1738 n.rna_type.identifier != to_type]:
1739 new_node = nodes.new(to_type)
1740 for attr in attrs_to_pass:
1741 if hasattr(node, attr) and hasattr(new_node, attr):
1742 setattr(new_node, attr, getattr(node, attr))
1743 # set image datablock of dst to image of src
1744 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1745 if node.image:
1746 new_node.image = node.image
1747 # Special cases
1748 if new_node.type == 'SWITCH':
1749 new_node.hide = True
1750 # Dictionaries: src_sockets and dst_sockets:
1751 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1752 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1753 # in 'INPUTS' and 'OUTPUTS':
1754 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1755 # socket entry:
1756 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1757 src_sockets = {
1758 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1759 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1761 dst_sockets = {
1762 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1763 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1765 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1766 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1767 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1768 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1769 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1770 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1771 # enumerate in inputs, then in outputs
1772 # find name, default value and links of socket
1773 for i, socket in enumerate(in_out):
1774 the_name = socket.name
1775 dval = None
1776 # Not every socket, especially in outputs has "default_value"
1777 if hasattr(socket, 'default_value'):
1778 dval = socket.default_value
1779 socket_links = []
1780 for lnk in socket.links:
1781 socket_links.append(lnk)
1782 # check type of socket to fill proper keys.
1783 for the_type in types_order_one:
1784 if socket.type == the_type:
1785 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1786 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1787 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1788 # Check which of the types in inputs/outputs is considered to be "main".
1789 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1790 for type_check in types_order_one:
1791 if sockets[in_out_name][type_check]:
1792 sockets[in_out_name]['MAIN'] = type_check
1793 break
1795 matches = {
1796 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1797 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1800 for inout, soctype in (
1801 ('INPUTS', 'MAIN',),
1802 ('INPUTS', 'SHADER',),
1803 ('INPUTS', 'RGBA',),
1804 ('INPUTS', 'VECTOR',),
1805 ('INPUTS', 'VALUE',),
1806 ('OUTPUTS', 'MAIN',),
1807 ('OUTPUTS', 'SHADER',),
1808 ('OUTPUTS', 'RGBA',),
1809 ('OUTPUTS', 'VECTOR',),
1810 ('OUTPUTS', 'VALUE',),
1812 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1813 if soctype == 'MAIN':
1814 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1815 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1816 else:
1817 sc = src_sockets[inout][soctype]
1818 dt = dst_sockets[inout][soctype]
1819 # start with 'dt' to determine number of possibilities.
1820 for i, soc in enumerate(dt):
1821 # if src main has enough entries - match them with dst main sockets by indexes.
1822 if len(sc) > i:
1823 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1824 # add 'VALUE_NAME' criterion to inputs.
1825 if inout == 'INPUTS' and soctype == 'VALUE':
1826 for s in sc:
1827 if s[2] == soc[2]: # if names match
1828 # append src (index, dval), dst (index, dval)
1829 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1831 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1832 # This creates better links when relinking textures.
1833 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1834 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1836 # Pass default values and RELINK:
1837 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1838 # INPUTS: Base on matches in proper order.
1839 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1840 # pass dvals
1841 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1842 new_node.inputs[dst_i].default_value = src_dval
1843 # Special case: switch to math
1844 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1845 new_node.type == 'MATH' and\
1846 tp == 'MAIN':
1847 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1848 new_node.inputs[dst_i].default_value = new_dst_dval
1849 if node.type == 'MIX_RGB':
1850 if node.blend_type in [o[0] for o in operations]:
1851 new_node.operation = node.blend_type
1852 # Special case: switch from math to some types
1853 if node.type == 'MATH' and\
1854 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1855 tp == 'MAIN':
1856 for i in range(3):
1857 new_node.inputs[dst_i].default_value[i] = src_dval
1858 if new_node.type == 'MIX_RGB':
1859 if node.operation in [t[0] for t in blend_types]:
1860 new_node.blend_type = node.operation
1861 # Set Fac of MIX_RGB to 1.0
1862 new_node.inputs[0].default_value = 1.0
1863 # make link only when dst matching input is not linked already.
1864 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1865 in_src_link = node.inputs[src_i].links[0]
1866 in_dst_socket = new_node.inputs[dst_i]
1867 links.new(in_src_link.from_socket, in_dst_socket)
1868 links.remove(in_src_link)
1869 # OUTPUTS: Base on matches in proper order.
1870 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1871 for out_src_link in node.outputs[src_i].links:
1872 out_dst_socket = new_node.outputs[dst_i]
1873 links.new(out_dst_socket, out_src_link.to_socket)
1874 # relink rest inputs if possible, no criteria
1875 for src_inp in node.inputs:
1876 for dst_inp in new_node.inputs:
1877 if src_inp.links and not dst_inp.links:
1878 src_link = src_inp.links[0]
1879 links.new(src_link.from_socket, dst_inp)
1880 links.remove(src_link)
1881 # relink rest outputs if possible, base on node kind if any left.
1882 for src_o in node.outputs:
1883 for out_src_link in src_o.links:
1884 for dst_o in new_node.outputs:
1885 if src_o.type == dst_o.type:
1886 links.new(dst_o, out_src_link.to_socket)
1887 # relink rest outputs no criteria if any left. Link all from first output.
1888 for src_o in node.outputs:
1889 for out_src_link in src_o.links:
1890 if new_node.outputs:
1891 links.new(new_node.outputs[0], out_src_link.to_socket)
1892 nodes.remove(node)
1893 force_update(context)
1894 return {'FINISHED'}
1897 class NWMergeNodes(Operator, NWBase):
1898 bl_idname = "node.nw_merge_nodes"
1899 bl_label = "Merge Nodes"
1900 bl_description = "Merge Selected Nodes"
1901 bl_options = {'REGISTER', 'UNDO'}
1903 mode: EnumProperty(
1904 name="mode",
1905 description="All possible blend types, boolean operations and math operations",
1906 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],
1908 merge_type: EnumProperty(
1909 name="merge type",
1910 description="Type of Merge to be used",
1911 items=(
1912 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1913 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1914 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
1915 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1916 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1917 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
1918 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
1922 # Check if the link connects to a node that is in selected_nodes
1923 # If not, then check recursively for each link in the nodes outputs.
1924 # If yes, return True. If the recursion stops without finding a node
1925 # in selected_nodes, it returns False. The depth is used to prevent
1926 # getting stuck in a loop because of an already present cycle.
1927 @staticmethod
1928 def link_creates_cycle(link, selected_nodes, depth=0)->bool:
1929 if depth > 255:
1930 # We're stuck in a cycle, but that cycle was already present,
1931 # so we return False.
1932 # NOTE: The number 255 is arbitrary, but seems to work well.
1933 return False
1934 node = link.to_node
1935 if node in selected_nodes:
1936 return True
1937 if not node.outputs:
1938 return False
1939 for output in node.outputs:
1940 if output.is_linked:
1941 for olink in output.links:
1942 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1):
1943 return True
1944 # None of the outputs found a node in selected_nodes, so there is no cycle.
1945 return False
1947 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
1948 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
1949 # be connected. The last one is assumed to be a multi input socket.
1950 # For convenience the node is returned.
1951 @staticmethod
1952 def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices):
1953 # The y-location of the last node
1954 loc_y = nodes_list[-1][2]
1955 if merge_position == 'CENTER':
1956 # Average the y-location
1957 for i in range(len(nodes_list)-1):
1958 loc_y += nodes_list[i][2]
1959 loc_y = loc_y/len(nodes_list)
1960 new_node = nodes.new(node_name)
1961 new_node.hide = do_hide
1962 new_node.location.x = loc_x
1963 new_node.location.y = loc_y
1964 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
1965 prev_links = []
1966 outputs_for_multi_input = []
1967 for i,node in enumerate(selected_nodes):
1968 node.select = False
1969 # Search for the first node which had output links that do not create
1970 # a cycle, which we can then reconnect afterwards.
1971 if prev_links == [] and node.outputs[0].is_linked:
1972 prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)]
1973 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
1974 # To get the placement to look right we need to reverse the order in which we connect the
1975 # outputs to the multi input socket.
1976 if i < len(socket_indices) - 1:
1977 ind = socket_indices[i]
1978 links.new(node.outputs[0], new_node.inputs[ind])
1979 else:
1980 outputs_for_multi_input.insert(0, node.outputs[0])
1981 if outputs_for_multi_input != []:
1982 ind = socket_indices[-1]
1983 for output in outputs_for_multi_input:
1984 links.new(output, new_node.inputs[ind])
1985 if prev_links != []:
1986 for link in prev_links:
1987 links.new(new_node.outputs[0], link.to_node.inputs[0])
1988 return new_node
1990 def execute(self, context):
1991 settings = context.preferences.addons[__name__].preferences
1992 merge_hide = settings.merge_hide
1993 merge_position = settings.merge_position # 'center' or 'bottom'
1995 do_hide = False
1996 do_hide_shader = False
1997 if merge_hide == 'ALWAYS':
1998 do_hide = True
1999 do_hide_shader = True
2000 elif merge_hide == 'NON_SHADER':
2001 do_hide = True
2003 tree_type = context.space_data.node_tree.type
2004 if tree_type == 'GEOMETRY':
2005 node_type = 'GeometryNode'
2006 if tree_type == 'COMPOSITING':
2007 node_type = 'CompositorNode'
2008 elif tree_type == 'SHADER':
2009 node_type = 'ShaderNode'
2010 elif tree_type == 'TEXTURE':
2011 node_type = 'TextureNode'
2012 nodes, links = get_nodes_links(context)
2013 mode = self.mode
2014 merge_type = self.merge_type
2015 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2016 # 'ZCOMBINE' works only if mode == 'MIX'
2017 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2018 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2019 merge_type = 'MIX'
2020 mode = 'MIX'
2021 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
2022 merge_type = 'AUTO'
2023 # The MixRGB node and math nodes used for geometry nodes are of type 'ShaderNode'
2024 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
2025 node_type = 'ShaderNode'
2026 selected_mix = [] # entry = [index, loc]
2027 selected_shader = [] # entry = [index, loc]
2028 selected_geometry = [] # entry = [index, loc]
2029 selected_math = [] # entry = [index, loc]
2030 selected_vector = [] # entry = [index, loc]
2031 selected_z = [] # entry = [index, loc]
2032 selected_alphaover = [] # entry = [index, loc]
2034 for i, node in enumerate(nodes):
2035 if node.select and node.outputs:
2036 if merge_type == 'AUTO':
2037 for (type, types_list, dst) in (
2038 ('SHADER', ('MIX', 'ADD'), selected_shader),
2039 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2040 ('RGBA', [t[0] for t in blend_types], selected_mix),
2041 ('VALUE', [t[0] for t in operations], selected_math),
2042 ('VECTOR', [], selected_vector),
2044 output = get_first_enabled_output(node)
2045 output_type = output.type
2046 valid_mode = mode in types_list
2047 # When mode is 'MIX' we have to cheat since the mix node is not used in
2048 # geometry nodes.
2049 if tree_type == 'GEOMETRY':
2050 if mode == 'MIX':
2051 if output_type == 'VALUE' and type == 'VALUE':
2052 valid_mode = True
2053 elif output_type == 'VECTOR' and type == 'VECTOR':
2054 valid_mode = True
2055 elif type == 'GEOMETRY':
2056 valid_mode = True
2057 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2058 # Cheat that output type is 'RGBA',
2059 # and that 'MIX' exists in math operations list.
2060 # This way when selected_mix list is analyzed:
2061 # Node data will be appended even though it doesn't meet requirements.
2062 elif output_type != 'SHADER' and mode == 'MIX':
2063 output_type = 'RGBA'
2064 valid_mode = True
2065 if output_type == type and valid_mode:
2066 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2067 else:
2068 for (type, types_list, dst) in (
2069 ('SHADER', ('MIX', 'ADD'), selected_shader),
2070 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2071 ('MIX', [t[0] for t in blend_types], selected_mix),
2072 ('MATH', [t[0] for t in operations], selected_math),
2073 ('ZCOMBINE', ('MIX', ), selected_z),
2074 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2076 if merge_type == type and mode in types_list:
2077 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2078 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2079 # use only 'Mix' nodes for merging.
2080 # For that we add selected_math list to selected_mix list and clear selected_math.
2081 if selected_mix and selected_math and merge_type == 'AUTO':
2082 selected_mix += selected_math
2083 selected_math = []
2084 for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]:
2085 if not nodes_list:
2086 continue
2087 count_before = len(nodes)
2088 # sort list by loc_x - reversed
2089 nodes_list.sort(key=lambda k: k[1], reverse=True)
2090 # get maximum loc_x
2091 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2092 nodes_list.sort(key=lambda k: k[2], reverse=True)
2094 # Change the node type for math nodes in a geometry node tree.
2095 if tree_type == 'GEOMETRY':
2096 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
2097 node_type = 'ShaderNode'
2098 if mode == 'MIX':
2099 mode = 'ADD'
2100 else:
2101 node_type = 'GeometryNode'
2102 if merge_position == 'CENTER':
2103 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)
2104 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2105 if do_hide:
2106 loc_y += 40
2107 else:
2108 loc_y += 80
2109 else:
2110 loc_y = nodes_list[len(nodes_list) - 1][2]
2111 offset_y = 100
2112 if not do_hide:
2113 offset_y = 200
2114 if nodes_list == selected_shader and not do_hide_shader:
2115 offset_y = 150.0
2116 the_range = len(nodes_list) - 1
2117 if len(nodes_list) == 1:
2118 the_range = 1
2119 was_multi = False
2120 for i in range(the_range):
2121 if nodes_list == selected_mix:
2122 add_type = node_type + 'MixRGB'
2123 add = nodes.new(add_type)
2124 add.blend_type = mode
2125 if mode != 'MIX':
2126 add.inputs[0].default_value = 1.0
2127 add.show_preview = False
2128 add.hide = do_hide
2129 if do_hide:
2130 loc_y = loc_y - 50
2131 first = 1
2132 second = 2
2133 add.width_hidden = 100.0
2134 elif nodes_list == selected_math:
2135 add_type = node_type + 'Math'
2136 add = nodes.new(add_type)
2137 add.operation = mode
2138 add.hide = do_hide
2139 if do_hide:
2140 loc_y = loc_y - 50
2141 first = 0
2142 second = 1
2143 add.width_hidden = 100.0
2144 elif nodes_list == selected_shader:
2145 if mode == 'MIX':
2146 add_type = node_type + 'MixShader'
2147 add = nodes.new(add_type)
2148 add.hide = do_hide_shader
2149 if do_hide_shader:
2150 loc_y = loc_y - 50
2151 first = 1
2152 second = 2
2153 add.width_hidden = 100.0
2154 elif mode == 'ADD':
2155 add_type = node_type + 'AddShader'
2156 add = nodes.new(add_type)
2157 add.hide = do_hide_shader
2158 if do_hide_shader:
2159 loc_y = loc_y - 50
2160 first = 0
2161 second = 1
2162 add.width_hidden = 100.0
2163 elif nodes_list == selected_geometry:
2164 if mode in ('JOIN', 'MIX'):
2165 add_type = node_type + 'JoinGeometry'
2166 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0])
2167 else:
2168 add_type = node_type + 'Boolean'
2169 indices = [0,1] if mode == 'DIFFERENCE' else [1]
2170 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices)
2171 add.operation = mode
2172 was_multi = True
2173 break
2174 elif nodes_list == selected_vector:
2175 add_type = node_type + 'VectorMath'
2176 add = nodes.new(add_type)
2177 add.operation = mode
2178 add.hide = do_hide
2179 if do_hide:
2180 loc_y = loc_y - 50
2181 first = 0
2182 second = 1
2183 add.width_hidden = 100.0
2184 elif nodes_list == selected_z:
2185 add = nodes.new('CompositorNodeZcombine')
2186 add.show_preview = False
2187 add.hide = do_hide
2188 if do_hide:
2189 loc_y = loc_y - 50
2190 first = 0
2191 second = 2
2192 add.width_hidden = 100.0
2193 elif nodes_list == selected_alphaover:
2194 add = nodes.new('CompositorNodeAlphaOver')
2195 add.show_preview = False
2196 add.hide = do_hide
2197 if do_hide:
2198 loc_y = loc_y - 50
2199 first = 1
2200 second = 2
2201 add.width_hidden = 100.0
2202 add.location = loc_x, loc_y
2203 loc_y += offset_y
2204 add.select = True
2206 # This has already been handled separately
2207 if was_multi:
2208 continue
2209 count_adds = i + 1
2210 count_after = len(nodes)
2211 index = count_after - 1
2212 first_selected = nodes[nodes_list[0][0]]
2213 # "last" node has been added as first, so its index is count_before.
2214 last_add = nodes[count_before]
2215 # Create list of invalid indexes.
2216 invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
2218 # Special case:
2219 # Two nodes were selected and first selected has no output links, second selected has output links.
2220 # Then add links from last add to all links 'to_socket' of out links of second selected.
2221 first_selected_output = get_first_enabled_output(first_selected)
2222 if len(nodes_list) == 2:
2223 if not first_selected_output.links:
2224 second_selected = nodes[nodes_list[1][0]]
2225 for ss_link in second_selected.outputs[0].links:
2226 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2227 # Link only if "to_node" index not in invalid indexes list.
2228 if not self.link_creates_cycle(ss_link, invalid_nodes):
2229 links.new(last_add.outputs[0], ss_link.to_socket)
2230 # add links from last_add to all links 'to_socket' of out links of first selected.
2231 for fs_link in first_selected_output.links:
2232 # Link only if "to_node" index not in invalid indexes list.
2233 if not self.link_creates_cycle(fs_link, invalid_nodes):
2234 links.new(last_add.outputs[0], fs_link.to_socket)
2235 # add link from "first" selected and "first" add node
2236 node_to = nodes[count_after - 1]
2237 links.new(first_selected_output, node_to.inputs[first])
2238 if node_to.type == 'ZCOMBINE':
2239 for fs_out in first_selected.outputs:
2240 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
2241 links.new(fs_out, node_to.inputs[1])
2242 break
2243 # add links between added ADD nodes and between selected and ADD nodes
2244 for i in range(count_adds):
2245 if i < count_adds - 1:
2246 node_from = nodes[index]
2247 node_to = nodes[index - 1]
2248 node_to_input_i = first
2249 node_to_z_i = 1 # if z combine - link z to first z input
2250 links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
2251 if node_to.type == 'ZCOMBINE':
2252 for from_out in node_from.outputs:
2253 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
2254 links.new(from_out, node_to.inputs[node_to_z_i])
2255 if len(nodes_list) > 1:
2256 node_from = nodes[nodes_list[i + 1][0]]
2257 node_to = nodes[index]
2258 node_to_input_i = second
2259 node_to_z_i = 3 # if z combine - link z to second z input
2260 links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
2261 if node_to.type == 'ZCOMBINE':
2262 for from_out in node_from.outputs:
2263 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
2264 links.new(from_out, node_to.inputs[node_to_z_i])
2265 index -= 1
2266 # set "last" of added nodes as active
2267 nodes.active = last_add
2268 for i, x, y, dx, h in nodes_list:
2269 nodes[i].select = False
2271 return {'FINISHED'}
2274 class NWBatchChangeNodes(Operator, NWBase):
2275 bl_idname = "node.nw_batch_change"
2276 bl_label = "Batch Change"
2277 bl_description = "Batch Change Blend Type and Math Operation"
2278 bl_options = {'REGISTER', 'UNDO'}
2280 blend_type: EnumProperty(
2281 name="Blend Type",
2282 items=blend_types + navs,
2284 operation: EnumProperty(
2285 name="Operation",
2286 items=operations + navs,
2289 def execute(self, context):
2290 blend_type = self.blend_type
2291 operation = self.operation
2292 for node in context.selected_nodes:
2293 if node.type == 'MIX_RGB' or node.bl_idname == 'GeometryNodeAttributeMix':
2294 if not blend_type in [nav[0] for nav in navs]:
2295 node.blend_type = blend_type
2296 else:
2297 if blend_type == 'NEXT':
2298 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2299 #index = blend_types.index(node.blend_type)
2300 if index == len(blend_types) - 1:
2301 node.blend_type = blend_types[0][0]
2302 else:
2303 node.blend_type = blend_types[index + 1][0]
2305 if blend_type == 'PREV':
2306 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2307 if index == 0:
2308 node.blend_type = blend_types[len(blend_types) - 1][0]
2309 else:
2310 node.blend_type = blend_types[index - 1][0]
2312 if node.type == 'MATH' or node.bl_idname == 'GeometryNodeAttributeMath':
2313 if not operation in [nav[0] for nav in navs]:
2314 node.operation = operation
2315 else:
2316 if operation == 'NEXT':
2317 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2318 #index = operations.index(node.operation)
2319 if index == len(operations) - 1:
2320 node.operation = operations[0][0]
2321 else:
2322 node.operation = operations[index + 1][0]
2324 if operation == 'PREV':
2325 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2326 #index = operations.index(node.operation)
2327 if index == 0:
2328 node.operation = operations[len(operations) - 1][0]
2329 else:
2330 node.operation = operations[index - 1][0]
2332 return {'FINISHED'}
2335 class NWChangeMixFactor(Operator, NWBase):
2336 bl_idname = "node.nw_factor"
2337 bl_label = "Change Factor"
2338 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2339 bl_options = {'REGISTER', 'UNDO'}
2341 # option: Change factor.
2342 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2343 # Else - change factor by option value.
2344 option: FloatProperty()
2346 def execute(self, context):
2347 nodes, links = get_nodes_links(context)
2348 option = self.option
2349 selected = [] # entry = index
2350 for si, node in enumerate(nodes):
2351 if node.select:
2352 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2353 selected.append(si)
2355 for si in selected:
2356 fac = nodes[si].inputs[0]
2357 nodes[si].hide = False
2358 if option in {0.0, 1.0}:
2359 fac.default_value = option
2360 else:
2361 fac.default_value += option
2363 return {'FINISHED'}
2366 class NWCopySettings(Operator, NWBase):
2367 bl_idname = "node.nw_copy_settings"
2368 bl_label = "Copy Settings"
2369 bl_description = "Copy Settings of Active Node to Selected Nodes"
2370 bl_options = {'REGISTER', 'UNDO'}
2372 @classmethod
2373 def poll(cls, context):
2374 valid = False
2375 if nw_check(context):
2376 if (
2377 context.active_node is not None and
2378 context.active_node.type != 'FRAME'
2380 valid = True
2381 return valid
2383 def execute(self, context):
2384 node_active = context.active_node
2385 node_selected = context.selected_nodes
2387 # Error handling
2388 if not (len(node_selected) > 1):
2389 self.report({'ERROR'}, "2 nodes must be selected at least")
2390 return {'CANCELLED'}
2392 # Check if active node is in the selection
2393 selected_node_names = [n.name for n in node_selected]
2394 if node_active.name not in selected_node_names:
2395 self.report({'ERROR'}, "No active node")
2396 return {'CANCELLED'}
2398 # Get nodes in selection by type
2399 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2401 if not (len(valid_nodes) > 1) and node_active:
2402 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2403 return {'CANCELLED'}
2405 if len(valid_nodes) != len(node_selected):
2406 # Report nodes that are not valid
2407 valid_node_names = [n.name for n in valid_nodes]
2408 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2409 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2411 # Reference original
2412 orig = node_active
2413 #node_selected_names = [n.name for n in node_selected]
2415 # Output list
2416 success_names = []
2418 # Deselect all nodes
2419 for i in node_selected:
2420 i.select = False
2422 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2423 # Run through all other nodes
2424 for node in valid_nodes[1:]:
2426 # Check for frame node
2427 parent = node.parent if node.parent else None
2428 node_loc = [node.location.x, node.location.y]
2430 # Select original to duplicate
2431 orig.select = True
2433 # Duplicate selected node
2434 bpy.ops.node.duplicate()
2435 new_node = context.selected_nodes[0]
2437 # Deselect copy
2438 new_node.select = False
2440 # Properties to copy
2441 node_tree = node.id_data
2442 props_to_copy = 'bl_idname name location height width'.split(' ')
2444 # Input and outputs
2445 reconnections = []
2446 mappings = chain.from_iterable([node.inputs, node.outputs])
2447 for i in (i for i in mappings if i.is_linked):
2448 for L in i.links:
2449 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2451 # Properties
2452 props = {j: getattr(node, j) for j in props_to_copy}
2453 props_to_copy.pop(0)
2455 for prop in props_to_copy:
2456 setattr(new_node, prop, props[prop])
2458 # Get the node tree to remove the old node
2459 nodes = node_tree.nodes
2460 nodes.remove(node)
2461 new_node.name = props['name']
2463 if parent:
2464 new_node.parent = parent
2465 new_node.location = node_loc
2467 for str_from, str_to in reconnections:
2468 node_tree.links.new(eval(str_from), eval(str_to))
2470 success_names.append(new_node.name)
2472 orig.select = True
2473 node_tree.nodes.active = orig
2474 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2475 return {'FINISHED'}
2478 class NWCopyLabel(Operator, NWBase):
2479 bl_idname = "node.nw_copy_label"
2480 bl_label = "Copy Label"
2481 bl_options = {'REGISTER', 'UNDO'}
2483 option: EnumProperty(
2484 name="option",
2485 description="Source of name of label",
2486 items=(
2487 ('FROM_ACTIVE', 'from active', 'from active node',),
2488 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2489 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2493 def execute(self, context):
2494 nodes, links = get_nodes_links(context)
2495 option = self.option
2496 active = nodes.active
2497 if option == 'FROM_ACTIVE':
2498 if active:
2499 src_label = active.label
2500 for node in [n for n in nodes if n.select and nodes.active != n]:
2501 node.label = src_label
2502 elif option == 'FROM_NODE':
2503 selected = [n for n in nodes if n.select]
2504 for node in selected:
2505 for input in node.inputs:
2506 if input.links:
2507 src = input.links[0].from_node
2508 node.label = src.label
2509 break
2510 elif option == 'FROM_SOCKET':
2511 selected = [n for n in nodes if n.select]
2512 for node in selected:
2513 for input in node.inputs:
2514 if input.links:
2515 src = input.links[0].from_socket
2516 node.label = src.name
2517 break
2519 return {'FINISHED'}
2522 class NWClearLabel(Operator, NWBase):
2523 bl_idname = "node.nw_clear_label"
2524 bl_label = "Clear Label"
2525 bl_options = {'REGISTER', 'UNDO'}
2527 option: BoolProperty()
2529 def execute(self, context):
2530 nodes, links = get_nodes_links(context)
2531 for node in [n for n in nodes if n.select]:
2532 node.label = ''
2534 return {'FINISHED'}
2536 def invoke(self, context, event):
2537 if self.option:
2538 return self.execute(context)
2539 else:
2540 return context.window_manager.invoke_confirm(self, event)
2543 class NWModifyLabels(Operator, NWBase):
2544 """Modify Labels of all selected nodes"""
2545 bl_idname = "node.nw_modify_labels"
2546 bl_label = "Modify Labels"
2547 bl_options = {'REGISTER', 'UNDO'}
2549 prepend: StringProperty(
2550 name="Add to Beginning"
2552 append: StringProperty(
2553 name="Add to End"
2555 replace_from: StringProperty(
2556 name="Text to Replace"
2558 replace_to: StringProperty(
2559 name="Replace with"
2562 def execute(self, context):
2563 nodes, links = get_nodes_links(context)
2564 for node in [n for n in nodes if n.select]:
2565 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2567 return {'FINISHED'}
2569 def invoke(self, context, event):
2570 self.prepend = ""
2571 self.append = ""
2572 self.remove = ""
2573 return context.window_manager.invoke_props_dialog(self)
2576 class NWAddTextureSetup(Operator, NWBase):
2577 bl_idname = "node.nw_add_texture"
2578 bl_label = "Texture Setup"
2579 bl_description = "Add Texture Node Setup to Selected Shaders"
2580 bl_options = {'REGISTER', 'UNDO'}
2582 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2584 @classmethod
2585 def poll(cls, context):
2586 if nw_check(context):
2587 space = context.space_data
2588 if space.tree_type == 'ShaderNodeTree':
2589 return True
2590 return False
2592 def execute(self, context):
2593 nodes, links = get_nodes_links(context)
2595 texture_types = [x.nodetype for x in
2596 get_nodes_from_category('Texture', context)]
2597 selected_nodes = [n for n in nodes if n.select]
2599 for node in selected_nodes:
2600 if not node.inputs:
2601 continue
2603 input_index = 0
2604 target_input = node.inputs[0]
2605 for input in node.inputs:
2606 if input.enabled:
2607 input_index += 1
2608 if not input.is_linked:
2609 target_input = input
2610 break
2611 else:
2612 self.report({'WARNING'}, "No free inputs for node: " + node.name)
2613 continue
2615 x_offset = 0
2616 padding = 40.0
2617 locx = node.location.x
2618 locy = node.location.y - (input_index * padding)
2620 is_texture_node = node.rna_type.identifier in texture_types
2621 use_environment_texture = node.type == 'BACKGROUND'
2623 # Add an image texture before normal shader nodes.
2624 if not is_texture_node:
2625 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
2626 image_texture_node = nodes.new(image_texture_type)
2627 x_offset = x_offset + image_texture_node.width + padding
2628 image_texture_node.location = [locx - x_offset, locy]
2629 nodes.active = image_texture_node
2630 links.new(image_texture_node.outputs[0], target_input)
2632 # The mapping setup following this will connect to the firrst input of this image texture.
2633 target_input = image_texture_node.inputs[0]
2635 node.select = False
2637 if is_texture_node or self.add_mapping:
2638 # Add Mapping node.
2639 mapping_node = nodes.new('ShaderNodeMapping')
2640 x_offset = x_offset + mapping_node.width + padding
2641 mapping_node.location = [locx - x_offset, locy]
2642 links.new(mapping_node.outputs[0], target_input)
2644 # Add Texture Coordinates node.
2645 tex_coord_node = nodes.new('ShaderNodeTexCoord')
2646 x_offset = x_offset + tex_coord_node.width + padding
2647 tex_coord_node.location = [locx - x_offset, locy]
2649 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
2650 use_generated_coordinates = is_procedural_texture or use_environment_texture
2651 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
2652 links.new(tex_coord_output, mapping_node.inputs[0])
2654 return {'FINISHED'}
2657 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2658 bl_idname = "node.nw_add_textures_for_principled"
2659 bl_label = "Principled Texture Setup"
2660 bl_description = "Add Texture Node Setup for Principled BSDF"
2661 bl_options = {'REGISTER', 'UNDO'}
2663 directory: StringProperty(
2664 name='Directory',
2665 subtype='DIR_PATH',
2666 default='',
2667 description='Folder to search in for image files'
2669 files: CollectionProperty(
2670 type=bpy.types.OperatorFileListElement,
2671 options={'HIDDEN', 'SKIP_SAVE'}
2674 relative_path: BoolProperty(
2675 name='Relative Path',
2676 description='Set the file path relative to the blend file, when possible',
2677 default=True
2680 order = [
2681 "filepath",
2682 "files",
2685 def draw(self, context):
2686 layout = self.layout
2687 layout.alignment = 'LEFT'
2689 layout.prop(self, 'relative_path')
2691 @classmethod
2692 def poll(cls, context):
2693 valid = False
2694 if nw_check(context):
2695 space = context.space_data
2696 if space.tree_type == 'ShaderNodeTree':
2697 valid = True
2698 return valid
2700 def execute(self, context):
2701 # Check if everything is ok
2702 if not self.directory:
2703 self.report({'INFO'}, 'No Folder Selected')
2704 return {'CANCELLED'}
2705 if not self.files[:]:
2706 self.report({'INFO'}, 'No Files Selected')
2707 return {'CANCELLED'}
2709 nodes, links = get_nodes_links(context)
2710 active_node = nodes.active
2711 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
2712 self.report({'INFO'}, 'Select Principled BSDF')
2713 return {'CANCELLED'}
2715 # Helper_functions
2716 def split_into__components(fname):
2717 # Split filename into components
2718 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2719 # Remove extension
2720 fname = path.splitext(fname)[0]
2721 # Remove digits
2722 fname = ''.join(i for i in fname if not i.isdigit())
2723 # Separate CamelCase by space
2724 fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",fname)
2725 # Replace common separators with SPACE
2726 separators = ['_', '.', '-', '__', '--', '#']
2727 for sep in separators:
2728 fname = fname.replace(sep, ' ')
2730 components = fname.split(' ')
2731 components = [c.lower() for c in components]
2732 return components
2734 # Filter textures names for texturetypes in filenames
2735 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2736 tags = context.preferences.addons[__name__].preferences.principled_tags
2737 normal_abbr = tags.normal.split(' ')
2738 bump_abbr = tags.bump.split(' ')
2739 gloss_abbr = tags.gloss.split(' ')
2740 rough_abbr = tags.rough.split(' ')
2741 socketnames = [
2742 ['Displacement', tags.displacement.split(' '), None],
2743 ['Base Color', tags.base_color.split(' '), None],
2744 ['Subsurface Color', tags.sss_color.split(' '), None],
2745 ['Metallic', tags.metallic.split(' '), None],
2746 ['Specular', tags.specular.split(' '), None],
2747 ['Roughness', rough_abbr + gloss_abbr, None],
2748 ['Normal', normal_abbr + bump_abbr, None],
2749 ['Transmission', tags.transmission.split(' '), None],
2750 ['Emission', tags.emission.split(' '), None],
2751 ['Alpha', tags.alpha.split(' '), None],
2752 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
2755 # Look through texture_types and set value as filename of first matched file
2756 def match_files_to_socket_names():
2757 for sname in socketnames:
2758 for file in self.files:
2759 fname = file.name
2760 filenamecomponents = split_into__components(fname)
2761 matches = set(sname[1]).intersection(set(filenamecomponents))
2762 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2763 if matches:
2764 sname[2] = fname
2765 break
2767 match_files_to_socket_names()
2768 # Remove socketnames without found files
2769 socketnames = [s for s in socketnames if s[2]
2770 and path.exists(self.directory+s[2])]
2771 if not socketnames:
2772 self.report({'INFO'}, 'No matching images found')
2773 print('No matching images found')
2774 return {'CANCELLED'}
2776 # Don't override path earlier as os.path is used to check the absolute path
2777 import_path = self.directory
2778 if self.relative_path:
2779 if bpy.data.filepath:
2780 try:
2781 import_path = bpy.path.relpath(self.directory)
2782 except ValueError:
2783 pass
2785 # Add found images
2786 print('\nMatched Textures:')
2787 texture_nodes = []
2788 disp_texture = None
2789 ao_texture = None
2790 normal_node = None
2791 roughness_node = None
2792 for i, sname in enumerate(socketnames):
2793 print(i, sname[0], sname[2])
2795 # DISPLACEMENT NODES
2796 if sname[0] == 'Displacement':
2797 disp_texture = nodes.new(type='ShaderNodeTexImage')
2798 img = bpy.data.images.load(path.join(import_path, sname[2]))
2799 disp_texture.image = img
2800 disp_texture.label = 'Displacement'
2801 if disp_texture.image:
2802 disp_texture.image.colorspace_settings.is_data = True
2804 # Add displacement offset nodes
2805 disp_node = nodes.new(type='ShaderNodeDisplacement')
2806 # Align the Displacement node under the active Principled BSDF node
2807 disp_node.location = active_node.location + Vector((100, -700))
2808 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
2810 # TODO Turn on true displacement in the material
2811 # Too complicated for now
2813 # Find output node
2814 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2815 if output_node:
2816 if not output_node[0].inputs[2].is_linked:
2817 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
2819 continue
2821 # AMBIENT OCCLUSION TEXTURE
2822 if sname[0] == 'Ambient Occlusion':
2823 ao_texture = nodes.new(type='ShaderNodeTexImage')
2824 img = bpy.data.images.load(path.join(import_path, sname[2]))
2825 ao_texture.image = img
2826 ao_texture.label = sname[0]
2827 if ao_texture.image:
2828 ao_texture.image.colorspace_settings.is_data = True
2830 continue
2832 if not active_node.inputs[sname[0]].is_linked:
2833 # No texture node connected -> add texture node with new image
2834 texture_node = nodes.new(type='ShaderNodeTexImage')
2835 img = bpy.data.images.load(path.join(import_path, sname[2]))
2836 texture_node.image = img
2838 # NORMAL NODES
2839 if sname[0] == 'Normal':
2840 # Test if new texture node is normal or bump map
2841 fname_components = split_into__components(sname[2])
2842 match_normal = set(normal_abbr).intersection(set(fname_components))
2843 match_bump = set(bump_abbr).intersection(set(fname_components))
2844 if match_normal:
2845 # If Normal add normal node in between
2846 normal_node = nodes.new(type='ShaderNodeNormalMap')
2847 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
2848 elif match_bump:
2849 # If Bump add bump node in between
2850 normal_node = nodes.new(type='ShaderNodeBump')
2851 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
2853 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
2854 normal_node_texture = texture_node
2856 elif sname[0] == 'Roughness':
2857 # Test if glossy or roughness map
2858 fname_components = split_into__components(sname[2])
2859 match_rough = set(rough_abbr).intersection(set(fname_components))
2860 match_gloss = set(gloss_abbr).intersection(set(fname_components))
2862 if match_rough:
2863 # If Roughness nothing to to
2864 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2866 elif match_gloss:
2867 # If Gloss Map add invert node
2868 invert_node = nodes.new(type='ShaderNodeInvert')
2869 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
2871 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
2872 roughness_node = texture_node
2874 else:
2875 # This is a simple connection Texture --> Input slot
2876 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2878 # Use non-color for all but 'Base Color' Textures
2879 if not sname[0] in ['Base Color', 'Emission'] and texture_node.image:
2880 texture_node.image.colorspace_settings.is_data = True
2882 else:
2883 # If already texture connected. add to node list for alignment
2884 texture_node = active_node.inputs[sname[0]].links[0].from_node
2886 # This are all connected texture nodes
2887 texture_nodes.append(texture_node)
2888 texture_node.label = sname[0]
2890 if disp_texture:
2891 texture_nodes.append(disp_texture)
2893 if ao_texture:
2894 # We want the ambient occlusion texture to be the top most texture node
2895 texture_nodes.insert(0, ao_texture)
2897 # Alignment
2898 for i, texture_node in enumerate(texture_nodes):
2899 offset = Vector((-550, (i * -280) + 200))
2900 texture_node.location = active_node.location + offset
2902 if normal_node:
2903 # Extra alignment if normal node was added
2904 normal_node.location = normal_node_texture.location + Vector((300, 0))
2906 if roughness_node:
2907 # Alignment of invert node if glossy map
2908 invert_node.location = roughness_node.location + Vector((300, 0))
2910 # Add texture input + mapping
2911 mapping = nodes.new(type='ShaderNodeMapping')
2912 mapping.location = active_node.location + Vector((-1050, 0))
2913 if len(texture_nodes) > 1:
2914 # If more than one texture add reroute node in between
2915 reroute = nodes.new(type='NodeReroute')
2916 texture_nodes.append(reroute)
2917 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
2918 reroute.location = tex_coords + Vector((-50, -120))
2919 for texture_node in texture_nodes:
2920 link = links.new(texture_node.inputs[0], reroute.outputs[0])
2921 link = links.new(reroute.inputs[0], mapping.outputs[0])
2922 else:
2923 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
2925 # Connect texture_coordiantes to mapping node
2926 texture_input = nodes.new(type='ShaderNodeTexCoord')
2927 texture_input.location = mapping.location + Vector((-200, 0))
2928 link = links.new(mapping.inputs[0], texture_input.outputs[2])
2930 # Create frame around tex coords and mapping
2931 frame = nodes.new(type='NodeFrame')
2932 frame.label = 'Mapping'
2933 mapping.parent = frame
2934 texture_input.parent = frame
2935 frame.update()
2937 # Create frame around texture nodes
2938 frame = nodes.new(type='NodeFrame')
2939 frame.label = 'Textures'
2940 for tnode in texture_nodes:
2941 tnode.parent = frame
2942 frame.update()
2944 # Just to be sure
2945 active_node.select = False
2946 nodes.update()
2947 links.update()
2948 force_update(context)
2949 return {'FINISHED'}
2952 class NWAddReroutes(Operator, NWBase):
2953 """Add Reroute Nodes and link them to outputs of selected nodes"""
2954 bl_idname = "node.nw_add_reroutes"
2955 bl_label = "Add Reroutes"
2956 bl_description = "Add Reroutes to Outputs"
2957 bl_options = {'REGISTER', 'UNDO'}
2959 option: EnumProperty(
2960 name="option",
2961 items=[
2962 ('ALL', 'to all', 'Add to all outputs'),
2963 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2964 ('LINKED', 'to linked', 'Add only to linked outputs'),
2968 def execute(self, context):
2969 tree_type = context.space_data.node_tree.type
2970 option = self.option
2971 nodes, links = get_nodes_links(context)
2972 # output valid when option is 'all' or when 'loose' output has no links
2973 valid = False
2974 post_select = [] # nodes to be selected after execution
2975 # create reroutes and recreate links
2976 for node in [n for n in nodes if n.select]:
2977 if node.outputs:
2978 x = node.location.x
2979 y = node.location.y
2980 width = node.width
2981 # unhide 'REROUTE' nodes to avoid issues with location.y
2982 if node.type == 'REROUTE':
2983 node.hide = False
2984 # When node is hidden - width_hidden not usable.
2985 # Hack needed to calculate real width
2986 if node.hide:
2987 bpy.ops.node.select_all(action='DESELECT')
2988 helper = nodes.new('NodeReroute')
2989 helper.select = True
2990 node.select = True
2991 # resize node and helper to zero. Then check locations to calculate width
2992 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2993 width = 2.0 * (helper.location.x - node.location.x)
2994 # restore node location
2995 node.location = x, y
2996 # delete helper
2997 node.select = False
2998 # only helper is selected now
2999 bpy.ops.node.delete()
3000 x = node.location.x + width + 20.0
3001 if node.type != 'REROUTE':
3002 y -= 35.0
3003 y_offset = -22.0
3004 loc = x, y
3005 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
3006 for out_i, output in enumerate(node.outputs):
3007 pass_used = False # initial value to be analyzed if 'R_LAYERS'
3008 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3009 if node.type != 'R_LAYERS':
3010 pass_used = True
3011 else: # if 'R_LAYERS' check if output represent used render pass
3012 node_scene = node.scene
3013 node_layer = node.layer
3014 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3015 if output.name == 'Alpha':
3016 pass_used = True
3017 else:
3018 # check entries in global 'rl_outputs' variable
3019 for rlo in rl_outputs:
3020 if output.name in {rlo.output_name, rlo.exr_output_name}:
3021 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
3022 break
3023 if pass_used:
3024 valid = ((option == 'ALL') or
3025 (option == 'LOOSE' and not output.links) or
3026 (option == 'LINKED' and output.links))
3027 # Add reroutes only if valid, but offset location in all cases.
3028 if valid:
3029 n = nodes.new('NodeReroute')
3030 nodes.active = n
3031 for link in output.links:
3032 links.new(n.outputs[0], link.to_socket)
3033 links.new(output, n.inputs[0])
3034 n.location = loc
3035 post_select.append(n)
3036 reroutes_count += 1
3037 y += y_offset
3038 loc = x, y
3039 # disselect the node so that after execution of script only newly created nodes are selected
3040 node.select = False
3041 # nicer reroutes distribution along y when node.hide
3042 if node.hide:
3043 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
3044 for reroute in [r for r in nodes if r.select]:
3045 reroute.location.y -= y_translate
3046 for node in post_select:
3047 node.select = True
3049 return {'FINISHED'}
3052 class NWLinkActiveToSelected(Operator, NWBase):
3053 """Link active node to selected nodes basing on various criteria"""
3054 bl_idname = "node.nw_link_active_to_selected"
3055 bl_label = "Link Active Node to Selected"
3056 bl_options = {'REGISTER', 'UNDO'}
3058 replace: BoolProperty()
3059 use_node_name: BoolProperty()
3060 use_outputs_names: BoolProperty()
3062 @classmethod
3063 def poll(cls, context):
3064 valid = False
3065 if nw_check(context):
3066 if context.active_node is not None:
3067 if context.active_node.select:
3068 valid = True
3069 return valid
3071 def execute(self, context):
3072 nodes, links = get_nodes_links(context)
3073 replace = self.replace
3074 use_node_name = self.use_node_name
3075 use_outputs_names = self.use_outputs_names
3076 active = nodes.active
3077 selected = [node for node in nodes if node.select and node != active]
3078 outputs = [] # Only usable outputs of active nodes will be stored here.
3079 for out in active.outputs:
3080 if active.type != 'R_LAYERS':
3081 outputs.append(out)
3082 else:
3083 # 'R_LAYERS' node type needs special handling.
3084 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3085 # Only outputs that represent used passes should be taken into account
3086 # Check if pass represented by output is used.
3087 # global 'rl_outputs' list will be used for that
3088 for rlo in rl_outputs:
3089 pass_used = False # initial value. Will be set to True if pass is used
3090 if out.name == 'Alpha':
3091 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3092 pass_used = True
3093 elif out.name in {rlo.output_name, rlo.exr_output_name}:
3094 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3095 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
3096 break
3097 if pass_used:
3098 outputs.append(out)
3099 doit = True # Will be changed to False when links successfully added to previous output.
3100 for out in outputs:
3101 if doit:
3102 for node in selected:
3103 dst_name = node.name # Will be compared with src_name if needed.
3104 # When node has label - use it as dst_name
3105 if node.label:
3106 dst_name = node.label
3107 valid = True # Initial value. Will be changed to False if names don't match.
3108 src_name = dst_name # If names not used - this assignment will keep valid = True.
3109 if use_node_name:
3110 # Set src_name to source node name or label
3111 src_name = active.name
3112 if active.label:
3113 src_name = active.label
3114 elif use_outputs_names:
3115 src_name = (out.name, )
3116 for rlo in rl_outputs:
3117 if out.name in {rlo.output_name, rlo.exr_output_name}:
3118 src_name = (rlo.output_name, rlo.exr_output_name)
3119 if dst_name not in src_name:
3120 valid = False
3121 if valid:
3122 for input in node.inputs:
3123 if input.type == out.type or node.type == 'REROUTE':
3124 if replace or not input.is_linked:
3125 links.new(out, input)
3126 if not use_node_name and not use_outputs_names:
3127 doit = False
3128 break
3130 return {'FINISHED'}
3133 class NWAlignNodes(Operator, NWBase):
3134 '''Align the selected nodes neatly in a row/column'''
3135 bl_idname = "node.nw_align_nodes"
3136 bl_label = "Align Nodes"
3137 bl_options = {'REGISTER', 'UNDO'}
3138 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3140 def execute(self, context):
3141 nodes, links = get_nodes_links(context)
3142 margin = self.margin
3144 selection = []
3145 for node in nodes:
3146 if node.select and node.type != 'FRAME':
3147 selection.append(node)
3149 # If no nodes are selected, align all nodes
3150 active_loc = None
3151 if not selection:
3152 selection = nodes
3153 elif nodes.active in selection:
3154 active_loc = copy(nodes.active.location) # make a copy, not a reference
3156 # Check if nodes should be laid out horizontally or vertically
3157 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3158 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3159 x_range = max(x_locs) - min(x_locs)
3160 y_range = max(y_locs) - min(y_locs)
3161 mid_x = (max(x_locs) + min(x_locs)) / 2
3162 mid_y = (max(y_locs) + min(y_locs)) / 2
3163 horizontal = x_range > y_range
3165 # Sort selection by location of node mid-point
3166 if horizontal:
3167 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3168 else:
3169 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3171 # Alignment
3172 current_pos = 0
3173 for node in selection:
3174 current_margin = margin
3175 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3177 if horizontal:
3178 node.location.x = current_pos
3179 current_pos += current_margin + node.dimensions.x
3180 node.location.y = mid_y + (node.dimensions.y / 2)
3181 else:
3182 node.location.y = current_pos
3183 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3184 node.location.x = mid_x - (node.dimensions.x / 2)
3186 # If active node is selected, center nodes around it
3187 if active_loc is not None:
3188 active_loc_diff = active_loc - nodes.active.location
3189 for node in selection:
3190 node.location += active_loc_diff
3191 else: # Position nodes centered around where they used to be
3192 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])
3193 new_mid = (max(locs) + min(locs)) / 2
3194 for node in selection:
3195 if horizontal:
3196 node.location.x += (mid_x - new_mid)
3197 else:
3198 node.location.y += (mid_y - new_mid)
3200 return {'FINISHED'}
3203 class NWSelectParentChildren(Operator, NWBase):
3204 bl_idname = "node.nw_select_parent_child"
3205 bl_label = "Select Parent or Children"
3206 bl_options = {'REGISTER', 'UNDO'}
3208 option: EnumProperty(
3209 name="option",
3210 items=(
3211 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3212 ('CHILD', 'Select Children', 'Select members of selected frame'),
3216 def execute(self, context):
3217 nodes, links = get_nodes_links(context)
3218 option = self.option
3219 selected = [node for node in nodes if node.select]
3220 if option == 'PARENT':
3221 for sel in selected:
3222 parent = sel.parent
3223 if parent:
3224 parent.select = True
3225 else: # option == 'CHILD'
3226 for sel in selected:
3227 children = [node for node in nodes if node.parent == sel]
3228 for kid in children:
3229 kid.select = True
3231 return {'FINISHED'}
3234 class NWDetachOutputs(Operator, NWBase):
3235 """Detach outputs of selected node leaving inputs linked"""
3236 bl_idname = "node.nw_detach_outputs"
3237 bl_label = "Detach Outputs"
3238 bl_options = {'REGISTER', 'UNDO'}
3240 def execute(self, context):
3241 nodes, links = get_nodes_links(context)
3242 selected = context.selected_nodes
3243 bpy.ops.node.duplicate_move_keep_inputs()
3244 new_nodes = context.selected_nodes
3245 bpy.ops.node.select_all(action="DESELECT")
3246 for node in selected:
3247 node.select = True
3248 bpy.ops.node.delete_reconnect()
3249 for new_node in new_nodes:
3250 new_node.select = True
3251 bpy.ops.transform.translate('INVOKE_DEFAULT')
3253 return {'FINISHED'}
3256 class NWLinkToOutputNode(Operator):
3257 """Link to Composite node or Material Output node"""
3258 bl_idname = "node.nw_link_out"
3259 bl_label = "Connect to Output"
3260 bl_options = {'REGISTER', 'UNDO'}
3262 @classmethod
3263 def poll(cls, context):
3264 valid = False
3265 if nw_check(context):
3266 if context.active_node is not None:
3267 for out in context.active_node.outputs:
3268 if is_visible_socket(out):
3269 valid = True
3270 break
3271 return valid
3273 def execute(self, context):
3274 nodes, links = get_nodes_links(context)
3275 active = nodes.active
3276 output_index = None
3277 tree_type = context.space_data.tree_type
3278 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
3279 'WORLD': 'ShaderNodeOutputWorld',
3280 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
3281 output_type = {
3282 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
3283 'CompositorNodeTree': 'CompositorNodeComposite',
3284 'TextureNodeTree': 'TextureNodeOutput',
3285 'GeometryNodeTree': 'NodeGroupOutput',
3286 }[tree_type]
3287 for node in nodes:
3288 # check whether the node is an output node and,
3289 # if supported, whether it's the active one
3290 if node.rna_type.identifier == output_type \
3291 and (node.is_active_output if hasattr(node, 'is_active_output')
3292 else True):
3293 output_node = node
3294 break
3295 else: # No output node exists
3296 bpy.ops.node.select_all(action="DESELECT")
3297 output_node = nodes.new(output_type)
3298 output_node.location.x = active.location.x + active.dimensions.x + 80
3299 output_node.location.y = active.location.y
3301 if active.outputs:
3302 for i, output in enumerate(active.outputs):
3303 if is_visible_socket(output):
3304 output_index = i
3305 break
3306 for i, output in enumerate(active.outputs):
3307 if output.type == output_node.inputs[0].type and is_visible_socket(output):
3308 output_index = i
3309 break
3311 out_input_index = 0
3312 if tree_type == 'ShaderNodeTree':
3313 if active.outputs[output_index].name == 'Volume':
3314 out_input_index = 1
3315 elif active.outputs[output_index].name == 'Displacement':
3316 out_input_index = 2
3317 elif tree_type == 'GeometryNodeTree':
3318 if active.outputs[output_index].type != 'GEOMETRY':
3319 return {'CANCELLED'}
3320 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3322 force_update(context) # viewport render does not update
3324 return {'FINISHED'}
3327 class NWMakeLink(Operator, NWBase):
3328 """Make a link from one socket to another"""
3329 bl_idname = 'node.nw_make_link'
3330 bl_label = 'Make Link'
3331 bl_options = {'REGISTER', 'UNDO'}
3332 from_socket: IntProperty()
3333 to_socket: IntProperty()
3335 def execute(self, context):
3336 nodes, links = get_nodes_links(context)
3338 n1 = nodes[context.scene.NWLazySource]
3339 n2 = nodes[context.scene.NWLazyTarget]
3341 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3343 force_update(context)
3345 return {'FINISHED'}
3348 class NWCallInputsMenu(Operator, NWBase):
3349 """Link from this output"""
3350 bl_idname = 'node.nw_call_inputs_menu'
3351 bl_label = 'Make Link'
3352 bl_options = {'REGISTER', 'UNDO'}
3353 from_socket: IntProperty()
3355 def execute(self, context):
3356 nodes, links = get_nodes_links(context)
3358 context.scene.NWSourceSocket = self.from_socket
3360 n1 = nodes[context.scene.NWLazySource]
3361 n2 = nodes[context.scene.NWLazyTarget]
3362 if len(n2.inputs) > 1:
3363 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3364 elif len(n2.inputs) == 1:
3365 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3366 return {'FINISHED'}
3369 class NWAddSequence(Operator, NWBase, ImportHelper):
3370 """Add an Image Sequence"""
3371 bl_idname = 'node.nw_add_sequence'
3372 bl_label = 'Import Image Sequence'
3373 bl_options = {'REGISTER', 'UNDO'}
3375 directory: StringProperty(
3376 subtype="DIR_PATH"
3378 filename: StringProperty(
3379 subtype="FILE_NAME"
3381 files: CollectionProperty(
3382 type=bpy.types.OperatorFileListElement,
3383 options={'HIDDEN', 'SKIP_SAVE'}
3385 relative_path: BoolProperty(
3386 name='Relative Path',
3387 description='Set the file path relative to the blend file, when possible',
3388 default=True
3391 def draw(self, context):
3392 layout = self.layout
3393 layout.alignment = 'LEFT'
3395 layout.prop(self, 'relative_path')
3397 def execute(self, context):
3398 nodes, links = get_nodes_links(context)
3399 directory = self.directory
3400 filename = self.filename
3401 files = self.files
3402 tree = context.space_data.node_tree
3404 # DEBUG
3405 # print ("\nDIR:", directory)
3406 # print ("FN:", filename)
3407 # print ("Fs:", list(f.name for f in files), '\n')
3409 if tree.type == 'SHADER':
3410 node_type = "ShaderNodeTexImage"
3411 elif tree.type == 'COMPOSITING':
3412 node_type = "CompositorNodeImage"
3413 else:
3414 self.report({'ERROR'}, "Unsupported Node Tree type!")
3415 return {'CANCELLED'}
3417 if not files[0].name and not filename:
3418 self.report({'ERROR'}, "No file chosen")
3419 return {'CANCELLED'}
3420 elif files[0].name and (not filename or not path.exists(directory+filename)):
3421 # User has selected multiple files without an active one, or the active one is non-existant
3422 filename = files[0].name
3424 if not path.exists(directory+filename):
3425 self.report({'ERROR'}, filename+" does not exist!")
3426 return {'CANCELLED'}
3428 without_ext = '.'.join(filename.split('.')[:-1])
3430 # if last digit isn't a number, it's not a sequence
3431 if not without_ext[-1].isdigit():
3432 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3433 return {'CANCELLED'}
3436 extension = filename.split('.')[-1]
3437 reverse = without_ext[::-1] # reverse string
3439 count_numbers = 0
3440 for char in reverse:
3441 if char.isdigit():
3442 count_numbers += 1
3443 else:
3444 break
3446 without_num = without_ext[:count_numbers*-1]
3448 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3450 num_frames = len(files)
3452 nodes_list = [node for node in nodes]
3453 if nodes_list:
3454 nodes_list.sort(key=lambda k: k.location.x)
3455 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3456 yloc = 0
3457 for node in nodes:
3458 node.select = False
3459 yloc += node_mid_pt(node, 'y')
3460 yloc = yloc/len(nodes)
3461 else:
3462 xloc = 0
3463 yloc = 0
3465 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3467 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3468 node = nodes.active
3469 node.label = name_with_hashes
3471 filepath = directory+(without_ext+'.'+extension)
3472 if self.relative_path:
3473 if bpy.data.filepath:
3474 try:
3475 filepath = bpy.path.relpath(filepath)
3476 except ValueError:
3477 pass
3479 img = bpy.data.images.load(filepath)
3480 img.source = 'SEQUENCE'
3481 img.name = name_with_hashes
3482 node.image = img
3483 image_user = node.image_user if tree.type == 'SHADER' else node
3484 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
3485 image_user.frame_duration = num_frames
3487 return {'FINISHED'}
3490 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
3491 """Add multiple images at once"""
3492 bl_idname = 'node.nw_add_multiple_images'
3493 bl_label = 'Open Selected Images'
3494 bl_options = {'REGISTER', 'UNDO'}
3495 directory: StringProperty(
3496 subtype="DIR_PATH"
3498 files: CollectionProperty(
3499 type=bpy.types.OperatorFileListElement,
3500 options={'HIDDEN', 'SKIP_SAVE'}
3503 def execute(self, context):
3504 nodes, links = get_nodes_links(context)
3506 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3508 if context.space_data.node_tree.type == 'SHADER':
3509 node_type = "ShaderNodeTexImage"
3510 elif context.space_data.node_tree.type == 'COMPOSITING':
3511 node_type = "CompositorNodeImage"
3512 else:
3513 self.report({'ERROR'}, "Unsupported Node Tree type!")
3514 return {'CANCELLED'}
3516 new_nodes = []
3517 for f in self.files:
3518 fname = f.name
3520 node = nodes.new(node_type)
3521 new_nodes.append(node)
3522 node.label = fname
3523 node.hide = True
3524 node.width_hidden = 100
3525 node.location.x = xloc
3526 node.location.y = yloc
3527 yloc -= 40
3529 img = bpy.data.images.load(self.directory+fname)
3530 node.image = img
3532 # shift new nodes up to center of tree
3533 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3534 for node in nodes:
3535 if node in new_nodes:
3536 node.select = True
3537 node.location.y += (list_size/2)
3538 else:
3539 node.select = False
3540 return {'FINISHED'}
3543 class NWViewerFocus(bpy.types.Operator):
3544 """Set the viewer tile center to the mouse position"""
3545 bl_idname = "node.nw_viewer_focus"
3546 bl_label = "Viewer Focus"
3548 x: bpy.props.IntProperty()
3549 y: bpy.props.IntProperty()
3551 @classmethod
3552 def poll(cls, context):
3553 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3555 def execute(self, context):
3556 return {'FINISHED'}
3558 def invoke(self, context, event):
3559 render = context.scene.render
3560 space = context.space_data
3561 percent = render.resolution_percentage*0.01
3563 nodes, links = get_nodes_links(context)
3564 viewers = [n for n in nodes if n.type == 'VIEWER']
3566 if viewers:
3567 mlocx = event.mouse_region_x
3568 mlocy = event.mouse_region_y
3569 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
3571 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3572 region_x = context.region.width
3573 region_y = context.region.height
3575 region_center_x = context.region.width / 2
3576 region_center_y = context.region.height / 2
3578 bd_x = render.resolution_x * percent * space.backdrop_zoom
3579 bd_y = render.resolution_y * percent * space.backdrop_zoom
3581 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
3582 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
3584 margin_x = region_center_x - backdrop_center_x
3585 margin_y = region_center_y - backdrop_center_y
3587 abs_mouse_x = (mlocx - margin_x) / bd_x
3588 abs_mouse_y = (mlocy - margin_y) / bd_y
3590 for node in viewers:
3591 node.center_x = abs_mouse_x
3592 node.center_y = abs_mouse_y
3593 else:
3594 return {'PASS_THROUGH'}
3596 return self.execute(context)
3599 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3600 """Save the current viewer node to an image file"""
3601 bl_idname = "node.nw_save_viewer"
3602 bl_label = "Save This Image"
3603 filepath: StringProperty(subtype="FILE_PATH")
3604 filename_ext: EnumProperty(
3605 name="Format",
3606 description="Choose the file format to save to",
3607 items=(('.bmp', "BMP", ""),
3608 ('.rgb', 'IRIS', ""),
3609 ('.png', 'PNG', ""),
3610 ('.jpg', 'JPEG', ""),
3611 ('.jp2', 'JPEG2000', ""),
3612 ('.tga', 'TARGA', ""),
3613 ('.cin', 'CINEON', ""),
3614 ('.dpx', 'DPX', ""),
3615 ('.exr', 'OPEN_EXR', ""),
3616 ('.hdr', 'HDR', ""),
3617 ('.tif', 'TIFF', "")),
3618 default='.png',
3621 @classmethod
3622 def poll(cls, context):
3623 valid = False
3624 if nw_check(context):
3625 if context.space_data.tree_type == 'CompositorNodeTree':
3626 if "Viewer Node" in [i.name for i in bpy.data.images]:
3627 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3628 valid = True
3629 return valid
3631 def execute(self, context):
3632 fp = self.filepath
3633 if fp:
3634 formats = {
3635 '.bmp': 'BMP',
3636 '.rgb': 'IRIS',
3637 '.png': 'PNG',
3638 '.jpg': 'JPEG',
3639 '.jpeg': 'JPEG',
3640 '.jp2': 'JPEG2000',
3641 '.tga': 'TARGA',
3642 '.cin': 'CINEON',
3643 '.dpx': 'DPX',
3644 '.exr': 'OPEN_EXR',
3645 '.hdr': 'HDR',
3646 '.tiff': 'TIFF',
3647 '.tif': 'TIFF'}
3648 basename, ext = path.splitext(fp)
3649 old_render_format = context.scene.render.image_settings.file_format
3650 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3651 context.area.type = "IMAGE_EDITOR"
3652 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3653 context.area.spaces[0].image.save_render(fp)
3654 context.area.type = "NODE_EDITOR"
3655 context.scene.render.image_settings.file_format = old_render_format
3656 return {'FINISHED'}
3659 class NWResetNodes(bpy.types.Operator):
3660 """Reset Nodes in Selection"""
3661 bl_idname = "node.nw_reset_nodes"
3662 bl_label = "Reset Nodes"
3663 bl_options = {'REGISTER', 'UNDO'}
3665 @classmethod
3666 def poll(cls, context):
3667 space = context.space_data
3668 return space.type == 'NODE_EDITOR'
3670 def execute(self, context):
3671 node_active = context.active_node
3672 node_selected = context.selected_nodes
3673 node_ignore = ["FRAME","REROUTE", "GROUP"]
3675 # Check if one node is selected at least
3676 if not (len(node_selected) > 0):
3677 self.report({'ERROR'}, "1 node must be selected at least")
3678 return {'CANCELLED'}
3680 active_node_name = node_active.name if node_active.select else None
3681 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3683 # Create output lists
3684 selected_node_names = [n.name for n in node_selected]
3685 success_names = []
3687 # Reset all valid children in a frame
3688 node_active_is_frame = False
3689 if len(node_selected) == 1 and node_active.type == "FRAME":
3690 node_tree = node_active.id_data
3691 children = [n for n in node_tree.nodes if n.parent == node_active]
3692 if children:
3693 valid_nodes = [n for n in children if n.type not in node_ignore]
3694 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3695 node_active_is_frame = True
3697 # Check if valid nodes in selection
3698 if not (len(valid_nodes) > 0):
3699 # Check for frames only
3700 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3701 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3702 self.report({'ERROR'}, "Please select only 1 frame to reset")
3703 else:
3704 self.report({'ERROR'}, "No valid node(s) in selection")
3705 return {'CANCELLED'}
3707 # Report nodes that are not valid
3708 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3709 valid_node_names = [n.name for n in valid_nodes]
3710 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3711 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3713 # Deselect all nodes
3714 for i in node_selected:
3715 i.select = False
3717 # Run through all valid nodes
3718 for node in valid_nodes:
3720 parent = node.parent if node.parent else None
3721 node_loc = [node.location.x, node.location.y]
3723 node_tree = node.id_data
3724 props_to_copy = 'bl_idname name location height width'.split(' ')
3726 reconnections = []
3727 mappings = chain.from_iterable([node.inputs, node.outputs])
3728 for i in (i for i in mappings if i.is_linked):
3729 for L in i.links:
3730 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3732 props = {j: getattr(node, j) for j in props_to_copy}
3734 new_node = node_tree.nodes.new(props['bl_idname'])
3735 props_to_copy.pop(0)
3737 for prop in props_to_copy:
3738 setattr(new_node, prop, props[prop])
3740 nodes = node_tree.nodes
3741 nodes.remove(node)
3742 new_node.name = props['name']
3744 if parent:
3745 new_node.parent = parent
3746 new_node.location = node_loc
3748 for str_from, str_to in reconnections:
3749 node_tree.links.new(eval(str_from), eval(str_to))
3751 new_node.select = False
3752 success_names.append(new_node.name)
3754 # Reselect all nodes
3755 if selected_node_names and node_active_is_frame is False:
3756 for i in selected_node_names:
3757 node_tree.nodes[i].select = True
3759 if active_node_name is not None:
3760 node_tree.nodes[active_node_name].select = True
3761 node_tree.nodes.active = node_tree.nodes[active_node_name]
3763 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3764 return {'FINISHED'}
3768 # P A N E L
3771 def drawlayout(context, layout, mode='non-panel'):
3772 tree_type = context.space_data.tree_type
3774 col = layout.column(align=True)
3775 col.menu(NWMergeNodesMenu.bl_idname)
3776 col.separator()
3778 col = layout.column(align=True)
3779 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3780 col.separator()
3782 if tree_type == 'ShaderNodeTree':
3783 col = layout.column(align=True)
3784 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3785 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3786 col.separator()
3788 col = layout.column(align=True)
3789 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3790 col.operator(NWSwapLinks.bl_idname)
3791 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3792 col.separator()
3794 col = layout.column(align=True)
3795 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3796 if tree_type != 'GeometryNodeTree':
3797 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3798 col.separator()
3800 col = layout.column(align=True)
3801 if mode == 'panel':
3802 row = col.row(align=True)
3803 row.operator(NWClearLabel.bl_idname).option = True
3804 row.operator(NWModifyLabels.bl_idname)
3805 else:
3806 col.operator(NWClearLabel.bl_idname).option = True
3807 col.operator(NWModifyLabels.bl_idname)
3808 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3809 col.separator()
3810 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3811 col.separator()
3813 col = layout.column(align=True)
3814 if tree_type == 'CompositorNodeTree':
3815 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
3816 if tree_type != 'GeometryNodeTree':
3817 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
3818 col.separator()
3820 col = layout.column(align=True)
3821 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3822 col.separator()
3824 col = layout.column(align=True)
3825 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
3826 col.separator()
3828 col = layout.column(align=True)
3829 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3830 col.separator()
3833 class NodeWranglerPanel(Panel, NWBase):
3834 bl_idname = "NODE_PT_nw_node_wrangler"
3835 bl_space_type = 'NODE_EDITOR'
3836 bl_label = "Node Wrangler"
3837 bl_region_type = "UI"
3838 bl_category = "Node Wrangler"
3840 prepend: StringProperty(
3841 name='prepend',
3843 append: StringProperty()
3844 remove: StringProperty()
3846 def draw(self, context):
3847 self.layout.label(text="(Quick access: Shift+W)")
3848 drawlayout(context, self.layout, mode='panel')
3852 # M E N U S
3854 class NodeWranglerMenu(Menu, NWBase):
3855 bl_idname = "NODE_MT_nw_node_wrangler_menu"
3856 bl_label = "Node Wrangler"
3858 def draw(self, context):
3859 self.layout.operator_context = 'INVOKE_DEFAULT'
3860 drawlayout(context, self.layout)
3863 class NWMergeNodesMenu(Menu, NWBase):
3864 bl_idname = "NODE_MT_nw_merge_nodes_menu"
3865 bl_label = "Merge Selected Nodes"
3867 def draw(self, context):
3868 type = context.space_data.tree_type
3869 layout = self.layout
3870 if type == 'ShaderNodeTree':
3871 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
3872 if type == 'GeometryNodeTree':
3873 layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes")
3874 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3875 else:
3876 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
3877 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3878 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
3879 props.mode = 'MIX'
3880 props.merge_type = 'ZCOMBINE'
3881 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3882 props.mode = 'MIX'
3883 props.merge_type = 'ALPHAOVER'
3885 class NWMergeGeometryMenu(Menu, NWBase):
3886 bl_idname = "NODE_MT_nw_merge_geometry_menu"
3887 bl_label = "Merge Selected Nodes using Geometry Nodes"
3888 def draw(self, context):
3889 layout = self.layout
3890 # The boolean node + Join Geometry node
3891 for type, name, description in geo_combine_operations:
3892 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3893 props.mode = type
3894 props.merge_type = 'GEOMETRY'
3896 class NWMergeShadersMenu(Menu, NWBase):
3897 bl_idname = "NODE_MT_nw_merge_shaders_menu"
3898 bl_label = "Merge Selected Nodes using Shaders"
3900 def draw(self, context):
3901 layout = self.layout
3902 for type in ('MIX', 'ADD'):
3903 props = layout.operator(NWMergeNodes.bl_idname, text=type)
3904 props.mode = type
3905 props.merge_type = 'SHADER'
3908 class NWMergeMixMenu(Menu, NWBase):
3909 bl_idname = "NODE_MT_nw_merge_mix_menu"
3910 bl_label = "Merge Selected Nodes using Mix"
3912 def draw(self, context):
3913 layout = self.layout
3914 for type, name, description in blend_types:
3915 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3916 props.mode = type
3917 props.merge_type = 'MIX'
3920 class NWConnectionListOutputs(Menu, NWBase):
3921 bl_idname = "NODE_MT_nw_connection_list_out"
3922 bl_label = "From:"
3924 def draw(self, context):
3925 layout = self.layout
3926 nodes, links = get_nodes_links(context)
3928 n1 = nodes[context.scene.NWLazySource]
3929 for index, output in enumerate(n1.outputs):
3930 # Only show sockets that are exposed.
3931 if output.enabled:
3932 layout.operator(NWCallInputsMenu.bl_idname, text=output.name, icon="RADIOBUT_OFF").from_socket=index
3935 class NWConnectionListInputs(Menu, NWBase):
3936 bl_idname = "NODE_MT_nw_connection_list_in"
3937 bl_label = "To:"
3939 def draw(self, context):
3940 layout = self.layout
3941 nodes, links = get_nodes_links(context)
3943 n2 = nodes[context.scene.NWLazyTarget]
3945 for index, input in enumerate(n2.inputs):
3946 # Only show sockets that are exposed.
3947 # This prevents, for example, the scale value socket
3948 # of the vector math node being added to the list when
3949 # the mode is not 'SCALE'.
3950 if input.enabled:
3951 op = layout.operator(NWMakeLink.bl_idname, text=input.name, icon="FORWARD")
3952 op.from_socket = context.scene.NWSourceSocket
3953 op.to_socket = index
3956 class NWMergeMathMenu(Menu, NWBase):
3957 bl_idname = "NODE_MT_nw_merge_math_menu"
3958 bl_label = "Merge Selected Nodes using Math"
3960 def draw(self, context):
3961 layout = self.layout
3962 for type, name, description in operations:
3963 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3964 props.mode = type
3965 props.merge_type = 'MATH'
3968 class NWBatchChangeNodesMenu(Menu, NWBase):
3969 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3970 bl_label = "Batch Change Selected Nodes"
3972 def draw(self, context):
3973 layout = self.layout
3974 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3975 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3978 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3979 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3980 bl_label = "Batch Change Blend Type"
3982 def draw(self, context):
3983 layout = self.layout
3984 for type, name, description in blend_types:
3985 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3986 props.blend_type = type
3987 props.operation = 'CURRENT'
3990 class NWBatchChangeOperationMenu(Menu, NWBase):
3991 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3992 bl_label = "Batch Change Math Operation"
3994 def draw(self, context):
3995 layout = self.layout
3996 for type, name, description in operations:
3997 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3998 props.blend_type = 'CURRENT'
3999 props.operation = type
4002 class NWCopyToSelectedMenu(Menu, NWBase):
4003 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
4004 bl_label = "Copy to Selected"
4006 def draw(self, context):
4007 layout = self.layout
4008 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
4009 layout.menu(NWCopyLabelMenu.bl_idname)
4012 class NWCopyLabelMenu(Menu, NWBase):
4013 bl_idname = "NODE_MT_nw_copy_label_menu"
4014 bl_label = "Copy Label"
4016 def draw(self, context):
4017 layout = self.layout
4018 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
4019 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
4020 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
4023 class NWAddReroutesMenu(Menu, NWBase):
4024 bl_idname = "NODE_MT_nw_add_reroutes_menu"
4025 bl_label = "Add Reroutes"
4026 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
4028 def draw(self, context):
4029 layout = self.layout
4030 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
4031 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
4032 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
4035 class NWLinkActiveToSelectedMenu(Menu, NWBase):
4036 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
4037 bl_label = "Link Active to Selected"
4039 def draw(self, context):
4040 layout = self.layout
4041 layout.menu(NWLinkStandardMenu.bl_idname)
4042 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
4043 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
4046 class NWLinkStandardMenu(Menu, NWBase):
4047 bl_idname = "NODE_MT_nw_link_standard_menu"
4048 bl_label = "To All Selected"
4050 def draw(self, context):
4051 layout = self.layout
4052 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4053 props.replace = False
4054 props.use_node_name = False
4055 props.use_outputs_names = False
4056 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4057 props.replace = True
4058 props.use_node_name = False
4059 props.use_outputs_names = False
4062 class NWLinkUseNodeNameMenu(Menu, NWBase):
4063 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
4064 bl_label = "Use Node Name/Label"
4066 def draw(self, context):
4067 layout = self.layout
4068 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4069 props.replace = False
4070 props.use_node_name = True
4071 props.use_outputs_names = False
4072 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4073 props.replace = True
4074 props.use_node_name = True
4075 props.use_outputs_names = False
4078 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
4079 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
4080 bl_label = "Use Outputs Names"
4082 def draw(self, context):
4083 layout = self.layout
4084 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4085 props.replace = False
4086 props.use_node_name = False
4087 props.use_outputs_names = True
4088 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4089 props.replace = True
4090 props.use_node_name = False
4091 props.use_outputs_names = True
4094 class NWAttributeMenu(bpy.types.Menu):
4095 bl_idname = "NODE_MT_nw_node_attribute_menu"
4096 bl_label = "Attributes"
4098 @classmethod
4099 def poll(cls, context):
4100 valid = False
4101 if nw_check(context):
4102 snode = context.space_data
4103 valid = snode.tree_type == 'ShaderNodeTree'
4104 return valid
4106 def draw(self, context):
4107 l = self.layout
4108 nodes, links = get_nodes_links(context)
4109 mat = context.object.active_material
4111 objs = []
4112 for obj in bpy.data.objects:
4113 for slot in obj.material_slots:
4114 if slot.material == mat:
4115 objs.append(obj)
4116 attrs = []
4117 for obj in objs:
4118 if obj.data.attributes:
4119 for attr in obj.data.attributes:
4120 attrs.append(attr.name)
4121 attrs = list(set(attrs)) # get a unique list
4123 if attrs:
4124 for attr in attrs:
4125 l.operator(NWAddAttrNode.bl_idname, text=attr).attr_name = attr
4126 else:
4127 l.label(text="No attributes on objects with this material")
4130 class NWSwitchNodeTypeMenu(Menu, NWBase):
4131 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4132 bl_label = "Switch Type to..."
4134 def draw(self, context):
4135 layout = self.layout
4136 categories = [c for c in node_categories_iter(context)
4137 if c.name not in ['Group', 'Script']]
4138 for cat in categories:
4139 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4140 if hasattr(bpy.types, idname):
4141 layout.menu(idname)
4142 else:
4143 layout.label(text="Unable to load altered node lists.")
4144 layout.label(text="Please re-enable Node Wrangler.")
4145 break
4148 def draw_switch_category_submenu(self, context):
4149 layout = self.layout
4150 if self.category.name == 'Layout':
4151 for node in self.category.items(context):
4152 if node.nodetype != 'NodeFrame':
4153 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4154 props.to_type = node.nodetype
4155 else:
4156 for node in self.category.items(context):
4157 if isinstance(node, NodeItemCustom):
4158 node.draw(self, layout, context)
4159 continue
4160 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4161 props.to_type = node.nodetype
4164 # APPENDAGES TO EXISTING UI
4168 def select_parent_children_buttons(self, context):
4169 layout = self.layout
4170 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4171 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4174 def attr_nodes_menu_func(self, context):
4175 col = self.layout.column(align=True)
4176 col.menu("NODE_MT_nw_node_attribute_menu")
4177 col.separator()
4180 def multipleimages_menu_func(self, context):
4181 col = self.layout.column(align=True)
4182 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4183 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4184 col.separator()
4187 def bgreset_menu_func(self, context):
4188 self.layout.operator(NWResetBG.bl_idname)
4191 def save_viewer_menu_func(self, context):
4192 if nw_check(context):
4193 if context.space_data.tree_type == 'CompositorNodeTree':
4194 if context.scene.node_tree.nodes.active:
4195 if context.scene.node_tree.nodes.active.type == "VIEWER":
4196 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4199 def reset_nodes_button(self, context):
4200 node_active = context.active_node
4201 node_selected = context.selected_nodes
4202 node_ignore = ["FRAME","REROUTE", "GROUP"]
4204 # Check if active node is in the selection and respective type
4205 if (len(node_selected) == 1) and node_active and node_active.select and node_active.type not in node_ignore:
4206 row = self.layout.row()
4207 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4208 self.layout.separator()
4210 elif (len(node_selected) == 1) and node_active and node_active.select and node_active.type == "FRAME":
4211 row = self.layout.row()
4212 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4213 self.layout.separator()
4217 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4219 switch_category_menus = []
4220 addon_keymaps = []
4221 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4222 # props entry: (property name, property value)
4223 kmi_defs = (
4224 # MERGE NODES
4225 # NWMergeNodes with Ctrl (AUTO).
4226 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4227 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4228 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4229 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4230 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4231 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4232 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4233 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4234 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4235 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4236 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4237 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4238 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4239 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4240 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4241 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4242 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4243 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4244 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4245 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4246 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4247 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4248 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4249 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4250 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4251 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4252 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4253 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4254 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4255 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4256 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4257 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4258 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4259 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4260 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4261 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4262 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4263 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4264 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4265 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4266 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4267 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4268 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4269 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4270 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4271 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4272 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4273 # NWMergeNodes with Ctrl Shift (MATH)
4274 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4275 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4276 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4277 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4278 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4279 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4280 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4281 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4282 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4283 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4284 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4285 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4286 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4287 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4288 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4289 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4290 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4291 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4292 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4293 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4294 # BATCH CHANGE NODES
4295 # NWBatchChangeNodes with Alt
4296 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4297 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4298 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4299 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4300 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4301 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4302 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4303 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4304 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4305 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4306 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4307 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4308 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4309 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4310 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4311 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4312 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4313 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4314 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4315 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4316 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4317 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4318 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4319 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4320 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4321 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4322 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4323 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4324 # LINK ACTIVE TO SELECTED
4325 # Don't use names, don't replace links (K)
4326 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4327 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4328 # Don't use names, replace links (Shift K)
4329 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4330 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4331 # Use node name, don't replace links (')
4332 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4333 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4334 # Use node name, replace links (Shift ')
4335 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4336 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4337 # Don't use names, don't replace links (;)
4338 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4339 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4340 # Don't use names, replace links (')
4341 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4342 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4343 # CHANGE MIX FACTOR
4344 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4345 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4346 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4347 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4348 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4349 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4350 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4351 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4352 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4353 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4354 # CLEAR LABEL (Alt L)
4355 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4356 # MODIFY LABEL (Alt Shift L)
4357 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4358 # Copy Label from active to selected
4359 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4360 # DETACH OUTPUTS (Alt Shift D)
4361 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4362 # LINK TO OUTPUT NODE (O)
4363 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4364 # SELECT PARENT/CHILDREN
4365 # Select Children
4366 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4367 # Select Parent
4368 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4369 # Add Texture Setup
4370 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4371 # Add Principled BSDF Texture Setup
4372 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4373 # Reset backdrop
4374 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4375 # Delete unused
4376 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4377 # Frame Selected
4378 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4379 # Swap Links
4380 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Links"),
4381 # Preview Node
4382 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
4383 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
4384 # Reload Images
4385 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4386 # Lazy Mix
4387 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4388 # Lazy Connect
4389 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
4390 # Lazy Connect with Menu
4391 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4392 # Viewer Tile Center
4393 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4394 # Align Nodes
4395 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4396 # Reset Nodes (Back Space)
4397 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4398 # MENUS
4399 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
4400 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4401 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4402 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4403 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4404 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4408 classes = (
4409 NWPrincipledPreferences,
4410 NWNodeWrangler,
4411 NWLazyMix,
4412 NWLazyConnect,
4413 NWDeleteUnused,
4414 NWSwapLinks,
4415 NWResetBG,
4416 NWAddAttrNode,
4417 NWPreviewNode,
4418 NWFrameSelected,
4419 NWReloadImages,
4420 NWSwitchNodeType,
4421 NWMergeNodes,
4422 NWBatchChangeNodes,
4423 NWChangeMixFactor,
4424 NWCopySettings,
4425 NWCopyLabel,
4426 NWClearLabel,
4427 NWModifyLabels,
4428 NWAddTextureSetup,
4429 NWAddPrincipledSetup,
4430 NWAddReroutes,
4431 NWLinkActiveToSelected,
4432 NWAlignNodes,
4433 NWSelectParentChildren,
4434 NWDetachOutputs,
4435 NWLinkToOutputNode,
4436 NWMakeLink,
4437 NWCallInputsMenu,
4438 NWAddSequence,
4439 NWAddMultipleImages,
4440 NWViewerFocus,
4441 NWSaveViewer,
4442 NWResetNodes,
4443 NodeWranglerPanel,
4444 NodeWranglerMenu,
4445 NWMergeNodesMenu,
4446 NWMergeShadersMenu,
4447 NWMergeGeometryMenu,
4448 NWMergeMixMenu,
4449 NWConnectionListOutputs,
4450 NWConnectionListInputs,
4451 NWMergeMathMenu,
4452 NWBatchChangeNodesMenu,
4453 NWBatchChangeBlendTypeMenu,
4454 NWBatchChangeOperationMenu,
4455 NWCopyToSelectedMenu,
4456 NWCopyLabelMenu,
4457 NWAddReroutesMenu,
4458 NWLinkActiveToSelectedMenu,
4459 NWLinkStandardMenu,
4460 NWLinkUseNodeNameMenu,
4461 NWLinkUseOutputsNamesMenu,
4462 NWAttributeMenu,
4463 NWSwitchNodeTypeMenu,
4466 def register():
4467 from bpy.utils import register_class
4469 # props
4470 bpy.types.Scene.NWBusyDrawing = StringProperty(
4471 name="Busy Drawing!",
4472 default="",
4473 description="An internal property used to store only the first mouse position")
4474 bpy.types.Scene.NWLazySource = StringProperty(
4475 name="Lazy Source!",
4476 default="x",
4477 description="An internal property used to store the first node in a Lazy Connect operation")
4478 bpy.types.Scene.NWLazyTarget = StringProperty(
4479 name="Lazy Target!",
4480 default="x",
4481 description="An internal property used to store the last node in a Lazy Connect operation")
4482 bpy.types.Scene.NWSourceSocket = IntProperty(
4483 name="Source Socket!",
4484 default=0,
4485 description="An internal property used to store the source socket in a Lazy Connect operation")
4486 bpy.types.NodeSocketInterface.NWViewerSocket = BoolProperty(
4487 name="NW Socket",
4488 default=False,
4489 description="An internal property used to determine if a socket is generated by the addon"
4492 for cls in classes:
4493 register_class(cls)
4495 # keymaps
4496 addon_keymaps.clear()
4497 kc = bpy.context.window_manager.keyconfigs.addon
4498 if kc:
4499 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
4500 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
4501 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
4502 if props:
4503 for prop, value in props:
4504 setattr(kmi.properties, prop, value)
4505 addon_keymaps.append((km, kmi))
4507 # menu items
4508 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
4509 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4510 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
4511 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
4512 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4513 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4514 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
4515 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
4517 # switch submenus
4518 switch_category_menus.clear()
4519 for cat in node_categories_iter(None):
4520 if cat.name not in ['Group', 'Script']:
4521 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4522 switch_category_type = type(idname, (bpy.types.Menu,), {
4523 "bl_space_type": 'NODE_EDITOR',
4524 "bl_label": cat.name,
4525 "category": cat,
4526 "poll": cat.poll,
4527 "draw": draw_switch_category_submenu,
4530 switch_category_menus.append(switch_category_type)
4532 bpy.utils.register_class(switch_category_type)
4535 def unregister():
4536 from bpy.utils import unregister_class
4538 # props
4539 del bpy.types.Scene.NWBusyDrawing
4540 del bpy.types.Scene.NWLazySource
4541 del bpy.types.Scene.NWLazyTarget
4542 del bpy.types.Scene.NWSourceSocket
4543 del bpy.types.NodeSocketInterface.NWViewerSocket
4545 for cat_types in switch_category_menus:
4546 bpy.utils.unregister_class(cat_types)
4547 switch_category_menus.clear()
4549 # keymaps
4550 for km, kmi in addon_keymaps:
4551 km.keymap_items.remove(kmi)
4552 addon_keymaps.clear()
4554 # menuitems
4555 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
4556 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4557 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
4558 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
4559 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4560 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
4561 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
4562 bpy.types.NODE_MT_node.remove(reset_nodes_button)
4564 for cls in classes:
4565 unregister_class(cls)
4567 if __name__ == "__main__":
4568 register()