Node Wrangler: Improved accuracy on Align Nodes operator #104551
@ -63,7 +63,15 @@ def drawlayout(context, layout, mode='non-panel'):
|
|||||||
col.separator()
|
col.separator()
|
||||||
|
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
col.operator(operators.NWAlignNodes.bl_idname, icon='CENTER_ONLY')
|
col.operator(operators.NWAlignNodes.bl_idname, text='Auto-Align Nodes', icon='CENTER_ONLY').mode = 'AUTOMATIC'
|
||||||
|
if mode == 'panel':
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator(operators.NWAlignNodes.bl_idname, text='Align X').mode = 'HORIZONTAL'
|
||||||
|
row.operator(operators.NWAlignNodes.bl_idname, text='Align Y').mode = 'VERTICAL'
|
||||||
|
else:
|
||||||
|
col.operator(operators.NWAlignNodes.bl_idname, text='Align X').mode = 'HORIZONTAL'
|
||||||
|
col.operator(operators.NWAlignNodes.bl_idname, text='Align Y').mode = 'VERTICAL'
|
||||||
|
|
||||||
col.separator()
|
col.separator()
|
||||||
|
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
|
@ -12,6 +12,7 @@ from bpy.props import (
|
|||||||
IntProperty,
|
IntProperty,
|
||||||
StringProperty,
|
StringProperty,
|
||||||
FloatVectorProperty,
|
FloatVectorProperty,
|
||||||
|
IntVectorProperty,
|
||||||
CollectionProperty,
|
CollectionProperty,
|
||||||
)
|
)
|
||||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||||
@ -29,7 +30,8 @@ from .utils.draw import draw_callback_nodeoutline
|
|||||||
from .utils.paths import match_files_to_socket_names, split_into_components
|
from .utils.paths import match_files_to_socket_names, split_into_components
|
||||||
from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links, is_viewer_socket, is_viewer_link,
|
from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links, is_viewer_socket, is_viewer_link,
|
||||||
get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check,
|
get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check,
|
||||||
nw_check_space_type, NWBase, get_first_enabled_output, is_visible_socket, viewer_socket_name)
|
nw_check_space_type, NWBase, get_first_enabled_output, is_visible_socket, temporary_unframe,
|
||||||
|
viewer_socket_name)
|
||||||
|
|
||||||
class NWLazyMix(Operator, NWBase):
|
class NWLazyMix(Operator, NWBase):
|
||||||
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
|
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
|
||||||
@ -2142,69 +2144,137 @@ class NWAlignNodes(Operator, NWBase):
|
|||||||
bl_idname = "node.nw_align_nodes"
|
bl_idname = "node.nw_align_nodes"
|
||||||
bl_label = "Align Nodes"
|
bl_label = "Align Nodes"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
|
|
||||||
|
mode: EnumProperty(
|
||||||
|
name='Align Mode',
|
||||||
|
default='AUTOMATIC',
|
||||||
|
items=(
|
||||||
|
('AUTOMATIC', 'Auto-Align', ''),
|
||||||
|
('HORIZONTAL', 'Align X', ''),
|
||||||
|
('VERTICAL', 'Align Y', ''),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def description(cls, context, props):
|
||||||
|
if props.mode == 'AUTOMATIC':
|
||||||
|
return "Aligns nodes horizontally/vertically based on which direction takes more space"
|
||||||
|
elif props.mode == 'HORIZONTAL':
|
||||||
|
return "Aligns nodes in a row from left to right"
|
||||||
|
elif props.mode == 'VERTICAL':
|
||||||
|
return "Aligns nodes in a column from top to bottom"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
selection = (node for node in context.selected_nodes if node.type != 'FRAME')
|
||||||
|
for index, _ in enumerate(selection):
|
||||||
|
if index >= 1:
|
||||||
|
return nw_check(context)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_midpoint(node, axis):
|
||||||
|
reroute_width = 10
|
||||||
|
weird_offset = 10
|
||||||
|
|
||||||
|
if axis == 'X':
|
||||||
|
width = reroute_width if (node.type == 'REROUTE') else node.dimensions.x
|
||||||
|
return node.location.x + (0.5 * width)
|
||||||
|
|
||||||
|
elif axis == 'Y':
|
||||||
|
if (node.type == 'REROUTE'):
|
||||||
|
return node.location.y - (0.5 * reroute_width)
|
||||||
|
elif node.hide:
|
||||||
|
return node.location.y - weird_offset
|
||||||
|
else:
|
||||||
|
return node.location.y - (0.5 * node.dimensions.y)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
selection = [node for node in context.selected_nodes if node.type != 'FRAME']
|
||||||
margin = self.margin
|
active_node = context.active_node
|
||||||
|
prefs = context.preferences.addons[__package__].preferences
|
||||||
|
margin_x, margin_y = prefs.align_nodes_margin
|
||||||
|
|
||||||
selection = []
|
# Somehow hidden nodes would come out 10 units higher that non-hidden nodes when aligned, so this offset has to exist
|
||||||
for node in nodes:
|
weird_offset = 10
|
||||||
if node.select and node.type != 'FRAME':
|
|
||||||
selection.append(node)
|
|
||||||
|
|
||||||
# If no nodes are selected, align all nodes
|
# node.dimensions for reroutes indicate (16.0, 16.0) but using that in calculations puts reroutes off-center
|
||||||
|
# At least on a purely visual basis, the dimensions of a reroute node seem to be closer to 10 units. (At least for 1.0 unit scale)
|
||||||
|
reroute_width = 10
|
||||||
|
|
||||||
|
with temporary_unframe(nodes=selection):
|
||||||
active_loc = None
|
active_loc = None
|
||||||
if not selection:
|
if active_node in selection:
|
||||||
selection = nodes
|
active_loc = copy(active_node.location) # make a copy, not a reference
|
||||||
elif nodes.active in selection:
|
|
||||||
active_loc = copy(nodes.active.location) # make a copy, not a reference
|
x_locs = [self.get_midpoint(n, axis='X') for n in selection]
|
||||||
|
y_locs = [self.get_midpoint(n, axis='Y') for n in selection]
|
||||||
|
|
||||||
# Check if nodes should be laid out horizontally or vertically
|
|
||||||
# use dimension to get center of node, not corner
|
|
||||||
x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]
|
|
||||||
y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
|
|
||||||
x_range = max(x_locs) - min(x_locs)
|
x_range = max(x_locs) - min(x_locs)
|
||||||
y_range = max(y_locs) - min(y_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
|
horizontal = x_range > y_range
|
||||||
|
|
||||||
|
mid_x = 0.5 * (max(x_locs) + min(x_locs))
|
||||||
|
mid_y = 0.5 * (max(y_locs) + min(y_locs))
|
||||||
|
|
||||||
# Sort selection by location of node mid-point
|
# Sort selection by location of node mid-point
|
||||||
if horizontal:
|
if horizontal:
|
||||||
selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
|
selection.sort(key=lambda n: n.location.x + (n.dimensions.x / 2))
|
||||||
else:
|
else:
|
||||||
selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
|
selection.sort(key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
|
||||||
|
|
||||||
|
if self.mode != 'AUTOMATIC':
|
||||||
|
horizontal = (self.mode == 'HORIZONTAL')
|
||||||
|
|
||||||
# Alignment
|
# Alignment
|
||||||
current_pos = 0
|
current_pos = 0
|
||||||
for node in selection:
|
|
||||||
current_margin = margin
|
|
||||||
current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
|
|
||||||
|
|
||||||
if horizontal:
|
if horizontal:
|
||||||
|
for node in selection:
|
||||||
|
if node.type != 'REROUTE':
|
||||||
node.location.x = current_pos
|
node.location.x = current_pos
|
||||||
current_pos += current_margin + node.dimensions.x
|
node.location.y = (mid_y + weird_offset) if node.hide else mid_y + (0.5 * node.dimensions.y)
|
||||||
node.location.y = mid_y + (node.dimensions.y / 2)
|
|
||||||
|
current_pos += margin_x + node.dimensions.x
|
||||||
else:
|
else:
|
||||||
node.location.y = current_pos
|
node.location.x = current_pos + (0.5 * reroute_width)
|
||||||
current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
|
node.location.y = mid_y
|
||||||
node.location.x = mid_x - (node.dimensions.x / 2)
|
|
||||||
|
current_pos += margin_x + reroute_width
|
||||||
|
|
||||||
|
else:
|
||||||
|
for node in selection:
|
||||||
|
if node.type != 'REROUTE':
|
||||||
|
node.location.x = mid_x - (0.5 * node.dimensions.x)
|
||||||
|
node.location.y = (current_pos - (0.5 * node.dimensions.y) +
|
||||||
|
weird_offset) if node.hide else current_pos
|
||||||
|
|
||||||
|
current_pos -= margin_y + node.dimensions.y
|
||||||
|
else:
|
||||||
|
node.location.x = mid_x
|
||||||
|
node.location.y = current_pos - (0.5 * reroute_width)
|
||||||
|
|
||||||
|
current_pos -= margin_y + reroute_width
|
||||||
|
|
||||||
# If active node is selected, center nodes around it
|
# If active node is selected, center nodes around it
|
||||||
if active_loc is not None:
|
if active_loc is not None:
|
||||||
active_loc_diff = active_loc - nodes.active.location
|
active_loc_diff = (active_loc - active_node.location)
|
||||||
for node in selection:
|
for node in selection:
|
||||||
node.location += active_loc_diff
|
node.location += active_loc_diff
|
||||||
else: # 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:
|
else:
|
||||||
node.location.y += (mid_y - new_mid)
|
new_x_locs = [self.get_midpoint(n, axis='X') for n in selection]
|
||||||
|
new_y_locs = [self.get_midpoint(n, axis='Y') for n in selection]
|
||||||
|
|
||||||
|
new_x_mid = 0.5 * (max(new_x_locs) + min(new_x_locs))
|
||||||
|
new_y_mid = 0.5 * (max(new_y_locs) + min(new_y_locs))
|
||||||
|
|
||||||
|
x_diff = mid_x - new_x_mid
|
||||||
|
y_diff = mid_y - new_y_mid
|
||||||
|
|
||||||
|
for node in selection:
|
||||||
|
node.location.x += x_diff
|
||||||
|
node.location.y += y_diff
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import EnumProperty, BoolProperty, StringProperty
|
from bpy.props import EnumProperty, BoolProperty, StringProperty, IntVectorProperty
|
||||||
from nodeitems_utils import node_categories_iter
|
from nodeitems_utils import node_categories_iter
|
||||||
|
|
||||||
from . import operators
|
from . import operators
|
||||||
@ -102,11 +102,25 @@ class NWNodeWrangler(bpy.types.AddonPreferences):
|
|||||||
default=False,
|
default=False,
|
||||||
description="Expand this box into a list of all naming tags for principled texture setup"
|
description="Expand this box into a list of all naming tags for principled texture setup"
|
||||||
)
|
)
|
||||||
|
align_nodes_margin: IntVectorProperty(
|
||||||
|
name="Margin",
|
||||||
|
default=(50, 15),
|
||||||
|
subtype="XYZ",
|
||||||
|
size=2,
|
||||||
|
min=0,
|
||||||
|
soft_min=0,
|
||||||
|
soft_max=200,
|
||||||
|
description='The amount of space between nodes during when the Align Nodes operator is called'
|
||||||
|
)
|
||||||
principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
|
principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
col = layout.column()
|
col = layout.column(heading="Margin (Align Nodes):")
|
||||||
|
col.prop(self, "align_nodes_margin", text="")
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
col.label(text="Merge Node Options:")
|
||||||
col.prop(self, "merge_position")
|
col.prop(self, "merge_position")
|
||||||
col.prop(self, "merge_hide")
|
col.prop(self, "merge_hide")
|
||||||
|
|
||||||
|
@ -229,6 +229,22 @@ def is_visible_socket(socket):
|
|||||||
return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
|
return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
|
||||||
|
|
||||||
|
|
||||||
|
class temporary_unframe(): # Context manager for temporarily unparenting nodes from their frames
|
||||||
|
def __init__(self, nodes):
|
||||||
|
self.parent_dict = {}
|
||||||
|
for node in nodes:
|
||||||
|
if node.parent is not None:
|
||||||
|
self.parent_dict[node] = node.parent
|
||||||
|
node.parent = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
for node, parent in self.parent_dict.items():
|
||||||
|
node.parent = parent
|
||||||
|
|
||||||
|
|
||||||
class NWBase:
|
class NWBase:
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
|
Loading…
Reference in New Issue
Block a user