From 39aba28381b6c26631b0bb8d9619677b603c1bde Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Fri, 24 Mar 2023 04:02:59 +0800 Subject: [PATCH 01/10] Fix #99904: Fixed Unintuitive Selection Behavior during Align Nodes This commit adds a poll check for the Align Nodes operator that only allows execution if at least two non- frame nodes are selected. This is intended to address two behaviors in the operator that work exactly as intended but are neither intuitive for the end user. First, frames are ignored by this operator, as they're not relevant to the calculations being done. However, this is not made apparent to the user in any way. The poll function check hopes to address this by not allowing the user to call the operator in cases where only frames are selected. Second, is that when the operator is given an empty selection, it is coded to act as if all nodes were selected. This doesn't seem to offer much in the way of practical utility, since a user can easily just call Select All before calling the operator. If anything, it seems to be more a cause for confusion, as can be seen in the relevant issue. Since the poll function does not allow execution for empty selections, the aforementioned behavior has been rendered obsolete and was removed. Overall, the new behavior of being unable to call Align Nodes when no nodes are selected would seem to be more in line with how a user might reasonably expect the operator to behave. --- node_wrangler/operators.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 0b56fe3f7..fb88a8c00 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -2339,21 +2339,22 @@ class NWAlignNodes(Operator, NWBase): bl_options = {'REGISTER', 'UNDO'} margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes') + @classmethod + def poll(cls, context): + selection = [node for node in context.selected_nodes if node.type != 'FRAME'] + if len(selection) < 2: + return False + + return nw_check(context) + def execute(self, context): - nodes, links = get_nodes_links(context) + selection = [node for node in context.selected_nodes if node.type != 'FRAME'] + active_node = context.active_node margin = self.margin - selection = [] - for node in nodes: - if node.select and node.type != 'FRAME': - selection.append(node) - - # If no nodes are selected, align all nodes active_loc = None - if not selection: - selection = nodes - elif nodes.active in selection: - active_loc = copy(nodes.active.location) # make a copy, not a reference + if active_node in selection: + active_loc = copy(active_node.location) # make a copy, not a reference # Check if nodes should be laid out horizontally or vertically # use dimension to get center of node, not corner @@ -2388,7 +2389,7 @@ class NWAlignNodes(Operator, NWBase): # If active node is selected, center nodes around it 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: node.location += active_loc_diff else: # Position nodes centered around where they used to be -- 2.30.2 From cbe34040be7074222f4fdc749bd1770459e04543 Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Fri, 24 Mar 2023 14:16:23 +0800 Subject: [PATCH 02/10] Fix: Node Wrangler's Align Nodes inaccurately aligning nodes in frames The calculations for the Align Nodes operator currently uses node.location, which is relative to the frame a node is parented to. This works fine when the selected nodes are not in a frame, or when they're all parented under the same frame, but breaks in cases where the nodes are inside separate frames. The commit fixes this by implementing a context manager that temporarily unframes the selected nodes, allowing the operator to do its calculations using the nodes' absolute locations, before returning the nodes to their parent frames after execution. This allows the operator to work in whatever configuration the selected nodes are framed in. --- node_wrangler/operators.py | 85 ++++++++++++++++++------------------ node_wrangler/utils/nodes.py | 16 +++++++ 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index fb88a8c00..f69544550 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -26,7 +26,7 @@ from .utils.draw import draw_callback_nodeoutline from .utils.paths import match_files_to_socket_names, split_into_components from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_active_tree, get_nodes_links, is_viewer_socket, is_viewer_link, get_group_output_node, get_output_location, force_update, get_internal_socket, - nw_check, NWBase, get_first_enabled_output, is_visible_socket, viewer_socket_name) + nw_check, NWBase, get_first_enabled_output, is_visible_socket, temporary_unframe, viewer_socket_name) class NWLazyMix(Operator, NWBase): @@ -2352,55 +2352,56 @@ class NWAlignNodes(Operator, NWBase): active_node = context.active_node margin = self.margin - active_loc = None - if active_node in selection: - active_loc = copy(active_node.location) # make a copy, not a reference + with temporary_unframe(nodes=selection): + active_loc = None + if active_node in selection: + active_loc = copy(active_node.location) # make a copy, not a reference - # 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) - 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 * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes + # 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) + 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: - node.location.x = current_pos - current_pos += current_margin + node.dimensions.x - node.location.y = mid_y + (node.dimensions.y / 2) + selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2)) else: - node.location.y = current_pos - current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment - node.location.x = mid_x - (node.dimensions.x / 2) + selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True) - # If active node is selected, center nodes around it - if active_loc is not None: - active_loc_diff = active_loc - active_node.location - for node in selection: - 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 + # Alignment + 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: - node.location.x += (mid_x - new_mid) + 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 += (mid_y - new_mid) + node.location.y = current_pos + current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment + node.location.x = mid_x - (node.dimensions.x / 2) + + # If active node is selected, center nodes around it + if active_loc is not None: + active_loc_diff = active_loc - active_node.location + for node in selection: + 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: + node.location.y += (mid_y - new_mid) return {'FINISHED'} diff --git a/node_wrangler/utils/nodes.py b/node_wrangler/utils/nodes.py index 2214c161e..84fe5a575 100644 --- a/node_wrangler/utils/nodes.py +++ b/node_wrangler/utils/nodes.py @@ -250,6 +250,22 @@ def is_visible_socket(socket): 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: @classmethod def poll(cls, context): -- 2.30.2 From 8e834b4433825d8597cc0845b3ae16000637daef Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Sat, 8 Apr 2023 14:25:24 +0800 Subject: [PATCH 03/10] Node Wrangler: Refactored Align Nodes operator Refactored NWAlignNodes to make it easier to implement future changes. This is done as a cleanup and precursor to later commits planned on this branch. Notable changes in this commit are as follows: 1.) Used .sort() instead of sorted(). This avoids making an extra copy of the list, and is more succinct and reflective on the original code's intention of an in-place sort. 2.) Moved is_horizontal checks to occur out of loops. The variable doesn't change per node, so it makes more sense to not repeatedly check it while iterating nodes. Putting it outside of loops also separates horizontal and vertical alignment behavior, which would be needed for later bug fixes down the line. 3.) Changed all instances of "x / 2" to "0.5 * x". Both cases of dividing by 2 and multiplying by 0.5 occur in the code, this commit changed all of them to be consistent. It could be debated which one is more readable, but multiplying by a factor would probably be more workable if ever options for left or right-aligning and top or bottom-aligning would be implemented in the future. --- node_wrangler/operators.py | 50 ++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index f69544550..bbc6547da 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -2359,49 +2359,57 @@ class NWAlignNodes(Operator, NWBase): # 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_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] + y_locs = [n.location.y - (0.5 * n.dimensions.y) 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 + mid_x = 0.5 * (max(x_locs) + min(x_locs)) + mid_y = 0.5 * (max(y_locs) + min(y_locs)) 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)) + selection.sort(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) + selection.sort(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 * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes + if horizontal: + for node in selection: + current_margin = (0.5 * margin) if node.hide else margin - if horizontal: node.location.x = current_pos + node.location.y = mid_y + (0.5 * node.dimensions.y) current_pos += current_margin + node.dimensions.x - node.location.y = mid_y + (node.dimensions.y / 2) - else: + else: + for node in selection: + current_margin = (0.5 * margin) if node.hide else margin + node.location.y = current_pos + node.location.x = mid_x - (0.5 * node.dimensions.x) current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment - node.location.x = mid_x - (node.dimensions.x / 2) # If active node is selected, center nodes around it if active_loc is not None: active_loc_diff = active_loc - active_node.location for node in selection: 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 + + elif horizontal: # Position nodes centered around where they used to be + new_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] + new_mid = 0.5 * (max(new_locs) + min(new_locs)) + x_diff = mid_x - new_mid + for node in selection: - if horizontal: - node.location.x += (mid_x - new_mid) - else: - node.location.y += (mid_y - new_mid) + node.location.x += x_diff + else: + new_locs = [n.location.y - (0.5 * n.dimensions.y) for n in selection] + new_mid = 0.5 * (max(new_locs) + min(new_locs)) + y_diff = mid_y - new_mid + + for node in selection: + node.location.y += y_diff return {'FINISHED'} -- 2.30.2 From 4d9b5451fc81bbf8a8bc3a14a6ba0e4787f379e4 Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Sun, 9 Apr 2023 00:15:01 +0800 Subject: [PATCH 04/10] Fix: Inaccurate alignment for hidden nodes in Node Wrangler's Align Nodes For collapsed/hidden nodes, their origin is located on their middle-left point, instead of the top-left corner like in uncollapsed nodes. This wasn't fully taken into consideration by the original code, leading to inaccuracies when aligning hidden nodes, which this commit aims to address. The changes implemented by this commit are as follows: 1.) Whether a node is hidden or not is no longer considered during horizontal alignment, which avoids uneven spacing. As hiding a node only affects its vertical dimensions, not its horizontal one. 2.) Added corrective offsets for when vertically aligning hidden nodes. Due to how their origins are positioned, the original code would align hidden nodes by their bottoms, instead of their middle like in non-hidden nodes. So an extra offset was needed to align hidden nodes by their center. 3.) Improved consistency when aligning non-hidden and hidden nodes together. The original code's inaccuracies become most apparent when mixing both types of nodes together, where uneven margins and overlapping frequently occurs. --- node_wrangler/operators.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index bbc6547da..0d62fa64c 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -2375,20 +2375,24 @@ class NWAlignNodes(Operator, NWBase): # Alignment current_pos = 0 + weird_offset = 10 # Somehow hidden nodes would come out 10 units higher that non-hidden nodes when aligned, so this offset has to exist + if horizontal: for node in selection: - current_margin = (0.5 * margin) if node.hide else margin - node.location.x = current_pos node.location.y = mid_y + (0.5 * node.dimensions.y) - current_pos += current_margin + node.dimensions.x + if node.hide: + node.location.y -= (0.5 * node.dimensions.y) - weird_offset + + current_pos += margin + node.dimensions.x else: for node in selection: - current_margin = (0.5 * margin) if node.hide else margin - node.location.y = current_pos + if node.hide: + node.location.y -= (0.5 * node.dimensions.y) - weird_offset + node.location.x = mid_x - (0.5 * node.dimensions.x) - current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment + current_pos -= 0.3 * margin + node.dimensions.y # use half-margin for vertical alignment # If active node is selected, center nodes around it if active_loc is not None: -- 2.30.2 From f8816bd075121cfb6b26f18aec3685ee82608088 Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Tue, 11 Apr 2023 15:54:36 +0800 Subject: [PATCH 05/10] Fix: Nodes drifting downward when Align Nodes are repeatedly applied The commit preceding this one introduced code that applied corrective offsets to hidden nodes so that they would be visually aligned with non- hidden nodes. A separate calculation for calculating midpoints did not factor in those offsets, leading to a bug wherein nodes slowly inch downward when a horizontal align was applied repeatedly. This behavior occurs specifically during horizontal alignment where the active node is not selected. The changes in this commit aim to fix that behavior by accounting for said corrective offsets during midpoint calculation. Some stylistic code changes are also applied to relevant code for better readability. --- node_wrangler/operators.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 0d62fa64c..9c7a03313 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -2352,6 +2352,9 @@ class NWAlignNodes(Operator, NWBase): active_node = context.active_node margin = self.margin + # Somehow hidden nodes would come out 10 units higher that non-hidden nodes when aligned, so this offset has to exist + weird_offset = 10 + with temporary_unframe(nodes=selection): active_loc = None if active_node in selection: @@ -2360,12 +2363,19 @@ class NWAlignNodes(Operator, NWBase): # Check if nodes should be laid out horizontally or vertically # use dimension to get center of node, not corner x_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] - y_locs = [n.location.y - (0.5 * n.dimensions.y) for n in selection] + y_locs = [n.location.y - weird_offset if n.hide + else n.location.y - (0.5 * n.dimensions.y) for n in selection] + x_range = max(x_locs) - min(x_locs) y_range = max(y_locs) - min(y_locs) + horizontal = x_range > y_range + + #Undo corrective offsets for hidden nodes if alignment is horizontal + if not horizontal: + y_locs = [n.location.y - (0.5 * n.dimensions.y) for n in selection] + mid_x = 0.5 * (max(x_locs) + min(x_locs)) mid_y = 0.5 * (max(y_locs) + min(y_locs)) - horizontal = x_range > y_range # Sort selection by location of node mid-point if horizontal: @@ -2375,23 +2385,18 @@ class NWAlignNodes(Operator, NWBase): # Alignment current_pos = 0 - weird_offset = 10 # Somehow hidden nodes would come out 10 units higher that non-hidden nodes when aligned, so this offset has to exist if horizontal: for node in selection: node.location.x = current_pos - node.location.y = mid_y + (0.5 * node.dimensions.y) - if node.hide: - node.location.y -= (0.5 * node.dimensions.y) - weird_offset - + node.location.y = (mid_y + weird_offset) if node.hide else mid_y + (0.5 * node.dimensions.y) + current_pos += margin + node.dimensions.x else: for node in selection: - node.location.y = current_pos - if node.hide: - node.location.y -= (0.5 * node.dimensions.y) - weird_offset - 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 -= 0.3 * margin + node.dimensions.y # use half-margin for vertical alignment # If active node is selected, center nodes around it -- 2.30.2 From e42d6620c5bbe10865c922696cd11925900a1b15 Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Wed, 12 Apr 2023 13:55:16 +0800 Subject: [PATCH 06/10] Node Wrangler: Added explicit axis options for Align Nodes operator The Align Nodes operator has functionality for both horizontal and vertical alignment, but which axis was used is something that was automatically decided by the operator. While this is sufficient in most of cases, there is a good number of instances in which the automatic alignment is not ideal. Such as when a user may want to turn a row of nodes into a column, and vice versa, for example. This commit aims to make the operator more flexible by giving the user the option to explicitly choose which axis they would want the alignment to occur in. Additional buttons in the UI panel and context menu were added for these modes, currently named "Align X" and "Align Y". The "Align Nodes" button has been also renamed to "Auto-Align Nodes" for better clarity. In conjunction to that, a property is added in the addon's preferences menu that allows the user to specify different spacings for X and Y alignment. This gets rid of the hard coded (0.3 * margin) expression for vertical alignment, and allows the user to customize the operator whatever margins they find ideal. Layout changes in the user preferences UI have been made to accomodate this property, how this was approached is tentative and could be change given subsequent review/feedback. --- node_wrangler/interface.py | 10 +++++++++- node_wrangler/operators.py | 37 +++++++++++++++++++++++++++++------- node_wrangler/preferences.py | 18 ++++++++++++++++-- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/node_wrangler/interface.py b/node_wrangler/interface.py index 5c477dcc4..76938f31c 100644 --- a/node_wrangler/interface.py +++ b/node_wrangler/interface.py @@ -65,7 +65,15 @@ def drawlayout(context, layout, mode='non-panel'): col.separator() 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 = layout.column(align=True) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 9c7a03313..0f356079c 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -10,6 +10,7 @@ from bpy.props import ( IntProperty, StringProperty, FloatVectorProperty, + IntVectorProperty, CollectionProperty, ) from bpy_extras.io_utils import ImportHelper, ExportHelper @@ -2337,7 +2338,25 @@ class NWAlignNodes(Operator, NWBase): bl_idname = "node.nw_align_nodes" bl_label = "Align Nodes" 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): @@ -2350,7 +2369,8 @@ class NWAlignNodes(Operator, NWBase): def execute(self, context): selection = [node for node in context.selected_nodes if node.type != 'FRAME'] active_node = context.active_node - margin = self.margin + prefs = context.preferences.addons[__package__].preferences + margin_x, margin_y = prefs.align_nodes_margin # Somehow hidden nodes would come out 10 units higher that non-hidden nodes when aligned, so this offset has to exist weird_offset = 10 @@ -2363,14 +2383,14 @@ class NWAlignNodes(Operator, NWBase): # Check if nodes should be laid out horizontally or vertically # use dimension to get center of node, not corner x_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] - y_locs = [n.location.y - weird_offset if n.hide - else n.location.y - (0.5 * n.dimensions.y) for n in selection] + y_locs = [n.location.y - weird_offset if n.hide + else n.location.y - (0.5 * n.dimensions.y) for n in selection] x_range = max(x_locs) - min(x_locs) y_range = max(y_locs) - min(y_locs) horizontal = x_range > y_range - #Undo corrective offsets for hidden nodes if alignment is horizontal + # Undo corrective offsets for hidden nodes if alignment is horizontal if not horizontal: y_locs = [n.location.y - (0.5 * n.dimensions.y) for n in selection] @@ -2383,6 +2403,9 @@ class NWAlignNodes(Operator, NWBase): else: selection.sort(key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True) + if self.mode != 'AUTOMATIC': + horizontal = self.mode == 'HORIZONTAL' + # Alignment current_pos = 0 @@ -2391,13 +2414,13 @@ class NWAlignNodes(Operator, NWBase): node.location.x = current_pos node.location.y = (mid_y + weird_offset) if node.hide else mid_y + (0.5 * node.dimensions.y) - current_pos += margin + node.dimensions.x + current_pos += margin_x + node.dimensions.x else: for node in selection: 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 -= 0.3 * margin + node.dimensions.y # use half-margin for vertical alignment + current_pos -= margin_y + node.dimensions.y # If active node is selected, center nodes around it if active_loc is not None: diff --git a/node_wrangler/preferences.py b/node_wrangler/preferences.py index 970f6a0fc..a1e2ac973 100644 --- a/node_wrangler/preferences.py +++ b/node_wrangler/preferences.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later 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 . import operators @@ -104,11 +104,25 @@ class NWNodeWrangler(bpy.types.AddonPreferences): default=False, 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) def draw(self, context): layout = self.layout - col = layout.column() + split = layout.split(factor=0.40) + col = split.column(heading="Margin (Align Nodes):") + col.prop(self, "align_nodes_margin", text="") + + col = split.column(heading="Merge Node Options:") col.prop(self, "merge_position") col.prop(self, "merge_hide") -- 2.30.2 From 58a0f9ee31ede6898ba20419aef50f3be55ff737 Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Sun, 16 Apr 2023 22:58:35 +0800 Subject: [PATCH 07/10] Fix: Uneven spacing on reroutes in Node Wrangler's Align Nodes Node dimensions are taken into account in order to properly align nodes, but the dimensions of reroutes lead to them getting placed off-center during alignment. According to the API, reroutes have a dimension of (16, 16) units, but visually, dimensions of 10 units instead of 16 seem to be closer to what the user sees. The changes in this commit make it so that the calculations use those dimensions instead, which results in visually more even spacing between nodes and reroutes. Additionally, the fact that a hidden state (where node.hide == True) would not affect placement calculation for reroutes, as opposed to hidden nodes where it does, having separated placement logic for reroutes and nodes seems justified enough to implement. Correcting for the position of reroutes however, does seem to introduce drift perpendicular to whatever axis the alignment takes place in. At the cost of doing more calculations, addressing this actually compresses the code as the nodes now always have to be repositioned in both axes to avoid drift. --- node_wrangler/operators.py | 59 +++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 0f356079c..042e58528 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -2373,7 +2373,11 @@ class NWAlignNodes(Operator, NWBase): margin_x, margin_y = prefs.align_nodes_margin # Somehow hidden nodes would come out 10 units higher that non-hidden nodes when aligned, so this offset has to exist - weird_offset = 10 + weird_offset = 10 + + # 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 @@ -2390,10 +2394,6 @@ class NWAlignNodes(Operator, NWBase): y_range = max(y_locs) - min(y_locs) horizontal = x_range > y_range - # Undo corrective offsets for hidden nodes if alignment is horizontal - if not horizontal: - y_locs = [n.location.y - (0.5 * n.dimensions.y) for n in selection] - mid_x = 0.5 * (max(x_locs) + min(x_locs)) mid_y = 0.5 * (max(y_locs) + min(y_locs)) @@ -2411,36 +2411,49 @@ class NWAlignNodes(Operator, NWBase): if horizontal: for node in selection: - node.location.x = current_pos - node.location.y = (mid_y + weird_offset) if node.hide else mid_y + (0.5 * node.dimensions.y) + if node.type != 'REROUTE': + node.location.x = current_pos + node.location.y = (mid_y + weird_offset) if node.hide else mid_y + (0.5 * node.dimensions.y) + + current_pos += margin_x + node.dimensions.x + else: + node.location.x = current_pos + (0.5 * reroute_width) + node.location.y = mid_y + + current_pos += margin_x + reroute_width - current_pos += margin_x + node.dimensions.x else: for node in selection: - 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 + 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 + 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_loc is not None: active_loc_diff = active_loc - active_node.location for node in selection: node.location += active_loc_diff - - elif horizontal: # Position nodes centered around where they used to be - new_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] - new_mid = 0.5 * (max(new_locs) + min(new_locs)) - x_diff = mid_x - new_mid - + else: + new_x_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] + new_y_locs = [n.location.y - weird_offset if n.hide + else n.location.y - (0.5 * n.dimensions.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 - else: - new_locs = [n.location.y - (0.5 * n.dimensions.y) for n in selection] - new_mid = 0.5 * (max(new_locs) + min(new_locs)) - y_diff = mid_y - new_mid - - for node in selection: node.location.y += y_diff return {'FINISHED'} -- 2.30.2 From 0c96a3fdedb1cc6b3d9655b4b9240a1aa16a3da0 Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Mon, 17 Apr 2023 04:16:19 +0800 Subject: [PATCH 08/10] Node Wrangler: Cleaned up NWAlignNodes operator Final cleanup of the operator before opening it as a pull request. Moved the midpoint calculation to its own staticmethod since it is called multiple times, and being named "get_midpoint" should make it what it is doing clear for anybody reading the code. Additionally, trailing whitespace has been cleaned out, and some parentheses are added to make certain expressions clearer. --- node_wrangler/operators.py | 53 ++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 042e58528..fb53410e5 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -2340,8 +2340,8 @@ class NWAlignNodes(Operator, NWBase): bl_options = {'REGISTER', 'UNDO'} mode: EnumProperty( - name='Align Mode', - default='AUTOMATIC', + name='Align Mode', + default='AUTOMATIC', items=( ('AUTOMATIC', 'Auto-Align', ''), ('HORIZONTAL', 'Align X', ''), @@ -2366,6 +2366,23 @@ class NWAlignNodes(Operator, NWBase): return nw_check(context) + @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): selection = [node for node in context.selected_nodes if node.type != 'FRAME'] active_node = context.active_node @@ -2374,21 +2391,18 @@ class NWAlignNodes(Operator, NWBase): # Somehow hidden nodes would come out 10 units higher that non-hidden nodes when aligned, so this offset has to exist weird_offset = 10 - + # 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 + reroute_width = 10 with temporary_unframe(nodes=selection): active_loc = None if active_node in selection: active_loc = copy(active_node.location) # make a copy, not a reference - # Check if nodes should be laid out horizontally or vertically - # use dimension to get center of node, not corner - x_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] - y_locs = [n.location.y - weird_offset if n.hide - else n.location.y - (0.5 * n.dimensions.y) for n in selection] + x_locs = [self.get_midpoint(n, axis='X') for n in selection] + y_locs = [self.get_midpoint(n, axis='Y') for n in selection] x_range = max(x_locs) - min(x_locs) y_range = max(y_locs) - min(y_locs) @@ -2404,7 +2418,7 @@ class NWAlignNodes(Operator, NWBase): selection.sort(key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True) if self.mode != 'AUTOMATIC': - horizontal = self.mode == 'HORIZONTAL' + horizontal = (self.mode == 'HORIZONTAL') # Alignment current_pos = 0 @@ -2426,32 +2440,31 @@ class NWAlignNodes(Operator, NWBase): 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 + 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.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_loc is not None: - active_loc_diff = active_loc - active_node.location + active_loc_diff = (active_loc - active_node.location) for node in selection: node.location += active_loc_diff else: - new_x_locs = [n.location.x + (0.5 * n.dimensions.x) for n in selection] - new_y_locs = [n.location.y - weird_offset if n.hide - else n.location.y - (0.5 * n.dimensions.y) for n in selection] - + 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 -- 2.30.2 From 8e80eeef0672964e3641e991782e98c894f2c78f Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Mon, 17 Apr 2023 05:47:29 +0800 Subject: [PATCH 09/10] Node Wrangler: Refactored NWAlignNodes poll function Forgot to add this one in the cleanup commit, but this refactors the poll function to use a generator expression instead of a list. This avoids having to make an entire list of selected nodes just to find out whether at least two nodes are selected. --- node_wrangler/operators.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index fb53410e5..00595f44b 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -2360,11 +2360,12 @@ class NWAlignNodes(Operator, NWBase): @classmethod def poll(cls, context): - selection = [node for node in context.selected_nodes if node.type != 'FRAME'] - if len(selection) < 2: - return False + 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 nw_check(context) + return False @staticmethod def get_midpoint(node, axis): -- 2.30.2 From 06271f6d4ea54b9c1f0e5006826a895304a17997 Mon Sep 17 00:00:00 2001 From: Quackarooni Date: Tue, 18 Apr 2023 01:40:50 +0800 Subject: [PATCH 10/10] Node Wrangler: Changed Preferences layout from double to single-column Changed the positioning of the Margin property to be single-column. This avoids concern with certain labels being obscure when the preference window is not fullscreen or is rather small. --- node_wrangler/preferences.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node_wrangler/preferences.py b/node_wrangler/preferences.py index a1e2ac973..4f9c48005 100644 --- a/node_wrangler/preferences.py +++ b/node_wrangler/preferences.py @@ -118,11 +118,11 @@ class NWNodeWrangler(bpy.types.AddonPreferences): def draw(self, context): layout = self.layout - split = layout.split(factor=0.40) - col = split.column(heading="Margin (Align Nodes):") + col = layout.column(heading="Margin (Align Nodes):") col.prop(self, "align_nodes_margin", text="") + col.separator() - col = split.column(heading="Merge Node Options:") + col.label(text="Merge Node Options:") col.prop(self, "merge_position") col.prop(self, "merge_hide") -- 2.30.2