Newer
Older
# SPDX-License-Identifier: GPL-2.0-or-later
"name": "Node Wrangler",
"author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
"version": (3, 41),
"location": "Node Editor Toolbar or Shift-W",
"description": "Various tools to enhance and speed up node-based workflow",
"warning": "",
"doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
"category": "Node",
import bpy, blf, bgl
from bpy.types import Operator, Panel, Menu
from bpy.props import (
FloatProperty,
EnumProperty,
BoolProperty,
IntProperty,
StringProperty,
FloatVectorProperty,
CollectionProperty,
)
from bpy_extras.io_utils import ImportHelper, ExportHelper
from gpu_extras.batch import batch_for_shader
from nodeitems_utils import node_categories_iter, NodeItemCustom
from math import cos, sin, pi, hypot
from os import path
from copy import copy
from collections import namedtuple
#################
# rl_outputs:
# list of outputs of Input Render Layer
# with attributes determining if pass is used,
# and MultiLayer EXR outputs names and corresponding render engines
#
# rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
RL_entry('use_pass_uv', 'UV', 'UV', True, True),
RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
RL_entry('use_pass_z', 'Z', 'Depth', True, True),
)
# list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
# used list, not tuple for easy merging with other lists.
blend_types = [
('MIX', 'Mix', 'Mix Mode'),
('ADD', 'Add', 'Add Mode'),
('MULTIPLY', 'Multiply', 'Multiply Mode'),
('SUBTRACT', 'Subtract', 'Subtract Mode'),
('SCREEN', 'Screen', 'Screen Mode'),
('DIVIDE', 'Divide', 'Divide Mode'),
('DIFFERENCE', 'Difference', 'Difference Mode'),
('DARKEN', 'Darken', 'Darken Mode'),
('LIGHTEN', 'Lighten', 'Lighten Mode'),
('OVERLAY', 'Overlay', 'Overlay Mode'),
('DODGE', 'Dodge', 'Dodge Mode'),
('BURN', 'Burn', 'Burn Mode'),
('HUE', 'Hue', 'Hue Mode'),
('SATURATION', 'Saturation', 'Saturation Mode'),
('VALUE', 'Value', 'Value Mode'),
('COLOR', 'Color', 'Color Mode'),
('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
# list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
# used list, not tuple for easy merging with other lists.
operations = [
('ADD', 'Add', 'Add Mode'),
('SUBTRACT', 'Subtract', 'Subtract Mode'),
('MULTIPLY', 'Multiply', 'Multiply Mode'),
('DIVIDE', 'Divide', 'Divide Mode'),
('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
('SINE', 'Sine', 'Sine Mode'),
('COSINE', 'Cosine', 'Cosine Mode'),
('TANGENT', 'Tangent', 'Tangent Mode'),
('ARCSINE', 'Arcsine', 'Arcsine Mode'),
('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
('POWER', 'Power', 'Power Mode'),
('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
('SQRT', 'Square Root', 'Square Root Mode'),
('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
('EXPONENT', 'Exponent', 'Exponent Mode'),
('MINIMUM', 'Minimum', 'Minimum Mode'),
('MAXIMUM', 'Maximum', 'Maximum Mode'),
('LESS_THAN', 'Less Than', 'Less Than Mode'),
('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
('SIGN', 'Sign', 'Sign Mode'),
('COMPARE', 'Compare', 'Compare Mode'),
('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
('FRACT', 'Fraction', 'Fraction Mode'),
('MODULO', 'Modulo', 'Modulo Mode'),
('SNAP', 'Snap', 'Snap Mode'),
('WRAP', 'Wrap', 'Wrap Mode'),
('PINGPONG', 'Pingpong', 'Pingpong Mode'),
('ABSOLUTE', 'Absolute', 'Absolute Mode'),
('ROUND', 'Round', 'Round Mode'),
('FLOOR', 'Floor', 'Floor Mode'),
('CEIL', 'Ceil', 'Ceil Mode'),
('TRUNCATE', 'Truncate', 'Truncate Mode'),
('RADIANS', 'To Radians', 'To Radians Mode'),
('DEGREES', 'To Degrees', 'To Degrees Mode'),
# Operations used by the geometry boolean node and join geometry node
geo_combine_operations = [
('JOIN', 'Join Geometry', 'Join Geometry Mode'),
('INTERSECT', 'Intersect', 'Intersect Mode'),
('UNION', 'Union', 'Union Mode'),
('DIFFERENCE', 'Difference', 'Difference Mode'),
]
# in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
# used list, not tuple for easy merging with other lists.
navs = [
('CURRENT', 'Current', 'Leave at current state'),
('NEXT', 'Next', 'Next blend type/operation'),
('PREV', 'Prev', 'Previous blend type/operation'),
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
]
draw_color_sets = {
"red_white": (
(1.0, 1.0, 1.0, 0.7),
(1.0, 0.0, 0.0, 0.7),
(0.8, 0.2, 0.2, 1.0)
),
"green": (
(0.0, 0.0, 0.0, 1.0),
(0.38, 0.77, 0.38, 1.0),
(0.38, 0.77, 0.38, 1.0)
),
"yellow": (
(0.0, 0.0, 0.0, 1.0),
(0.77, 0.77, 0.16, 1.0),
(0.77, 0.77, 0.16, 1.0)
),
"purple": (
(0.0, 0.0, 0.0, 1.0),
(0.38, 0.38, 0.77, 1.0),
(0.38, 0.38, 0.77, 1.0)
),
"grey": (
(0.0, 0.0, 0.0, 1.0),
(0.63, 0.63, 0.63, 1.0),
(0.63, 0.63, 0.63, 1.0)
),
"black": (
(1.0, 1.0, 1.0, 0.7),
(0.0, 0.0, 0.0, 0.7),
(0.2, 0.2, 0.2, 1.0)
viewer_socket_name = "tmp_viewer"
def get_nodes_from_category(category_name, context):
for category in node_categories_iter(context):
if category.name == category_name:
return sorted(category.items(context), key=lambda node: node.label)
def get_first_enabled_output(node):
for output in node.outputs:
if output.enabled:
return output
else:
return node.outputs[0]
def is_visible_socket(socket):
return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
def nice_hotkey_name(punc):
# convert the ugly string name into the actual character
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
nice_name = {
'LEFTMOUSE': "LMB",
'MIDDLEMOUSE': "MMB",
'RIGHTMOUSE': "RMB",
'WHEELUPMOUSE': "Wheel Up",
'WHEELDOWNMOUSE': "Wheel Down",
'WHEELINMOUSE': "Wheel In",
'WHEELOUTMOUSE': "Wheel Out",
'ZERO': "0",
'ONE': "1",
'TWO': "2",
'THREE': "3",
'FOUR': "4",
'FIVE': "5",
'SIX': "6",
'SEVEN': "7",
'EIGHT': "8",
'NINE': "9",
'OSKEY': "Super",
'RET': "Enter",
'LINE_FEED': "Enter",
'SEMI_COLON': ";",
'PERIOD': ".",
'COMMA': ",",
'QUOTE': '"',
'MINUS': "-",
'SLASH': "/",
'BACK_SLASH': "\\",
'EQUAL': "=",
'NUMPAD_1': "Numpad 1",
'NUMPAD_2': "Numpad 2",
'NUMPAD_3': "Numpad 3",
'NUMPAD_4': "Numpad 4",
'NUMPAD_5': "Numpad 5",
'NUMPAD_6': "Numpad 6",
'NUMPAD_7': "Numpad 7",
'NUMPAD_8': "Numpad 8",
'NUMPAD_9': "Numpad 9",
'NUMPAD_0': "Numpad 0",
'NUMPAD_PERIOD': "Numpad .",
'NUMPAD_SLASH': "Numpad /",
'NUMPAD_ASTERIX': "Numpad *",
'NUMPAD_MINUS': "Numpad -",
'NUMPAD_ENTER': "Numpad Enter",
'NUMPAD_PLUS': "Numpad +",
}
try:
return nice_name[punc]
except KeyError:
return punc.replace("_", " ").title()
def force_update(context):
context.space_data.node_tree.update_tag()
prefs = bpy.context.preferences.system
Brecht Van Lommel
committed
return prefs.dpi * prefs.pixel_size / 72
def node_mid_pt(node, axis):
if axis == 'x':
d = node.location.x + (node.dimensions.x / 2)
elif axis == 'y':
d = node.location.y - (node.dimensions.y / 2)
else:
d = 0
return d
def autolink(node1, node2, links):
link_made = False
available_inputs = [inp for inp in node2.inputs if inp.enabled]
available_outputs = [outp for outp in node1.outputs if outp.enabled]
for outp in available_outputs:
for inp in available_inputs:
if not inp.is_linked and inp.name == outp.name:
link_made = True
links.new(outp, inp)
return True
for outp in available_outputs:
for inp in available_inputs:
if not inp.is_linked and inp.type == outp.type:
link_made = True
links.new(outp, inp)
return True
# force some connection even if the type doesn't match
if available_outputs:
for inp in available_inputs:
if not inp.is_linked:
link_made = True
links.new(available_outputs[0], inp)
return True
# even if no sockets are open, force one of matching type
for outp in available_outputs:
for inp in available_inputs:
if inp.type == outp.type:
link_made = True
links.new(outp, inp)
return True
# do something!
for outp in available_outputs:
for inp in available_inputs:
link_made = True
links.new(outp, inp)
return True
print("Could not make a link from " + node1.name + " to " + node2.name)
return link_made
def abs_node_location(node):
abs_location = node.location
if node.parent is None:
return abs_location
return abs_location + abs_node_location(node.parent)
def node_at_pos(nodes, context, event):
nodes_under_mouse = []
target_node = None
store_mouse_cursor(context, event)
x, y = context.space_data.cursor_location
# Make a list of each corner (and middle of border) for each node.
# Will be sorted to find nearest point and thus nearest node
node_points_with_dist = []
for node in nodes:
skipnode = False
if node.type != 'FRAME': # no point trying to link to a frame node
dimx = node.dimensions.x/dpifac()
dimy = node.dimensions.y/dpifac()
locx, locy = abs_node_location(node)
node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
for node in nodes:
if node.type != 'FRAME' and skipnode == False:
locx, locy = abs_node_location(node)
dimx = node.dimensions.x/dpifac()
dimy = node.dimensions.y/dpifac()
if (locx <= x <= locx + dimx) and \
(locy - dimy <= y <= locy):
nodes_under_mouse.append(node)
if len(nodes_under_mouse) == 1:
if nodes_under_mouse[0] != nearest_node:
target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
else:
target_node = nearest_node # else use the nearest node
return target_node
def store_mouse_cursor(context, event):
space = context.space_data
v2d = context.region.view2d
tree = space.edit_tree
# convert mouse position to the View2D for later node placement
if context.region.type == 'WINDOW':
space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
else:
space.cursor_location = tree.view_center
def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
vertices = ((x1, y1), (x2, y2))
vertex_colors = ((colour[0]+(1.0-colour[0])/4,
colour[1]+(1.0-colour[1])/4,
colour[2]+(1.0-colour[2])/4,
colour[3]+(1.0-colour[3])/4),
colour)
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
shader.bind()
batch.draw(shader)
def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
radius = radius * dpifac()
sides = 12
vertices = [(radius * cos(i * 2 * pi / sides) + mx,
radius * sin(i * 2 * pi / sides) + my)
for i in range(sides + 1)]
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
shader.bind()
shader.uniform_float("color", colour)
batch.draw(shader)
def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
area_width = bpy.context.area.width
nlocx, nlocy = abs_node_location(node)
nlocx = (nlocx+1)*dpifac()
nlocy = (nlocy+1)*dpifac()
ndimx = node.dimensions.x
ndimy = node.dimensions.y
if node.hide:
nlocx += -1
nlocy += 5
if node.type == 'REROUTE':
#nlocx += 1
nlocy -= 1
ndimx = 0
ndimy = 0
radius += 6
# Top left corner
mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
for i in range(sides+1):
if (4<=i<=8):
if mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
vertices.append((cosine,sine))
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
shader.bind()
shader.uniform_float("color", colour)
batch.draw(shader)
mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
for i in range(sides+1):
if (0<=i<=4):
if mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
vertices.append((cosine,sine))
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
shader.bind()
shader.uniform_float("color", colour)
batch.draw(shader)
mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
for i in range(sides+1):
if (8<=i<=12):
if mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
vertices.append((cosine,sine))
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
shader.bind()
shader.uniform_float("color", colour)
batch.draw(shader)
mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
for i in range(sides+1):
if (12<=i<=16):
if mx < area_width:
cosine = radius * cos(i * 2 * pi / sides) + mx
sine = radius * sin(i * 2 * pi / sides) + my
vertices.append((cosine,sine))
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
shader.bind()
shader.uniform_float("color", colour)
batch.draw(shader)
# prepare drawing all edges in one batch
vertices = []
indices = []
id_last = 0
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
if m1x < area_width and m2x < area_width:
vertices.extend([(m2x-radius,m2y), (m2x,m2y),
(m1x,m1y), (m1x-radius,m1y)])
indices.extend([(id_last, id_last+1, id_last+3),
(id_last+3, id_last+1, id_last+2)])
id_last += 4
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
m1x = min(m1x, area_width)
m2x = min(m2x, area_width)
vertices.extend([(m1x,m1y), (m2x,m1y),
(m2x,m1y+radius), (m1x,m1y+radius)])
indices.extend([(id_last, id_last+1, id_last+3),
(id_last+3, id_last+1, id_last+2)])
id_last += 4
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
if m1x < area_width and m2x < area_width:
vertices.extend([(m1x,m2y), (m1x+radius,m2y),
(m1x+radius,m1y), (m1x,m1y)])
indices.extend([(id_last, id_last+1, id_last+3),
(id_last+3, id_last+1, id_last+2)])
id_last += 4
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
m1x = min(m1x, area_width)
m2x = min(m2x, area_width)
vertices.extend([(m1x,m2y), (m2x,m2y),
(m2x,m1y-radius), (m1x,m1y-radius)])
indices.extend([(id_last, id_last+1, id_last+3),
(id_last+3, id_last+1, id_last+2)])
# now draw all edges in one batch
if len(vertices) != 0:
batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
shader.bind()
shader.uniform_float("color", colour)
batch.draw(shader)
def draw_callback_nodeoutline(self, context, mode):
if self.mouse_path:
bgl.glLineWidth(1)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
nodes, links = get_nodes_links(context)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
col_outer = (1.0, 0.2, 0.2, 0.4)
col_inner = (0.0, 0.0, 0.0, 0.5)
col_circle_inner = (0.3, 0.05, 0.05, 1.0)
col_outer = (0.4, 0.6, 1.0, 0.4)
col_inner = (0.0, 0.0, 0.0, 0.5)
col_circle_inner = (0.08, 0.15, .3, 1.0)
col_outer = (0.2, 1.0, 0.2, 0.4)
col_inner = (0.0, 0.0, 0.0, 0.5)
col_circle_inner = (0.05, 0.3, 0.05, 1.0)
m1x = self.mouse_path[0][0]
m1y = self.mouse_path[0][1]
m2x = self.mouse_path[-1][0]
m2y = self.mouse_path[-1][1]
n1 = nodes[context.scene.NWLazySource]
n2 = nodes[context.scene.NWLazyTarget]
col_outer = (0.4, 0.4, 0.4, 0.4)
col_inner = (0.0, 0.0, 0.0, 0.5)
col_circle_inner = (0.2, 0.2, 0.2, 1.0)
draw_rounded_node_border(shader, n1, radius=6, colour=col_outer) # outline
draw_rounded_node_border(shader, n1, radius=5, colour=col_inner) # inner
draw_rounded_node_border(shader, n2, radius=6, colour=col_outer) # outline
draw_rounded_node_border(shader, n2, radius=5, colour=col_inner) # inner
draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
# circle outline
draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
# circle inner
draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
bgl.glDisable(bgl.GL_BLEND)
bgl.glDisable(bgl.GL_LINE_SMOOTH)
def get_active_tree(context):
tree = context.space_data.node_tree
# Get nodes from currently edited tree.
# If user is editing a group, space_data.node_tree is still the base level (outside group).
# context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
# the same as context.active_node, the user is in a group.
# Check recursively until we find the real active node_tree:
if tree.nodes.active:
while tree.nodes.active != context.active_node:
tree = tree.nodes.active.node_tree
path.append(tree)
return tree, path
def get_nodes_links(context):
tree, path = get_active_tree(context)
return tree.nodes, tree.links
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
def is_viewer_socket(socket):
# checks if a internal socket is a valid viewer socket
return socket.name == viewer_socket_name and socket.NWViewerSocket
def get_internal_socket(socket):
#get the internal socket from a socket inside or outside the group
node = socket.node
if node.type == 'GROUP_OUTPUT':
source_iterator = node.inputs
iterator = node.id_data.outputs
elif node.type == 'GROUP_INPUT':
source_iterator = node.outputs
iterator = node.id_data.inputs
elif hasattr(node, "node_tree"):
if socket.is_output:
source_iterator = node.outputs
iterator = node.node_tree.outputs
else:
source_iterator = node.inputs
iterator = node.node_tree.inputs
else:
return None
for i, s in enumerate(source_iterator):
if s == socket:
break
return iterator[i]
def is_viewer_link(link, output_node):
if link.to_node == output_node and link.to_socket == output_node.inputs[0]:
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
return True
if link.to_node.type == 'GROUP_OUTPUT':
socket = get_internal_socket(link.to_socket)
if is_viewer_socket(socket):
return True
return False
def get_group_output_node(tree):
for node in tree.nodes:
if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
return node
def get_output_location(tree):
# get right-most location
sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
max_xloc_node = sorted_by_xloc[-1]
# get average y location
sum_yloc = 0
for node in tree.nodes:
sum_yloc += node.location.y
loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
loc_y = sum_yloc / len(tree.nodes)
return loc_x, loc_y
# Principled prefs
class NWPrincipledPreferences(bpy.types.PropertyGroup):
name='Base Color',
default='diffuse diff albedo base col color',
description='Naming Components for Base Color maps')
name='Subsurface Color',
default='sss subsurface',
description='Naming Components for Subsurface Color maps')
name='Metallic',
default='metallic metalness metal mtl',
description='Naming Components for metallness maps')
name='Specular',
default='specularity specular spec spc',
description='Naming Components for Specular maps')
name='Normal',
default='normal nor nrm nrml norm',
description='Naming Components for Normal maps')
name='Bump',
default='bump bmp',
description='Naming Components for bump maps')
name='Roughness',
default='roughness rough rgh',
description='Naming Components for roughness maps')
default='gloss glossy glossiness',
description='Naming Components for glossy maps')
default='displacement displace disp dsp height heightmap',
description='Naming Components for displacement maps')
transmission: StringProperty(
name='Transmission',
default='transmission transparency',
description='Naming Components for transmission maps')
emission: StringProperty(
name='Emission',
default='emission emissive emit',
description='Naming Components for emission maps')
alpha: StringProperty(
name='Alpha',
default='alpha opacity',
description='Naming Components for alpha maps')
ambient_occlusion: StringProperty(
name='Ambient Occlusion',
default='ao ambient occlusion',
description='Naming Components for AO maps')
# Addon prefs
class NWNodeWrangler(bpy.types.AddonPreferences):
bl_idname = __name__
name="Hide Mix nodes",
items=(
("ALWAYS", "Always", "Always collapse the new merge nodes"),
("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
("NEVER", "Never", "Never collapse the new merge nodes")
),
default='NON_SHADER',
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
name="Mix Node Position",
items=(
("CENTER", "Center", "Place the Mix node between the two nodes"),
("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
),
default='CENTER',
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
name="Show Hotkey List",
default=False,
description="Expand this box into a list of all the hotkeys for functions in this addon"
)
name=" Filter by Name",
default="",
description="Show only hotkeys that have this text in their name",
options={'TEXTEDIT_UPDATE'}
name="Show Principled naming tags",
default=False,
description="Expand this box into a list of all naming tags for principled texture setup"
)
principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "merge_position")
col.prop(self, "merge_hide")
col = box.column(align=True)
col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
if self.show_principled_lists:
tags = self.principled_tags
col.prop(tags, "base_color")
col.prop(tags, "sss_color")
col.prop(tags, "metallic")
col.prop(tags, "specular")
col.prop(tags, "rough")
col.prop(tags, "gloss")
col.prop(tags, "normal")
col.prop(tags, "bump")
col.prop(tags, "displacement")
col.prop(tags, "transmission")
col.prop(tags, "emission")
col.prop(tags, "alpha")
col.prop(tags, "ambient_occlusion")
box = layout.box()
col = box.column(align=True)
hotkey_button_name = "Show Hotkey List"
if self.show_hotkey_list:
hotkey_button_name = "Hide Hotkey List"
col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
if self.show_hotkey_list:
col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
col.separator()
for hotkey in kmi_defs:
if self.hotkey_list_filter.lower() in hotkey_name.lower():
row = col.row(align=True)
row.label(text=hotkey_name)
keystr = nice_hotkey_name(hotkey[1])
if hotkey[4]:
keystr = "Alt " + keystr
keystr = "Ctrl " + keystr
row.label(text=keystr)
def nw_check(context):
space = context.space_data
valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
valid = False
if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
valid = True
return valid
class NWBase:
@classmethod
def poll(cls, context):
return nw_check(context)
# OPERATORS
class NWLazyMix(Operator, NWBase):
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
bl_idname = "node.nw_lazy_mix"
bl_label = "Mix Nodes"
bl_options = {'REGISTER', 'UNDO'}
def modal(self, context, event):
context.area.tag_redraw()
nodes, links = get_nodes_links(context)
cont = True
start_pos = [event.mouse_region_x, event.mouse_region_y]
node1 = None
if not context.scene.NWBusyDrawing:
node1 = node_at_pos(nodes, context, event)
if node1:
context.scene.NWBusyDrawing = node1.name
else:
if context.scene.NWBusyDrawing != 'STOP':
node1 = nodes[context.scene.NWBusyDrawing]
context.scene.NWLazySource = node1.name
context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
if event.type == 'MOUSEMOVE':
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
end_pos = [event.mouse_region_x, event.mouse_region_y]
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
node2 = None
node2 = node_at_pos(nodes, context, event)
if node2:
context.scene.NWBusyDrawing = node2.name
if node1 == node2:
cont = False
if cont:
if node1 and node2:
for node in nodes:
node.select = False
node1.select = True
node2.select = True
bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
context.scene.NWBusyDrawing = ""
return {'FINISHED'}
elif event.type == 'ESC':
print('cancelled')
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'NODE_EDITOR':
# the arguments we pass the the callback
args = (self, context, 'MIX')
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
self.mouse_path = []
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
class NWLazyConnect(Operator, NWBase):
"""Connect two nodes without clicking a specific socket (automatically determined"""
bl_idname = "node.nw_lazy_connect"
bl_label = "Lazy Connect"
bl_options = {'REGISTER', 'UNDO'}
def modal(self, context, event):
context.area.tag_redraw()
nodes, links = get_nodes_links(context)
cont = True
start_pos = [event.mouse_region_x, event.mouse_region_y]
node1 = None
if not context.scene.NWBusyDrawing:
node1 = node_at_pos(nodes, context, event)
if node1:
context.scene.NWBusyDrawing = node1.name
else:
if context.scene.NWBusyDrawing != 'STOP':
node1 = nodes[context.scene.NWBusyDrawing]
context.scene.NWLazySource = node1.name
context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
if event.type == 'MOUSEMOVE':
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
end_pos = [event.mouse_region_x, event.mouse_region_y]
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
node2 = None
node2 = node_at_pos(nodes, context, event)
if node2:
context.scene.NWBusyDrawing = node2.name
if node1 == node2:
cont = False
link_success = False
if cont:
if node1 and node2:
original_sel = []
original_unsel = []
for node in nodes:
if node.select == True:
node.select = False
original_sel.append(node)
else:
original_unsel.append(node)
node1.select = True
node2.select = True
#link_success = autolink(node1, node2, links)
if self.with_menu:
if len(node1.outputs) > 1 and node2.inputs:
bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
elif len(node1.outputs) == 1:
bpy.ops.node.nw_call_inputs_menu(from_socket=0)
else:
link_success = autolink(node1, node2, links)
for node in original_sel:
node.select = True
for node in original_unsel:
node.select = False
if link_success:
context.scene.NWBusyDrawing = ""
return {'FINISHED'}
elif event.type == 'ESC':
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'NODE_EDITOR':
nodes, links = get_nodes_links(context)
node = node_at_pos(nodes, context, event)
if node:
context.scene.NWBusyDrawing = node.name
# the arguments we pass the the callback
mode = "LINK"
if self.with_menu:
mode = "LINKMENU"
args = (self, context, mode)
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
self.mouse_path = []
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
class NWDeleteUnused(Operator, NWBase):
"""Delete all nodes whose output is not used"""
bl_idname = 'node.nw_del_unused'
bl_label = 'Delete Unused Nodes'
bl_options = {'REGISTER', 'UNDO'}
delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
Greg
committed
end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
if node.type in end_types:
return False
for output in node.outputs:
if output.links:
return False
return True
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
if context.space_data.node_tree.nodes:
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
# Store selection
selection = []
for node in nodes:
if node.select == True:
selection.append(node.name)
for node in nodes:
node.select = False
deleted_nodes = []
temp_deleted_nodes = []
del_unused_iterations = len(nodes)
for it in range(0, del_unused_iterations):
temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
for node in nodes:
node.select = True
deleted_nodes.append(node.name)
bpy.ops.node.delete()
if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
break
Greg
committed
if self.delete_frames:
repeat = True
while repeat:
frames_in_use = []
frames = []
repeat = False
for node in nodes:
if node.parent:
frames_in_use.append(node.parent)
for node in nodes:
if node.type == 'FRAME' and node not in frames_in_use:
Greg
committed
frames.append(node)
if node.parent:
repeat = True # repeat for nested frames
for node in frames:
if node not in frames_in_use:
node.select = True
deleted_nodes.append(node.name)
bpy.ops.node.delete()
if self.delete_muted:
for node in nodes:
if node.mute:
node.select = True
deleted_nodes.append(node.name)
bpy.ops.node.delete_reconnect()
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
# get unique list of deleted nodes (iterations would count the same node more than once)
deleted_nodes = list(set(deleted_nodes))
for n in deleted_nodes:
self.report({'INFO'}, "Node " + n + " deleted")
num_deleted = len(deleted_nodes)
n = ' node'
if num_deleted > 1:
n += 's'
if num_deleted:
self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
else:
self.report({'INFO'}, "Nothing deleted")
# Restore selection
nodes, links = get_nodes_links(context)
for node in nodes:
if node.name in selection:
node.select = True
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class NWSwapLinks(Operator, NWBase):
"""Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
bl_idname = 'node.nw_swap_links'
bl_label = 'Swap Links'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
if context.selected_nodes:
valid = len(context.selected_nodes) <= 2
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
selected_nodes = context.selected_nodes
n1 = selected_nodes[0]
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
# Swap outputs
if len(selected_nodes) == 2:
n2 = selected_nodes[1]
if n1.outputs and n2.outputs:
n1_outputs = []
n2_outputs = []
out_index = 0
for output in n1.outputs:
if output.links:
for link in output.links:
n1_outputs.append([out_index, link.to_socket])
links.remove(link)
out_index += 1
out_index = 0
for output in n2.outputs:
if output.links:
for link in output.links:
n2_outputs.append([out_index, link.to_socket])
links.remove(link)
out_index += 1
for connection in n1_outputs:
try:
links.new(n2.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
for connection in n2_outputs:
try:
links.new(n1.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
else:
if n1.outputs or n2.outputs:
self.report({'WARNING'}, "One of the nodes has no outputs!")
else:
self.report({'WARNING'}, "Neither of the nodes have outputs!")
# Swap Inputs
elif len(selected_nodes) == 1:
if n1.inputs and n1.inputs[0].is_multi_input:
self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
return {'FINISHED'}
if n1.inputs:
types = []
i=0
for i1 in n1.inputs:
if i1.is_linked and not i1.is_multi_input:
similar_types = 0
for i2 in n1.inputs:
if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
similar_types += 1
types.append ([i1, similar_types, i])
i += 1
types.sort(key=lambda k: k[1], reverse=True)
if types:
t = types[0]
if t[1] == 2:
for i2 in n1.inputs:
if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
pair = [t[0], i2]
i1f = pair[0].links[0].from_socket
i1t = pair[0].links[0].to_socket
i2f = pair[1].links[0].from_socket
i2t = pair[1].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
if t[1] == 1:
if len(types) == 1:
fs = t[0].links[0].from_socket
i = t[2]
links.remove(t[0].links[0])
if i+1 == len(n1.inputs):
i = -1
i += 1
while n1.inputs[i].is_linked:
i += 1
links.new(fs, n1.inputs[i])
elif len(types) == 2:
i1f = types[0][0].links[0].from_socket
i1t = types[0][0].links[0].to_socket
i2f = types[1][0].links[0].from_socket
i2t = types[1][0].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
else:
self.report({'WARNING'}, "This node has no input connections to swap!")
else:
self.report({'WARNING'}, "This node has no inputs to swap!")
return {'FINISHED'}
class NWResetBG(Operator, NWBase):
"""Reset the zoom and position of the background image"""
bl_idname = 'node.nw_bg_reset'
bl_label = 'Reset Backdrop'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
snode = context.space_data
valid = snode.tree_type == 'CompositorNodeTree'
return valid
def execute(self, context):
context.space_data.backdrop_zoom = 1
context.space_data.backdrop_offset[0] = 0
context.space_data.backdrop_offset[1] = 0
return {'FINISHED'}
class NWAddAttrNode(Operator, NWBase):
"""Add an Attribute node with this name"""
bl_idname = 'node.nw_add_attr_node'
bl_label = 'Add UV map'
bl_options = {'REGISTER', 'UNDO'}
attr_name: StringProperty()
def execute(self, context):
bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
nodes, links = get_nodes_links(context)
nodes.active.attribute_name = self.attr_name
return {'FINISHED'}
class NWPreviewNode(Operator, NWBase):
bl_idname = "node.nw_preview_node"
bl_label = "Preview Node"
bl_description = "Connect active node to the Node Group output or the Material Output"
bl_options = {'REGISTER', 'UNDO'}
# If false, the operator is not executed if the current node group happens to be a geometry nodes group.
# This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
# Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
run_in_geometry_nodes: BoolProperty(default=True)
def __init__(self):
self.shader_output_type = ""
self.shader_output_ident = ""
@classmethod
def poll(cls, context):
if nw_check(context):
space = context.space_data
if space.tree_type == 'ShaderNodeTree' or space.tree_type == 'GeometryNodeTree':
if context.active_node:
if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
return True
else:
return True
return False
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
#check if a viewer output already exists in a node group otherwise create
if hasattr(node, "node_tree"):
index = None
if len(node.node_tree.outputs):
free_socket = None
for i, socket in enumerate(node.node_tree.outputs):
if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
#if viewer output is already used but leads to the same socket we can still use it
is_used = self.is_socket_used_other_mats(socket)
if is_used:
if connect_socket == None:
continue
groupout = get_group_output_node(node.node_tree)
groupout_input = groupout.inputs[i]
links = groupout_input.links
if connect_socket not in [link.from_socket for link in links]:
continue
index=i
break
if not free_socket:
free_socket = i
if not index and free_socket:
index = free_socket
if not index:
#create viewer socket
node.node_tree.outputs.new(socket_type, viewer_socket_name)
index = len(node.node_tree.outputs) - 1
node.node_tree.outputs[index].NWViewerSocket = True
return index
def init_shader_variables(self, space, shader_type):
if shader_type == 'OBJECT':
if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
self.shader_output_type = "OUTPUT_MATERIAL"
self.shader_output_ident = "ShaderNodeOutputMaterial"
self.shader_output_type = "OUTPUT_LIGHT"
self.shader_output_ident = "ShaderNodeOutputLight"
elif shader_type == 'WORLD':
self.shader_output_type = "OUTPUT_WORLD"
self.shader_output_ident = "ShaderNodeOutputWorld"
def get_shader_output_node(self, tree):
for node in tree.nodes:
if node.type == self.shader_output_type and node.is_active_output == True:
return node
@classmethod
def ensure_group_output(cls, tree):
#check if a group output node exists otherwise create
groupout = get_group_output_node(tree)
if not groupout:
groupout = tree.nodes.new('NodeGroupOutput')
loc_x, loc_y = get_output_location(tree)
groupout.location.x = loc_x
groupout.location.y = loc_y
groupout.select = False
# So that we don't keep on adding new group outputs
groupout.is_active_output = True
return groupout
@classmethod
def search_sockets(cls, node, sockets, index=None):
# recursively scan nodes for viewer sockets and store in list
for i, input_socket in enumerate(node.inputs):
if index and i != index:
continue
if len(input_socket.links):
link = input_socket.links[0]
next_node = link.from_node
external_socket = link.from_socket
if hasattr(next_node, "node_tree"):
for socket_index, s in enumerate(next_node.outputs):
if s == external_socket:
break
socket = next_node.node_tree.outputs[socket_index]
if is_viewer_socket(socket) and socket not in sockets:
sockets.append(socket)
#continue search inside of node group but restrict socket to where we came from
groupout = get_group_output_node(next_node.node_tree)
cls.search_sockets(groupout, sockets, index=socket_index)
@classmethod
def scan_nodes(cls, tree, sockets):
# get all viewer sockets in a material tree
for node in tree.nodes:
if hasattr(node, "node_tree"):
for socket in node.node_tree.outputs:
if is_viewer_socket(socket) and (socket not in sockets):
sockets.append(socket)
cls.scan_nodes(node.node_tree, sockets)
def link_leads_to_used_socket(self, link):
#return True if link leads to a socket that is already used in this material
socket = get_internal_socket(link.to_socket)
return (socket and self.is_socket_used_active_mat(socket))
def is_socket_used_active_mat(self, socket):
#ensure used sockets in active material is calculated and check given socket
if not hasattr(self, "used_viewer_sockets_active_mat"):
self.used_viewer_sockets_active_mat = []
materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
if materialout:
self.search_sockets(materialout, self.used_viewer_sockets_active_mat)
return socket in self.used_viewer_sockets_active_mat
def is_socket_used_other_mats(self, socket):
#ensure used sockets in other materials are calculated and check given socket
if not hasattr(self, "used_viewer_sockets_other_mats"):
self.used_viewer_sockets_other_mats = []
for mat in bpy.data.materials:
if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
continue
# get viewer node
materialout = self.get_shader_output_node(mat.node_tree)
if materialout:
self.search_sockets(materialout, self.used_viewer_sockets_other_mats)
return socket in self.used_viewer_sockets_other_mats
def invoke(self, context, event):
space = context.space_data
# Ignore operator when running in wrong context.
if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
return {'PASS_THROUGH'}
shader_type = space.shader_type
self.init_shader_variables(space, shader_type)
mlocx = event.mouse_region_x
mlocy = event.mouse_region_y
select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
if 'FINISHED' in select_node: # only run if mouse click is on a node
active_tree, path_to_tree = get_active_tree(context)
nodes, links = active_tree.nodes, active_tree.links
base_node_tree = space.node_tree
active = nodes.active
# For geometry node trees we just connect to the group output
if space.tree_type == "GeometryNodeTree":
valid = False
if active:
for out in active.outputs:
if is_visible_socket(out):
valid = True
break
# Exit early
if not valid:
return {'FINISHED'}
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
delete_sockets = []
# Scan through all nodes in tree including nodes inside of groups to find viewer sockets
self.scan_nodes(base_node_tree, delete_sockets)
# Find (or create if needed) the output of this node tree
geometryoutput = self.ensure_group_output(base_node_tree)
# Analyze outputs, make links
out_i = None
valid_outputs = []
for i, out in enumerate(active.outputs):
if is_visible_socket(out) and out.type == 'GEOMETRY':
valid_outputs.append(i)
if valid_outputs:
out_i = valid_outputs[0] # Start index of node's outputs
for i, valid_i in enumerate(valid_outputs):
for out_link in active.outputs[valid_i].links:
if is_viewer_link(out_link, geometryoutput):
if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
if i < len(valid_outputs) - 1:
out_i = valid_outputs[i + 1]
else:
out_i = valid_outputs[0]
make_links = [] # store sockets for new links
if active.outputs:
# If there is no 'GEOMETRY' output type - We can't preview the node
if out_i is None:
return {'FINISHED'}
socket_type = 'GEOMETRY'
# Find an input socket of the output of type geometry
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
geometryoutindex = None
for i,inp in enumerate(geometryoutput.inputs):
if inp.type == socket_type:
geometryoutindex = i
break
if geometryoutindex is None:
# Create geometry socket
geometryoutput.inputs.new(socket_type, 'Geometry')
geometryoutindex = len(geometryoutput.inputs) - 1
make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
output_socket = geometryoutput.inputs[geometryoutindex]
for li_from, li_to in make_links:
base_node_tree.links.new(li_from, li_to)
tree = base_node_tree
link_end = output_socket
while tree.nodes.active != active:
node = tree.nodes.active
index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
link_start = node.outputs[index]
node_socket = node.node_tree.outputs[index]
if node_socket in delete_sockets:
delete_sockets.remove(node_socket)
tree.links.new(link_start, link_end)
# Iterate
link_end = self.ensure_group_output(node.node_tree).inputs[index]
tree = tree.nodes.active.node_tree
tree.links.new(active.outputs[out_i], link_end)
# Delete sockets
for socket in delete_sockets:
tree = socket.id_data
tree.outputs.remove(socket)
nodes.active = active
active.select = True
force_update(context)
return {'FINISHED'}
# What follows is code for the shader editor
output_types = [x.nodetype for x in
get_nodes_from_category('Output', context)]
if active:
if active.rna_type.identifier not in output_types:
for out in active.outputs:
if is_visible_socket(out):
valid = True
break
# get material_output node
materialout = None # placeholder node
delete_sockets = []
#scan through all nodes in tree including nodes inside of groups to find viewer sockets
self.scan_nodes(base_node_tree, delete_sockets)
materialout = self.get_shader_output_node(base_node_tree)
if not materialout:
materialout = base_node_tree.nodes.new(self.shader_output_ident)
materialout.location = get_output_location(base_node_tree)
materialout.select = False
out_i = None
valid_outputs = []
for i, out in enumerate(active.outputs):
if is_visible_socket(out):
valid_outputs.append(i)
if valid_outputs:
out_i = valid_outputs[0] # Start index of node's outputs
for i, valid_i in enumerate(valid_outputs):
for out_link in active.outputs[valid_i].links:
if is_viewer_link(out_link, materialout):
if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
if i < len(valid_outputs) - 1:
out_i = valid_outputs[i + 1]
else:
out_i = valid_outputs[0]
make_links = [] # store sockets for new links
if active.outputs:
socket_type = 'NodeSocketShader'
materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
output_socket = materialout.inputs[materialout_index]
for li_from, li_to in make_links:
base_node_tree.links.new(li_from, li_to)
# Create links through node groups until we reach the active node
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
tree = base_node_tree
link_end = output_socket
while tree.nodes.active != active:
node = tree.nodes.active
index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
link_start = node.outputs[index]
node_socket = node.node_tree.outputs[index]
if node_socket in delete_sockets:
delete_sockets.remove(node_socket)
tree.links.new(link_start, link_end)
# Iterate
link_end = self.ensure_group_output(node.node_tree).inputs[index]
tree = tree.nodes.active.node_tree
tree.links.new(active.outputs[out_i], link_end)
# Delete sockets
for socket in delete_sockets:
if not self.is_socket_used_other_mats(socket):
tree = socket.id_data
tree.outputs.remove(socket)
nodes.active = active
active.select = True
return {'FINISHED'}
else:
return {'CANCELLED'}
class NWFrameSelected(Operator, NWBase):
bl_idname = "node.nw_frame_selected"
bl_label = "Frame Selected"
bl_description = "Add a frame node and parent the selected nodes to it"
bl_options = {'REGISTER', 'UNDO'}
label_prop: StringProperty(
name='Label',
description='The visual name of the frame node',
default=' '
)
use_custom_color_prop: BoolProperty(
name="Custom Color",
description="Use custom color for the frame node",
default=False
)
color_prop: FloatVectorProperty(
name="Color",
description="The color of the frame node",
default=(0.604, 0.604, 0.604),
min=0, max=1, step=1, precision=3,
subtype='COLOR_GAMMA', size=3
)
def draw(self, context):
layout = self.layout
layout.prop(self, 'label_prop')
layout.prop(self, 'use_custom_color_prop')
col = layout.column()
col.active = self.use_custom_color_prop
col.prop(self, 'color_prop', text="")
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = []
for node in nodes:
if node.select == True:
selected.append(node)
bpy.ops.node.add_node(type='NodeFrame')
frm = nodes.active
frm.label = self.label_prop
frm.use_custom_color = self.use_custom_color_prop
frm.color = self.color_prop
for node in selected:
node.parent = frm
return {'FINISHED'}
class NWReloadImages(Operator):
bl_idname = "node.nw_reload_images"
bl_label = "Reload Images"
bl_description = "Update all the image nodes to match their files on disk"
@classmethod
def poll(cls, context):
valid = False
if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
if context.active_node is not None:
for out in context.active_node.outputs:
if is_visible_socket(out):
valid = True
break
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
num_reloaded = 0
for node in nodes:
if node.type in image_types:
if node.type == "TEXTURE":
if node.texture: # node has texture assigned
if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
if node.texture.image: # texture has image assigned
node.texture.image.reload()
num_reloaded += 1
else:
if node.image:
node.image.reload()
num_reloaded += 1
if num_reloaded:
self.report({'INFO'}, "Reloaded images")
print("Reloaded " + str(num_reloaded) + " images")
return {'FINISHED'}
else:
self.report({'WARNING'}, "No images found to reload in this node tree")
return {'CANCELLED'}
class NWSwitchNodeType(Operator, NWBase):
"""Switch type of selected nodes """
bl_idname = "node.nw_swtch_node_type"
bl_label = "Switch Node Type"
bl_options = {'REGISTER', 'UNDO'}
to_type: StringProperty(
name="Switch to type",
default = '',
)
def execute(self, context):
to_type = self.to_type
if len(to_type) == 0:
return {'CANCELLED'}
nodes, links = get_nodes_links(context)
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
# Those types of nodes will not swap.
src_excludes = ('NodeFrame')
# Those attributes of nodes will be copied if possible
attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
'show_options', 'show_preview', 'show_texture',
'use_alpha', 'use_clamp', 'use_custom_color', 'location'
)
selected = [n for n in nodes if n.select]
reselect = []
for node in [n for n in selected if
n.rna_type.identifier not in src_excludes and
n.rna_type.identifier != to_type]:
new_node = nodes.new(to_type)
for attr in attrs_to_pass:
if hasattr(node, attr) and hasattr(new_node, attr):
setattr(new_node, attr, getattr(node, attr))
# set image datablock of dst to image of src
if hasattr(node, 'image') and hasattr(new_node, 'image'):
if node.image:
new_node.image = node.image
# Special cases
if new_node.type == 'SWITCH':
new_node.hide = True
# Dictionaries: src_sockets and dst_sockets:
# 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
# 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
# in 'INPUTS' and 'OUTPUTS':
# 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
# socket entry:
# (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
src_sockets = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
}
dst_sockets = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
}
types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
# check src node to set src_sockets values and dst node to set dst_sockets dict values
for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
# Check node's inputs and outputs and fill proper entries in "sockets" dict
for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
# enumerate in inputs, then in outputs
# find name, default value and links of socket
for i, socket in enumerate(in_out):
the_name = socket.name
dval = None
# Not every socket, especially in outputs has "default_value"
if hasattr(socket, 'default_value'):
dval = socket.default_value
socket_links = []
for lnk in socket.links:
socket_links.append(lnk)
# check type of socket to fill proper keys.
for the_type in types_order_one:
if socket.type == the_type:
# create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
# entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
# Check which of the types in inputs/outputs is considered to be "main".
# Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
for type_check in types_order_one:
if sockets[in_out_name][type_check]:
sockets[in_out_name]['MAIN'] = type_check
break
matches = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
}
for inout, soctype in (
('INPUTS', 'MAIN',),
('INPUTS', 'SHADER',),
('INPUTS', 'RGBA',),
('INPUTS', 'VECTOR',),
('INPUTS', 'VALUE',),
('OUTPUTS', 'MAIN',),
('OUTPUTS', 'SHADER',),
('OUTPUTS', 'RGBA',),
('OUTPUTS', 'VECTOR',),
('OUTPUTS', 'VALUE',),
):
if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
if soctype == 'MAIN':
sc = src_sockets[inout][src_sockets[inout]['MAIN']]
dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
else:
sc = src_sockets[inout][soctype]
dt = dst_sockets[inout][soctype]
# start with 'dt' to determine number of possibilities.
for i, soc in enumerate(dt):
# if src main has enough entries - match them with dst main sockets by indexes.
if len(sc) > i:
matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
# add 'VALUE_NAME' criterion to inputs.
if inout == 'INPUTS' and soctype == 'VALUE':
for s in sc:
if s[2] == soc[2]: # if names match
# append src (index, dval), dst (index, dval)
matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
# When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
# This creates better links when relinking textures.
if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
# Pass default values and RELINK:
for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
# INPUTS: Base on matches in proper order.
for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
# pass dvals
if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
new_node.inputs[dst_i].default_value = src_dval
# Special case: switch to math
if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
new_node.type == 'MATH' and\
tp == 'MAIN':
new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
new_node.inputs[dst_i].default_value = new_dst_dval
if node.type == 'MIX_RGB':
if node.blend_type in [o[0] for o in operations]:
new_node.operation = node.blend_type
# Special case: switch from math to some types
if node.type == 'MATH' and\
new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
tp == 'MAIN':
for i in range(3):
new_node.inputs[dst_i].default_value[i] = src_dval
if new_node.type == 'MIX_RGB':
if node.operation in [t[0] for t in blend_types]:
new_node.blend_type = node.operation
# Set Fac of MIX_RGB to 1.0
new_node.inputs[0].default_value = 1.0
# make link only when dst matching input is not linked already.
if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
in_src_link = node.inputs[src_i].links[0]
in_dst_socket = new_node.inputs[dst_i]
links.new(in_src_link.from_socket, in_dst_socket)
links.remove(in_src_link)
# OUTPUTS: Base on matches in proper order.
for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
for out_src_link in node.outputs[src_i].links:
out_dst_socket = new_node.outputs[dst_i]
links.new(out_dst_socket, out_src_link.to_socket)
# relink rest inputs if possible, no criteria
for src_inp in node.inputs:
for dst_inp in new_node.inputs:
if src_inp.links and not dst_inp.links:
src_link = src_inp.links[0]
links.new(src_link.from_socket, dst_inp)
links.remove(src_link)
# relink rest outputs if possible, base on node kind if any left.
for src_o in node.outputs:
for out_src_link in src_o.links:
for dst_o in new_node.outputs:
if src_o.type == dst_o.type:
links.new(dst_o, out_src_link.to_socket)
# relink rest outputs no criteria if any left. Link all from first output.
for src_o in node.outputs:
for out_src_link in src_o.links:
if new_node.outputs:
links.new(new_node.outputs[0], out_src_link.to_socket)
nodes.remove(node)
force_update(context)
return {'FINISHED'}
class NWMergeNodes(Operator, NWBase):
bl_idname = "node.nw_merge_nodes"
bl_label = "Merge Nodes"
bl_description = "Merge Selected Nodes"
bl_options = {'REGISTER', 'UNDO'}
name="mode",
description="All possible blend types, boolean operations and math operations",
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],
name="merge type",
description="Type of Merge to be used",
items=(
('AUTO', 'Auto', 'Automatic Output Type Detection'),
('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
('MIX', 'Mix Node', 'Merge using Mix Nodes'),
('MATH', 'Math Node', 'Merge using Math Nodes'),
('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
# Check if the link connects to a node that is in selected_nodes
# If not, then check recursively for each link in the nodes outputs.
# If yes, return True. If the recursion stops without finding a node
# in selected_nodes, it returns False. The depth is used to prevent
# getting stuck in a loop because of an already present cycle.
@staticmethod
def link_creates_cycle(link, selected_nodes, depth=0)->bool:
if depth > 255:
# We're stuck in a cycle, but that cycle was already present,
# NOTE: The number 255 is arbitrary, but seems to work well.
return False
node = link.to_node
if node in selected_nodes:
return True
if not node.outputs:
return False
for output in node.outputs:
if output.is_linked:
for olink in output.links:
if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1):
return True
# None of the outputs found a node in selected_nodes, so there is no cycle.
return False
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
# Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
# The parameters `socket_indices` gives the indices of the node sockets in the order that they should
# be connected. The last one is assumed to be a multi input socket.
# For convenience the node is returned.
@staticmethod
def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices):
# The y-location of the last node
loc_y = nodes_list[-1][2]
if merge_position == 'CENTER':
# Average the y-location
for i in range(len(nodes_list)-1):
loc_y += nodes_list[i][2]
loc_y = loc_y/len(nodes_list)
new_node = nodes.new(node_name)
new_node.hide = do_hide
new_node.location.x = loc_x
new_node.location.y = loc_y
selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
prev_links = []
outputs_for_multi_input = []
for i,node in enumerate(selected_nodes):
node.select = False
# Search for the first node which had output links that do not create
# a cycle, which we can then reconnect afterwards.
if prev_links == [] and node.outputs[0].is_linked:
prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)]
# Get the index of the socket, the last one is a multi input, and is thus used repeatedly
# To get the placement to look right we need to reverse the order in which we connect the
# outputs to the multi input socket.
if i < len(socket_indices) - 1:
ind = socket_indices[i]
links.new(node.outputs[0], new_node.inputs[ind])
else:
outputs_for_multi_input.insert(0, node.outputs[0])
if outputs_for_multi_input != []:
ind = socket_indices[-1]
for output in outputs_for_multi_input:
links.new(output, new_node.inputs[ind])
if prev_links != []:
for link in prev_links:
links.new(new_node.outputs[0], link.to_node.inputs[0])
return new_node
def execute(self, context):
settings = context.preferences.addons[__name__].preferences
merge_hide = settings.merge_hide
merge_position = settings.merge_position # 'center' or 'bottom'
do_hide = False
do_hide_shader = False
if merge_hide == 'ALWAYS':
do_hide = True
do_hide_shader = True
elif merge_hide == 'NON_SHADER':
do_hide = True
tree_type = context.space_data.node_tree.type
if tree_type == 'GEOMETRY':
node_type = 'GeometryNode'
if tree_type == 'COMPOSITING':
node_type = 'CompositorNode'
elif tree_type == 'SHADER':
node_type = 'ShaderNode'
elif tree_type == 'TEXTURE':
node_type = 'TextureNode'
nodes, links = get_nodes_links(context)
mode = self.mode
merge_type = self.merge_type
# Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
# 'ZCOMBINE' works only if mode == 'MIX'
# Setting mode to None prevents trying to add 'ZCOMBINE' node.
if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
merge_type = 'MIX'
mode = 'MIX'
if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
merge_type = 'AUTO'
# The MixRGB node and math nodes used for geometry nodes are of type 'ShaderNode'
if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
selected_mix = [] # entry = [index, loc]
selected_shader = [] # entry = [index, loc]
selected_geometry = [] # entry = [index, loc]
selected_math = [] # entry = [index, loc]
selected_vector = [] # entry = [index, loc]
selected_z = [] # entry = [index, loc]
selected_alphaover = [] # entry = [index, loc]
for i, node in enumerate(nodes):
if node.select and node.outputs:
if merge_type == 'AUTO':
for (type, types_list, dst) in (
('SHADER', ('MIX', 'ADD'), selected_shader),
('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
('RGBA', [t[0] for t in blend_types], selected_mix),
('VALUE', [t[0] for t in operations], selected_math),
('VECTOR', [], selected_vector),
output = get_first_enabled_output(node)
output_type = output.type
valid_mode = mode in types_list
# When mode is 'MIX' we have to cheat since the mix node is not used in
# geometry nodes.
if tree_type == 'GEOMETRY':
if mode == 'MIX':
if output_type == 'VALUE' and type == 'VALUE':
valid_mode = True
elif output_type == 'VECTOR' and type == 'VECTOR':
valid_mode = True
elif type == 'GEOMETRY':
valid_mode = True
# When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
# Cheat that output type is 'RGBA',
# and that 'MIX' exists in math operations list.
# This way when selected_mix list is analyzed:
# Node data will be appended even though it doesn't meet requirements.
elif output_type != 'SHADER' and mode == 'MIX':
output_type = 'RGBA'
valid_mode = True
if output_type == type and valid_mode:
dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
else:
for (type, types_list, dst) in (
('SHADER', ('MIX', 'ADD'), selected_shader),
('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
('MIX', [t[0] for t in blend_types], selected_mix),
('MATH', [t[0] for t in operations], selected_math),
('ZCOMBINE', ('MIX', ), selected_z),
('ALPHAOVER', ('MIX', ), selected_alphaover),
if merge_type == type and mode in types_list:
dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
# When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
# use only 'Mix' nodes for merging.
# For that we add selected_math list to selected_mix list and clear selected_math.
if selected_mix and selected_math and merge_type == 'AUTO':
selected_mix += selected_math
selected_math = []
for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]:
if not nodes_list:
continue
count_before = len(nodes)
# sort list by loc_x - reversed
nodes_list.sort(key=lambda k: k[1], reverse=True)
# get maximum loc_x
loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
nodes_list.sort(key=lambda k: k[2], reverse=True)
# Change the node type for math nodes in a geometry node tree.
if tree_type == 'GEOMETRY':
if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
node_type = 'ShaderNode'
if mode == 'MIX':
mode = 'ADD'
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
node_type = 'GeometryNode'
if merge_position == 'CENTER':
loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2 # average yloc of last two nodes (lowest two)
if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
if do_hide:
loc_y += 40
else:
loc_y += 80
else:
loc_y = nodes_list[len(nodes_list) - 1][2]
offset_y = 100
if not do_hide:
offset_y = 200
if nodes_list == selected_shader and not do_hide_shader:
offset_y = 150.0
the_range = len(nodes_list) - 1
if len(nodes_list) == 1:
the_range = 1
was_multi = False
for i in range(the_range):
if nodes_list == selected_mix:
add_type = node_type + 'MixRGB'
add = nodes.new(add_type)
add.blend_type = mode
if mode != 'MIX':
add.inputs[0].default_value = 1.0
add.show_preview = False
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 1
second = 2
add.width_hidden = 100.0
elif nodes_list == selected_math:
add_type = node_type + 'Math'
add = nodes.new(add_type)
add.operation = mode
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_shader:
if mode == 'MIX':
add_type = node_type + 'MixShader'
add.hide = do_hide_shader
if do_hide_shader:
loc_y = loc_y - 50
first = 1
second = 2
add.width_hidden = 100.0
elif mode == 'ADD':
add_type = node_type + 'AddShader'
add.hide = do_hide_shader
if do_hide_shader:
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
elif nodes_list == selected_geometry:
if mode in ('JOIN', 'MIX'):
add_type = node_type + 'JoinGeometry'
add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0])
else:
add_type = node_type + 'Boolean'
indices = [0,1] if mode == 'DIFFERENCE' else [1]
add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices)
add.operation = mode
was_multi = True
break
elif nodes_list == selected_vector:
add_type = node_type + 'VectorMath'
add = nodes.new(add_type)
add.operation = mode
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_z:
add = nodes.new('CompositorNodeZcombine')
add.show_preview = False
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 0
second = 2
add.width_hidden = 100.0
elif nodes_list == selected_alphaover:
add = nodes.new('CompositorNodeAlphaOver')
add.show_preview = False
add.hide = do_hide
if do_hide:
loc_y = loc_y - 50
first = 1
Loading
Loading full blame...