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:
parent
8366db5686
commit
760364b233
156
node_wrangler.py
156
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
|
||||
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()
|
||||
|
|
Loading…
Reference in New Issue