Campbell Barton
e8da6131fd
Move copyright text to SPDX-FileCopyrightText or set to the Blender Foundation so "make check_licenses" now runs without warnings.
520 lines
14 KiB
Python
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()
|