blender-addons/node_arrange.py
Campbell Barton e8da6131fd License headers: use SPDX-FileCopyrightText for all addons
Move copyright text to SPDX-FileCopyrightText or set to the
Blender Foundation so "make check_licenses" now runs without warnings.
2023-06-15 16:54:05 +10:00

520 lines
14 KiB
Python

# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
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": "{BLENDER_MANUAL_URL}/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()