Node Wrangler: Rewrite 'Align Nodes' function

The previous behaviour was flawed:
1. Repeatedly aligning the same selection of nodes would space them further and further apart each time
2. The user had to choose between "Horizontal" and "Vertical", which gave the opposite of intended behavior

The new behavior:
1. Nodes are spaced evenly and consistently apart
2. Whether the nodes are aligned vertically or horizontally is now determined automatically
This commit is contained in:
Greg Zaal 2015-04-14 15:58:23 +02:00
parent 8366db5686
commit 760364b233
1 changed files with 58 additions and 98 deletions

View File

@ -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
margin = 80
selection = []
for node in nodes:
if node.select and node.type != 'FRAME':
selection.append(node)
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
# If no nodes are selected, align all nodes
if not selection:
selection = nodes
# reselect selected frames
for i in frames_reselect:
nodes[i].select = True
# restore active node
nodes.active = active
# 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'}
@ -3176,6 +3141,10 @@ def drawlayout(context, layout, mode='non-panel'):
col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
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()