1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
17 from bpy
.types
import Operator
, Panel
, Menu
18 from bpy
.props
import (
27 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
28 from gpu_extras
.batch
import batch_for_shader
29 from mathutils
import Vector
30 from nodeitems_utils
import node_categories_iter
, NodeItemCustom
31 from math
import cos
, sin
, pi
, hypot
35 from itertools
import chain
37 from collections
import namedtuple
41 # list of outputs of Input Render Layer
42 # with attributes determining if pass is used,
43 # and MultiLayer EXR outputs names and corresponding render engines
45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
46 RL_entry
= namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
48 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
49 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
50 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
51 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
52 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
53 RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
54 RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
55 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
56 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
57 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
58 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
59 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
60 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
61 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
62 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
63 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
64 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
65 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
66 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
67 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
68 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
69 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
70 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
71 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
72 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
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.
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.
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.
154 ('CURRENT', 'Current', 'Leave at current state'),
155 ('NEXT', 'Next', 'Next blend type/operation'),
156 ('PREV', 'Prev', 'Previous blend type/operation'),
161 (1.0, 1.0, 1.0, 0.7),
162 (1.0, 0.0, 0.0, 0.7),
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)
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)
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)
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)
186 (1.0, 1.0, 1.0, 0.7),
187 (0.0, 0.0, 0.0, 0.7),
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
:
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
213 'MIDDLEMOUSE': "MMB",
215 'WHEELUPMOUSE': "Wheel Up",
216 'WHEELDOWNMOUSE': "Wheel Down",
217 'WHEELINMOUSE': "Wheel In",
218 'WHEELOUTMOUSE': "Wheel Out",
231 'LINE_FEED': "Enter",
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 +",
258 return nice_name
[punc
]
260 return punc
.replace("_", " ").title()
263 def force_update(context
):
264 context
.space_data
.node_tree
.update_tag()
268 prefs
= bpy
.context
.preferences
.system
269 return prefs
.dpi
* prefs
.pixel_size
/ 72
272 def node_mid_pt(node
, axis
):
274 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
276 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
282 def autolink(node1
, node2
, links
):
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
:
293 for outp
in available_outputs
:
294 for inp
in available_inputs
:
295 if not inp
.is_linked
and inp
.type == outp
.type:
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
:
305 links
.new(available_outputs
[0], inp
)
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:
317 for outp
in available_outputs
:
318 for inp
in available_inputs
:
323 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
326 def abs_node_location(node
):
327 abs_location
= node
.location
328 if node
.parent
is None:
330 return abs_location
+ abs_node_location(node
.parent
)
332 def node_at_pos(nodes
, context
, event
):
333 nodes_under_mouse
= []
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
= []
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
)
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]
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
375 target_node
= nearest_node
# else use the nearest node
377 target_node
= nearest_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
)
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),
404 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
408 def draw_circle_2d_filled(mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
409 radius
= radius
* dpifac()
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
})
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
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
436 if node
.type == 'REROUTE':
443 shader
= gpu
.shader
.from_builtin('UNIFORM_COLOR')
444 shader
.uniform_float("color", colour
)
447 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
449 for i
in range(sides
+1):
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
})
460 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
462 for i
in range(sides
+1):
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
})
473 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
475 for i
in range(sides
+1):
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
})
485 # Bottom right corner
486 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
488 for i
in range(sides
+1):
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
})
498 # prepare drawing all edges in one batch
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)])
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)])
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)])
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
)
549 def draw_callback_nodeoutline(self
, context
, mode
):
551 gpu
.state
.blend_set('ALPHA')
553 nodes
, links
= get_nodes_links(context
)
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)
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
]
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
590 draw_circle_2d_filled(m1x
, m1y
, 7, col_outer
)
591 draw_circle_2d_filled(m2x
, m2y
, 7, col_outer
)
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
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
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
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"):
632 source_iterator
= node
.outputs
633 iterator
= node
.node_tree
.outputs
635 source_iterator
= node
.inputs
636 iterator
= node
.node_tree
.inputs
640 for i
, s
in enumerate(source_iterator
):
645 def is_viewer_link(link
, output_node
):
646 if link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
648 if link
.to_node
.type == 'GROUP_OUTPUT':
649 socket
= get_internal_socket(link
.to_socket
)
650 if is_viewer_socket(socket
):
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:
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
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
)
674 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
675 base_color
: StringProperty(
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(
685 default
='metallic metalness metal mtl',
686 description
='Naming Components for metallness maps')
687 specular
: StringProperty(
689 default
='specularity specular spec spc',
690 description
='Naming Components for Specular maps')
691 normal
: StringProperty(
693 default
='normal nor nrm nrml norm',
694 description
='Naming Components for Normal maps')
695 bump
: StringProperty(
698 description
='Naming Components for bump maps')
699 rough
: StringProperty(
701 default
='roughness rough rgh',
702 description
='Naming Components for roughness maps')
703 gloss
: StringProperty(
705 default
='gloss glossy glossiness',
706 description
='Naming Components for glossy maps')
707 displacement
: StringProperty(
709 default
='displacement displace disp dsp height heightmap',
710 description
='Naming Components for displacement maps')
711 transmission
: StringProperty(
713 default
='transmission transparency',
714 description
='Naming Components for transmission maps')
715 emission
: StringProperty(
717 default
='emission emissive emit',
718 description
='Naming Components for emission maps')
719 alpha
: StringProperty(
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')
729 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
732 merge_hide
: EnumProperty(
733 name
="Hide Mix nodes",
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",
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")
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",
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",
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",
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
):
770 col
= layout
.column()
771 col
.prop(self
, "merge_position")
772 col
.prop(self
, "merge_hide")
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")
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")
803 for hotkey
in kmi_defs
:
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])
812 keystr
= "Shift " + keystr
814 keystr
= "Alt " + keystr
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
):
835 def poll(cls
, context
):
836 return nw_check(context
)
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
)
851 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
854 if not context
.scene
.NWBusyDrawing
:
855 node1
= node_at_pos(nodes
, context
, event
)
857 context
.scene
.NWBusyDrawing
= node1
.name
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')
873 node2
= node_at_pos(nodes
, context
, event
)
875 context
.scene
.NWBusyDrawing
= node2
.name
887 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
889 context
.scene
.NWBusyDrawing
= ""
892 elif event
.type == 'ESC':
894 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
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')
909 context
.window_manager
.modal_handler_add(self
)
910 return {'RUNNING_MODAL'}
912 self
.report({'WARNING'}, "View3D not found, cannot run operator")
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
)
928 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
931 if not context
.scene
.NWBusyDrawing
:
932 node1
= node_at_pos(nodes
, context
, event
)
934 context
.scene
.NWBusyDrawing
= node1
.name
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')
950 node2
= node_at_pos(nodes
, context
, event
)
952 context
.scene
.NWBusyDrawing
= node2
.name
963 if node
.select
== True:
965 original_sel
.append(node
)
967 original_unsel
.append(node
)
971 #link_success = autolink(node1, node2, links)
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)
978 link_success
= autolink(node1
, node2
, links
)
980 for node
in original_sel
:
982 for node
in original_unsel
:
986 force_update(context
)
987 context
.scene
.NWBusyDrawing
= ""
990 elif event
.type == 'ESC':
991 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
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
)
1001 context
.scene
.NWBusyDrawing
= node
.name
1003 # the arguments we pass the the callback
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'}
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
:
1037 for output
in node
.outputs
:
1043 def poll(cls
, context
):
1045 if nw_check(context
):
1046 if context
.space_data
.node_tree
.nodes
:
1050 def execute(self
, context
):
1051 nodes
, links
= get_nodes_links(context
)
1056 if node
.select
== True:
1057 selection
.append(node
.name
)
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
1068 if self
.is_unused_node(node
):
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
1076 if self
.delete_frames
:
1084 frames_in_use
.append(node
.parent
)
1086 if node
.type == 'FRAME' and node
not in frames_in_use
:
1089 repeat
= True # repeat for nested frames
1091 if node
not in frames_in_use
:
1093 deleted_nodes
.append(node
.name
)
1094 bpy
.ops
.node
.delete()
1096 if self
.delete_muted
:
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
)
1112 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1114 self
.report({'INFO'}, "Nothing deleted")
1117 nodes
, links
= get_nodes_links(context
)
1119 if node
.name
in selection
:
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'}
1134 def poll(cls
, context
):
1136 if nw_check(context
):
1137 if context
.selected_nodes
:
1138 valid
= len(context
.selected_nodes
) <= 2
1141 def execute(self
, context
):
1142 nodes
, links
= get_nodes_links(context
)
1143 selected_nodes
= context
.selected_nodes
1144 n1
= selected_nodes
[0]
1147 if len(selected_nodes
) == 2:
1148 n2
= selected_nodes
[1]
1149 if n1
.outputs
and n2
.outputs
:
1154 for output
in n1
.outputs
:
1156 for link
in output
.links
:
1157 n1_outputs
.append([out_index
, link
.to_socket
])
1162 for output
in n2
.outputs
:
1164 for link
in output
.links
:
1165 n2_outputs
.append([out_index
, link
.to_socket
])
1169 for connection
in n1_outputs
:
1171 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1173 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1174 for connection
in n2_outputs
:
1176 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1178 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1180 if n1
.outputs
or n2
.outputs
:
1181 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1183 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
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!")
1193 for i1
in n1
.inputs
:
1194 if i1
.is_linked
and not i1
.is_multi_input
:
1196 for i2
in n1
.inputs
:
1197 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1199 types
.append ([i1
, similar_types
, i
])
1201 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1206 for i2
in n1
.inputs
:
1207 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
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
1217 fs
= t
[0].links
[0].from_socket
1219 links
.remove(t
[0].links
[0])
1220 if i
+1 == len(n1
.inputs
):
1223 while n1
.inputs
[i
].is_linked
:
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
1235 self
.report({'WARNING'}, "This node has no input connections to swap!")
1237 self
.report({'WARNING'}, "This node has no inputs to swap!")
1239 force_update(context
)
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'}
1250 def poll(cls
, context
):
1252 if nw_check(context
):
1253 snode
= context
.space_data
1254 valid
= snode
.tree_type
== 'CompositorNodeTree'
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
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
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)
1290 self
.shader_output_type
= ""
1291 self
.shader_output_ident
= ""
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":
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"):
1309 if len(node
.node_tree
.outputs
):
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
)
1316 if connect_socket
== None:
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
]:
1327 if not index
and free_socket
:
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
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"
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:
1356 def ensure_group_output(cls
, tree
):
1357 #check if a group output node exists otherwise create
1358 groupout
= get_group_output_node(tree
)
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
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
:
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
:
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
)
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
)
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"):
1422 materialout
= self
.get_shader_output_node(mat
.node_tree
)
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":
1448 for out
in active
.outputs
:
1449 if is_visible_socket(out
):
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
1467 for i
, out
in enumerate(active
.outputs
):
1468 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1469 valid_outputs
.append(i
)
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]
1479 out_i
= valid_outputs
[0]
1481 make_links
= [] # store sockets for new links
1483 # If there is no 'GEOMETRY' output type - We can't preview the node
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
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
)
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
)
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
)
1528 # What follows is code for the shader editor
1529 output_types
= [x
.nodetype
for x
in
1530 get_nodes_from_category('Output', context
)]
1533 if active
.rna_type
.identifier
not in output_types
:
1534 for out
in active
.outputs
:
1535 if is_visible_socket(out
):
1539 # get material_output node
1540 materialout
= None # placeholder node
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
)
1548 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1549 materialout
.location
= get_output_location(base_node_tree
)
1550 materialout
.select
= False
1554 for i
, out
in enumerate(active
.outputs
):
1555 if is_visible_socket(out
):
1556 valid_outputs
.append(i
)
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]
1566 out_i
= valid_outputs
[0]
1568 make_links
= [] # store sockets for new links
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
)
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
)
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
)
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(
1617 description
='The visual name of the frame node',
1620 use_custom_color_prop
: BoolProperty(
1621 name
="Custom Color",
1622 description
="Use custom color for the frame node",
1625 color_prop
: FloatVectorProperty(
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
)
1645 if node
.select
== True:
1646 selected
.append(node
)
1648 bpy
.ops
.node
.add_node(type='NodeFrame')
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
:
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"
1666 def poll(cls
, context
):
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
):
1676 def execute(self
, context
):
1677 nodes
, links
= get_nodes_links(context
)
1678 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
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()
1694 self
.report({'INFO'}, "Reloaded images")
1695 print("Reloaded " + str(num_reloaded
) + " images")
1696 force_update(context
)
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",
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
]
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'):
1739 new_node
.image
= node
.image
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.
1749 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1751 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1752 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
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
1769 # Not every socket, especially in outputs has "default_value"
1770 if hasattr(socket
, 'default_value'):
1771 dval
= socket
.default_value
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
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']]
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.
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':
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
]:
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\
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\
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
)
1886 force_update(context
)
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'}
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(
1903 description
="Type of Merge to be used",
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.
1921 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
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.
1928 if node
in selected_nodes
:
1930 if not node
.outputs
:
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):
1937 # None of the outputs found a node in selected_nodes, so there is no cycle.
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.
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
]
1959 outputs_for_multi_input
= []
1960 for i
,node
in enumerate(selected_nodes
):
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
])
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])
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'
1989 do_hide_shader
= False
1990 if merge_hide
== 'ALWAYS':
1992 do_hide_shader
= True
1993 elif merge_hide
== 'NON_SHADER':
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
)
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':
2014 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
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
2042 if tree_type
== 'GEOMETRY':
2044 if output_type
== 'VALUE' and type == 'VALUE':
2046 elif output_type
== 'VECTOR' and type == 'VECTOR':
2048 elif type == 'GEOMETRY':
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'
2058 if output_type
== type and valid_mode
:
2059 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
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
2077 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2080 count_before
= len(nodes
)
2081 # sort list by loc_x - reversed
2082 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
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'
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
2103 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2107 if nodes_list
== selected_shader
and not do_hide_shader
:
2109 the_range
= len(nodes_list
) - 1
2110 if len(nodes_list
) == 1:
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
2119 add
.inputs
[0].default_value
= 1.0
2120 add
.show_preview
= False
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
2136 add
.width_hidden
= 100.0
2137 elif nodes_list
== selected_shader
:
2139 add_type
= node_type
+ 'MixShader'
2140 add
= nodes
.new(add_type
)
2141 add
.hide
= do_hide_shader
2146 add
.width_hidden
= 100.0
2148 add_type
= node_type
+ 'AddShader'
2149 add
= nodes
.new(add_type
)
2150 add
.hide
= do_hide_shader
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])
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
2167 elif nodes_list
== selected_vector
:
2168 add_type
= node_type
+ 'VectorMath'
2169 add
= nodes
.new(add_type
)
2170 add
.operation
= mode
2176 add
.width_hidden
= 100.0
2177 elif nodes_list
== selected_z
:
2178 add
= nodes
.new('CompositorNodeZcombine')
2179 add
.show_preview
= False
2185 add
.width_hidden
= 100.0
2186 elif nodes_list
== selected_alphaover
:
2187 add
= nodes
.new('CompositorNodeAlphaOver')
2188 add
.show_preview
= False
2194 add
.width_hidden
= 100.0
2195 add
.location
= loc_x
, loc_y
2199 # This has already been handled separately
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
)]
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])
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
])
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
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(
2275 items
=blend_types
+ navs
,
2277 operation
: EnumProperty(
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
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]
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]
2301 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
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
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]
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)
2321 node
.operation
= operations
[len(operations
) - 1][0]
2323 node
.operation
= operations
[index
- 1][0]
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
):
2345 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2349 fac
= nodes
[si
].inputs
[0]
2350 nodes
[si
].hide
= False
2351 if option
in {0.0, 1.0}:
2352 fac
.default_value
= option
2354 fac
.default_value
+= option
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'}
2366 def poll(cls
, context
):
2368 if nw_check(context
):
2370 context
.active_node
is not None and
2371 context
.active_node
.type != 'FRAME'
2376 def execute(self
, context
):
2377 node_active
= context
.active_node
2378 node_selected
= context
.selected_nodes
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
2406 #node_selected_names = [n.name for n in node_selected]
2411 # Deselect all nodes
2412 for i
in node_selected
:
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
2426 # Duplicate selected node
2427 bpy
.ops
.node
.duplicate()
2428 new_node
= context
.selected_nodes
[0]
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(' ')
2439 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2440 for i
in (i
for i
in mappings
if i
.is_linked
):
2442 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
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
2454 new_node
.name
= props
['name']
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
)
2466 node_tree
.nodes
.active
= orig
2467 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
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(
2478 description
="Source of name of label",
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':
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
:
2500 src
= input.links
[0].from_node
2501 node
.label
= src
.label
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
:
2508 src
= input.links
[0].from_socket
2509 node
.label
= src
.name
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
]:
2529 def invoke(self
, context
, event
):
2531 return self
.execute(context
)
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(
2548 replace_from
: StringProperty(
2549 name
="Text to Replace"
2551 replace_to
: StringProperty(
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
2562 def invoke(self
, context
, event
):
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)
2578 def poll(cls
, context
):
2579 if nw_check(context
):
2580 space
= context
.space_data
2581 if space
.tree_type
== 'ShaderNodeTree':
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
:
2597 target_input
= node
.inputs
[0]
2598 for input in node
.inputs
:
2601 if not input.is_linked
:
2602 target_input
= input
2605 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
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]
2630 if is_texture_node
or self
.add_mapping
:
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])
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(
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',
2678 def draw(self
, context
):
2679 layout
= self
.layout
2680 layout
.alignment
= 'LEFT'
2682 layout
.prop(self
, 'relative_path')
2685 def poll(cls
, context
):
2687 if nw_check(context
):
2688 space
= context
.space_data
2689 if space
.tree_type
== 'ShaderNodeTree':
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'}
2709 def split_into__components(fname
):
2710 # Split filename into components
2711 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2713 fname
= path
.splitext(fname
)[0]
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
]
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(' ')
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
:
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)
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])]
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
:
2774 import_path
= bpy
.path
.relpath(self
.directory
)
2779 print('\nMatched Textures:')
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
2807 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
2809 if not output_node
[0].inputs
[2].is_linked
:
2810 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
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
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
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
))
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])
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
))
2856 # If Roughness nothing to to
2857 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
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
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
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]
2884 texture_nodes
.append(disp_texture
)
2887 # We want the ambient occlusion texture to be the top most texture node
2888 texture_nodes
.insert(0, ao_texture
)
2891 for i
, texture_node
in enumerate(texture_nodes
):
2892 offset
= Vector((-550, (i
* -280) + 200))
2893 texture_node
.location
= active_node
.location
+ offset
2896 # Extra alignment if normal node was added
2897 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
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])
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
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
2938 active_node
.select
= False
2941 force_update(context
)
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(
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
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
]:
2974 # unhide 'REROUTE' nodes to avoid issues with location.y
2975 if node
.type == 'REROUTE':
2977 # When node is hidden - width_hidden not usable.
2978 # Hack needed to calculate real width
2980 bpy
.ops
.node
.select_all(action
='DESELECT')
2981 helper
= nodes
.new('NodeReroute')
2982 helper
.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
2991 # only helper is selected now
2992 bpy
.ops
.node
.delete()
2993 x
= node
.location
.x
+ width
+ 20.0
2994 if node
.type != 'REROUTE':
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':
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':
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
)
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.
3022 n
= nodes
.new('NodeReroute')
3024 for link
in output
.links
:
3025 links
.new(n
.outputs
[0], link
.to_socket
)
3026 links
.new(output
, n
.inputs
[0])
3028 post_select
.append(n
)
3032 # disselect the node so that after execution of script only newly created nodes are selected
3034 # nicer reroutes distribution along y when 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
:
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()
3056 def poll(cls
, context
):
3058 if nw_check(context
):
3059 if context
.active_node
is not None:
3060 if context
.active_node
.select
:
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':
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.
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
)
3092 doit
= True # Will be changed to False when links successfully added to previous output.
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
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.
3103 # Set src_name to source node name or label
3104 src_name
= active
.name
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
:
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
:
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
3139 if node
.select
and node
.type != 'FRAME':
3140 selection
.append(node
)
3142 # If no nodes are selected, align all 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
3160 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3162 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
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
3171 node
.location
.x
= current_pos
3172 current_pos
+= current_margin
+ node
.dimensions
.x
3173 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
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
:
3189 node
.location
.x
+= (mid_x
- new_mid
)
3191 node
.location
.y
+= (mid_y
- new_mid
)
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(
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
:
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
:
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
:
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')
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'}
3256 def poll(cls
, context
):
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
):
3266 def execute(self
, context
):
3267 nodes
, links
= get_nodes_links(context
)
3268 active
= nodes
.active
3270 tree_type
= context
.space_data
.tree_type
3271 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
3272 'WORLD': 'ShaderNodeOutputWorld',
3273 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
3275 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
3276 'CompositorNodeTree': 'CompositorNodeComposite',
3277 'TextureNodeTree': 'TextureNodeOutput',
3278 'GeometryNodeTree': 'NodeGroupOutput',
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')
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
3295 for i
, output
in enumerate(active
.outputs
):
3296 if is_visible_socket(output
):
3299 for i
, output
in enumerate(active
.outputs
):
3300 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3305 if tree_type
== 'ShaderNodeTree':
3306 if active
.outputs
[output_index
].name
== 'Volume':
3308 elif active
.outputs
[output_index
].name
== 'Displacement':
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
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
)
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])
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(
3371 filename
: StringProperty(
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',
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
3395 tree
= context
.space_data
.node_tree
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"
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
3433 for char
in reverse
:
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
]
3447 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3448 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3452 yloc
+= node_mid_pt(node
, 'y')
3453 yloc
= yloc
/len(nodes
)
3458 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3460 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3462 node
.label
= name_with_hashes
3464 filepath
= directory
+(without_ext
+'.'+extension
)
3465 if self
.relative_path
:
3466 if bpy
.data
.filepath
:
3468 filepath
= bpy
.path
.relpath(filepath
)
3472 img
= bpy
.data
.images
.load(filepath
)
3473 img
.source
= 'SEQUENCE'
3474 img
.name
= name_with_hashes
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
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(
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"
3506 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3507 return {'CANCELLED'}
3510 for f
in self
.files
:
3513 node
= nodes
.new(node_type
)
3514 new_nodes
.append(node
)
3517 node
.width_hidden
= 100
3518 node
.location
.x
= xloc
3519 node
.location
.y
= yloc
3522 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3525 # shift new nodes up to center of tree
3526 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3528 if node
in new_nodes
:
3530 node
.location
.y
+= (list_size
/2)
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()
3545 def poll(cls
, context
):
3546 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
3548 def execute(self
, context
):
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']
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
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(
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', "")),
3615 def poll(cls
, context
):
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
3624 def execute(self
, context
):
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
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'}
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
]
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
]
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")
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
:
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(' ')
3720 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
3721 for i
in (i
for i
in mappings
if i
.is_linked
):
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
3735 new_node
.name
= props
['name']
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
)))
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
)
3771 col
= layout
.column(align
=True)
3772 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
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')
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')
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')
3793 col
= layout
.column(align
=True)
3795 row
= col
.row(align
=True)
3796 row
.operator(NWClearLabel
.bl_idname
).option
= True
3797 row
.operator(NWModifyLabels
.bl_idname
)
3799 col
.operator(NWClearLabel
.bl_idname
).option
= True
3800 col
.operator(NWModifyLabels
.bl_idname
)
3801 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
3803 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
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')
3813 col
= layout
.column(align
=True)
3814 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
3817 col
= layout
.column(align
=True)
3818 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
3821 col
= layout
.column(align
=True)
3822 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
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(
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')
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")
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")
3873 props
.merge_type
= 'ZCOMBINE'
3874 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
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
)
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)
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
)
3910 props
.merge_type
= 'MIX'
3913 class NWConnectionListOutputs(Menu
, NWBase
):
3914 bl_idname
= "NODE_MT_nw_connection_list_out"
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.
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"
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'.
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
)
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"
4092 def poll(cls
, context
):
4094 if nw_check(context
):
4095 snode
= context
.space_data
4096 valid
= snode
.tree_type
== 'ShaderNodeTree'
4099 def draw(self
, context
):
4101 nodes
, links
= get_nodes_links(context
)
4102 mat
= context
.object.active_material
4105 for obj
in bpy
.data
.objects
:
4106 for slot
in obj
.material_slots
:
4107 if slot
.material
== mat
:
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
4118 l
.operator(NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
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
):
4136 layout
.label(text
="Unable to load altered node lists.")
4137 layout
.label(text
="Please re-enable Node Wrangler.")
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
4149 for node
in self
.category
.items(context
):
4150 if isinstance(node
, NodeItemCustom
):
4151 node
.draw(self
, layout
, context
)
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")
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")
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
= []
4214 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4215 # props entry: (property name, property value)
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)"),
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
4359 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4361 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
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"),
4367 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4369 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4371 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4373 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Links"),
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"),
4378 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
4380 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
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"),
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"),
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"),
4402 NWPrincipledPreferences
,
4422 NWAddPrincipledSetup
,
4424 NWLinkActiveToSelected
,
4426 NWSelectParentChildren
,
4432 NWAddMultipleImages
,
4440 NWMergeGeometryMenu
,
4442 NWConnectionListOutputs
,
4443 NWConnectionListInputs
,
4445 NWBatchChangeNodesMenu
,
4446 NWBatchChangeBlendTypeMenu
,
4447 NWBatchChangeOperationMenu
,
4448 NWCopyToSelectedMenu
,
4451 NWLinkActiveToSelectedMenu
,
4453 NWLinkUseNodeNameMenu
,
4454 NWLinkUseOutputsNamesMenu
,
4456 NWSwitchNodeTypeMenu
,
4460 from bpy
.utils
import register_class
4463 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
4464 name
="Busy Drawing!",
4466 description
="An internal property used to store only the first mouse position")
4467 bpy
.types
.Scene
.NWLazySource
= StringProperty(
4468 name
="Lazy Source!",
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!",
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!",
4478 description
="An internal property used to store the source socket in a Lazy Connect operation")
4479 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
4482 description
="An internal property used to determine if a socket is generated by the addon"
4489 addon_keymaps
.clear()
4490 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
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
)
4496 for prop
, value
in props
:
4497 setattr(kmi
.properties
, prop
, value
)
4498 addon_keymaps
.append((km
, kmi
))
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
)
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
,
4520 "draw": draw_switch_category_submenu
,
4523 switch_category_menus
.append(switch_category_type
)
4525 bpy
.utils
.register_class(switch_category_type
)
4529 from bpy
.utils
import unregister_class
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()
4543 for km
, kmi
in addon_keymaps
:
4544 km
.keymap_items
.remove(kmi
)
4545 addon_keymaps
.clear()
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
)
4558 unregister_class(cls
)
4560 if __name__
== "__main__":