Fix T71100: Node Wrangler creates nodes on linked node trees
[blender-addons.git] / node_wrangler.py
blobaa3b6bc25ab91c57a4db24b728fd135d3beecc57
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, 42),
7 "blender": (3, 4, 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
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('POLYLINE_SMOOTH_COLOR')
394 shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
395 shader.uniform_float("lineWidth", size * dpifac())
397 vertices = ((x1, y1), (x2, y2))
398 vertex_colors = ((colour[0]+(1.0-colour[0])/4,
399 colour[1]+(1.0-colour[1])/4,
400 colour[2]+(1.0-colour[2])/4,
401 colour[3]+(1.0-colour[3])/4),
402 colour)
404 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
405 batch.draw(shader)
408 def draw_circle_2d_filled(mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
409 radius = radius * dpifac()
410 sides = 12
411 vertices = [(radius * cos(i * 2 * pi / sides) + mx,
412 radius * sin(i * 2 * pi / sides) + my)
413 for i in range(sides + 1)]
415 shader = gpu.shader.from_builtin('UNIFORM_COLOR')
416 shader.uniform_float("color", colour)
417 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
418 batch.draw(shader)
421 def draw_rounded_node_border(node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
422 area_width = bpy.context.area.width
423 sides = 16
424 radius = radius*dpifac()
426 nlocx, nlocy = abs_node_location(node)
428 nlocx = (nlocx+1)*dpifac()
429 nlocy = (nlocy+1)*dpifac()
430 ndimx = node.dimensions.x
431 ndimy = node.dimensions.y
433 if node.hide:
434 nlocx += -1
435 nlocy += 5
436 if node.type == 'REROUTE':
437 #nlocx += 1
438 nlocy -= 1
439 ndimx = 0
440 ndimy = 0
441 radius += 6
443 shader = gpu.shader.from_builtin('UNIFORM_COLOR')
444 shader.uniform_float("color", colour)
446 # Top left corner
447 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
448 vertices = [(mx,my)]
449 for i in range(sides+1):
450 if (4<=i<=8):
451 if mx < area_width:
452 cosine = radius * cos(i * 2 * pi / sides) + mx
453 sine = radius * sin(i * 2 * pi / sides) + my
454 vertices.append((cosine,sine))
456 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
457 batch.draw(shader)
459 # Top right corner
460 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
461 vertices = [(mx,my)]
462 for i in range(sides+1):
463 if (0<=i<=4):
464 if mx < area_width:
465 cosine = radius * cos(i * 2 * pi / sides) + mx
466 sine = radius * sin(i * 2 * pi / sides) + my
467 vertices.append((cosine,sine))
469 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
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))
482 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
483 batch.draw(shader)
485 # Bottom right corner
486 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
487 vertices = [(mx,my)]
488 for i in range(sides+1):
489 if (12<=i<=16):
490 if mx < area_width:
491 cosine = radius * cos(i * 2 * pi / sides) + mx
492 sine = radius * sin(i * 2 * pi / sides) + my
493 vertices.append((cosine,sine))
495 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
496 batch.draw(shader)
498 # prepare drawing all edges in one batch
499 vertices = []
500 indices = []
501 id_last = 0
503 # Left edge
504 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
505 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
506 if m1x < area_width and m2x < area_width:
507 vertices.extend([(m2x-radius,m2y), (m2x,m2y),
508 (m1x,m1y), (m1x-radius,m1y)])
509 indices.extend([(id_last, id_last+1, id_last+3),
510 (id_last+3, id_last+1, id_last+2)])
511 id_last += 4
513 # Top edge
514 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
515 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
516 m1x = min(m1x, area_width)
517 m2x = min(m2x, area_width)
518 vertices.extend([(m1x,m1y), (m2x,m1y),
519 (m2x,m1y+radius), (m1x,m1y+radius)])
520 indices.extend([(id_last, id_last+1, id_last+3),
521 (id_last+3, id_last+1, id_last+2)])
522 id_last += 4
524 # Right edge
525 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
526 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
527 if m1x < area_width and m2x < area_width:
528 vertices.extend([(m1x,m2y), (m1x+radius,m2y),
529 (m1x+radius,m1y), (m1x,m1y)])
530 indices.extend([(id_last, id_last+1, id_last+3),
531 (id_last+3, id_last+1, id_last+2)])
532 id_last += 4
534 # Bottom edge
535 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
536 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
537 m1x = min(m1x, area_width)
538 m2x = min(m2x, area_width)
539 vertices.extend([(m1x,m2y), (m2x,m2y),
540 (m2x,m1y-radius), (m1x,m1y-radius)])
541 indices.extend([(id_last, id_last+1, id_last+3),
542 (id_last+3, id_last+1, id_last+2)])
544 # now draw all edges in one batch
545 if len(vertices) != 0:
546 batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
547 batch.draw(shader)
549 def draw_callback_nodeoutline(self, context, mode):
550 if self.mouse_path:
551 gpu.state.blend_set('ALPHA')
553 nodes, links = get_nodes_links(context)
555 if mode == "LINK":
556 col_outer = (1.0, 0.2, 0.2, 0.4)
557 col_inner = (0.0, 0.0, 0.0, 0.5)
558 col_circle_inner = (0.3, 0.05, 0.05, 1.0)
559 elif mode == "LINKMENU":
560 col_outer = (0.4, 0.6, 1.0, 0.4)
561 col_inner = (0.0, 0.0, 0.0, 0.5)
562 col_circle_inner = (0.08, 0.15, .3, 1.0)
563 elif mode == "MIX":
564 col_outer = (0.2, 1.0, 0.2, 0.4)
565 col_inner = (0.0, 0.0, 0.0, 0.5)
566 col_circle_inner = (0.05, 0.3, 0.05, 1.0)
568 m1x = self.mouse_path[0][0]
569 m1y = self.mouse_path[0][1]
570 m2x = self.mouse_path[-1][0]
571 m2y = self.mouse_path[-1][1]
573 n1 = nodes[context.scene.NWLazySource]
574 n2 = nodes[context.scene.NWLazyTarget]
576 if n1 == n2:
577 col_outer = (0.4, 0.4, 0.4, 0.4)
578 col_inner = (0.0, 0.0, 0.0, 0.5)
579 col_circle_inner = (0.2, 0.2, 0.2, 1.0)
581 draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
582 draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
583 draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
584 draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
586 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
587 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
589 # circle outline
590 draw_circle_2d_filled(m1x, m1y, 7, col_outer)
591 draw_circle_2d_filled(m2x, m2y, 7, col_outer)
593 # circle inner
594 draw_circle_2d_filled(m1x, m1y, 5, col_circle_inner)
595 draw_circle_2d_filled(m2x, m2y, 5, col_circle_inner)
597 gpu.state.blend_set('NONE')
599 def get_active_tree(context):
600 tree = context.space_data.node_tree
601 path = []
602 # Get nodes from currently edited tree.
603 # If user is editing a group, space_data.node_tree is still the base level (outside group).
604 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
605 # the same as context.active_node, the user is in a group.
606 # Check recursively until we find the real active node_tree:
607 if tree.nodes.active:
608 while tree.nodes.active != context.active_node:
609 tree = tree.nodes.active.node_tree
610 path.append(tree)
611 return tree, path
613 def get_nodes_links(context):
614 tree, path = get_active_tree(context)
615 return tree.nodes, tree.links
617 def is_viewer_socket(socket):
618 # checks if a internal socket is a valid viewer socket
619 return socket.name == viewer_socket_name and socket.NWViewerSocket
621 def get_internal_socket(socket):
622 #get the internal socket from a socket inside or outside the group
623 node = socket.node
624 if node.type == 'GROUP_OUTPUT':
625 source_iterator = node.inputs
626 iterator = node.id_data.outputs
627 elif node.type == 'GROUP_INPUT':
628 source_iterator = node.outputs
629 iterator = node.id_data.inputs
630 elif hasattr(node, "node_tree"):
631 if socket.is_output:
632 source_iterator = node.outputs
633 iterator = node.node_tree.outputs
634 else:
635 source_iterator = node.inputs
636 iterator = node.node_tree.inputs
637 else:
638 return None
640 for i, s in enumerate(source_iterator):
641 if s == socket:
642 break
643 return iterator[i]
645 def is_viewer_link(link, output_node):
646 if link.to_node == output_node and link.to_socket == output_node.inputs[0]:
647 return True
648 if link.to_node.type == 'GROUP_OUTPUT':
649 socket = get_internal_socket(link.to_socket)
650 if is_viewer_socket(socket):
651 return True
652 return False
654 def get_group_output_node(tree):
655 for node in tree.nodes:
656 if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
657 return node
659 def get_output_location(tree):
660 # get right-most location
661 sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
662 max_xloc_node = sorted_by_xloc[-1]
664 # get average y location
665 sum_yloc = 0
666 for node in tree.nodes:
667 sum_yloc += node.location.y
669 loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
670 loc_y = sum_yloc / len(tree.nodes)
671 return loc_x, loc_y
673 # Principled prefs
674 class NWPrincipledPreferences(bpy.types.PropertyGroup):
675 base_color: StringProperty(
676 name='Base Color',
677 default='diffuse diff albedo base col color',
678 description='Naming Components for Base Color maps')
679 sss_color: StringProperty(
680 name='Subsurface Color',
681 default='sss subsurface',
682 description='Naming Components for Subsurface Color maps')
683 metallic: StringProperty(
684 name='Metallic',
685 default='metallic metalness metal mtl',
686 description='Naming Components for metallness maps')
687 specular: StringProperty(
688 name='Specular',
689 default='specularity specular spec spc',
690 description='Naming Components for Specular maps')
691 normal: StringProperty(
692 name='Normal',
693 default='normal nor nrm nrml norm',
694 description='Naming Components for Normal maps')
695 bump: StringProperty(
696 name='Bump',
697 default='bump bmp',
698 description='Naming Components for bump maps')
699 rough: StringProperty(
700 name='Roughness',
701 default='roughness rough rgh',
702 description='Naming Components for roughness maps')
703 gloss: StringProperty(
704 name='Gloss',
705 default='gloss glossy glossiness',
706 description='Naming Components for glossy maps')
707 displacement: StringProperty(
708 name='Displacement',
709 default='displacement displace disp dsp height heightmap',
710 description='Naming Components for displacement maps')
711 transmission: StringProperty(
712 name='Transmission',
713 default='transmission transparency',
714 description='Naming Components for transmission maps')
715 emission: StringProperty(
716 name='Emission',
717 default='emission emissive emit',
718 description='Naming Components for emission maps')
719 alpha: StringProperty(
720 name='Alpha',
721 default='alpha opacity',
722 description='Naming Components for alpha maps')
723 ambient_occlusion: StringProperty(
724 name='Ambient Occlusion',
725 default='ao ambient occlusion',
726 description='Naming Components for AO maps')
728 # Addon prefs
729 class NWNodeWrangler(bpy.types.AddonPreferences):
730 bl_idname = __name__
732 merge_hide: EnumProperty(
733 name="Hide Mix nodes",
734 items=(
735 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
736 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
737 ("NEVER", "Never", "Never collapse the new merge nodes")
739 default='NON_SHADER',
740 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
741 merge_position: EnumProperty(
742 name="Mix Node Position",
743 items=(
744 ("CENTER", "Center", "Place the Mix node between the two nodes"),
745 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
747 default='CENTER',
748 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
750 show_hotkey_list: BoolProperty(
751 name="Show Hotkey List",
752 default=False,
753 description="Expand this box into a list of all the hotkeys for functions in this addon"
755 hotkey_list_filter: StringProperty(
756 name=" Filter by Name",
757 default="",
758 description="Show only hotkeys that have this text in their name",
759 options={'TEXTEDIT_UPDATE'}
761 show_principled_lists: BoolProperty(
762 name="Show Principled naming tags",
763 default=False,
764 description="Expand this box into a list of all naming tags for principled texture setup"
766 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
768 def draw(self, context):
769 layout = self.layout
770 col = layout.column()
771 col.prop(self, "merge_position")
772 col.prop(self, "merge_hide")
774 box = layout.box()
775 col = box.column(align=True)
776 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
777 if self.show_principled_lists:
778 tags = self.principled_tags
780 col.prop(tags, "base_color")
781 col.prop(tags, "sss_color")
782 col.prop(tags, "metallic")
783 col.prop(tags, "specular")
784 col.prop(tags, "rough")
785 col.prop(tags, "gloss")
786 col.prop(tags, "normal")
787 col.prop(tags, "bump")
788 col.prop(tags, "displacement")
789 col.prop(tags, "transmission")
790 col.prop(tags, "emission")
791 col.prop(tags, "alpha")
792 col.prop(tags, "ambient_occlusion")
794 box = layout.box()
795 col = box.column(align=True)
796 hotkey_button_name = "Show Hotkey List"
797 if self.show_hotkey_list:
798 hotkey_button_name = "Hide Hotkey List"
799 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
800 if self.show_hotkey_list:
801 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
802 col.separator()
803 for hotkey in kmi_defs:
804 if hotkey[7]:
805 hotkey_name = hotkey[7]
807 if self.hotkey_list_filter.lower() in hotkey_name.lower():
808 row = col.row(align=True)
809 row.label(text=hotkey_name)
810 keystr = nice_hotkey_name(hotkey[1])
811 if hotkey[4]:
812 keystr = "Shift " + keystr
813 if hotkey[5]:
814 keystr = "Alt " + keystr
815 if hotkey[3]:
816 keystr = "Ctrl " + keystr
817 row.label(text=keystr)
821 def nw_check(context):
822 space = context.space_data
823 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
825 if (space.type == 'NODE_EDITOR'
826 and space.node_tree is not None
827 and space.node_tree.library is None
828 and space.tree_type in valid_trees):
829 return True
831 return False
833 class NWBase:
834 @classmethod
835 def poll(cls, context):
836 return nw_check(context)
839 # OPERATORS
840 class NWLazyMix(Operator, NWBase):
841 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
842 bl_idname = "node.nw_lazy_mix"
843 bl_label = "Mix Nodes"
844 bl_options = {'REGISTER', 'UNDO'}
846 def modal(self, context, event):
847 context.area.tag_redraw()
848 nodes, links = get_nodes_links(context)
849 cont = True
851 start_pos = [event.mouse_region_x, event.mouse_region_y]
853 node1 = None
854 if not context.scene.NWBusyDrawing:
855 node1 = node_at_pos(nodes, context, event)
856 if node1:
857 context.scene.NWBusyDrawing = node1.name
858 else:
859 if context.scene.NWBusyDrawing != 'STOP':
860 node1 = nodes[context.scene.NWBusyDrawing]
862 context.scene.NWLazySource = node1.name
863 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
865 if event.type == 'MOUSEMOVE':
866 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
868 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
869 end_pos = [event.mouse_region_x, event.mouse_region_y]
870 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
872 node2 = None
873 node2 = node_at_pos(nodes, context, event)
874 if node2:
875 context.scene.NWBusyDrawing = node2.name
877 if node1 == node2:
878 cont = False
880 if cont:
881 if node1 and node2:
882 for node in nodes:
883 node.select = False
884 node1.select = True
885 node2.select = True
887 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
889 context.scene.NWBusyDrawing = ""
890 return {'FINISHED'}
892 elif event.type == 'ESC':
893 print('cancelled')
894 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
895 return {'CANCELLED'}
897 return {'RUNNING_MODAL'}
899 def invoke(self, context, event):
900 if context.area.type == 'NODE_EDITOR':
901 # the arguments we pass the the callback
902 args = (self, context, 'MIX')
903 # Add the region OpenGL drawing callback
904 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
905 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
907 self.mouse_path = []
909 context.window_manager.modal_handler_add(self)
910 return {'RUNNING_MODAL'}
911 else:
912 self.report({'WARNING'}, "View3D not found, cannot run operator")
913 return {'CANCELLED'}
916 class NWLazyConnect(Operator, NWBase):
917 """Connect two nodes without clicking a specific socket (automatically determined"""
918 bl_idname = "node.nw_lazy_connect"
919 bl_label = "Lazy Connect"
920 bl_options = {'REGISTER', 'UNDO'}
921 with_menu: BoolProperty()
923 def modal(self, context, event):
924 context.area.tag_redraw()
925 nodes, links = get_nodes_links(context)
926 cont = True
928 start_pos = [event.mouse_region_x, event.mouse_region_y]
930 node1 = None
931 if not context.scene.NWBusyDrawing:
932 node1 = node_at_pos(nodes, context, event)
933 if node1:
934 context.scene.NWBusyDrawing = node1.name
935 else:
936 if context.scene.NWBusyDrawing != 'STOP':
937 node1 = nodes[context.scene.NWBusyDrawing]
939 context.scene.NWLazySource = node1.name
940 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
942 if event.type == 'MOUSEMOVE':
943 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
945 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
946 end_pos = [event.mouse_region_x, event.mouse_region_y]
947 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
949 node2 = None
950 node2 = node_at_pos(nodes, context, event)
951 if node2:
952 context.scene.NWBusyDrawing = node2.name
954 if node1 == node2:
955 cont = False
957 link_success = False
958 if cont:
959 if node1 and node2:
960 original_sel = []
961 original_unsel = []
962 for node in nodes:
963 if node.select == True:
964 node.select = False
965 original_sel.append(node)
966 else:
967 original_unsel.append(node)
968 node1.select = True
969 node2.select = True
971 #link_success = autolink(node1, node2, links)
972 if self.with_menu:
973 if len(node1.outputs) > 1 and node2.inputs:
974 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
975 elif len(node1.outputs) == 1:
976 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
977 else:
978 link_success = autolink(node1, node2, links)
980 for node in original_sel:
981 node.select = True
982 for node in original_unsel:
983 node.select = False
985 if link_success:
986 force_update(context)
987 context.scene.NWBusyDrawing = ""
988 return {'FINISHED'}
990 elif event.type == 'ESC':
991 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
992 return {'CANCELLED'}
994 return {'RUNNING_MODAL'}
996 def invoke(self, context, event):
997 if context.area.type == 'NODE_EDITOR':
998 nodes, links = get_nodes_links(context)
999 node = node_at_pos(nodes, context, event)
1000 if node:
1001 context.scene.NWBusyDrawing = node.name
1003 # the arguments we pass the the callback
1004 mode = "LINK"
1005 if self.with_menu:
1006 mode = "LINKMENU"
1007 args = (self, context, mode)
1008 # Add the region OpenGL drawing callback
1009 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1010 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1012 self.mouse_path = []
1014 context.window_manager.modal_handler_add(self)
1015 return {'RUNNING_MODAL'}
1016 else:
1017 self.report({'WARNING'}, "View3D not found, cannot run operator")
1018 return {'CANCELLED'}
1021 class NWDeleteUnused(Operator, NWBase):
1022 """Delete all nodes whose output is not used"""
1023 bl_idname = 'node.nw_del_unused'
1024 bl_label = 'Delete Unused Nodes'
1025 bl_options = {'REGISTER', 'UNDO'}
1027 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1028 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1030 def is_unused_node(self, node):
1031 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1032 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1033 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1034 if node.type in end_types:
1035 return False
1037 for output in node.outputs:
1038 if output.links:
1039 return False
1040 return True
1042 @classmethod
1043 def poll(cls, context):
1044 valid = False
1045 if nw_check(context):
1046 if context.space_data.node_tree.nodes:
1047 valid = True
1048 return valid
1050 def execute(self, context):
1051 nodes, links = get_nodes_links(context)
1053 # Store selection
1054 selection = []
1055 for node in nodes:
1056 if node.select == True:
1057 selection.append(node.name)
1059 for node in nodes:
1060 node.select = False
1062 deleted_nodes = []
1063 temp_deleted_nodes = []
1064 del_unused_iterations = len(nodes)
1065 for it in range(0, del_unused_iterations):
1066 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1067 for node in nodes:
1068 if self.is_unused_node(node):
1069 node.select = True
1070 deleted_nodes.append(node.name)
1071 bpy.ops.node.delete()
1073 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1074 break
1076 if self.delete_frames:
1077 repeat = True
1078 while repeat:
1079 frames_in_use = []
1080 frames = []
1081 repeat = False
1082 for node in nodes:
1083 if node.parent:
1084 frames_in_use.append(node.parent)
1085 for node in nodes:
1086 if node.type == 'FRAME' and node not in frames_in_use:
1087 frames.append(node)
1088 if node.parent:
1089 repeat = True # repeat for nested frames
1090 for node in frames:
1091 if node not in frames_in_use:
1092 node.select = True
1093 deleted_nodes.append(node.name)
1094 bpy.ops.node.delete()
1096 if self.delete_muted:
1097 for node in nodes:
1098 if node.mute:
1099 node.select = True
1100 deleted_nodes.append(node.name)
1101 bpy.ops.node.delete_reconnect()
1103 # get unique list of deleted nodes (iterations would count the same node more than once)
1104 deleted_nodes = list(set(deleted_nodes))
1105 for n in deleted_nodes:
1106 self.report({'INFO'}, "Node " + n + " deleted")
1107 num_deleted = len(deleted_nodes)
1108 n = ' node'
1109 if num_deleted > 1:
1110 n += 's'
1111 if num_deleted:
1112 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1113 else:
1114 self.report({'INFO'}, "Nothing deleted")
1116 # Restore selection
1117 nodes, links = get_nodes_links(context)
1118 for node in nodes:
1119 if node.name in selection:
1120 node.select = True
1121 return {'FINISHED'}
1123 def invoke(self, context, event):
1124 return context.window_manager.invoke_confirm(self, event)
1127 class NWSwapLinks(Operator, NWBase):
1128 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1129 bl_idname = 'node.nw_swap_links'
1130 bl_label = 'Swap Links'
1131 bl_options = {'REGISTER', 'UNDO'}
1133 @classmethod
1134 def poll(cls, context):
1135 valid = False
1136 if nw_check(context):
1137 if context.selected_nodes:
1138 valid = len(context.selected_nodes) <= 2
1139 return valid
1141 def execute(self, context):
1142 nodes, links = get_nodes_links(context)
1143 selected_nodes = context.selected_nodes
1144 n1 = selected_nodes[0]
1146 # Swap outputs
1147 if len(selected_nodes) == 2:
1148 n2 = selected_nodes[1]
1149 if n1.outputs and n2.outputs:
1150 n1_outputs = []
1151 n2_outputs = []
1153 out_index = 0
1154 for output in n1.outputs:
1155 if output.links:
1156 for link in output.links:
1157 n1_outputs.append([out_index, link.to_socket])
1158 links.remove(link)
1159 out_index += 1
1161 out_index = 0
1162 for output in n2.outputs:
1163 if output.links:
1164 for link in output.links:
1165 n2_outputs.append([out_index, link.to_socket])
1166 links.remove(link)
1167 out_index += 1
1169 for connection in n1_outputs:
1170 try:
1171 links.new(n2.outputs[connection[0]], connection[1])
1172 except:
1173 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1174 for connection in n2_outputs:
1175 try:
1176 links.new(n1.outputs[connection[0]], connection[1])
1177 except:
1178 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1179 else:
1180 if n1.outputs or n2.outputs:
1181 self.report({'WARNING'}, "One of the nodes has no outputs!")
1182 else:
1183 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1185 # Swap Inputs
1186 elif len(selected_nodes) == 1:
1187 if n1.inputs and n1.inputs[0].is_multi_input:
1188 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1189 return {'FINISHED'}
1190 if n1.inputs:
1191 types = []
1193 for i1 in n1.inputs:
1194 if i1.is_linked and not i1.is_multi_input:
1195 similar_types = 0
1196 for i2 in n1.inputs:
1197 if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
1198 similar_types += 1
1199 types.append ([i1, similar_types, i])
1200 i += 1
1201 types.sort(key=lambda k: k[1], reverse=True)
1203 if types:
1204 t = types[0]
1205 if t[1] == 2:
1206 for i2 in n1.inputs:
1207 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1208 pair = [t[0], i2]
1209 i1f = pair[0].links[0].from_socket
1210 i1t = pair[0].links[0].to_socket
1211 i2f = pair[1].links[0].from_socket
1212 i2t = pair[1].links[0].to_socket
1213 links.new(i1f, i2t)
1214 links.new(i2f, i1t)
1215 if t[1] == 1:
1216 if len(types) == 1:
1217 fs = t[0].links[0].from_socket
1218 i = t[2]
1219 links.remove(t[0].links[0])
1220 if i+1 == len(n1.inputs):
1221 i = -1
1222 i += 1
1223 while n1.inputs[i].is_linked:
1224 i += 1
1225 links.new(fs, n1.inputs[i])
1226 elif len(types) == 2:
1227 i1f = types[0][0].links[0].from_socket
1228 i1t = types[0][0].links[0].to_socket
1229 i2f = types[1][0].links[0].from_socket
1230 i2t = types[1][0].links[0].to_socket
1231 links.new(i1f, i2t)
1232 links.new(i2f, i1t)
1234 else:
1235 self.report({'WARNING'}, "This node has no input connections to swap!")
1236 else:
1237 self.report({'WARNING'}, "This node has no inputs to swap!")
1239 force_update(context)
1240 return {'FINISHED'}
1243 class NWResetBG(Operator, NWBase):
1244 """Reset the zoom and position of the background image"""
1245 bl_idname = 'node.nw_bg_reset'
1246 bl_label = 'Reset Backdrop'
1247 bl_options = {'REGISTER', 'UNDO'}
1249 @classmethod
1250 def poll(cls, context):
1251 valid = False
1252 if nw_check(context):
1253 snode = context.space_data
1254 valid = snode.tree_type == 'CompositorNodeTree'
1255 return valid
1257 def execute(self, context):
1258 context.space_data.backdrop_zoom = 1
1259 context.space_data.backdrop_offset[0] = 0
1260 context.space_data.backdrop_offset[1] = 0
1261 return {'FINISHED'}
1264 class NWAddAttrNode(Operator, NWBase):
1265 """Add an Attribute node with this name"""
1266 bl_idname = 'node.nw_add_attr_node'
1267 bl_label = 'Add UV map'
1268 bl_options = {'REGISTER', 'UNDO'}
1270 attr_name: StringProperty()
1272 def execute(self, context):
1273 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1274 nodes, links = get_nodes_links(context)
1275 nodes.active.attribute_name = self.attr_name
1276 return {'FINISHED'}
1278 class NWPreviewNode(Operator, NWBase):
1279 bl_idname = "node.nw_preview_node"
1280 bl_label = "Preview Node"
1281 bl_description = "Connect active node to the Node Group output or the Material Output"
1282 bl_options = {'REGISTER', 'UNDO'}
1284 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1285 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1286 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1287 run_in_geometry_nodes: BoolProperty(default=True)
1289 def __init__(self):
1290 self.shader_output_type = ""
1291 self.shader_output_ident = ""
1293 @classmethod
1294 def poll(cls, context):
1295 if nw_check(context):
1296 space = context.space_data
1297 if space.tree_type == 'ShaderNodeTree' or space.tree_type == 'GeometryNodeTree':
1298 if context.active_node:
1299 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1300 return True
1301 else:
1302 return True
1303 return False
1305 def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
1306 #check if a viewer output already exists in a node group otherwise create
1307 if hasattr(node, "node_tree"):
1308 index = None
1309 if len(node.node_tree.outputs):
1310 free_socket = None
1311 for i, socket in enumerate(node.node_tree.outputs):
1312 if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
1313 #if viewer output is already used but leads to the same socket we can still use it
1314 is_used = self.is_socket_used_other_mats(socket)
1315 if is_used:
1316 if connect_socket == None:
1317 continue
1318 groupout = get_group_output_node(node.node_tree)
1319 groupout_input = groupout.inputs[i]
1320 links = groupout_input.links
1321 if connect_socket not in [link.from_socket for link in links]:
1322 continue
1323 index=i
1324 break
1325 if not free_socket:
1326 free_socket = i
1327 if not index and free_socket:
1328 index = free_socket
1330 if not index:
1331 #create viewer socket
1332 node.node_tree.outputs.new(socket_type, viewer_socket_name)
1333 index = len(node.node_tree.outputs) - 1
1334 node.node_tree.outputs[index].NWViewerSocket = True
1335 return index
1337 def init_shader_variables(self, space, shader_type):
1338 if shader_type == 'OBJECT':
1339 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1340 self.shader_output_type = "OUTPUT_MATERIAL"
1341 self.shader_output_ident = "ShaderNodeOutputMaterial"
1342 else:
1343 self.shader_output_type = "OUTPUT_LIGHT"
1344 self.shader_output_ident = "ShaderNodeOutputLight"
1346 elif shader_type == 'WORLD':
1347 self.shader_output_type = "OUTPUT_WORLD"
1348 self.shader_output_ident = "ShaderNodeOutputWorld"
1350 def get_shader_output_node(self, tree):
1351 for node in tree.nodes:
1352 if node.type == self.shader_output_type and node.is_active_output == True:
1353 return node
1355 @classmethod
1356 def ensure_group_output(cls, tree):
1357 #check if a group output node exists otherwise create
1358 groupout = get_group_output_node(tree)
1359 if not groupout:
1360 groupout = tree.nodes.new('NodeGroupOutput')
1361 loc_x, loc_y = get_output_location(tree)
1362 groupout.location.x = loc_x
1363 groupout.location.y = loc_y
1364 groupout.select = False
1365 # So that we don't keep on adding new group outputs
1366 groupout.is_active_output = True
1367 return groupout
1369 @classmethod
1370 def search_sockets(cls, node, sockets, index=None):
1371 # recursively scan nodes for viewer sockets and store in list
1372 for i, input_socket in enumerate(node.inputs):
1373 if index and i != index:
1374 continue
1375 if len(input_socket.links):
1376 link = input_socket.links[0]
1377 next_node = link.from_node
1378 external_socket = link.from_socket
1379 if hasattr(next_node, "node_tree"):
1380 for socket_index, s in enumerate(next_node.outputs):
1381 if s == external_socket:
1382 break
1383 socket = next_node.node_tree.outputs[socket_index]
1384 if is_viewer_socket(socket) and socket not in sockets:
1385 sockets.append(socket)
1386 #continue search inside of node group but restrict socket to where we came from
1387 groupout = get_group_output_node(next_node.node_tree)
1388 cls.search_sockets(groupout, sockets, index=socket_index)
1390 @classmethod
1391 def scan_nodes(cls, tree, sockets):
1392 # get all viewer sockets in a material tree
1393 for node in tree.nodes:
1394 if hasattr(node, "node_tree"):
1395 for socket in node.node_tree.outputs:
1396 if is_viewer_socket(socket) and (socket not in sockets):
1397 sockets.append(socket)
1398 cls.scan_nodes(node.node_tree, sockets)
1400 def link_leads_to_used_socket(self, link):
1401 #return True if link leads to a socket that is already used in this material
1402 socket = get_internal_socket(link.to_socket)
1403 return (socket and self.is_socket_used_active_mat(socket))
1405 def is_socket_used_active_mat(self, socket):
1406 #ensure used sockets in active material is calculated and check given socket
1407 if not hasattr(self, "used_viewer_sockets_active_mat"):
1408 self.used_viewer_sockets_active_mat = []
1409 materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
1410 if materialout:
1411 self.search_sockets(materialout, self.used_viewer_sockets_active_mat)
1412 return socket in self.used_viewer_sockets_active_mat
1414 def is_socket_used_other_mats(self, socket):
1415 #ensure used sockets in other materials are calculated and check given socket
1416 if not hasattr(self, "used_viewer_sockets_other_mats"):
1417 self.used_viewer_sockets_other_mats = []
1418 for mat in bpy.data.materials:
1419 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
1420 continue
1421 # get viewer node
1422 materialout = self.get_shader_output_node(mat.node_tree)
1423 if materialout:
1424 self.search_sockets(materialout, self.used_viewer_sockets_other_mats)
1425 return socket in self.used_viewer_sockets_other_mats
1427 def invoke(self, context, event):
1428 space = context.space_data
1429 # Ignore operator when running in wrong context.
1430 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
1431 return {'PASS_THROUGH'}
1433 shader_type = space.shader_type
1434 self.init_shader_variables(space, shader_type)
1435 mlocx = event.mouse_region_x
1436 mlocy = event.mouse_region_y
1437 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
1438 if 'FINISHED' in select_node: # only run if mouse click is on a node
1439 active_tree, path_to_tree = get_active_tree(context)
1440 nodes, links = active_tree.nodes, active_tree.links
1441 base_node_tree = space.node_tree
1442 active = nodes.active
1444 # For geometry node trees we just connect to the group output
1445 if space.tree_type == "GeometryNodeTree":
1446 valid = False
1447 if active:
1448 for out in active.outputs:
1449 if is_visible_socket(out):
1450 valid = True
1451 break
1452 # Exit early
1453 if not valid:
1454 return {'FINISHED'}
1456 delete_sockets = []
1458 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1459 self.scan_nodes(base_node_tree, delete_sockets)
1461 # Find (or create if needed) the output of this node tree
1462 geometryoutput = self.ensure_group_output(base_node_tree)
1464 # Analyze outputs, make links
1465 out_i = None
1466 valid_outputs = []
1467 for i, out in enumerate(active.outputs):
1468 if is_visible_socket(out) and out.type == 'GEOMETRY':
1469 valid_outputs.append(i)
1470 if valid_outputs:
1471 out_i = valid_outputs[0] # Start index of node's outputs
1472 for i, valid_i in enumerate(valid_outputs):
1473 for out_link in active.outputs[valid_i].links:
1474 if is_viewer_link(out_link, geometryoutput):
1475 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1476 if i < len(valid_outputs) - 1:
1477 out_i = valid_outputs[i + 1]
1478 else:
1479 out_i = valid_outputs[0]
1481 make_links = [] # store sockets for new links
1482 if active.outputs:
1483 # If there is no 'GEOMETRY' output type - We can't preview the node
1484 if out_i is None:
1485 return {'FINISHED'}
1486 socket_type = 'GEOMETRY'
1487 # Find an input socket of the output of type geometry
1488 geometryoutindex = None
1489 for i,inp in enumerate(geometryoutput.inputs):
1490 if inp.type == socket_type:
1491 geometryoutindex = i
1492 break
1493 if geometryoutindex is None:
1494 # Create geometry socket
1495 geometryoutput.inputs.new(socket_type, 'Geometry')
1496 geometryoutindex = len(geometryoutput.inputs) - 1
1498 make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
1499 output_socket = geometryoutput.inputs[geometryoutindex]
1500 for li_from, li_to in make_links:
1501 base_node_tree.links.new(li_from, li_to)
1502 tree = base_node_tree
1503 link_end = output_socket
1504 while tree.nodes.active != active:
1505 node = tree.nodes.active
1506 index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1507 link_start = node.outputs[index]
1508 node_socket = node.node_tree.outputs[index]
1509 if node_socket in delete_sockets:
1510 delete_sockets.remove(node_socket)
1511 tree.links.new(link_start, link_end)
1512 # Iterate
1513 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1514 tree = tree.nodes.active.node_tree
1515 tree.links.new(active.outputs[out_i], link_end)
1517 # Delete sockets
1518 for socket in delete_sockets:
1519 tree = socket.id_data
1520 tree.outputs.remove(socket)
1522 nodes.active = active
1523 active.select = True
1524 force_update(context)
1525 return {'FINISHED'}
1528 # What follows is code for the shader editor
1529 output_types = [x.nodetype for x in
1530 get_nodes_from_category('Output', context)]
1531 valid = False
1532 if active:
1533 if active.rna_type.identifier not in output_types:
1534 for out in active.outputs:
1535 if is_visible_socket(out):
1536 valid = True
1537 break
1538 if valid:
1539 # get material_output node
1540 materialout = None # placeholder node
1541 delete_sockets = []
1543 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1544 self.scan_nodes(base_node_tree, delete_sockets)
1546 materialout = self.get_shader_output_node(base_node_tree)
1547 if not materialout:
1548 materialout = base_node_tree.nodes.new(self.shader_output_ident)
1549 materialout.location = get_output_location(base_node_tree)
1550 materialout.select = False
1551 # Analyze outputs
1552 out_i = None
1553 valid_outputs = []
1554 for i, out in enumerate(active.outputs):
1555 if is_visible_socket(out):
1556 valid_outputs.append(i)
1557 if valid_outputs:
1558 out_i = valid_outputs[0] # Start index of node's outputs
1559 for i, valid_i in enumerate(valid_outputs):
1560 for out_link in active.outputs[valid_i].links:
1561 if is_viewer_link(out_link, materialout):
1562 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1563 if i < len(valid_outputs) - 1:
1564 out_i = valid_outputs[i + 1]
1565 else:
1566 out_i = valid_outputs[0]
1568 make_links = [] # store sockets for new links
1569 if active.outputs:
1570 socket_type = 'NodeSocketShader'
1571 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
1572 make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
1573 output_socket = materialout.inputs[materialout_index]
1574 for li_from, li_to in make_links:
1575 base_node_tree.links.new(li_from, li_to)
1577 # Create links through node groups until we reach the active node
1578 tree = base_node_tree
1579 link_end = output_socket
1580 while tree.nodes.active != active:
1581 node = tree.nodes.active
1582 index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1583 link_start = node.outputs[index]
1584 node_socket = node.node_tree.outputs[index]
1585 if node_socket in delete_sockets:
1586 delete_sockets.remove(node_socket)
1587 tree.links.new(link_start, link_end)
1588 # Iterate
1589 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1590 tree = tree.nodes.active.node_tree
1591 tree.links.new(active.outputs[out_i], link_end)
1593 # Delete sockets
1594 for socket in delete_sockets:
1595 if not self.is_socket_used_other_mats(socket):
1596 tree = socket.id_data
1597 tree.outputs.remove(socket)
1599 nodes.active = active
1600 active.select = True
1602 force_update(context)
1604 return {'FINISHED'}
1605 else:
1606 return {'CANCELLED'}
1609 class NWFrameSelected(Operator, NWBase):
1610 bl_idname = "node.nw_frame_selected"
1611 bl_label = "Frame Selected"
1612 bl_description = "Add a frame node and parent the selected nodes to it"
1613 bl_options = {'REGISTER', 'UNDO'}
1615 label_prop: StringProperty(
1616 name='Label',
1617 description='The visual name of the frame node',
1618 default=' '
1620 use_custom_color_prop: BoolProperty(
1621 name="Custom Color",
1622 description="Use custom color for the frame node",
1623 default=False
1625 color_prop: FloatVectorProperty(
1626 name="Color",
1627 description="The color of the frame node",
1628 default=(0.604, 0.604, 0.604),
1629 min=0, max=1, step=1, precision=3,
1630 subtype='COLOR_GAMMA', size=3
1633 def draw(self, context):
1634 layout = self.layout
1635 layout.prop(self, 'label_prop')
1636 layout.prop(self, 'use_custom_color_prop')
1637 col = layout.column()
1638 col.active = self.use_custom_color_prop
1639 col.prop(self, 'color_prop', text="")
1641 def execute(self, context):
1642 nodes, links = get_nodes_links(context)
1643 selected = []
1644 for node in nodes:
1645 if node.select == True:
1646 selected.append(node)
1648 bpy.ops.node.add_node(type='NodeFrame')
1649 frm = nodes.active
1650 frm.label = self.label_prop
1651 frm.use_custom_color = self.use_custom_color_prop
1652 frm.color = self.color_prop
1654 for node in selected:
1655 node.parent = frm
1657 return {'FINISHED'}
1660 class NWReloadImages(Operator):
1661 bl_idname = "node.nw_reload_images"
1662 bl_label = "Reload Images"
1663 bl_description = "Update all the image nodes to match their files on disk"
1665 @classmethod
1666 def poll(cls, context):
1667 valid = False
1668 if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
1669 if context.active_node is not None:
1670 for out in context.active_node.outputs:
1671 if is_visible_socket(out):
1672 valid = True
1673 break
1674 return valid
1676 def execute(self, context):
1677 nodes, links = get_nodes_links(context)
1678 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1679 num_reloaded = 0
1680 for node in nodes:
1681 if node.type in image_types:
1682 if node.type == "TEXTURE":
1683 if node.texture: # node has texture assigned
1684 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1685 if node.texture.image: # texture has image assigned
1686 node.texture.image.reload()
1687 num_reloaded += 1
1688 else:
1689 if node.image:
1690 node.image.reload()
1691 num_reloaded += 1
1693 if num_reloaded:
1694 self.report({'INFO'}, "Reloaded images")
1695 print("Reloaded " + str(num_reloaded) + " images")
1696 force_update(context)
1697 return {'FINISHED'}
1698 else:
1699 self.report({'WARNING'}, "No images found to reload in this node tree")
1700 return {'CANCELLED'}
1703 class NWSwitchNodeType(Operator, NWBase):
1704 """Switch type of selected nodes """
1705 bl_idname = "node.nw_swtch_node_type"
1706 bl_label = "Switch Node Type"
1707 bl_options = {'REGISTER', 'UNDO'}
1709 to_type: StringProperty(
1710 name="Switch to type",
1711 default = '',
1714 def execute(self, context):
1715 to_type = self.to_type
1716 if len(to_type) == 0:
1717 return {'CANCELLED'}
1719 nodes, links = get_nodes_links(context)
1720 # Those types of nodes will not swap.
1721 src_excludes = ('NodeFrame')
1722 # Those attributes of nodes will be copied if possible
1723 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1724 'show_options', 'show_preview', 'show_texture',
1725 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1727 selected = [n for n in nodes if n.select]
1728 reselect = []
1729 for node in [n for n in selected if
1730 n.rna_type.identifier not in src_excludes and
1731 n.rna_type.identifier != to_type]:
1732 new_node = nodes.new(to_type)
1733 for attr in attrs_to_pass:
1734 if hasattr(node, attr) and hasattr(new_node, attr):
1735 setattr(new_node, attr, getattr(node, attr))
1736 # set image datablock of dst to image of src
1737 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1738 if node.image:
1739 new_node.image = node.image
1740 # Special cases
1741 if new_node.type == 'SWITCH':
1742 new_node.hide = True
1743 # Dictionaries: src_sockets and dst_sockets:
1744 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1745 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1746 # in 'INPUTS' and 'OUTPUTS':
1747 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1748 # socket entry:
1749 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1750 src_sockets = {
1751 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1752 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1754 dst_sockets = {
1755 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1756 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1758 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1759 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1760 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1761 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1762 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1763 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1764 # enumerate in inputs, then in outputs
1765 # find name, default value and links of socket
1766 for i, socket in enumerate(in_out):
1767 the_name = socket.name
1768 dval = None
1769 # Not every socket, especially in outputs has "default_value"
1770 if hasattr(socket, 'default_value'):
1771 dval = socket.default_value
1772 socket_links = []
1773 for lnk in socket.links:
1774 socket_links.append(lnk)
1775 # check type of socket to fill proper keys.
1776 for the_type in types_order_one:
1777 if socket.type == the_type:
1778 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1779 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1780 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1781 # Check which of the types in inputs/outputs is considered to be "main".
1782 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1783 for type_check in types_order_one:
1784 if sockets[in_out_name][type_check]:
1785 sockets[in_out_name]['MAIN'] = type_check
1786 break
1788 matches = {
1789 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1790 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1793 for inout, soctype in (
1794 ('INPUTS', 'MAIN',),
1795 ('INPUTS', 'SHADER',),
1796 ('INPUTS', 'RGBA',),
1797 ('INPUTS', 'VECTOR',),
1798 ('INPUTS', 'VALUE',),
1799 ('OUTPUTS', 'MAIN',),
1800 ('OUTPUTS', 'SHADER',),
1801 ('OUTPUTS', 'RGBA',),
1802 ('OUTPUTS', 'VECTOR',),
1803 ('OUTPUTS', 'VALUE',),
1805 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1806 if soctype == 'MAIN':
1807 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1808 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1809 else:
1810 sc = src_sockets[inout][soctype]
1811 dt = dst_sockets[inout][soctype]
1812 # start with 'dt' to determine number of possibilities.
1813 for i, soc in enumerate(dt):
1814 # if src main has enough entries - match them with dst main sockets by indexes.
1815 if len(sc) > i:
1816 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1817 # add 'VALUE_NAME' criterion to inputs.
1818 if inout == 'INPUTS' and soctype == 'VALUE':
1819 for s in sc:
1820 if s[2] == soc[2]: # if names match
1821 # append src (index, dval), dst (index, dval)
1822 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1824 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1825 # This creates better links when relinking textures.
1826 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1827 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1829 # Pass default values and RELINK:
1830 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1831 # INPUTS: Base on matches in proper order.
1832 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1833 # pass dvals
1834 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1835 new_node.inputs[dst_i].default_value = src_dval
1836 # Special case: switch to math
1837 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1838 new_node.type == 'MATH' and\
1839 tp == 'MAIN':
1840 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1841 new_node.inputs[dst_i].default_value = new_dst_dval
1842 if node.type == 'MIX_RGB':
1843 if node.blend_type in [o[0] for o in operations]:
1844 new_node.operation = node.blend_type
1845 # Special case: switch from math to some types
1846 if node.type == 'MATH' and\
1847 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1848 tp == 'MAIN':
1849 for i in range(3):
1850 new_node.inputs[dst_i].default_value[i] = src_dval
1851 if new_node.type == 'MIX_RGB':
1852 if node.operation in [t[0] for t in blend_types]:
1853 new_node.blend_type = node.operation
1854 # Set Fac of MIX_RGB to 1.0
1855 new_node.inputs[0].default_value = 1.0
1856 # make link only when dst matching input is not linked already.
1857 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1858 in_src_link = node.inputs[src_i].links[0]
1859 in_dst_socket = new_node.inputs[dst_i]
1860 links.new(in_src_link.from_socket, in_dst_socket)
1861 links.remove(in_src_link)
1862 # OUTPUTS: Base on matches in proper order.
1863 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1864 for out_src_link in node.outputs[src_i].links:
1865 out_dst_socket = new_node.outputs[dst_i]
1866 links.new(out_dst_socket, out_src_link.to_socket)
1867 # relink rest inputs if possible, no criteria
1868 for src_inp in node.inputs:
1869 for dst_inp in new_node.inputs:
1870 if src_inp.links and not dst_inp.links:
1871 src_link = src_inp.links[0]
1872 links.new(src_link.from_socket, dst_inp)
1873 links.remove(src_link)
1874 # relink rest outputs if possible, base on node kind if any left.
1875 for src_o in node.outputs:
1876 for out_src_link in src_o.links:
1877 for dst_o in new_node.outputs:
1878 if src_o.type == dst_o.type:
1879 links.new(dst_o, out_src_link.to_socket)
1880 # relink rest outputs no criteria if any left. Link all from first output.
1881 for src_o in node.outputs:
1882 for out_src_link in src_o.links:
1883 if new_node.outputs:
1884 links.new(new_node.outputs[0], out_src_link.to_socket)
1885 nodes.remove(node)
1886 force_update(context)
1887 return {'FINISHED'}
1890 class NWMergeNodes(Operator, NWBase):
1891 bl_idname = "node.nw_merge_nodes"
1892 bl_label = "Merge Nodes"
1893 bl_description = "Merge Selected Nodes"
1894 bl_options = {'REGISTER', 'UNDO'}
1896 mode: EnumProperty(
1897 name="mode",
1898 description="All possible blend types, boolean operations and math operations",
1899 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],
1901 merge_type: EnumProperty(
1902 name="merge type",
1903 description="Type of Merge to be used",
1904 items=(
1905 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1906 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1907 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
1908 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1909 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1910 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
1911 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
1915 # Check if the link connects to a node that is in selected_nodes
1916 # If not, then check recursively for each link in the nodes outputs.
1917 # If yes, return True. If the recursion stops without finding a node
1918 # in selected_nodes, it returns False. The depth is used to prevent
1919 # getting stuck in a loop because of an already present cycle.
1920 @staticmethod
1921 def link_creates_cycle(link, selected_nodes, depth=0)->bool:
1922 if depth > 255:
1923 # We're stuck in a cycle, but that cycle was already present,
1924 # so we return False.
1925 # NOTE: The number 255 is arbitrary, but seems to work well.
1926 return False
1927 node = link.to_node
1928 if node in selected_nodes:
1929 return True
1930 if not node.outputs:
1931 return False
1932 for output in node.outputs:
1933 if output.is_linked:
1934 for olink in output.links:
1935 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1):
1936 return True
1937 # None of the outputs found a node in selected_nodes, so there is no cycle.
1938 return False
1940 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
1941 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
1942 # be connected. The last one is assumed to be a multi input socket.
1943 # For convenience the node is returned.
1944 @staticmethod
1945 def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices):
1946 # The y-location of the last node
1947 loc_y = nodes_list[-1][2]
1948 if merge_position == 'CENTER':
1949 # Average the y-location
1950 for i in range(len(nodes_list)-1):
1951 loc_y += nodes_list[i][2]
1952 loc_y = loc_y/len(nodes_list)
1953 new_node = nodes.new(node_name)
1954 new_node.hide = do_hide
1955 new_node.location.x = loc_x
1956 new_node.location.y = loc_y
1957 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
1958 prev_links = []
1959 outputs_for_multi_input = []
1960 for i,node in enumerate(selected_nodes):
1961 node.select = False
1962 # Search for the first node which had output links that do not create
1963 # a cycle, which we can then reconnect afterwards.
1964 if prev_links == [] and node.outputs[0].is_linked:
1965 prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)]
1966 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
1967 # To get the placement to look right we need to reverse the order in which we connect the
1968 # outputs to the multi input socket.
1969 if i < len(socket_indices) - 1:
1970 ind = socket_indices[i]
1971 links.new(node.outputs[0], new_node.inputs[ind])
1972 else:
1973 outputs_for_multi_input.insert(0, node.outputs[0])
1974 if outputs_for_multi_input != []:
1975 ind = socket_indices[-1]
1976 for output in outputs_for_multi_input:
1977 links.new(output, new_node.inputs[ind])
1978 if prev_links != []:
1979 for link in prev_links:
1980 links.new(new_node.outputs[0], link.to_node.inputs[0])
1981 return new_node
1983 def execute(self, context):
1984 settings = context.preferences.addons[__name__].preferences
1985 merge_hide = settings.merge_hide
1986 merge_position = settings.merge_position # 'center' or 'bottom'
1988 do_hide = False
1989 do_hide_shader = False
1990 if merge_hide == 'ALWAYS':
1991 do_hide = True
1992 do_hide_shader = True
1993 elif merge_hide == 'NON_SHADER':
1994 do_hide = True
1996 tree_type = context.space_data.node_tree.type
1997 if tree_type == 'GEOMETRY':
1998 node_type = 'GeometryNode'
1999 if tree_type == 'COMPOSITING':
2000 node_type = 'CompositorNode'
2001 elif tree_type == 'SHADER':
2002 node_type = 'ShaderNode'
2003 elif tree_type == 'TEXTURE':
2004 node_type = 'TextureNode'
2005 nodes, links = get_nodes_links(context)
2006 mode = self.mode
2007 merge_type = self.merge_type
2008 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2009 # 'ZCOMBINE' works only if mode == 'MIX'
2010 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2011 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2012 merge_type = 'MIX'
2013 mode = 'MIX'
2014 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
2015 merge_type = 'AUTO'
2016 # The MixRGB node and math nodes used for geometry nodes are of type 'ShaderNode'
2017 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
2018 node_type = 'ShaderNode'
2019 selected_mix = [] # entry = [index, loc]
2020 selected_shader = [] # entry = [index, loc]
2021 selected_geometry = [] # entry = [index, loc]
2022 selected_math = [] # entry = [index, loc]
2023 selected_vector = [] # entry = [index, loc]
2024 selected_z = [] # entry = [index, loc]
2025 selected_alphaover = [] # entry = [index, loc]
2027 for i, node in enumerate(nodes):
2028 if node.select and node.outputs:
2029 if merge_type == 'AUTO':
2030 for (type, types_list, dst) in (
2031 ('SHADER', ('MIX', 'ADD'), selected_shader),
2032 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2033 ('RGBA', [t[0] for t in blend_types], selected_mix),
2034 ('VALUE', [t[0] for t in operations], selected_math),
2035 ('VECTOR', [], selected_vector),
2037 output = get_first_enabled_output(node)
2038 output_type = output.type
2039 valid_mode = mode in types_list
2040 # When mode is 'MIX' we have to cheat since the mix node is not used in
2041 # geometry nodes.
2042 if tree_type == 'GEOMETRY':
2043 if mode == 'MIX':
2044 if output_type == 'VALUE' and type == 'VALUE':
2045 valid_mode = True
2046 elif output_type == 'VECTOR' and type == 'VECTOR':
2047 valid_mode = True
2048 elif type == 'GEOMETRY':
2049 valid_mode = True
2050 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2051 # Cheat that output type is 'RGBA',
2052 # and that 'MIX' exists in math operations list.
2053 # This way when selected_mix list is analyzed:
2054 # Node data will be appended even though it doesn't meet requirements.
2055 elif output_type != 'SHADER' and mode == 'MIX':
2056 output_type = 'RGBA'
2057 valid_mode = True
2058 if output_type == type and valid_mode:
2059 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2060 else:
2061 for (type, types_list, dst) in (
2062 ('SHADER', ('MIX', 'ADD'), selected_shader),
2063 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2064 ('MIX', [t[0] for t in blend_types], selected_mix),
2065 ('MATH', [t[0] for t in operations], selected_math),
2066 ('ZCOMBINE', ('MIX', ), selected_z),
2067 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2069 if merge_type == type and mode in types_list:
2070 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2071 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2072 # use only 'Mix' nodes for merging.
2073 # For that we add selected_math list to selected_mix list and clear selected_math.
2074 if selected_mix and selected_math and merge_type == 'AUTO':
2075 selected_mix += selected_math
2076 selected_math = []
2077 for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]:
2078 if not nodes_list:
2079 continue
2080 count_before = len(nodes)
2081 # sort list by loc_x - reversed
2082 nodes_list.sort(key=lambda k: k[1], reverse=True)
2083 # get maximum loc_x
2084 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2085 nodes_list.sort(key=lambda k: k[2], reverse=True)
2087 # Change the node type for math nodes in a geometry node tree.
2088 if tree_type == 'GEOMETRY':
2089 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
2090 node_type = 'ShaderNode'
2091 if mode == 'MIX':
2092 mode = 'ADD'
2093 else:
2094 node_type = 'GeometryNode'
2095 if merge_position == 'CENTER':
2096 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)
2097 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2098 if do_hide:
2099 loc_y += 40
2100 else:
2101 loc_y += 80
2102 else:
2103 loc_y = nodes_list[len(nodes_list) - 1][2]
2104 offset_y = 100
2105 if not do_hide:
2106 offset_y = 200
2107 if nodes_list == selected_shader and not do_hide_shader:
2108 offset_y = 150.0
2109 the_range = len(nodes_list) - 1
2110 if len(nodes_list) == 1:
2111 the_range = 1
2112 was_multi = False
2113 for i in range(the_range):
2114 if nodes_list == selected_mix:
2115 add_type = node_type + 'MixRGB'
2116 add = nodes.new(add_type)
2117 add.blend_type = mode
2118 if mode != 'MIX':
2119 add.inputs[0].default_value = 1.0
2120 add.show_preview = False
2121 add.hide = do_hide
2122 if do_hide:
2123 loc_y = loc_y - 50
2124 first = 1
2125 second = 2
2126 add.width_hidden = 100.0
2127 elif nodes_list == selected_math:
2128 add_type = node_type + 'Math'
2129 add = nodes.new(add_type)
2130 add.operation = mode
2131 add.hide = do_hide
2132 if do_hide:
2133 loc_y = loc_y - 50
2134 first = 0
2135 second = 1
2136 add.width_hidden = 100.0
2137 elif nodes_list == selected_shader:
2138 if mode == 'MIX':
2139 add_type = node_type + 'MixShader'
2140 add = nodes.new(add_type)
2141 add.hide = do_hide_shader
2142 if do_hide_shader:
2143 loc_y = loc_y - 50
2144 first = 1
2145 second = 2
2146 add.width_hidden = 100.0
2147 elif mode == 'ADD':
2148 add_type = node_type + 'AddShader'
2149 add = nodes.new(add_type)
2150 add.hide = do_hide_shader
2151 if do_hide_shader:
2152 loc_y = loc_y - 50
2153 first = 0
2154 second = 1
2155 add.width_hidden = 100.0
2156 elif nodes_list == selected_geometry:
2157 if mode in ('JOIN', 'MIX'):
2158 add_type = node_type + 'JoinGeometry'
2159 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0])
2160 else:
2161 add_type = node_type + 'Boolean'
2162 indices = [0,1] if mode == 'DIFFERENCE' else [1]
2163 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices)
2164 add.operation = mode
2165 was_multi = True
2166 break
2167 elif nodes_list == selected_vector:
2168 add_type = node_type + 'VectorMath'
2169 add = nodes.new(add_type)
2170 add.operation = mode
2171 add.hide = do_hide
2172 if do_hide:
2173 loc_y = loc_y - 50
2174 first = 0
2175 second = 1
2176 add.width_hidden = 100.0
2177 elif nodes_list == selected_z:
2178 add = nodes.new('CompositorNodeZcombine')
2179 add.show_preview = False
2180 add.hide = do_hide
2181 if do_hide:
2182 loc_y = loc_y - 50
2183 first = 0
2184 second = 2
2185 add.width_hidden = 100.0
2186 elif nodes_list == selected_alphaover:
2187 add = nodes.new('CompositorNodeAlphaOver')
2188 add.show_preview = False
2189 add.hide = do_hide
2190 if do_hide:
2191 loc_y = loc_y - 50
2192 first = 1
2193 second = 2
2194 add.width_hidden = 100.0
2195 add.location = loc_x, loc_y
2196 loc_y += offset_y
2197 add.select = True
2199 # This has already been handled separately
2200 if was_multi:
2201 continue
2202 count_adds = i + 1
2203 count_after = len(nodes)
2204 index = count_after - 1
2205 first_selected = nodes[nodes_list[0][0]]
2206 # "last" node has been added as first, so its index is count_before.
2207 last_add = nodes[count_before]
2208 # Create list of invalid indexes.
2209 invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
2211 # Special case:
2212 # Two nodes were selected and first selected has no output links, second selected has output links.
2213 # Then add links from last add to all links 'to_socket' of out links of second selected.
2214 first_selected_output = get_first_enabled_output(first_selected)
2215 if len(nodes_list) == 2:
2216 if not first_selected_output.links:
2217 second_selected = nodes[nodes_list[1][0]]
2218 for ss_link in second_selected.outputs[0].links:
2219 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2220 # Link only if "to_node" index not in invalid indexes list.
2221 if not self.link_creates_cycle(ss_link, invalid_nodes):
2222 links.new(last_add.outputs[0], ss_link.to_socket)
2223 # add links from last_add to all links 'to_socket' of out links of first selected.
2224 for fs_link in first_selected_output.links:
2225 # Link only if "to_node" index not in invalid indexes list.
2226 if not self.link_creates_cycle(fs_link, invalid_nodes):
2227 links.new(last_add.outputs[0], fs_link.to_socket)
2228 # add link from "first" selected and "first" add node
2229 node_to = nodes[count_after - 1]
2230 links.new(first_selected_output, node_to.inputs[first])
2231 if node_to.type == 'ZCOMBINE':
2232 for fs_out in first_selected.outputs:
2233 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
2234 links.new(fs_out, node_to.inputs[1])
2235 break
2236 # add links between added ADD nodes and between selected and ADD nodes
2237 for i in range(count_adds):
2238 if i < count_adds - 1:
2239 node_from = nodes[index]
2240 node_to = nodes[index - 1]
2241 node_to_input_i = first
2242 node_to_z_i = 1 # if z combine - link z to first z input
2243 links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
2244 if node_to.type == 'ZCOMBINE':
2245 for from_out in node_from.outputs:
2246 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
2247 links.new(from_out, node_to.inputs[node_to_z_i])
2248 if len(nodes_list) > 1:
2249 node_from = nodes[nodes_list[i + 1][0]]
2250 node_to = nodes[index]
2251 node_to_input_i = second
2252 node_to_z_i = 3 # if z combine - link z to second z input
2253 links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
2254 if node_to.type == 'ZCOMBINE':
2255 for from_out in node_from.outputs:
2256 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
2257 links.new(from_out, node_to.inputs[node_to_z_i])
2258 index -= 1
2259 # set "last" of added nodes as active
2260 nodes.active = last_add
2261 for i, x, y, dx, h in nodes_list:
2262 nodes[i].select = False
2264 return {'FINISHED'}
2267 class NWBatchChangeNodes(Operator, NWBase):
2268 bl_idname = "node.nw_batch_change"
2269 bl_label = "Batch Change"
2270 bl_description = "Batch Change Blend Type and Math Operation"
2271 bl_options = {'REGISTER', 'UNDO'}
2273 blend_type: EnumProperty(
2274 name="Blend Type",
2275 items=blend_types + navs,
2277 operation: EnumProperty(
2278 name="Operation",
2279 items=operations + navs,
2282 def execute(self, context):
2283 blend_type = self.blend_type
2284 operation = self.operation
2285 for node in context.selected_nodes:
2286 if node.type == 'MIX_RGB' or node.bl_idname == 'GeometryNodeAttributeMix':
2287 if not blend_type in [nav[0] for nav in navs]:
2288 node.blend_type = blend_type
2289 else:
2290 if blend_type == 'NEXT':
2291 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2292 #index = blend_types.index(node.blend_type)
2293 if index == len(blend_types) - 1:
2294 node.blend_type = blend_types[0][0]
2295 else:
2296 node.blend_type = blend_types[index + 1][0]
2298 if blend_type == 'PREV':
2299 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2300 if index == 0:
2301 node.blend_type = blend_types[len(blend_types) - 1][0]
2302 else:
2303 node.blend_type = blend_types[index - 1][0]
2305 if node.type == 'MATH' or node.bl_idname == 'GeometryNodeAttributeMath':
2306 if not operation in [nav[0] for nav in navs]:
2307 node.operation = operation
2308 else:
2309 if operation == 'NEXT':
2310 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2311 #index = operations.index(node.operation)
2312 if index == len(operations) - 1:
2313 node.operation = operations[0][0]
2314 else:
2315 node.operation = operations[index + 1][0]
2317 if operation == 'PREV':
2318 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2319 #index = operations.index(node.operation)
2320 if index == 0:
2321 node.operation = operations[len(operations) - 1][0]
2322 else:
2323 node.operation = operations[index - 1][0]
2325 return {'FINISHED'}
2328 class NWChangeMixFactor(Operator, NWBase):
2329 bl_idname = "node.nw_factor"
2330 bl_label = "Change Factor"
2331 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2332 bl_options = {'REGISTER', 'UNDO'}
2334 # option: Change factor.
2335 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2336 # Else - change factor by option value.
2337 option: FloatProperty()
2339 def execute(self, context):
2340 nodes, links = get_nodes_links(context)
2341 option = self.option
2342 selected = [] # entry = index
2343 for si, node in enumerate(nodes):
2344 if node.select:
2345 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2346 selected.append(si)
2348 for si in selected:
2349 fac = nodes[si].inputs[0]
2350 nodes[si].hide = False
2351 if option in {0.0, 1.0}:
2352 fac.default_value = option
2353 else:
2354 fac.default_value += option
2356 return {'FINISHED'}
2359 class NWCopySettings(Operator, NWBase):
2360 bl_idname = "node.nw_copy_settings"
2361 bl_label = "Copy Settings"
2362 bl_description = "Copy Settings of Active Node to Selected Nodes"
2363 bl_options = {'REGISTER', 'UNDO'}
2365 @classmethod
2366 def poll(cls, context):
2367 valid = False
2368 if nw_check(context):
2369 if (
2370 context.active_node is not None and
2371 context.active_node.type != 'FRAME'
2373 valid = True
2374 return valid
2376 def execute(self, context):
2377 node_active = context.active_node
2378 node_selected = context.selected_nodes
2380 # Error handling
2381 if not (len(node_selected) > 1):
2382 self.report({'ERROR'}, "2 nodes must be selected at least")
2383 return {'CANCELLED'}
2385 # Check if active node is in the selection
2386 selected_node_names = [n.name for n in node_selected]
2387 if node_active.name not in selected_node_names:
2388 self.report({'ERROR'}, "No active node")
2389 return {'CANCELLED'}
2391 # Get nodes in selection by type
2392 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2394 if not (len(valid_nodes) > 1) and node_active:
2395 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2396 return {'CANCELLED'}
2398 if len(valid_nodes) != len(node_selected):
2399 # Report nodes that are not valid
2400 valid_node_names = [n.name for n in valid_nodes]
2401 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2402 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2404 # Reference original
2405 orig = node_active
2406 #node_selected_names = [n.name for n in node_selected]
2408 # Output list
2409 success_names = []
2411 # Deselect all nodes
2412 for i in node_selected:
2413 i.select = False
2415 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2416 # Run through all other nodes
2417 for node in valid_nodes[1:]:
2419 # Check for frame node
2420 parent = node.parent if node.parent else None
2421 node_loc = [node.location.x, node.location.y]
2423 # Select original to duplicate
2424 orig.select = True
2426 # Duplicate selected node
2427 bpy.ops.node.duplicate()
2428 new_node = context.selected_nodes[0]
2430 # Deselect copy
2431 new_node.select = False
2433 # Properties to copy
2434 node_tree = node.id_data
2435 props_to_copy = 'bl_idname name location height width'.split(' ')
2437 # Input and outputs
2438 reconnections = []
2439 mappings = chain.from_iterable([node.inputs, node.outputs])
2440 for i in (i for i in mappings if i.is_linked):
2441 for L in i.links:
2442 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2444 # Properties
2445 props = {j: getattr(node, j) for j in props_to_copy}
2446 props_to_copy.pop(0)
2448 for prop in props_to_copy:
2449 setattr(new_node, prop, props[prop])
2451 # Get the node tree to remove the old node
2452 nodes = node_tree.nodes
2453 nodes.remove(node)
2454 new_node.name = props['name']
2456 if parent:
2457 new_node.parent = parent
2458 new_node.location = node_loc
2460 for str_from, str_to in reconnections:
2461 node_tree.links.new(eval(str_from), eval(str_to))
2463 success_names.append(new_node.name)
2465 orig.select = True
2466 node_tree.nodes.active = orig
2467 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2468 return {'FINISHED'}
2471 class NWCopyLabel(Operator, NWBase):
2472 bl_idname = "node.nw_copy_label"
2473 bl_label = "Copy Label"
2474 bl_options = {'REGISTER', 'UNDO'}
2476 option: EnumProperty(
2477 name="option",
2478 description="Source of name of label",
2479 items=(
2480 ('FROM_ACTIVE', 'from active', 'from active node',),
2481 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2482 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2486 def execute(self, context):
2487 nodes, links = get_nodes_links(context)
2488 option = self.option
2489 active = nodes.active
2490 if option == 'FROM_ACTIVE':
2491 if active:
2492 src_label = active.label
2493 for node in [n for n in nodes if n.select and nodes.active != n]:
2494 node.label = src_label
2495 elif option == 'FROM_NODE':
2496 selected = [n for n in nodes if n.select]
2497 for node in selected:
2498 for input in node.inputs:
2499 if input.links:
2500 src = input.links[0].from_node
2501 node.label = src.label
2502 break
2503 elif option == 'FROM_SOCKET':
2504 selected = [n for n in nodes if n.select]
2505 for node in selected:
2506 for input in node.inputs:
2507 if input.links:
2508 src = input.links[0].from_socket
2509 node.label = src.name
2510 break
2512 return {'FINISHED'}
2515 class NWClearLabel(Operator, NWBase):
2516 bl_idname = "node.nw_clear_label"
2517 bl_label = "Clear Label"
2518 bl_options = {'REGISTER', 'UNDO'}
2520 option: BoolProperty()
2522 def execute(self, context):
2523 nodes, links = get_nodes_links(context)
2524 for node in [n for n in nodes if n.select]:
2525 node.label = ''
2527 return {'FINISHED'}
2529 def invoke(self, context, event):
2530 if self.option:
2531 return self.execute(context)
2532 else:
2533 return context.window_manager.invoke_confirm(self, event)
2536 class NWModifyLabels(Operator, NWBase):
2537 """Modify Labels of all selected nodes"""
2538 bl_idname = "node.nw_modify_labels"
2539 bl_label = "Modify Labels"
2540 bl_options = {'REGISTER', 'UNDO'}
2542 prepend: StringProperty(
2543 name="Add to Beginning"
2545 append: StringProperty(
2546 name="Add to End"
2548 replace_from: StringProperty(
2549 name="Text to Replace"
2551 replace_to: StringProperty(
2552 name="Replace with"
2555 def execute(self, context):
2556 nodes, links = get_nodes_links(context)
2557 for node in [n for n in nodes if n.select]:
2558 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2560 return {'FINISHED'}
2562 def invoke(self, context, event):
2563 self.prepend = ""
2564 self.append = ""
2565 self.remove = ""
2566 return context.window_manager.invoke_props_dialog(self)
2569 class NWAddTextureSetup(Operator, NWBase):
2570 bl_idname = "node.nw_add_texture"
2571 bl_label = "Texture Setup"
2572 bl_description = "Add Texture Node Setup to Selected Shaders"
2573 bl_options = {'REGISTER', 'UNDO'}
2575 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2577 @classmethod
2578 def poll(cls, context):
2579 if nw_check(context):
2580 space = context.space_data
2581 if space.tree_type == 'ShaderNodeTree':
2582 return True
2583 return False
2585 def execute(self, context):
2586 nodes, links = get_nodes_links(context)
2588 texture_types = [x.nodetype for x in
2589 get_nodes_from_category('Texture', context)]
2590 selected_nodes = [n for n in nodes if n.select]
2592 for node in selected_nodes:
2593 if not node.inputs:
2594 continue
2596 input_index = 0
2597 target_input = node.inputs[0]
2598 for input in node.inputs:
2599 if input.enabled:
2600 input_index += 1
2601 if not input.is_linked:
2602 target_input = input
2603 break
2604 else:
2605 self.report({'WARNING'}, "No free inputs for node: " + node.name)
2606 continue
2608 x_offset = 0
2609 padding = 40.0
2610 locx = node.location.x
2611 locy = node.location.y - (input_index * padding)
2613 is_texture_node = node.rna_type.identifier in texture_types
2614 use_environment_texture = node.type == 'BACKGROUND'
2616 # Add an image texture before normal shader nodes.
2617 if not is_texture_node:
2618 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
2619 image_texture_node = nodes.new(image_texture_type)
2620 x_offset = x_offset + image_texture_node.width + padding
2621 image_texture_node.location = [locx - x_offset, locy]
2622 nodes.active = image_texture_node
2623 links.new(image_texture_node.outputs[0], target_input)
2625 # The mapping setup following this will connect to the first input of this image texture.
2626 target_input = image_texture_node.inputs[0]
2628 node.select = False
2630 if is_texture_node or self.add_mapping:
2631 # Add Mapping node.
2632 mapping_node = nodes.new('ShaderNodeMapping')
2633 x_offset = x_offset + mapping_node.width + padding
2634 mapping_node.location = [locx - x_offset, locy]
2635 links.new(mapping_node.outputs[0], target_input)
2637 # Add Texture Coordinates node.
2638 tex_coord_node = nodes.new('ShaderNodeTexCoord')
2639 x_offset = x_offset + tex_coord_node.width + padding
2640 tex_coord_node.location = [locx - x_offset, locy]
2642 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
2643 use_generated_coordinates = is_procedural_texture or use_environment_texture
2644 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
2645 links.new(tex_coord_output, mapping_node.inputs[0])
2647 return {'FINISHED'}
2650 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2651 bl_idname = "node.nw_add_textures_for_principled"
2652 bl_label = "Principled Texture Setup"
2653 bl_description = "Add Texture Node Setup for Principled BSDF"
2654 bl_options = {'REGISTER', 'UNDO'}
2656 directory: StringProperty(
2657 name='Directory',
2658 subtype='DIR_PATH',
2659 default='',
2660 description='Folder to search in for image files'
2662 files: CollectionProperty(
2663 type=bpy.types.OperatorFileListElement,
2664 options={'HIDDEN', 'SKIP_SAVE'}
2667 relative_path: BoolProperty(
2668 name='Relative Path',
2669 description='Set the file path relative to the blend file, when possible',
2670 default=True
2673 order = [
2674 "filepath",
2675 "files",
2678 def draw(self, context):
2679 layout = self.layout
2680 layout.alignment = 'LEFT'
2682 layout.prop(self, 'relative_path')
2684 @classmethod
2685 def poll(cls, context):
2686 valid = False
2687 if nw_check(context):
2688 space = context.space_data
2689 if space.tree_type == 'ShaderNodeTree':
2690 valid = True
2691 return valid
2693 def execute(self, context):
2694 # Check if everything is ok
2695 if not self.directory:
2696 self.report({'INFO'}, 'No Folder Selected')
2697 return {'CANCELLED'}
2698 if not self.files[:]:
2699 self.report({'INFO'}, 'No Files Selected')
2700 return {'CANCELLED'}
2702 nodes, links = get_nodes_links(context)
2703 active_node = nodes.active
2704 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
2705 self.report({'INFO'}, 'Select Principled BSDF')
2706 return {'CANCELLED'}
2708 # Helper_functions
2709 def split_into__components(fname):
2710 # Split filename into components
2711 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2712 # Remove extension
2713 fname = path.splitext(fname)[0]
2714 # Remove digits
2715 fname = ''.join(i for i in fname if not i.isdigit())
2716 # Separate CamelCase by space
2717 fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",fname)
2718 # Replace common separators with SPACE
2719 separators = ['_', '.', '-', '__', '--', '#']
2720 for sep in separators:
2721 fname = fname.replace(sep, ' ')
2723 components = fname.split(' ')
2724 components = [c.lower() for c in components]
2725 return components
2727 # Filter textures names for texturetypes in filenames
2728 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2729 tags = context.preferences.addons[__name__].preferences.principled_tags
2730 normal_abbr = tags.normal.split(' ')
2731 bump_abbr = tags.bump.split(' ')
2732 gloss_abbr = tags.gloss.split(' ')
2733 rough_abbr = tags.rough.split(' ')
2734 socketnames = [
2735 ['Displacement', tags.displacement.split(' '), None],
2736 ['Base Color', tags.base_color.split(' '), None],
2737 ['Subsurface Color', tags.sss_color.split(' '), None],
2738 ['Metallic', tags.metallic.split(' '), None],
2739 ['Specular', tags.specular.split(' '), None],
2740 ['Roughness', rough_abbr + gloss_abbr, None],
2741 ['Normal', normal_abbr + bump_abbr, None],
2742 ['Transmission', tags.transmission.split(' '), None],
2743 ['Emission', tags.emission.split(' '), None],
2744 ['Alpha', tags.alpha.split(' '), None],
2745 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
2748 # Look through texture_types and set value as filename of first matched file
2749 def match_files_to_socket_names():
2750 for sname in socketnames:
2751 for file in self.files:
2752 fname = file.name
2753 filenamecomponents = split_into__components(fname)
2754 matches = set(sname[1]).intersection(set(filenamecomponents))
2755 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2756 if matches:
2757 sname[2] = fname
2758 break
2760 match_files_to_socket_names()
2761 # Remove socketnames without found files
2762 socketnames = [s for s in socketnames if s[2]
2763 and path.exists(self.directory+s[2])]
2764 if not socketnames:
2765 self.report({'INFO'}, 'No matching images found')
2766 print('No matching images found')
2767 return {'CANCELLED'}
2769 # Don't override path earlier as os.path is used to check the absolute path
2770 import_path = self.directory
2771 if self.relative_path:
2772 if bpy.data.filepath:
2773 try:
2774 import_path = bpy.path.relpath(self.directory)
2775 except ValueError:
2776 pass
2778 # Add found images
2779 print('\nMatched Textures:')
2780 texture_nodes = []
2781 disp_texture = None
2782 ao_texture = None
2783 normal_node = None
2784 roughness_node = None
2785 for i, sname in enumerate(socketnames):
2786 print(i, sname[0], sname[2])
2788 # DISPLACEMENT NODES
2789 if sname[0] == 'Displacement':
2790 disp_texture = nodes.new(type='ShaderNodeTexImage')
2791 img = bpy.data.images.load(path.join(import_path, sname[2]))
2792 disp_texture.image = img
2793 disp_texture.label = 'Displacement'
2794 if disp_texture.image:
2795 disp_texture.image.colorspace_settings.is_data = True
2797 # Add displacement offset nodes
2798 disp_node = nodes.new(type='ShaderNodeDisplacement')
2799 # Align the Displacement node under the active Principled BSDF node
2800 disp_node.location = active_node.location + Vector((100, -700))
2801 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
2803 # TODO Turn on true displacement in the material
2804 # Too complicated for now
2806 # Find output node
2807 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2808 if output_node:
2809 if not output_node[0].inputs[2].is_linked:
2810 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
2812 continue
2814 # AMBIENT OCCLUSION TEXTURE
2815 if sname[0] == 'Ambient Occlusion':
2816 ao_texture = nodes.new(type='ShaderNodeTexImage')
2817 img = bpy.data.images.load(path.join(import_path, sname[2]))
2818 ao_texture.image = img
2819 ao_texture.label = sname[0]
2820 if ao_texture.image:
2821 ao_texture.image.colorspace_settings.is_data = True
2823 continue
2825 if not active_node.inputs[sname[0]].is_linked:
2826 # No texture node connected -> add texture node with new image
2827 texture_node = nodes.new(type='ShaderNodeTexImage')
2828 img = bpy.data.images.load(path.join(import_path, sname[2]))
2829 texture_node.image = img
2831 # NORMAL NODES
2832 if sname[0] == 'Normal':
2833 # Test if new texture node is normal or bump map
2834 fname_components = split_into__components(sname[2])
2835 match_normal = set(normal_abbr).intersection(set(fname_components))
2836 match_bump = set(bump_abbr).intersection(set(fname_components))
2837 if match_normal:
2838 # If Normal add normal node in between
2839 normal_node = nodes.new(type='ShaderNodeNormalMap')
2840 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
2841 elif match_bump:
2842 # If Bump add bump node in between
2843 normal_node = nodes.new(type='ShaderNodeBump')
2844 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
2846 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
2847 normal_node_texture = texture_node
2849 elif sname[0] == 'Roughness':
2850 # Test if glossy or roughness map
2851 fname_components = split_into__components(sname[2])
2852 match_rough = set(rough_abbr).intersection(set(fname_components))
2853 match_gloss = set(gloss_abbr).intersection(set(fname_components))
2855 if match_rough:
2856 # If Roughness nothing to to
2857 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2859 elif match_gloss:
2860 # If Gloss Map add invert node
2861 invert_node = nodes.new(type='ShaderNodeInvert')
2862 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
2864 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
2865 roughness_node = texture_node
2867 else:
2868 # This is a simple connection Texture --> Input slot
2869 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2871 # Use non-color for all but 'Base Color' Textures
2872 if not sname[0] in ['Base Color', 'Emission'] and texture_node.image:
2873 texture_node.image.colorspace_settings.is_data = True
2875 else:
2876 # If already texture connected. add to node list for alignment
2877 texture_node = active_node.inputs[sname[0]].links[0].from_node
2879 # This are all connected texture nodes
2880 texture_nodes.append(texture_node)
2881 texture_node.label = sname[0]
2883 if disp_texture:
2884 texture_nodes.append(disp_texture)
2886 if ao_texture:
2887 # We want the ambient occlusion texture to be the top most texture node
2888 texture_nodes.insert(0, ao_texture)
2890 # Alignment
2891 for i, texture_node in enumerate(texture_nodes):
2892 offset = Vector((-550, (i * -280) + 200))
2893 texture_node.location = active_node.location + offset
2895 if normal_node:
2896 # Extra alignment if normal node was added
2897 normal_node.location = normal_node_texture.location + Vector((300, 0))
2899 if roughness_node:
2900 # Alignment of invert node if glossy map
2901 invert_node.location = roughness_node.location + Vector((300, 0))
2903 # Add texture input + mapping
2904 mapping = nodes.new(type='ShaderNodeMapping')
2905 mapping.location = active_node.location + Vector((-1050, 0))
2906 if len(texture_nodes) > 1:
2907 # If more than one texture add reroute node in between
2908 reroute = nodes.new(type='NodeReroute')
2909 texture_nodes.append(reroute)
2910 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
2911 reroute.location = tex_coords + Vector((-50, -120))
2912 for texture_node in texture_nodes:
2913 link = links.new(texture_node.inputs[0], reroute.outputs[0])
2914 link = links.new(reroute.inputs[0], mapping.outputs[0])
2915 else:
2916 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
2918 # Connect texture_coordiantes to mapping node
2919 texture_input = nodes.new(type='ShaderNodeTexCoord')
2920 texture_input.location = mapping.location + Vector((-200, 0))
2921 link = links.new(mapping.inputs[0], texture_input.outputs[2])
2923 # Create frame around tex coords and mapping
2924 frame = nodes.new(type='NodeFrame')
2925 frame.label = 'Mapping'
2926 mapping.parent = frame
2927 texture_input.parent = frame
2928 frame.update()
2930 # Create frame around texture nodes
2931 frame = nodes.new(type='NodeFrame')
2932 frame.label = 'Textures'
2933 for tnode in texture_nodes:
2934 tnode.parent = frame
2935 frame.update()
2937 # Just to be sure
2938 active_node.select = False
2939 nodes.update()
2940 links.update()
2941 force_update(context)
2942 return {'FINISHED'}
2945 class NWAddReroutes(Operator, NWBase):
2946 """Add Reroute Nodes and link them to outputs of selected nodes"""
2947 bl_idname = "node.nw_add_reroutes"
2948 bl_label = "Add Reroutes"
2949 bl_description = "Add Reroutes to Outputs"
2950 bl_options = {'REGISTER', 'UNDO'}
2952 option: EnumProperty(
2953 name="option",
2954 items=[
2955 ('ALL', 'to all', 'Add to all outputs'),
2956 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2957 ('LINKED', 'to linked', 'Add only to linked outputs'),
2961 def execute(self, context):
2962 tree_type = context.space_data.node_tree.type
2963 option = self.option
2964 nodes, links = get_nodes_links(context)
2965 # output valid when option is 'all' or when 'loose' output has no links
2966 valid = False
2967 post_select = [] # nodes to be selected after execution
2968 # create reroutes and recreate links
2969 for node in [n for n in nodes if n.select]:
2970 if node.outputs:
2971 x = node.location.x
2972 y = node.location.y
2973 width = node.width
2974 # unhide 'REROUTE' nodes to avoid issues with location.y
2975 if node.type == 'REROUTE':
2976 node.hide = False
2977 # When node is hidden - width_hidden not usable.
2978 # Hack needed to calculate real width
2979 if node.hide:
2980 bpy.ops.node.select_all(action='DESELECT')
2981 helper = nodes.new('NodeReroute')
2982 helper.select = True
2983 node.select = True
2984 # resize node and helper to zero. Then check locations to calculate width
2985 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2986 width = 2.0 * (helper.location.x - node.location.x)
2987 # restore node location
2988 node.location = x, y
2989 # delete helper
2990 node.select = False
2991 # only helper is selected now
2992 bpy.ops.node.delete()
2993 x = node.location.x + width + 20.0
2994 if node.type != 'REROUTE':
2995 y -= 35.0
2996 y_offset = -22.0
2997 loc = x, y
2998 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2999 for out_i, output in enumerate(node.outputs):
3000 pass_used = False # initial value to be analyzed if 'R_LAYERS'
3001 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3002 if node.type != 'R_LAYERS':
3003 pass_used = True
3004 else: # if 'R_LAYERS' check if output represent used render pass
3005 node_scene = node.scene
3006 node_layer = node.layer
3007 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3008 if output.name == 'Alpha':
3009 pass_used = True
3010 else:
3011 # check entries in global 'rl_outputs' variable
3012 for rlo in rl_outputs:
3013 if output.name in {rlo.output_name, rlo.exr_output_name}:
3014 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
3015 break
3016 if pass_used:
3017 valid = ((option == 'ALL') or
3018 (option == 'LOOSE' and not output.links) or
3019 (option == 'LINKED' and output.links))
3020 # Add reroutes only if valid, but offset location in all cases.
3021 if valid:
3022 n = nodes.new('NodeReroute')
3023 nodes.active = n
3024 for link in output.links:
3025 links.new(n.outputs[0], link.to_socket)
3026 links.new(output, n.inputs[0])
3027 n.location = loc
3028 post_select.append(n)
3029 reroutes_count += 1
3030 y += y_offset
3031 loc = x, y
3032 # disselect the node so that after execution of script only newly created nodes are selected
3033 node.select = False
3034 # nicer reroutes distribution along y when node.hide
3035 if node.hide:
3036 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
3037 for reroute in [r for r in nodes if r.select]:
3038 reroute.location.y -= y_translate
3039 for node in post_select:
3040 node.select = True
3042 return {'FINISHED'}
3045 class NWLinkActiveToSelected(Operator, NWBase):
3046 """Link active node to selected nodes basing on various criteria"""
3047 bl_idname = "node.nw_link_active_to_selected"
3048 bl_label = "Link Active Node to Selected"
3049 bl_options = {'REGISTER', 'UNDO'}
3051 replace: BoolProperty()
3052 use_node_name: BoolProperty()
3053 use_outputs_names: BoolProperty()
3055 @classmethod
3056 def poll(cls, context):
3057 valid = False
3058 if nw_check(context):
3059 if context.active_node is not None:
3060 if context.active_node.select:
3061 valid = True
3062 return valid
3064 def execute(self, context):
3065 nodes, links = get_nodes_links(context)
3066 replace = self.replace
3067 use_node_name = self.use_node_name
3068 use_outputs_names = self.use_outputs_names
3069 active = nodes.active
3070 selected = [node for node in nodes if node.select and node != active]
3071 outputs = [] # Only usable outputs of active nodes will be stored here.
3072 for out in active.outputs:
3073 if active.type != 'R_LAYERS':
3074 outputs.append(out)
3075 else:
3076 # 'R_LAYERS' node type needs special handling.
3077 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3078 # Only outputs that represent used passes should be taken into account
3079 # Check if pass represented by output is used.
3080 # global 'rl_outputs' list will be used for that
3081 for rlo in rl_outputs:
3082 pass_used = False # initial value. Will be set to True if pass is used
3083 if out.name == 'Alpha':
3084 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3085 pass_used = True
3086 elif out.name in {rlo.output_name, rlo.exr_output_name}:
3087 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3088 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
3089 break
3090 if pass_used:
3091 outputs.append(out)
3092 doit = True # Will be changed to False when links successfully added to previous output.
3093 for out in outputs:
3094 if doit:
3095 for node in selected:
3096 dst_name = node.name # Will be compared with src_name if needed.
3097 # When node has label - use it as dst_name
3098 if node.label:
3099 dst_name = node.label
3100 valid = True # Initial value. Will be changed to False if names don't match.
3101 src_name = dst_name # If names not used - this assignment will keep valid = True.
3102 if use_node_name:
3103 # Set src_name to source node name or label
3104 src_name = active.name
3105 if active.label:
3106 src_name = active.label
3107 elif use_outputs_names:
3108 src_name = (out.name, )
3109 for rlo in rl_outputs:
3110 if out.name in {rlo.output_name, rlo.exr_output_name}:
3111 src_name = (rlo.output_name, rlo.exr_output_name)
3112 if dst_name not in src_name:
3113 valid = False
3114 if valid:
3115 for input in node.inputs:
3116 if input.type == out.type or node.type == 'REROUTE':
3117 if replace or not input.is_linked:
3118 links.new(out, input)
3119 if not use_node_name and not use_outputs_names:
3120 doit = False
3121 break
3123 return {'FINISHED'}
3126 class NWAlignNodes(Operator, NWBase):
3127 '''Align the selected nodes neatly in a row/column'''
3128 bl_idname = "node.nw_align_nodes"
3129 bl_label = "Align Nodes"
3130 bl_options = {'REGISTER', 'UNDO'}
3131 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3133 def execute(self, context):
3134 nodes, links = get_nodes_links(context)
3135 margin = self.margin
3137 selection = []
3138 for node in nodes:
3139 if node.select and node.type != 'FRAME':
3140 selection.append(node)
3142 # If no nodes are selected, align all nodes
3143 active_loc = None
3144 if not selection:
3145 selection = nodes
3146 elif nodes.active in selection:
3147 active_loc = copy(nodes.active.location) # make a copy, not a reference
3149 # Check if nodes should be laid out horizontally or vertically
3150 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3151 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3152 x_range = max(x_locs) - min(x_locs)
3153 y_range = max(y_locs) - min(y_locs)
3154 mid_x = (max(x_locs) + min(x_locs)) / 2
3155 mid_y = (max(y_locs) + min(y_locs)) / 2
3156 horizontal = x_range > y_range
3158 # Sort selection by location of node mid-point
3159 if horizontal:
3160 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3161 else:
3162 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3164 # Alignment
3165 current_pos = 0
3166 for node in selection:
3167 current_margin = margin
3168 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3170 if horizontal:
3171 node.location.x = current_pos
3172 current_pos += current_margin + node.dimensions.x
3173 node.location.y = mid_y + (node.dimensions.y / 2)
3174 else:
3175 node.location.y = current_pos
3176 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3177 node.location.x = mid_x - (node.dimensions.x / 2)
3179 # If active node is selected, center nodes around it
3180 if active_loc is not None:
3181 active_loc_diff = active_loc - nodes.active.location
3182 for node in selection:
3183 node.location += active_loc_diff
3184 else: # Position nodes centered around where they used to be
3185 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])
3186 new_mid = (max(locs) + min(locs)) / 2
3187 for node in selection:
3188 if horizontal:
3189 node.location.x += (mid_x - new_mid)
3190 else:
3191 node.location.y += (mid_y - new_mid)
3193 return {'FINISHED'}
3196 class NWSelectParentChildren(Operator, NWBase):
3197 bl_idname = "node.nw_select_parent_child"
3198 bl_label = "Select Parent or Children"
3199 bl_options = {'REGISTER', 'UNDO'}
3201 option: EnumProperty(
3202 name="option",
3203 items=(
3204 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3205 ('CHILD', 'Select Children', 'Select members of selected frame'),
3209 def execute(self, context):
3210 nodes, links = get_nodes_links(context)
3211 option = self.option
3212 selected = [node for node in nodes if node.select]
3213 if option == 'PARENT':
3214 for sel in selected:
3215 parent = sel.parent
3216 if parent:
3217 parent.select = True
3218 else: # option == 'CHILD'
3219 for sel in selected:
3220 children = [node for node in nodes if node.parent == sel]
3221 for kid in children:
3222 kid.select = True
3224 return {'FINISHED'}
3227 class NWDetachOutputs(Operator, NWBase):
3228 """Detach outputs of selected node leaving inputs linked"""
3229 bl_idname = "node.nw_detach_outputs"
3230 bl_label = "Detach Outputs"
3231 bl_options = {'REGISTER', 'UNDO'}
3233 def execute(self, context):
3234 nodes, links = get_nodes_links(context)
3235 selected = context.selected_nodes
3236 bpy.ops.node.duplicate_move_keep_inputs()
3237 new_nodes = context.selected_nodes
3238 bpy.ops.node.select_all(action="DESELECT")
3239 for node in selected:
3240 node.select = True
3241 bpy.ops.node.delete_reconnect()
3242 for new_node in new_nodes:
3243 new_node.select = True
3244 bpy.ops.transform.translate('INVOKE_DEFAULT')
3246 return {'FINISHED'}
3249 class NWLinkToOutputNode(Operator):
3250 """Link to Composite node or Material Output node"""
3251 bl_idname = "node.nw_link_out"
3252 bl_label = "Connect to Output"
3253 bl_options = {'REGISTER', 'UNDO'}
3255 @classmethod
3256 def poll(cls, context):
3257 valid = False
3258 if nw_check(context):
3259 if context.active_node is not None:
3260 for out in context.active_node.outputs:
3261 if is_visible_socket(out):
3262 valid = True
3263 break
3264 return valid
3266 def execute(self, context):
3267 nodes, links = get_nodes_links(context)
3268 active = nodes.active
3269 output_index = None
3270 tree_type = context.space_data.tree_type
3271 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
3272 'WORLD': 'ShaderNodeOutputWorld',
3273 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
3274 output_type = {
3275 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
3276 'CompositorNodeTree': 'CompositorNodeComposite',
3277 'TextureNodeTree': 'TextureNodeOutput',
3278 'GeometryNodeTree': 'NodeGroupOutput',
3279 }[tree_type]
3280 for node in nodes:
3281 # check whether the node is an output node and,
3282 # if supported, whether it's the active one
3283 if node.rna_type.identifier == output_type \
3284 and (node.is_active_output if hasattr(node, 'is_active_output')
3285 else True):
3286 output_node = node
3287 break
3288 else: # No output node exists
3289 bpy.ops.node.select_all(action="DESELECT")
3290 output_node = nodes.new(output_type)
3291 output_node.location.x = active.location.x + active.dimensions.x + 80
3292 output_node.location.y = active.location.y
3294 if active.outputs:
3295 for i, output in enumerate(active.outputs):
3296 if is_visible_socket(output):
3297 output_index = i
3298 break
3299 for i, output in enumerate(active.outputs):
3300 if output.type == output_node.inputs[0].type and is_visible_socket(output):
3301 output_index = i
3302 break
3304 out_input_index = 0
3305 if tree_type == 'ShaderNodeTree':
3306 if active.outputs[output_index].name == 'Volume':
3307 out_input_index = 1
3308 elif active.outputs[output_index].name == 'Displacement':
3309 out_input_index = 2
3310 elif tree_type == 'GeometryNodeTree':
3311 if active.outputs[output_index].type != 'GEOMETRY':
3312 return {'CANCELLED'}
3313 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3315 force_update(context) # viewport render does not update
3317 return {'FINISHED'}
3320 class NWMakeLink(Operator, NWBase):
3321 """Make a link from one socket to another"""
3322 bl_idname = 'node.nw_make_link'
3323 bl_label = 'Make Link'
3324 bl_options = {'REGISTER', 'UNDO'}
3325 from_socket: IntProperty()
3326 to_socket: IntProperty()
3328 def execute(self, context):
3329 nodes, links = get_nodes_links(context)
3331 n1 = nodes[context.scene.NWLazySource]
3332 n2 = nodes[context.scene.NWLazyTarget]
3334 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3336 force_update(context)
3338 return {'FINISHED'}
3341 class NWCallInputsMenu(Operator, NWBase):
3342 """Link from this output"""
3343 bl_idname = 'node.nw_call_inputs_menu'
3344 bl_label = 'Make Link'
3345 bl_options = {'REGISTER', 'UNDO'}
3346 from_socket: IntProperty()
3348 def execute(self, context):
3349 nodes, links = get_nodes_links(context)
3351 context.scene.NWSourceSocket = self.from_socket
3353 n1 = nodes[context.scene.NWLazySource]
3354 n2 = nodes[context.scene.NWLazyTarget]
3355 if len(n2.inputs) > 1:
3356 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3357 elif len(n2.inputs) == 1:
3358 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3359 return {'FINISHED'}
3362 class NWAddSequence(Operator, NWBase, ImportHelper):
3363 """Add an Image Sequence"""
3364 bl_idname = 'node.nw_add_sequence'
3365 bl_label = 'Import Image Sequence'
3366 bl_options = {'REGISTER', 'UNDO'}
3368 directory: StringProperty(
3369 subtype="DIR_PATH"
3371 filename: StringProperty(
3372 subtype="FILE_NAME"
3374 files: CollectionProperty(
3375 type=bpy.types.OperatorFileListElement,
3376 options={'HIDDEN', 'SKIP_SAVE'}
3378 relative_path: BoolProperty(
3379 name='Relative Path',
3380 description='Set the file path relative to the blend file, when possible',
3381 default=True
3384 def draw(self, context):
3385 layout = self.layout
3386 layout.alignment = 'LEFT'
3388 layout.prop(self, 'relative_path')
3390 def execute(self, context):
3391 nodes, links = get_nodes_links(context)
3392 directory = self.directory
3393 filename = self.filename
3394 files = self.files
3395 tree = context.space_data.node_tree
3397 # DEBUG
3398 # print ("\nDIR:", directory)
3399 # print ("FN:", filename)
3400 # print ("Fs:", list(f.name for f in files), '\n')
3402 if tree.type == 'SHADER':
3403 node_type = "ShaderNodeTexImage"
3404 elif tree.type == 'COMPOSITING':
3405 node_type = "CompositorNodeImage"
3406 else:
3407 self.report({'ERROR'}, "Unsupported Node Tree type!")
3408 return {'CANCELLED'}
3410 if not files[0].name and not filename:
3411 self.report({'ERROR'}, "No file chosen")
3412 return {'CANCELLED'}
3413 elif files[0].name and (not filename or not path.exists(directory+filename)):
3414 # User has selected multiple files without an active one, or the active one is non-existent
3415 filename = files[0].name
3417 if not path.exists(directory+filename):
3418 self.report({'ERROR'}, filename+" does not exist!")
3419 return {'CANCELLED'}
3421 without_ext = '.'.join(filename.split('.')[:-1])
3423 # if last digit isn't a number, it's not a sequence
3424 if not without_ext[-1].isdigit():
3425 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3426 return {'CANCELLED'}
3429 extension = filename.split('.')[-1]
3430 reverse = without_ext[::-1] # reverse string
3432 count_numbers = 0
3433 for char in reverse:
3434 if char.isdigit():
3435 count_numbers += 1
3436 else:
3437 break
3439 without_num = without_ext[:count_numbers*-1]
3441 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3443 num_frames = len(files)
3445 nodes_list = [node for node in nodes]
3446 if nodes_list:
3447 nodes_list.sort(key=lambda k: k.location.x)
3448 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3449 yloc = 0
3450 for node in nodes:
3451 node.select = False
3452 yloc += node_mid_pt(node, 'y')
3453 yloc = yloc/len(nodes)
3454 else:
3455 xloc = 0
3456 yloc = 0
3458 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3460 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3461 node = nodes.active
3462 node.label = name_with_hashes
3464 filepath = directory+(without_ext+'.'+extension)
3465 if self.relative_path:
3466 if bpy.data.filepath:
3467 try:
3468 filepath = bpy.path.relpath(filepath)
3469 except ValueError:
3470 pass
3472 img = bpy.data.images.load(filepath)
3473 img.source = 'SEQUENCE'
3474 img.name = name_with_hashes
3475 node.image = img
3476 image_user = node.image_user if tree.type == 'SHADER' else node
3477 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
3478 image_user.frame_duration = num_frames
3480 return {'FINISHED'}
3483 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
3484 """Add multiple images at once"""
3485 bl_idname = 'node.nw_add_multiple_images'
3486 bl_label = 'Open Selected Images'
3487 bl_options = {'REGISTER', 'UNDO'}
3488 directory: StringProperty(
3489 subtype="DIR_PATH"
3491 files: CollectionProperty(
3492 type=bpy.types.OperatorFileListElement,
3493 options={'HIDDEN', 'SKIP_SAVE'}
3496 def execute(self, context):
3497 nodes, links = get_nodes_links(context)
3499 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3501 if context.space_data.node_tree.type == 'SHADER':
3502 node_type = "ShaderNodeTexImage"
3503 elif context.space_data.node_tree.type == 'COMPOSITING':
3504 node_type = "CompositorNodeImage"
3505 else:
3506 self.report({'ERROR'}, "Unsupported Node Tree type!")
3507 return {'CANCELLED'}
3509 new_nodes = []
3510 for f in self.files:
3511 fname = f.name
3513 node = nodes.new(node_type)
3514 new_nodes.append(node)
3515 node.label = fname
3516 node.hide = True
3517 node.width_hidden = 100
3518 node.location.x = xloc
3519 node.location.y = yloc
3520 yloc -= 40
3522 img = bpy.data.images.load(self.directory+fname)
3523 node.image = img
3525 # shift new nodes up to center of tree
3526 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3527 for node in nodes:
3528 if node in new_nodes:
3529 node.select = True
3530 node.location.y += (list_size/2)
3531 else:
3532 node.select = False
3533 return {'FINISHED'}
3536 class NWViewerFocus(bpy.types.Operator):
3537 """Set the viewer tile center to the mouse position"""
3538 bl_idname = "node.nw_viewer_focus"
3539 bl_label = "Viewer Focus"
3541 x: bpy.props.IntProperty()
3542 y: bpy.props.IntProperty()
3544 @classmethod
3545 def poll(cls, context):
3546 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3548 def execute(self, context):
3549 return {'FINISHED'}
3551 def invoke(self, context, event):
3552 render = context.scene.render
3553 space = context.space_data
3554 percent = render.resolution_percentage*0.01
3556 nodes, links = get_nodes_links(context)
3557 viewers = [n for n in nodes if n.type == 'VIEWER']
3559 if viewers:
3560 mlocx = event.mouse_region_x
3561 mlocy = event.mouse_region_y
3562 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
3564 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3565 region_x = context.region.width
3566 region_y = context.region.height
3568 region_center_x = context.region.width / 2
3569 region_center_y = context.region.height / 2
3571 bd_x = render.resolution_x * percent * space.backdrop_zoom
3572 bd_y = render.resolution_y * percent * space.backdrop_zoom
3574 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
3575 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
3577 margin_x = region_center_x - backdrop_center_x
3578 margin_y = region_center_y - backdrop_center_y
3580 abs_mouse_x = (mlocx - margin_x) / bd_x
3581 abs_mouse_y = (mlocy - margin_y) / bd_y
3583 for node in viewers:
3584 node.center_x = abs_mouse_x
3585 node.center_y = abs_mouse_y
3586 else:
3587 return {'PASS_THROUGH'}
3589 return self.execute(context)
3592 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3593 """Save the current viewer node to an image file"""
3594 bl_idname = "node.nw_save_viewer"
3595 bl_label = "Save This Image"
3596 filepath: StringProperty(subtype="FILE_PATH")
3597 filename_ext: EnumProperty(
3598 name="Format",
3599 description="Choose the file format to save to",
3600 items=(('.bmp', "BMP", ""),
3601 ('.rgb', 'IRIS', ""),
3602 ('.png', 'PNG', ""),
3603 ('.jpg', 'JPEG', ""),
3604 ('.jp2', 'JPEG2000', ""),
3605 ('.tga', 'TARGA', ""),
3606 ('.cin', 'CINEON', ""),
3607 ('.dpx', 'DPX', ""),
3608 ('.exr', 'OPEN_EXR', ""),
3609 ('.hdr', 'HDR', ""),
3610 ('.tif', 'TIFF', "")),
3611 default='.png',
3614 @classmethod
3615 def poll(cls, context):
3616 valid = False
3617 if nw_check(context):
3618 if context.space_data.tree_type == 'CompositorNodeTree':
3619 if "Viewer Node" in [i.name for i in bpy.data.images]:
3620 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3621 valid = True
3622 return valid
3624 def execute(self, context):
3625 fp = self.filepath
3626 if fp:
3627 formats = {
3628 '.bmp': 'BMP',
3629 '.rgb': 'IRIS',
3630 '.png': 'PNG',
3631 '.jpg': 'JPEG',
3632 '.jpeg': 'JPEG',
3633 '.jp2': 'JPEG2000',
3634 '.tga': 'TARGA',
3635 '.cin': 'CINEON',
3636 '.dpx': 'DPX',
3637 '.exr': 'OPEN_EXR',
3638 '.hdr': 'HDR',
3639 '.tiff': 'TIFF',
3640 '.tif': 'TIFF'}
3641 basename, ext = path.splitext(fp)
3642 old_render_format = context.scene.render.image_settings.file_format
3643 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3644 context.area.type = "IMAGE_EDITOR"
3645 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3646 context.area.spaces[0].image.save_render(fp)
3647 context.area.type = "NODE_EDITOR"
3648 context.scene.render.image_settings.file_format = old_render_format
3649 return {'FINISHED'}
3652 class NWResetNodes(bpy.types.Operator):
3653 """Reset Nodes in Selection"""
3654 bl_idname = "node.nw_reset_nodes"
3655 bl_label = "Reset Nodes"
3656 bl_options = {'REGISTER', 'UNDO'}
3658 @classmethod
3659 def poll(cls, context):
3660 space = context.space_data
3661 return space.type == 'NODE_EDITOR'
3663 def execute(self, context):
3664 node_active = context.active_node
3665 node_selected = context.selected_nodes
3666 node_ignore = ["FRAME","REROUTE", "GROUP"]
3668 # Check if one node is selected at least
3669 if not (len(node_selected) > 0):
3670 self.report({'ERROR'}, "1 node must be selected at least")
3671 return {'CANCELLED'}
3673 active_node_name = node_active.name if node_active.select else None
3674 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3676 # Create output lists
3677 selected_node_names = [n.name for n in node_selected]
3678 success_names = []
3680 # Reset all valid children in a frame
3681 node_active_is_frame = False
3682 if len(node_selected) == 1 and node_active.type == "FRAME":
3683 node_tree = node_active.id_data
3684 children = [n for n in node_tree.nodes if n.parent == node_active]
3685 if children:
3686 valid_nodes = [n for n in children if n.type not in node_ignore]
3687 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3688 node_active_is_frame = True
3690 # Check if valid nodes in selection
3691 if not (len(valid_nodes) > 0):
3692 # Check for frames only
3693 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3694 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3695 self.report({'ERROR'}, "Please select only 1 frame to reset")
3696 else:
3697 self.report({'ERROR'}, "No valid node(s) in selection")
3698 return {'CANCELLED'}
3700 # Report nodes that are not valid
3701 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3702 valid_node_names = [n.name for n in valid_nodes]
3703 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3704 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3706 # Deselect all nodes
3707 for i in node_selected:
3708 i.select = False
3710 # Run through all valid nodes
3711 for node in valid_nodes:
3713 parent = node.parent if node.parent else None
3714 node_loc = [node.location.x, node.location.y]
3716 node_tree = node.id_data
3717 props_to_copy = 'bl_idname name location height width'.split(' ')
3719 reconnections = []
3720 mappings = chain.from_iterable([node.inputs, node.outputs])
3721 for i in (i for i in mappings if i.is_linked):
3722 for L in i.links:
3723 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3725 props = {j: getattr(node, j) for j in props_to_copy}
3727 new_node = node_tree.nodes.new(props['bl_idname'])
3728 props_to_copy.pop(0)
3730 for prop in props_to_copy:
3731 setattr(new_node, prop, props[prop])
3733 nodes = node_tree.nodes
3734 nodes.remove(node)
3735 new_node.name = props['name']
3737 if parent:
3738 new_node.parent = parent
3739 new_node.location = node_loc
3741 for str_from, str_to in reconnections:
3742 node_tree.links.new(eval(str_from), eval(str_to))
3744 new_node.select = False
3745 success_names.append(new_node.name)
3747 # Reselect all nodes
3748 if selected_node_names and node_active_is_frame is False:
3749 for i in selected_node_names:
3750 node_tree.nodes[i].select = True
3752 if active_node_name is not None:
3753 node_tree.nodes[active_node_name].select = True
3754 node_tree.nodes.active = node_tree.nodes[active_node_name]
3756 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3757 return {'FINISHED'}
3761 # P A N E L
3764 def drawlayout(context, layout, mode='non-panel'):
3765 tree_type = context.space_data.tree_type
3767 col = layout.column(align=True)
3768 col.menu(NWMergeNodesMenu.bl_idname)
3769 col.separator()
3771 col = layout.column(align=True)
3772 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3773 col.separator()
3775 if tree_type == 'ShaderNodeTree':
3776 col = layout.column(align=True)
3777 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3778 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3779 col.separator()
3781 col = layout.column(align=True)
3782 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3783 col.operator(NWSwapLinks.bl_idname)
3784 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3785 col.separator()
3787 col = layout.column(align=True)
3788 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3789 if tree_type != 'GeometryNodeTree':
3790 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3791 col.separator()
3793 col = layout.column(align=True)
3794 if mode == 'panel':
3795 row = col.row(align=True)
3796 row.operator(NWClearLabel.bl_idname).option = True
3797 row.operator(NWModifyLabels.bl_idname)
3798 else:
3799 col.operator(NWClearLabel.bl_idname).option = True
3800 col.operator(NWModifyLabels.bl_idname)
3801 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3802 col.separator()
3803 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3804 col.separator()
3806 col = layout.column(align=True)
3807 if tree_type == 'CompositorNodeTree':
3808 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
3809 if tree_type != 'GeometryNodeTree':
3810 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
3811 col.separator()
3813 col = layout.column(align=True)
3814 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3815 col.separator()
3817 col = layout.column(align=True)
3818 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
3819 col.separator()
3821 col = layout.column(align=True)
3822 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3823 col.separator()
3826 class NodeWranglerPanel(Panel, NWBase):
3827 bl_idname = "NODE_PT_nw_node_wrangler"
3828 bl_space_type = 'NODE_EDITOR'
3829 bl_label = "Node Wrangler"
3830 bl_region_type = "UI"
3831 bl_category = "Node Wrangler"
3833 prepend: StringProperty(
3834 name='prepend',
3836 append: StringProperty()
3837 remove: StringProperty()
3839 def draw(self, context):
3840 self.layout.label(text="(Quick access: Shift+W)")
3841 drawlayout(context, self.layout, mode='panel')
3845 # M E N U S
3847 class NodeWranglerMenu(Menu, NWBase):
3848 bl_idname = "NODE_MT_nw_node_wrangler_menu"
3849 bl_label = "Node Wrangler"
3851 def draw(self, context):
3852 self.layout.operator_context = 'INVOKE_DEFAULT'
3853 drawlayout(context, self.layout)
3856 class NWMergeNodesMenu(Menu, NWBase):
3857 bl_idname = "NODE_MT_nw_merge_nodes_menu"
3858 bl_label = "Merge Selected Nodes"
3860 def draw(self, context):
3861 type = context.space_data.tree_type
3862 layout = self.layout
3863 if type == 'ShaderNodeTree':
3864 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
3865 if type == 'GeometryNodeTree':
3866 layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes")
3867 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3868 else:
3869 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
3870 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3871 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
3872 props.mode = 'MIX'
3873 props.merge_type = 'ZCOMBINE'
3874 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3875 props.mode = 'MIX'
3876 props.merge_type = 'ALPHAOVER'
3878 class NWMergeGeometryMenu(Menu, NWBase):
3879 bl_idname = "NODE_MT_nw_merge_geometry_menu"
3880 bl_label = "Merge Selected Nodes using Geometry Nodes"
3881 def draw(self, context):
3882 layout = self.layout
3883 # The boolean node + Join Geometry node
3884 for type, name, description in geo_combine_operations:
3885 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3886 props.mode = type
3887 props.merge_type = 'GEOMETRY'
3889 class NWMergeShadersMenu(Menu, NWBase):
3890 bl_idname = "NODE_MT_nw_merge_shaders_menu"
3891 bl_label = "Merge Selected Nodes using Shaders"
3893 def draw(self, context):
3894 layout = self.layout
3895 for type in ('MIX', 'ADD'):
3896 props = layout.operator(NWMergeNodes.bl_idname, text=type)
3897 props.mode = type
3898 props.merge_type = 'SHADER'
3901 class NWMergeMixMenu(Menu, NWBase):
3902 bl_idname = "NODE_MT_nw_merge_mix_menu"
3903 bl_label = "Merge Selected Nodes using Mix"
3905 def draw(self, context):
3906 layout = self.layout
3907 for type, name, description in blend_types:
3908 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3909 props.mode = type
3910 props.merge_type = 'MIX'
3913 class NWConnectionListOutputs(Menu, NWBase):
3914 bl_idname = "NODE_MT_nw_connection_list_out"
3915 bl_label = "From:"
3917 def draw(self, context):
3918 layout = self.layout
3919 nodes, links = get_nodes_links(context)
3921 n1 = nodes[context.scene.NWLazySource]
3922 for index, output in enumerate(n1.outputs):
3923 # Only show sockets that are exposed.
3924 if output.enabled:
3925 layout.operator(NWCallInputsMenu.bl_idname, text=output.name, icon="RADIOBUT_OFF").from_socket=index
3928 class NWConnectionListInputs(Menu, NWBase):
3929 bl_idname = "NODE_MT_nw_connection_list_in"
3930 bl_label = "To:"
3932 def draw(self, context):
3933 layout = self.layout
3934 nodes, links = get_nodes_links(context)
3936 n2 = nodes[context.scene.NWLazyTarget]
3938 for index, input in enumerate(n2.inputs):
3939 # Only show sockets that are exposed.
3940 # This prevents, for example, the scale value socket
3941 # of the vector math node being added to the list when
3942 # the mode is not 'SCALE'.
3943 if input.enabled:
3944 op = layout.operator(NWMakeLink.bl_idname, text=input.name, icon="FORWARD")
3945 op.from_socket = context.scene.NWSourceSocket
3946 op.to_socket = index
3949 class NWMergeMathMenu(Menu, NWBase):
3950 bl_idname = "NODE_MT_nw_merge_math_menu"
3951 bl_label = "Merge Selected Nodes using Math"
3953 def draw(self, context):
3954 layout = self.layout
3955 for type, name, description in operations:
3956 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3957 props.mode = type
3958 props.merge_type = 'MATH'
3961 class NWBatchChangeNodesMenu(Menu, NWBase):
3962 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3963 bl_label = "Batch Change Selected Nodes"
3965 def draw(self, context):
3966 layout = self.layout
3967 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3968 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3971 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3972 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3973 bl_label = "Batch Change Blend Type"
3975 def draw(self, context):
3976 layout = self.layout
3977 for type, name, description in blend_types:
3978 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3979 props.blend_type = type
3980 props.operation = 'CURRENT'
3983 class NWBatchChangeOperationMenu(Menu, NWBase):
3984 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3985 bl_label = "Batch Change Math Operation"
3987 def draw(self, context):
3988 layout = self.layout
3989 for type, name, description in operations:
3990 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3991 props.blend_type = 'CURRENT'
3992 props.operation = type
3995 class NWCopyToSelectedMenu(Menu, NWBase):
3996 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
3997 bl_label = "Copy to Selected"
3999 def draw(self, context):
4000 layout = self.layout
4001 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
4002 layout.menu(NWCopyLabelMenu.bl_idname)
4005 class NWCopyLabelMenu(Menu, NWBase):
4006 bl_idname = "NODE_MT_nw_copy_label_menu"
4007 bl_label = "Copy Label"
4009 def draw(self, context):
4010 layout = self.layout
4011 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
4012 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
4013 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
4016 class NWAddReroutesMenu(Menu, NWBase):
4017 bl_idname = "NODE_MT_nw_add_reroutes_menu"
4018 bl_label = "Add Reroutes"
4019 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
4021 def draw(self, context):
4022 layout = self.layout
4023 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
4024 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
4025 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
4028 class NWLinkActiveToSelectedMenu(Menu, NWBase):
4029 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
4030 bl_label = "Link Active to Selected"
4032 def draw(self, context):
4033 layout = self.layout
4034 layout.menu(NWLinkStandardMenu.bl_idname)
4035 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
4036 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
4039 class NWLinkStandardMenu(Menu, NWBase):
4040 bl_idname = "NODE_MT_nw_link_standard_menu"
4041 bl_label = "To All Selected"
4043 def draw(self, context):
4044 layout = self.layout
4045 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4046 props.replace = False
4047 props.use_node_name = False
4048 props.use_outputs_names = False
4049 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4050 props.replace = True
4051 props.use_node_name = False
4052 props.use_outputs_names = False
4055 class NWLinkUseNodeNameMenu(Menu, NWBase):
4056 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
4057 bl_label = "Use Node Name/Label"
4059 def draw(self, context):
4060 layout = self.layout
4061 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4062 props.replace = False
4063 props.use_node_name = True
4064 props.use_outputs_names = False
4065 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4066 props.replace = True
4067 props.use_node_name = True
4068 props.use_outputs_names = False
4071 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
4072 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
4073 bl_label = "Use Outputs Names"
4075 def draw(self, context):
4076 layout = self.layout
4077 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4078 props.replace = False
4079 props.use_node_name = False
4080 props.use_outputs_names = True
4081 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4082 props.replace = True
4083 props.use_node_name = False
4084 props.use_outputs_names = True
4087 class NWAttributeMenu(bpy.types.Menu):
4088 bl_idname = "NODE_MT_nw_node_attribute_menu"
4089 bl_label = "Attributes"
4091 @classmethod
4092 def poll(cls, context):
4093 valid = False
4094 if nw_check(context):
4095 snode = context.space_data
4096 valid = snode.tree_type == 'ShaderNodeTree'
4097 return valid
4099 def draw(self, context):
4100 l = self.layout
4101 nodes, links = get_nodes_links(context)
4102 mat = context.object.active_material
4104 objs = []
4105 for obj in bpy.data.objects:
4106 for slot in obj.material_slots:
4107 if slot.material == mat:
4108 objs.append(obj)
4109 attrs = []
4110 for obj in objs:
4111 if obj.data.attributes:
4112 for attr in obj.data.attributes:
4113 attrs.append(attr.name)
4114 attrs = list(set(attrs)) # get a unique list
4116 if attrs:
4117 for attr in attrs:
4118 l.operator(NWAddAttrNode.bl_idname, text=attr).attr_name = attr
4119 else:
4120 l.label(text="No attributes on objects with this material")
4123 class NWSwitchNodeTypeMenu(Menu, NWBase):
4124 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4125 bl_label = "Switch Type to..."
4127 def draw(self, context):
4128 layout = self.layout
4129 categories = [c for c in node_categories_iter(context)
4130 if c.name not in ['Group', 'Script']]
4131 for cat in categories:
4132 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4133 if hasattr(bpy.types, idname):
4134 layout.menu(idname)
4135 else:
4136 layout.label(text="Unable to load altered node lists.")
4137 layout.label(text="Please re-enable Node Wrangler.")
4138 break
4141 def draw_switch_category_submenu(self, context):
4142 layout = self.layout
4143 if self.category.name == 'Layout':
4144 for node in self.category.items(context):
4145 if node.nodetype != 'NodeFrame':
4146 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4147 props.to_type = node.nodetype
4148 else:
4149 for node in self.category.items(context):
4150 if isinstance(node, NodeItemCustom):
4151 node.draw(self, layout, context)
4152 continue
4153 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4154 props.to_type = node.nodetype
4157 # APPENDAGES TO EXISTING UI
4161 def select_parent_children_buttons(self, context):
4162 layout = self.layout
4163 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4164 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4167 def attr_nodes_menu_func(self, context):
4168 col = self.layout.column(align=True)
4169 col.menu("NODE_MT_nw_node_attribute_menu")
4170 col.separator()
4173 def multipleimages_menu_func(self, context):
4174 col = self.layout.column(align=True)
4175 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4176 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4177 col.separator()
4180 def bgreset_menu_func(self, context):
4181 self.layout.operator(NWResetBG.bl_idname)
4184 def save_viewer_menu_func(self, context):
4185 if nw_check(context):
4186 if context.space_data.tree_type == 'CompositorNodeTree':
4187 if context.scene.node_tree.nodes.active:
4188 if context.scene.node_tree.nodes.active.type == "VIEWER":
4189 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4192 def reset_nodes_button(self, context):
4193 node_active = context.active_node
4194 node_selected = context.selected_nodes
4195 node_ignore = ["FRAME","REROUTE", "GROUP"]
4197 # Check if active node is in the selection and respective type
4198 if (len(node_selected) == 1) and node_active and node_active.select and node_active.type not in node_ignore:
4199 row = self.layout.row()
4200 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4201 self.layout.separator()
4203 elif (len(node_selected) == 1) and node_active and node_active.select and node_active.type == "FRAME":
4204 row = self.layout.row()
4205 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4206 self.layout.separator()
4210 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4212 switch_category_menus = []
4213 addon_keymaps = []
4214 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4215 # props entry: (property name, property value)
4216 kmi_defs = (
4217 # MERGE NODES
4218 # NWMergeNodes with Ctrl (AUTO).
4219 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4220 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4221 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4222 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4223 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4224 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4225 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4226 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4227 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4228 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4229 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4230 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4231 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4232 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4233 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4234 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4235 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4236 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4237 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4238 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4239 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4240 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4241 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4242 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4243 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4244 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4245 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4246 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4247 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4248 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4249 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4250 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4251 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4252 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4253 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4254 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4255 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4256 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4257 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4258 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4259 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4260 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4261 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4262 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4263 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4264 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4265 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4266 # NWMergeNodes with Ctrl Shift (MATH)
4267 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4268 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4269 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4270 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4271 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4272 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4273 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4274 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4275 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4276 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4277 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4278 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4279 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4280 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4281 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4282 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4283 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4284 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4285 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4286 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4287 # BATCH CHANGE NODES
4288 # NWBatchChangeNodes with Alt
4289 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4290 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4291 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4292 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4293 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4294 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4295 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4296 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4297 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4298 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4299 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4300 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4301 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4302 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4303 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4304 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4305 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4306 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4307 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4308 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4309 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4310 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4311 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4312 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4313 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4314 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4315 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4316 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4317 # LINK ACTIVE TO SELECTED
4318 # Don't use names, don't replace links (K)
4319 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4320 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4321 # Don't use names, replace links (Shift K)
4322 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4323 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4324 # Use node name, don't replace links (')
4325 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4326 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4327 # Use node name, replace links (Shift ')
4328 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4329 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4330 # Don't use names, don't replace links (;)
4331 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4332 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4333 # Don't use names, replace links (')
4334 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4335 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4336 # CHANGE MIX FACTOR
4337 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4338 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4339 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4340 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4341 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4342 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4343 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4344 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4345 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4346 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4347 # CLEAR LABEL (Alt L)
4348 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4349 # MODIFY LABEL (Alt Shift L)
4350 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4351 # Copy Label from active to selected
4352 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4353 # DETACH OUTPUTS (Alt Shift D)
4354 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4355 # LINK TO OUTPUT NODE (O)
4356 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4357 # SELECT PARENT/CHILDREN
4358 # Select Children
4359 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4360 # Select Parent
4361 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4362 # Add Texture Setup
4363 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4364 # Add Principled BSDF Texture Setup
4365 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4366 # Reset backdrop
4367 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4368 # Delete unused
4369 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4370 # Frame Selected
4371 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4372 # Swap Links
4373 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Links"),
4374 # Preview Node
4375 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
4376 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
4377 # Reload Images
4378 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4379 # Lazy Mix
4380 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4381 # Lazy Connect
4382 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
4383 # Lazy Connect with Menu
4384 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4385 # Viewer Tile Center
4386 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4387 # Align Nodes
4388 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4389 # Reset Nodes (Back Space)
4390 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4391 # MENUS
4392 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
4393 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4394 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4395 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4396 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4397 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4401 classes = (
4402 NWPrincipledPreferences,
4403 NWNodeWrangler,
4404 NWLazyMix,
4405 NWLazyConnect,
4406 NWDeleteUnused,
4407 NWSwapLinks,
4408 NWResetBG,
4409 NWAddAttrNode,
4410 NWPreviewNode,
4411 NWFrameSelected,
4412 NWReloadImages,
4413 NWSwitchNodeType,
4414 NWMergeNodes,
4415 NWBatchChangeNodes,
4416 NWChangeMixFactor,
4417 NWCopySettings,
4418 NWCopyLabel,
4419 NWClearLabel,
4420 NWModifyLabels,
4421 NWAddTextureSetup,
4422 NWAddPrincipledSetup,
4423 NWAddReroutes,
4424 NWLinkActiveToSelected,
4425 NWAlignNodes,
4426 NWSelectParentChildren,
4427 NWDetachOutputs,
4428 NWLinkToOutputNode,
4429 NWMakeLink,
4430 NWCallInputsMenu,
4431 NWAddSequence,
4432 NWAddMultipleImages,
4433 NWViewerFocus,
4434 NWSaveViewer,
4435 NWResetNodes,
4436 NodeWranglerPanel,
4437 NodeWranglerMenu,
4438 NWMergeNodesMenu,
4439 NWMergeShadersMenu,
4440 NWMergeGeometryMenu,
4441 NWMergeMixMenu,
4442 NWConnectionListOutputs,
4443 NWConnectionListInputs,
4444 NWMergeMathMenu,
4445 NWBatchChangeNodesMenu,
4446 NWBatchChangeBlendTypeMenu,
4447 NWBatchChangeOperationMenu,
4448 NWCopyToSelectedMenu,
4449 NWCopyLabelMenu,
4450 NWAddReroutesMenu,
4451 NWLinkActiveToSelectedMenu,
4452 NWLinkStandardMenu,
4453 NWLinkUseNodeNameMenu,
4454 NWLinkUseOutputsNamesMenu,
4455 NWAttributeMenu,
4456 NWSwitchNodeTypeMenu,
4459 def register():
4460 from bpy.utils import register_class
4462 # props
4463 bpy.types.Scene.NWBusyDrawing = StringProperty(
4464 name="Busy Drawing!",
4465 default="",
4466 description="An internal property used to store only the first mouse position")
4467 bpy.types.Scene.NWLazySource = StringProperty(
4468 name="Lazy Source!",
4469 default="x",
4470 description="An internal property used to store the first node in a Lazy Connect operation")
4471 bpy.types.Scene.NWLazyTarget = StringProperty(
4472 name="Lazy Target!",
4473 default="x",
4474 description="An internal property used to store the last node in a Lazy Connect operation")
4475 bpy.types.Scene.NWSourceSocket = IntProperty(
4476 name="Source Socket!",
4477 default=0,
4478 description="An internal property used to store the source socket in a Lazy Connect operation")
4479 bpy.types.NodeSocketInterface.NWViewerSocket = BoolProperty(
4480 name="NW Socket",
4481 default=False,
4482 description="An internal property used to determine if a socket is generated by the addon"
4485 for cls in classes:
4486 register_class(cls)
4488 # keymaps
4489 addon_keymaps.clear()
4490 kc = bpy.context.window_manager.keyconfigs.addon
4491 if kc:
4492 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
4493 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
4494 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
4495 if props:
4496 for prop, value in props:
4497 setattr(kmi.properties, prop, value)
4498 addon_keymaps.append((km, kmi))
4500 # menu items
4501 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
4502 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4503 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
4504 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
4505 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4506 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4507 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
4508 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
4510 # switch submenus
4511 switch_category_menus.clear()
4512 for cat in node_categories_iter(None):
4513 if cat.name not in ['Group', 'Script']:
4514 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4515 switch_category_type = type(idname, (bpy.types.Menu,), {
4516 "bl_space_type": 'NODE_EDITOR',
4517 "bl_label": cat.name,
4518 "category": cat,
4519 "poll": cat.poll,
4520 "draw": draw_switch_category_submenu,
4523 switch_category_menus.append(switch_category_type)
4525 bpy.utils.register_class(switch_category_type)
4528 def unregister():
4529 from bpy.utils import unregister_class
4531 # props
4532 del bpy.types.Scene.NWBusyDrawing
4533 del bpy.types.Scene.NWLazySource
4534 del bpy.types.Scene.NWLazyTarget
4535 del bpy.types.Scene.NWSourceSocket
4536 del bpy.types.NodeSocketInterface.NWViewerSocket
4538 for cat_types in switch_category_menus:
4539 bpy.utils.unregister_class(cat_types)
4540 switch_category_menus.clear()
4542 # keymaps
4543 for km, kmi in addon_keymaps:
4544 km.keymap_items.remove(kmi)
4545 addon_keymaps.clear()
4547 # menuitems
4548 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
4549 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4550 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
4551 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
4552 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4553 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
4554 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
4555 bpy.types.NODE_MT_node.remove(reset_nodes_button)
4557 for cls in classes:
4558 unregister_class(cls)
4560 if __name__ == "__main__":
4561 register()