Skip to content
Snippets Groups Projects
node_arrange.py 13.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### 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, 2),
    
    	"blender": (2, 80, 4),
    	"location": "Node Editor > Properties > Trees",
    	"description": "Node Tree Arrangement Tools",
    	"warning": "",
    
        "doc_url": "https://docs.blender.org/manual/en/dev/addons/"
                   "node/node_arrange.html",
    
    	"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
    
    
    	#first arrange nodegroups
    	n_groups = []
    	for i in ntree.nodes:
    		if i.type == 'GROUP':
    			n_groups.append(i)
    
    	while n_groups:
    		j = n_groups.pop(0)
    		nodes_iterate(j.node_tree)
    		for i in j.node_tree.nodes:
    			if i.type == 'GROUP':
    				n_groups.append(i)
    
    	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 ntree.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()