From d883ddcd26fc31faaf8c3d5a9e54b28f4333eca8 Mon Sep 17 00:00:00 2001 From: Greg Zaal Date: Tue, 18 Feb 2014 19:11:04 +0200 Subject: Node Wrangler: Add more specific Lazy connection The original Lazy Connect works the same (Ctrl+RMB drag to make a new connection between guessed sockets) - this new function can be accessed with Ctrl+Shift+RMB drag and will show a menu for the user to choose exactly which sockets to connect. This can be useful when dealing with a large node tree, users want to make some connections without repeatedly zooming in and out or trying to click the exact socket they want. Just like with the normal Lazy Connect, you don't need to click on a node exactly, it will use the node nearest the mouse. Included in this update are: - More accurate nearest-node detection - Auto-linkage (used in Lazy Connect) now matches sockets by name first - Account for DPI settings other than 72 --- node_efficiency_tools.py | 354 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 312 insertions(+), 42 deletions(-) (limited to 'node_efficiency_tools.py') diff --git a/node_efficiency_tools.py b/node_efficiency_tools.py index 3f2aeeb3..9d53e779 100644 --- a/node_efficiency_tools.py +++ b/node_efficiency_tools.py @@ -19,7 +19,7 @@ bl_info = { 'name': "Node Wrangler (aka Nodes Efficiency Tools)", 'author': "Bartek Skorupa, Greg Zaal", - 'version': (3, 1), + 'version': (3, 2), 'blender': (2, 69, 0), 'location': "Node Editor Properties Panel or Ctrl-SPACE", 'description': "Various tools to enhance and speed up node-based workflow", @@ -32,7 +32,7 @@ bl_info = { import bpy, blf, bgl from bpy.types import Operator, Panel, Menu -from bpy.props import FloatProperty, EnumProperty, BoolProperty, StringProperty, FloatVectorProperty +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty, FloatVectorProperty from mathutils import Vector from math import cos, sin, pi, sqrt @@ -459,6 +459,10 @@ def hack_force_update(context, nodes): return False +def dpifac(): + return bpy.context.user_preferences.system.dpi/72 + + def is_end_node(node): bool = True for output in node.outputs: @@ -480,6 +484,14 @@ def node_mid_pt(node, axis): def autolink(node1, node2, links): link_made = False + + for outp in node1.outputs: + for inp in node2.inputs: + if not inp.is_linked and inp.name == outp.name: + link_made = True + links.new(outp, inp) + return True + for outp in node1.outputs: for inp in node2.inputs: if not inp.is_linked and inp.type == outp.type: @@ -521,22 +533,47 @@ def node_at_pos(nodes, context, event): store_mouse_cursor(context, event) x, y = context.space_data.cursor_location + x = x + y = y + + # 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: + locx = node.location.x + locy = node.location.y + dimx = node.dimensions.x/dpifac() + dimy = node.dimensions.y/dpifac() + node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - locy) ** 2)]) # Top Left + node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - locy) ** 2)]) # Top Right + node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - (locy-dimy)) ** 2)]) # Bottom Left + node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - (locy-dimy)) ** 2)]) # Bottom Right + + node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - locy) ** 2)]) # Mid Top + node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - (locy-dimy)) ** 2)]) # Mid Bottom + node_points_with_dist.append([node, sqrt((x - locx) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Mid Left + node_points_with_dist.append([node, sqrt((x - (locx+dimx)) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Mid Right + + #node_points_with_dist.append([node, sqrt((x - (locx+(dimx/2))) ** 2 + (y - (locy-(dimy/2))) ** 2)]) # Center - # nearest node - nodes_near_mouse = sorted(nodes, key=lambda k: sqrt((x - node_mid_pt(k, 'x')) ** 2 + (y - node_mid_pt(k, 'y')) ** 2)) + nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0] for node in nodes: - if (node.location.x <= x <= node.location.x + node.dimensions.x) and \ - (node.location.y - node.dimensions.y <= y <= node.location.y): + locx = node.location.x + locy = node.location.y + 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] != nodes_near_mouse[0]: + 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 = nodes_near_mouse[0] # else use the nearest node + target_node = nearest_node # else use the nearest node else: - target_node = nodes_near_mouse[0] + target_node = nearest_node return target_node @@ -554,16 +591,19 @@ def store_mouse_cursor(context, event): def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]): bgl.glEnable(bgl.GL_BLEND) - bgl.glColor4f(colour[0], colour[1], colour[2], colour[3]) bgl.glLineWidth(size) + bgl.glShadeModel(bgl.GL_SMOOTH) bgl.glBegin(bgl.GL_LINE_STRIP) try: + bgl.glColor4f(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) bgl.glVertex2f(x1, y1) + bgl.glColor4f(colour[0], colour[1], colour[2], colour[3]) bgl.glVertex2f(x2, y2) except: pass bgl.glEnd() + bgl.glShadeModel(bgl.GL_FLAT) def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]): @@ -578,41 +618,152 @@ def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]): bgl.glEnd() -def draw_callback_mixnodes(self, context, mode="MIX"): +def draw_rounded_node_border(node, radius=8, colour=[1.0, 1.0, 1.0, 0.7]): + bgl.glEnable(bgl.GL_BLEND) + settings = bpy.context.user_preferences.addons[__name__].preferences + if settings.bgl_antialiasing: + bgl.glEnable(bgl.GL_LINE_SMOOTH) + sides = 16 + bgl.glColor4f(colour[0], colour[1], colour[2], colour[3]) + + nlocx = (node.location.x+1)*dpifac() + nlocy = (node.location.y+1)*dpifac() + ndimx = node.dimensions.x + ndimy = node.dimensions.y + + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy) + bgl.glVertex2f(mx,my) + for i in range(sides+1): + if (4<=i<=8): + if mx != 12000 and my != 12000: # nodes that go over the view border give 12000 as coords + cosine = radius * cos(i * 2 * pi / sides) + mx + sine = radius * sin(i * 2 * pi / sides) + my + bgl.glVertex2f(cosine, sine) + bgl.glEnd() + + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy) + bgl.glVertex2f(mx,my) + for i in range(sides+1): + if (0<=i<=4): + if mx != 12000 and my != 12000: + cosine = radius * cos(i * 2 * pi / sides) + mx + sine = radius * sin(i * 2 * pi / sides) + my + bgl.glVertex2f(cosine, sine) + + bgl.glEnd() + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy) + bgl.glVertex2f(mx,my) + for i in range(sides+1): + if (8<=i<=12): + if mx != 12000 and my != 12000: + cosine = radius * cos(i * 2 * pi / sides) + mx + sine = radius * sin(i * 2 * pi / sides) + my + bgl.glVertex2f(cosine, sine) + bgl.glEnd() + + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy) + bgl.glVertex2f(mx,my) + for i in range(sides+1): + if (12<=i<=16): + if mx != 12000 and my != 12000: + cosine = radius * cos(i * 2 * pi / sides) + mx + sine = radius * sin(i * 2 * pi / sides) + my + bgl.glVertex2f(cosine, sine) + bgl.glEnd() + + + bgl.glBegin(bgl.GL_QUADS) + m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy) + m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy) + if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000: + bgl.glVertex2f(m2x-radius,m2y) # draw order is important, start with bottom left and go anti-clockwise + bgl.glVertex2f(m2x,m2y) + bgl.glVertex2f(m1x,m1y) + bgl.glVertex2f(m1x-radius,m1y) + bgl.glEnd() + + bgl.glBegin(bgl.GL_QUADS) + m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy) + m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy) + if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000: + bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise + bgl.glVertex2f(m2x,m2y) + bgl.glVertex2f(m2x,m1y+radius) + bgl.glVertex2f(m1x,m1y+radius) + bgl.glEnd() + + bgl.glBegin(bgl.GL_QUADS) + m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy) + m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy) + if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000: + bgl.glVertex2f(m2x,m2y) # draw order is important, start with bottom left and go anti-clockwise + bgl.glVertex2f(m2x+radius,m2y) + bgl.glVertex2f(m1x+radius,m1y) + bgl.glVertex2f(m1x,m1y) + bgl.glEnd() + + bgl.glBegin(bgl.GL_QUADS) + m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy) + m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy) + if m1x != 12000 and m1y != 12000 and m2x != 12000 and m2y != 12000: + bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise + bgl.glVertex2f(m2x,m2y) + bgl.glVertex2f(m2x,m1y-radius) + bgl.glVertex2f(m1x,m1y-radius) + bgl.glEnd() + + bgl.glDisable(bgl.GL_BLEND) + if settings.bgl_antialiasing: + bgl.glDisable(bgl.GL_LINE_SMOOTH) + + +def draw_callback_mixnodes(self, context, mode): if self.mouse_path: + nodes = context.space_data.node_tree.nodes settings = context.user_preferences.addons[__name__].preferences if settings.bgl_antialiasing: bgl.glEnable(bgl.GL_LINE_SMOOTH) - colors = [] - if mode == 'MIX': - colors = draw_color_sets['red_white'] - elif mode == 'RGBA': - colors = draw_color_sets['yellow'] - elif mode == 'VECTOR': - colors = draw_color_sets['purple'] - elif mode == 'VALUE': - colors = draw_color_sets['grey'] - elif mode == 'SHADER': - colors = draw_color_sets['green'] - else: - colors = draw_color_sets['black'] + if mode == "LINK": + 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] + if mode == "LINKMENU": + 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] + elif mode == "MIX": + 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] - # circle outline - draw_circle(m1x, m1y, 6, colors[0]) - draw_circle(m2x, m2y, 6, colors[0]) + n1 = nodes[context.scene.NWLazySource] + n2 = nodes[context.scene.NWLazyTarget] - draw_line(m1x, m1y, m2x, m2y, 4, colors[0]) # line outline - draw_line(m1x, m1y, m2x, m2y, 2, colors[1]) # line inner + draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline + draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner + draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline + draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner + + draw_line(m1x, m1y, m2x, m2y, 4, col_outer) # line outline + draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner + + # circle outline + draw_circle(m1x, m1y, 6, col_outer) + draw_circle(m2x, m2y, 6, col_outer) # circle inner - draw_circle(m1x, m1y, 5, colors[2]) - draw_circle(m2x, m2y, 5, colors[2]) + draw_circle(m1x, m1y, 5, col_circle_inner) + draw_circle(m2x, m2y, 5, col_circle_inner) # restore opengl defaults bgl.glLineWidth(1) @@ -748,6 +899,9 @@ class NWLazyMix(Operator, NWBase): 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)) @@ -804,6 +958,7 @@ class NWLazyConnect(Operator, NWBase): bl_idname = "node.nw_lazy_connect" bl_label = "Lazy Connect" bl_options = {'REGISTER', 'UNDO'} + with_menu = BoolProperty() def modal(self, context, event): context.area.tag_redraw() @@ -821,6 +976,9 @@ class NWLazyConnect(Operator, NWBase): 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)) @@ -850,7 +1008,14 @@ class NWLazyConnect(Operator, NWBase): node1.select = True node2.select = True - link_success = autolink(node1, node2, links) + #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 @@ -874,13 +1039,12 @@ class NWLazyConnect(Operator, NWBase): node = node_at_pos(nodes, context, event) if node: context.scene.NWBusyDrawing = node.name - if node.outputs: - context.scene.NWDrawColType = node.outputs[0].type - else: - context.scene.NWDrawColType = 'x' # the arguments we pass the the callback - args = (self, context, context.scene.NWDrawColType) + 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_mixnodes, args, 'WINDOW', 'POST_PIXEL') @@ -2232,7 +2396,6 @@ class NWAlignNodes(Operator, NWBase): parent = parent.parent loc_x += offset_x + w else: # if self.option == 'AXIS_Y' - #loc_x = (max_y_loc_x + max_y_w / 2.0 + min_y_loc_x + min_y_w / 2.0) / 2.0 loc_x = (max_x + max_x_w / 2.0 + min_x + min_x_w / 2.0) / 2.0 loc_y = min_y offset_y = (max_y - min_y + total_h - min_y_h) / (count - 1) @@ -2357,6 +2520,58 @@ class NWLinkToOutputNode(Operator, NWBase): return {'FINISHED'} +class NWMakeLink(Operator, NWBase): + """Make a link from one socket to another""" + bl_idname = 'node.nw_make_link' + bl_label = 'Make Link' + bl_options = {'REGISTER', 'UNDO'} + from_socket = IntProperty() + to_socket = IntProperty() + + @classmethod + def poll(cls, context): + snode = context.space_data + return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None) + + def execute(self, context): + nodes, links = get_nodes_links(context) + + n1 = nodes[context.scene.NWLazySource] + n2 = nodes[context.scene.NWLazyTarget] + + links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket]) + + hack_force_update(context, nodes) + + return {'FINISHED'} + + +class NWCallInputsMenu(Operator, NWBase): + """Link from this output""" + bl_idname = 'node.nw_call_inputs_menu' + bl_label = 'Make Link' + bl_options = {'REGISTER', 'UNDO'} + from_socket = IntProperty() + + @classmethod + def poll(cls, context): + snode = context.space_data + return (snode.type == 'NODE_EDITOR' and snode.node_tree is not None) + + def execute(self, context): + nodes, links = get_nodes_links(context) + + context.scene.NWSourceSocket = self.from_socket + + n1 = nodes[context.scene.NWLazySource] + n2 = nodes[context.scene.NWLazyTarget] + if len(n2.inputs) > 1: + bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname) + elif len(n2.inputs) == 1: + links.new(n1.outputs[self.from_socket], n2.inputs[0]) + return {'FINISHED'} + + # # P A N E L # @@ -2481,6 +2696,49 @@ class NWMergeMixMenu(Menu, NWBase): props.merge_type = 'MIX' +class NWConnectionListOutputs(Menu, NWBase): + bl_idname = "NODE_MT_nw_connection_list_out" + bl_label = "From:" + + def draw(self, context): + layout = self.layout + nodes, links = get_nodes_links(context) + + n1 = nodes[context.scene.NWLazySource] + + if n1.type == "R_LAYERS": + index=0 + for o in n1.outputs: + if o.enabled: # Check which passes the render layer has enabled + layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index + index+=1 + else: + index=0 + for o in n1.outputs: + layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index + index+=1 + + +class NWConnectionListInputs(Menu, NWBase): + bl_idname = "NODE_MT_nw_connection_list_in" + bl_label = "To:" + + def draw(self, context): + layout = self.layout + nodes, links = get_nodes_links(context) + + n2 = nodes[context.scene.NWLazyTarget] + + #print (self.from_socket) + + index = 0 + for i in n2.inputs: + op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD") + op.from_socket = context.scene.NWSourceSocket + op.to_socket = index + index+=1 + + class NWMergeMathMenu(Menu, NWBase): bl_idname = "NODE_MT_nw_merge_math_menu" bl_label = "Merge Selected Nodes using Math" @@ -3114,6 +3372,8 @@ kmi_defs = ( (NWLazyMix.bl_idname, 'RIGHTMOUSE', False, False, True, None, "Lazy Mix"), # Lazy Connect (NWLazyConnect.bl_idname, 'RIGHTMOUSE', True, False, False, None, "Lazy Connect"), + # Lazy Connect with Menu + (NWLazyConnect.bl_idname, 'RIGHTMOUSE', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"), # MENUS ('wm.call_menu', 'SPACE', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"), ('wm.call_menu', 'SLASH', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"), @@ -3131,10 +3391,18 @@ def register(): name="Busy Drawing!", default="", description="An internal property used to store only the first mouse position") - bpy.types.Scene.NWDrawColType = StringProperty( - name="Color Type!", + bpy.types.Scene.NWLazySource = StringProperty( + name="Lazy Source!", + default="x", + description="An internal property used to store the first node in a Lazy Connect operation") + bpy.types.Scene.NWLazyTarget = StringProperty( + name="Lazy Target!", default="x", - description="An internal property used to store the line color") + description="An internal property used to store the last node in a Lazy Connect operation") + bpy.types.Scene.NWSourceSocket = IntProperty( + name="Source Socket!", + default=0, + description="An internal property used to store the source socket in a Lazy Connect operation") bpy.utils.register_module(__name__) @@ -3157,7 +3425,9 @@ def register(): def unregister(): # props del bpy.types.Scene.NWBusyDrawing - del bpy.types.Scene.NWDrawColType + del bpy.types.Scene.NWLazySource + del bpy.types.Scene.NWLazyTarget + del bpy.types.Scene.NWSourceSocket bpy.utils.unregister_module(__name__) -- cgit v1.2.3