diff options
-rw-r--r-- | node_wrangler.py | 160 |
1 files changed, 60 insertions, 100 deletions
diff --git a/node_wrangler.py b/node_wrangler.py index 7926d674..7a1b3106 100644 --- a/node_wrangler.py +++ b/node_wrangler.py @@ -2671,98 +2671,63 @@ class NWLinkActiveToSelected(Operator, NWBase): class NWAlignNodes(Operator, NWBase): + '''Align the selected nodes neatly in a row/column''' bl_idname = "node.nw_align_nodes" - bl_label = "Align nodes" + bl_label = "Align Nodes" bl_options = {'REGISTER', 'UNDO'} - # option: 'Vertically', 'Horizontally' - option = EnumProperty( - name="option", - description="Direction", - items=( - ('AXIS_X', "Align Vertically", 'Align Vertically'), - ('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'), - ) - ) - def execute(self, context): + # TODO prop: lock active (arrange everything without moving active node) nodes, links = get_nodes_links(context) - selected = [] # entry = [index, loc.x, loc.y, width, height] - frames_reselect = [] # entry = frame node. will be used to reselect all selected frames - active = nodes.active - for i, node in enumerate(nodes): - total_w = 0.0 # total width of all nodes. Will be calculated later. - total_h = 0.0 # total height of all nodes. Will be calculated later - if node.select: - if node.type == 'FRAME': - node.select = False - frames_reselect.append(i) - else: - locx = node.location.x - locy = node.location.y - width = node.dimensions[0] - height = node.dimensions[1] - total_w += width # add nodes[i] width to total width of all nodes - total_h += height # add nodes[i] height to total height of all nodes - # calculate relative locations - parent = node.parent - while parent is not None: - locx += parent.location.x - locy += parent.location.y - parent = parent.parent - selected.append([i, locx, locy, width, height]) - count = len(selected) - if count > 1: # aligning makes sense only if at least 2 nodes are selected - selected_sorted_x = sorted(selected, key=lambda k: (k[1], -k[2])) - selected_sorted_y = sorted(selected, key=lambda k: (-k[2], k[1])) - min_x = selected_sorted_x[0][1] # min loc.x - min_x_loc_y = selected_sorted_x[0][2] # loc y of node with min loc x - min_x_w = selected_sorted_x[0][3] # width of node with max loc x - max_x = selected_sorted_x[count - 1][1] # max loc.x - max_x_loc_y = selected_sorted_x[count - 1][2] # loc y of node with max loc.x - max_x_w = selected_sorted_x[count - 1][3] # width of node with max loc.x - min_y = selected_sorted_y[0][2] # min loc.y - min_y_loc_x = selected_sorted_y[0][1] # loc.x of node with min loc.y - min_y_h = selected_sorted_y[0][4] # height of node with min loc.y - min_y_w = selected_sorted_y[0][3] # width of node with min loc.y - max_y = selected_sorted_y[count - 1][2] # max loc.y - max_y_loc_x = selected_sorted_y[count - 1][1] # loc x of node with max loc.y - max_y_w = selected_sorted_y[count - 1][3] # width of node with max loc.y - max_y_h = selected_sorted_y[count - 1][4] # height of node with max loc.y - - if self.option == 'AXIS_Y': # Horizontally. Equivelent of s -> x -> 0 with even spacing. - loc_x = min_x - #loc_y = (max_x_loc_y + min_x_loc_y) / 2.0 - loc_y = (max_y - max_y_h / 2.0 + min_y - min_y_h / 2.0) / 2.0 - offset_x = (max_x - min_x - total_w + max_x_w) / (count - 1) - for i, x, y, w, h in selected_sorted_x: - nodes[i].location.x = loc_x - nodes[i].location.y = loc_y + h / 2.0 - parent = nodes[i].parent - while parent is not None: - nodes[i].location.x -= parent.location.x - nodes[i].location.y -= parent.location.y - parent = parent.parent - loc_x += offset_x + w - else: # if self.option == 'AXIS_Y' - 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) - for i, x, y, w, h in selected_sorted_y: - nodes[i].location.x = loc_x - w / 2.0 - nodes[i].location.y = loc_y - parent = nodes[i].parent - while parent is not None: - nodes[i].location.x -= parent.location.x - nodes[i].location.y -= parent.location.y - parent = parent.parent - loc_y += offset_y - h - - # reselect selected frames - for i in frames_reselect: - nodes[i].select = True - # restore active node - nodes.active = active + margin = 80 + + selection = [] + for node in nodes: + if node.select and node.type != 'FRAME': + selection.append(node) + + # If no nodes are selected, align all nodes + if not selection: + selection = nodes + + # Check if nodes should be layed out horizontally or vertically + x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner + y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection] + x_range = max(x_locs) - min(x_locs) + y_range = max(y_locs) - min(y_locs) + mid_x = (max(x_locs) + min(x_locs)) / 2 + mid_y = (max(y_locs) + min(y_locs)) / 2 + horizontal = x_range > y_range + + # Sort selection by location of node mid-point + if horizontal: + selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2)) + else: + selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True) + + # Alignment + current_pos = 0 + for node in selection: + current_margin = margin + current_margin = current_margin / 2 if node.hide else current_margin # use a smaller margin for hidden nodes + + if horizontal: + node.location.x = current_pos + current_pos += current_margin + node.dimensions.x + node.location.y = mid_y + (node.dimensions.y / 2) + else: + node.location.y = current_pos + current_pos -= (current_margin / 2) + node.dimensions.y # use half-margin for vertical alignment + node.location.x = mid_x - (node.dimensions.x / 2) + + # Position nodes centered around where they used to be + locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection]) + new_mid = (max(locs) + min(locs)) / 2 + for node in selection: + if horizontal: + node.location.x += (mid_x - new_mid) + else: + node.location.y += (mid_y - new_mid) return {'FINISHED'} @@ -3177,6 +3142,10 @@ def drawlayout(context, layout, mode='non-panel'): col.separator() col = layout.column(align=True) + col.operator(NWAlignNodes.bl_idname, icon='ALIGN') + col.separator() + + col = layout.column(align=True) col.operator(NWDeleteUnused.bl_idname, icon='CANCEL') col.separator() @@ -3432,16 +3401,6 @@ class NWLinkUseOutputsNamesMenu(Menu, NWBase): props.use_outputs_names = True -class NWNodeAlignMenu(Menu, NWBase): - bl_idname = "NODE_MT_nw_node_align_menu" - bl_label = "Align Nodes" - - def draw(self, context): - layout = self.layout - layout.operator(NWAlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X' - layout.operator(NWAlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y' - - class NWVertColMenu(bpy.types.Menu): bl_idname = "NODE_MT_nw_node_vertex_color_menu" bl_label = "Vertex Colors" @@ -4071,11 +4030,12 @@ kmi_defs = ( (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"), # Viewer Tile Center (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"), + # Align Nodes + (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"), # MENUS ('wm.call_menu', 'SPACE', 'PRESS', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"), ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"), ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"), - ('wm.call_menu', 'EQUAL', 'PRESS', False, True, False, (('name', NWNodeAlignMenu.bl_idname),), "Node alignment menu"), ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"), ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"), ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"), @@ -4133,8 +4093,6 @@ def unregister(): del bpy.types.Scene.NWLazyTarget del bpy.types.Scene.NWSourceSocket - bpy.utils.unregister_module(__name__) - # keymaps for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) @@ -4150,5 +4108,7 @@ def unregister(): bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func) bpy.types.NODE_PT_category_CMP_INPUT.remove(multipleimages_menu_func) + bpy.utils.unregister_module(__name__) + if __name__ == "__main__": register() |