I copied some parts of the arrange_node add-on in my code to auto-arrange all shader nodes. The problem is this function only works if we select the shader editor and then click on one of the shader nodes.
How we can change the code to work properly?
Arrange_Nodes.rar (127.9 KB)
Any idea? I think we should change the context first. How we could do that?
Please someone help me on this, I’m stucked.
It doesn’t work because the script relies on reading bpy.types.Node.dimensions
which is not computed until the node tree has been rendered at least once.
You can get around this by computing the width yourself (see node_update_basis in node_draw.cc):
width = node.width * (dpi / 72)
So, in your function:
def nodes_arrange(ntree,nodelist, level):
...
change
widthmax = max([x.dimensions.x for x in nodelist])
to
ui_scale_fac = bpy.context.preferences.system.dpi / 72
widthmax = max([x.width * ui_scale_fac for x in nodelist])
You need to do this for the height as well.
Thank you for you answer, your method works perfect for the width. but about the height, there is some difference between dimensions.y and node.height.
So we should stay on dimensions, Any method to force update it without the first render?
OK, we need to update the entire nodes manually, now it’s working:
old_area = bpy.context.area.type
bpy.context.area.type = 'NODE_EDITOR'
bpy.context.area.spaces.active.tree_type = 'ShaderNodeTree'
old_object = bpy.context.view_layer.objects.active
for o in bpy.data.objects:
bpy.context.view_layer.objects.active = o
old_mat = o.active_material
for ms in o.material_slots:
mat = ms.material
o.active_material = mat
node_tree = mat.node_tree
bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1)
arrange_node_tree(node_tree)
o.active_material = old_mat
bpy.context.view_layer.objects.active = old_object
bpy.context.area.type = old_area
A forced redraw works if you don’t mind changing the area for a few frames.
Another option is to add new materials to a list and when you view the material’s node tree the first time, the nodes get arranged.
You mean some callback? (I’m 3dsMax user, I don’t know what we call it in Blender)
This is my code that works perfectly, but the problem is when I use command-line to run the code, it dosn’t work:
class values():
average_y = 0
x_last = 0
margin_x = 100
margin_y = 20
def arrange_nodes(ntree):
outputnodes = []
for node in ntree.nodes:
if not node.outputs:
for input in node.inputs:
if input.is_linked:
outputnodes.append(node)
break
if outputnodes is None:
return None
a = []
a.append([])
for i in outputnodes:
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
for x, nodes in enumerate(a):
a[x] = list(OrderedDict(zip(a[x], repeat(None))))
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
levelmax = level + 1
level = 0
values.x_last = 0
while level < levelmax:
values.average_y = 0
nodes = [x for x in a[level]]
nodes_arrange(ntree,nodes, level)
level = level + 1
return None
def nodes_arrange(ntree,nodelist, level):
parents = []
for node in nodelist:
parents.append(node.parent)
node.parent = None
ntree.nodes.update()
widthmax = max([x.dimensions.x for x in nodelist])
xpos = values.x_last - (widthmax + values.margin_x) if level != 0 else 0
values.x_last = xpos
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
y = y + values.margin_y
center = (0 + y) / 2
values.average_y = center - values.average_y
for i, node in enumerate(nodelist):
node.parent = parents[i]
def arrange_shader_nodes():
old_area = bpy.context.area.type
bpy.context.area.type = 'NODE_EDITOR'
bpy.context.area.spaces.active.tree_type = 'ShaderNodeTree'
old_object = bpy.context.view_layer.objects.active
for o in bpy.data.objects:
bpy.context.view_layer.objects.active = o
old_mat = o.active_material
for ms in o.material_slots:
mat = ms.material
o.active_material = mat
node_tree = mat.node_tree
bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1)
arrange_nodes(node_tree)
o.active_material = old_mat
bpy.context.view_layer.objects.active = old_object
bpy.context.area.type = old_area
it seems I should run the code after all UI stuff loaded completely, right?
I found the problem, we should change the area to NodeEditor properly, this is working:
def arrange_shader_nodes():
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
area.type = 'NODE_EDITOR'
area.spaces.active.tree_type = 'ShaderNodeTree'
for o in bpy.data.objects:
bpy.context.view_layer.objects.active = o
old_mat = o.active_material
for ms in o.material_slots:
mat = ms.material
o.active_material = mat
bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1)
arrange_nodes(mat.node_tree)
o.active_material = old_mat
area.type = 'VIEW_3D'