From 2a98d83b4bcb37e3b8de5ccc72c283a5135708ed Mon Sep 17 00:00:00 2001 From: meta-androcto Date: Thu, 5 Sep 2019 10:31:18 +1000 Subject: Node Arrange: 2.81, Thanks JuhaW: T66410 --- node_arrange.py | 520 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 node_arrange.py (limited to 'node_arrange.py') diff --git a/node_arrange.py b/node_arrange.py new file mode 100644 index 00000000..370cbeff --- /dev/null +++ b/node_arrange.py @@ -0,0 +1,520 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Node Arrange", + "author": "JuhaW", + "version": (0, 2, 1), + "blender": (2, 80, 4), + "location": "Node Editor > Properties > Trees", + "description": "Node Tree Arrangement Tools", + "warning": "", + "wiki_url": "", + "tracker_url": "https://github.com/JuhaW/NodeArrange/issues", + "category": "Node" +} + + +import sys +import bpy +from collections import OrderedDict +from itertools import repeat +import pprint +import pdb +from bpy.types import Operator, Panel +from bpy.props import ( + IntProperty, +) +from copy import copy + + +#From Node Wrangler +def get_nodes_linked(context): + tree = context.space_data.node_tree + + # Get nodes from currently edited tree. + # If user is editing a group, space_data.node_tree is still the base level (outside group). + # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not + # the same as context.active_node, the user is in a group. + # Check recursively until we find the real active node_tree: + if tree.nodes.active: + while tree.nodes.active != context.active_node: + tree = tree.nodes.active.node_tree + + return tree.nodes, tree.links + +class NA_OT_AlignNodes(Operator): + '''Align the selected nodes/Tidy loose nodes''' + bl_idname = "node.na_align_nodes" + bl_label = "Align Nodes" + bl_options = {'REGISTER', 'UNDO'} + margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes') + + def execute(self, context): + nodes, links = get_nodes_linked(context) + 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 + + # Check if nodes should be laid 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 * 0.5 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 * 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 - nodes.active.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'} + +class values(): + average_y = 0 + x_last = 0 + margin_x = 100 + mat_name = "" + margin_y = 20 + + +class NA_PT_NodePanel(Panel): + bl_label = "Node Arrange" + bl_space_type = "NODE_EDITOR" + bl_region_type = "UI" + bl_category = "Arrange" + + def draw(self, context): + if context.active_node is not None: + layout = self.layout + row = layout.row() + col = layout.column + row.operator('node.button') + + row = layout.row() + row.prop(bpy.context.scene, 'nodemargin_x', text="Margin x") + row = layout.row() + row.prop(bpy.context.scene, 'nodemargin_y', text="Margin y") + row = layout.row() + row.prop(context.scene, 'node_center', text="Center nodes") + + row = layout.row() + row.operator('node.na_align_nodes', text="Align to Selected") + + row = layout.row() + node = context.space_data.node_tree.nodes.active + if node and node.select: + row.prop(node, 'location', text = "Node X", index = 0) + row.prop(node, 'location', text = "Node Y", index = 1) + row = layout.row() + row.prop(node, 'width', text = "Node width") + + row = layout.row() + row.operator('node.button_odd') + +class NA_OT_NodeButton(Operator): + + '''Arrange Connected Nodes/Arrange All Nodes''' + bl_idname = 'node.button' + bl_label = 'Arrange All Nodes' + + def execute(self, context): + nodemargin(self, context) + bpy.context.space_data.node_tree.nodes.update() + bpy.ops.node.view_all() + + return {'FINISHED'} + + # not sure this is doing what you expect. + # blender.org/api/blender_python_api_current/bpy.types.Operator.html#invoke + def invoke(self, context, value): + values.mat_name = bpy.context.space_data.node_tree + nodemargin(self, context) + return {'FINISHED'} + + +class NA_OT_NodeButtonOdd(Operator): + + 'Show the nodes for this material' + bl_idname = 'node.button_odd' + bl_label = 'Select Unlinked' + + def execute(self, context): + values.mat_name = bpy.context.space_data.node_tree + #mat = bpy.context.object.active_material + nodes_iterate(context.space_data.node_tree, False) + return {'FINISHED'} + + +class NA_OT_NodeButtonCenter(Operator): + + 'Show the nodes for this material' + bl_idname = 'node.button_center' + bl_label = 'Center nodes (0,0)' + + def execute(self, context): + values.mat_name = "" # reset + mat = bpy.context.object.active_material + nodes_center(mat) + return {'FINISHED'} + + +def nodemargin(self, context): + + values.margin_x = context.scene.nodemargin_x + values.margin_y = context.scene.nodemargin_y + + ntree = context.space_data.node_tree + + nodes_iterate(ntree) + + # arrange nodes + this center nodes together + if context.scene.node_center: + nodes_center(ntree) + + +class NA_OT_ArrangeNodesOp(bpy.types.Operator): + bl_idname = 'node.arrange_nodetree' + bl_label = 'Nodes Private Op' + + mat_name : bpy.props.StringProperty() + margin_x : bpy.props.IntProperty(default=120) + margin_y : bpy.props.IntProperty(default=120) + + def nodemargin2(self, context): + mat = None + mat_found = bpy.data.materials.get(self.mat_name) + if self.mat_name and mat_found: + mat = mat_found + #print(mat) + + if not mat: + return + else: + values.mat_name = self.mat_name + scn = context.scene + scn.nodemargin_x = self.margin_x + scn.nodemargin_y = self.margin_y + nodes_iterate(mat) + if scn.node_center: + nodes_center(mat) + + def execute(self, context): + self.nodemargin2(context) + return {'FINISHED'} + + +def outputnode_search(ntree): # return node/None + + outputnodes = [] + for node in bpy.context.space_data.node_tree.nodes: + if not node.outputs: + for input in node.inputs: + if input.is_linked: + outputnodes.append(node) + break + + if not outputnodes: + print("No output node found") + return None + return outputnodes + + +############################################################### +def nodes_iterate(ntree, arrange=True): + + nodeoutput = outputnode_search(ntree) + if nodeoutput is None: + #print ("nodeoutput is None") + return None + a = [] + a.append([]) + for i in nodeoutput: + a[0].append(i) + + + level = 0 + + while a[level]: + a.append([]) + + for node in a[level]: + inputlist = [i for i in node.inputs if i.is_linked] + + if inputlist: + + for input in inputlist: + for nlinks in input.links: + node1 = nlinks.from_node + a[level + 1].append(node1) + + else: + pass + + level += 1 + + del a[level] + level -= 1 + + #remove duplicate nodes at the same level, first wins + for x, nodes in enumerate(a): + a[x] = list(OrderedDict(zip(a[x], repeat(None)))) + + #remove duplicate nodes in all levels, last wins + top = level + for row1 in range(top, 1, -1): + for col1 in a[row1]: + for row2 in range(row1-1, 0, -1): + for col2 in a[row2]: + if col1 == col2: + a[row2].remove(col2) + break + + """ + for x, i in enumerate(a): + print (x) + for j in i: + print (j) + #print() + """ + """ + #add node frames to nodelist + frames = [] + print ("Frames:") + print ("level:", level) + print ("a:",a) + for row in range(level, 0, -1): + + for i, node in enumerate(a[row]): + if node.parent: + print ("Frame found:", node.parent, node) + #if frame already added to the list ? + frame = node.parent + #remove node + del a[row][i] + if frame not in frames: + frames.append(frame) + #add frame to the same place than node was + a[row].insert(i, frame) + + pprint.pprint(a) + """ + #return None + ######################################## + + + + if not arrange: + nodelist = [j for i in a for j in i] + nodes_odd(ntree, nodelist=nodelist) + return None + + ######################################## + + levelmax = level + 1 + level = 0 + values.x_last = 0 + + while level < levelmax: + + values.average_y = 0 + nodes = [x for x in a[level]] + #print ("level, nodes:", level, nodes) + nodes_arrange(nodes, level) + + level = level + 1 + + return None + + +############################################################### +def nodes_odd(ntree, nodelist): + + nodes = ntree.nodes + for i in nodes: + i.select = False + + a = [x for x in nodes if x not in nodelist] + # print ("odd nodes:",a) + for i in a: + i.select = True + + +def nodes_arrange(nodelist, level): + + parents = [] + for node in nodelist: + parents.append(node.parent) + node.parent = None + bpy.context.space_data.node_tree.nodes.update() + + + #print ("nodes arrange def") + # node x positions + + widthmax = max([x.dimensions.x for x in nodelist]) + xpos = values.x_last - (widthmax + values.margin_x) if level != 0 else 0 + #print ("nodelist, xpos", nodelist,xpos) + values.x_last = xpos + + # node y positions + x = 0 + y = 0 + + for node in nodelist: + + if node.hide: + hidey = (node.dimensions.y / 2) - 8 + y = y - hidey + else: + hidey = 0 + + node.location.y = y + y = y - values.margin_y - node.dimensions.y + hidey + + node.location.x = xpos #if node.type != "FRAME" else xpos + 1200 + + y = y + values.margin_y + + center = (0 + y) / 2 + values.average_y = center - values.average_y + + #for node in nodelist: + + #node.location.y -= values.average_y + + for i, node in enumerate(nodelist): + node.parent = parents[i] + +def nodetree_get(mat): + + return mat.node_tree.nodes + + +def nodes_center(ntree): + + bboxminx = [] + bboxmaxx = [] + bboxmaxy = [] + bboxminy = [] + + for node in ntree.nodes: + if not node.parent: + bboxminx.append(node.location.x) + bboxmaxx.append(node.location.x + node.dimensions.x) + bboxmaxy.append(node.location.y) + bboxminy.append(node.location.y - node.dimensions.y) + + # print ("bboxminy:",bboxminy) + bboxminx = min(bboxminx) + bboxmaxx = max(bboxmaxx) + bboxminy = min(bboxminy) + bboxmaxy = max(bboxmaxy) + center_x = (bboxminx + bboxmaxx) / 2 + center_y = (bboxminy + bboxmaxy) / 2 + ''' + print ("minx:",bboxminx) + print ("maxx:",bboxmaxx) + print ("miny:",bboxminy) + print ("maxy:",bboxmaxy) + + print ("bboxes:", bboxminx, bboxmaxx, bboxmaxy, bboxminy) + print ("center x:",center_x) + print ("center y:",center_y) + ''' + + x = 0 + y = 0 + + for node in ntree.nodes: + + if not node.parent: + node.location.x -= center_x + node.location.y += -center_y + +classes = [ + NA_PT_NodePanel, + NA_OT_NodeButton, + NA_OT_NodeButtonOdd, + NA_OT_NodeButtonCenter, + NA_OT_ArrangeNodesOp, + NA_OT_AlignNodes +] + +def register(): + for c in classes: + bpy.utils.register_class(c) + + bpy.types.Scene.nodemargin_x = bpy.props.IntProperty(default=100, update=nodemargin) + bpy.types.Scene.nodemargin_y = bpy.props.IntProperty(default=20, update=nodemargin) + bpy.types.Scene.node_center = bpy.props.BoolProperty(default=True, update=nodemargin) + + + +def unregister(): + for c in classes: + bpy.utils.unregister_class(c) + + del bpy.types.Scene.nodemargin_x + del bpy.types.Scene.nodemargin_y + del bpy.types.Scene.node_center + +if __name__ == "__main__": + register() -- cgit v1.2.3