Newer
Older
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
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
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]:
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
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")
box = layout.box()
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
editor_is_valid = space.type == 'NODE_EDITOR'
valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
tree_is_valid = space.node_tree is not None and space.tree_type in valid_trees
return editor_is_valid and tree_is_valid
class NWBase:
@classmethod
def poll(cls, context):
return nw_check(context)
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
# 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':
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
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':
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
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()
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
# 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]
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
# 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:
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
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
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
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)
shader_types = [x[1] for x in shaders_shader_nodes_props]
mlocx = event.mouse_region_x
mlocy = event.mouse_region_y
select_node = bpy.ops.node.select(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'}
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
1894
1895
1896
1897
1898
1899
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
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
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[1] for x in shaders_output_nodes_props]
if active:
if active.type 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
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