1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Node Arrange",
10 "location": "Node Editor > Properties > Trees",
11 "description": "Node Tree Arrangement Tools",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_arrange.html",
14 "tracker_url": "https://github.com/JuhaW/NodeArrange/issues",
21 from collections
import OrderedDict
22 from itertools
import repeat
25 from bpy
.types
import Operator
, Panel
26 from bpy
.props
import (
33 def get_nodes_linked(context
):
34 tree
= context
.space_data
.node_tree
36 # Get nodes from currently edited tree.
37 # If user is editing a group, space_data.node_tree is still the base level (outside group).
38 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
39 # the same as context.active_node, the user is in a group.
40 # Check recursively until we find the real active node_tree:
42 while tree
.nodes
.active
!= context
.active_node
:
43 tree
= tree
.nodes
.active
.node_tree
45 return tree
.nodes
, tree
.links
47 class NA_OT_AlignNodes(Operator
):
48 '''Align the selected nodes/Tidy loose nodes'''
49 bl_idname
= "node.na_align_nodes"
50 bl_label
= "Align Nodes"
51 bl_options
= {'REGISTER', 'UNDO'}
52 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
54 def execute(self
, context
):
55 nodes
, links
= get_nodes_linked(context
)
60 if node
.select
and node
.type != 'FRAME':
61 selection
.append(node
)
63 # If no nodes are selected, align all nodes
67 elif nodes
.active
in selection
:
68 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
70 # Check if nodes should be laid out horizontally or vertically
71 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
72 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
73 x_range
= max(x_locs
) - min(x_locs
)
74 y_range
= max(y_locs
) - min(y_locs
)
75 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
76 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
77 horizontal
= x_range
> y_range
79 # Sort selection by location of node mid-point
81 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
83 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
87 for node
in selection
:
88 current_margin
= margin
89 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
92 node
.location
.x
= current_pos
93 current_pos
+= current_margin
+ node
.dimensions
.x
94 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
96 node
.location
.y
= current_pos
97 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
98 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
100 # If active node is selected, center nodes around it
101 if active_loc
is not None:
102 active_loc_diff
= active_loc
- nodes
.active
.location
103 for node
in selection
:
104 node
.location
+= active_loc_diff
105 else: # Position nodes centered around where they used to be
106 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
])
107 new_mid
= (max(locs
) + min(locs
)) / 2
108 for node
in selection
:
110 node
.location
.x
+= (mid_x
- new_mid
)
112 node
.location
.y
+= (mid_y
- new_mid
)
124 class NA_PT_NodePanel(Panel
):
125 bl_label
= "Node Arrange"
126 bl_space_type
= "NODE_EDITOR"
127 bl_region_type
= "UI"
128 bl_category
= "Arrange"
130 def draw(self
, context
):
131 if context
.active_node
is not None:
135 row
.operator('node.button')
138 row
.prop(bpy
.context
.scene
, 'nodemargin_x', text
="Margin x")
140 row
.prop(bpy
.context
.scene
, 'nodemargin_y', text
="Margin y")
142 row
.prop(context
.scene
, 'node_center', text
="Center nodes")
145 row
.operator('node.na_align_nodes', text
="Align to Selected")
148 node
= context
.space_data
.node_tree
.nodes
.active
149 if node
and node
.select
:
150 row
.prop(node
, 'location', text
= "Node X", index
= 0)
151 row
.prop(node
, 'location', text
= "Node Y", index
= 1)
153 row
.prop(node
, 'width', text
= "Node width")
156 row
.operator('node.button_odd')
158 class NA_OT_NodeButton(Operator
):
160 '''Arrange Connected Nodes/Arrange All Nodes'''
161 bl_idname
= 'node.button'
162 bl_label
= 'Arrange All Nodes'
164 def execute(self
, context
):
165 nodemargin(self
, context
)
166 bpy
.context
.space_data
.node_tree
.nodes
.update()
167 bpy
.ops
.node
.view_all()
171 # not sure this is doing what you expect.
172 # blender.org/api/blender_python_api_current/bpy.types.Operator.html#invoke
173 def invoke(self
, context
, value
):
174 values
.mat_name
= bpy
.context
.space_data
.node_tree
175 nodemargin(self
, context
)
179 class NA_OT_NodeButtonOdd(Operator
):
181 'Show the nodes for this material'
182 bl_idname
= 'node.button_odd'
183 bl_label
= 'Select Unlinked'
185 def execute(self
, context
):
186 values
.mat_name
= bpy
.context
.space_data
.node_tree
187 #mat = bpy.context.object.active_material
188 nodes_iterate(context
.space_data
.node_tree
, False)
192 class NA_OT_NodeButtonCenter(Operator
):
194 'Show the nodes for this material'
195 bl_idname
= 'node.button_center'
196 bl_label
= 'Center nodes (0,0)'
198 def execute(self
, context
):
199 values
.mat_name
= "" # reset
200 mat
= bpy
.context
.object.active_material
205 def nodemargin(self
, context
):
207 values
.margin_x
= context
.scene
.nodemargin_x
208 values
.margin_y
= context
.scene
.nodemargin_y
210 ntree
= context
.space_data
.node_tree
212 #first arrange nodegroups
214 for i
in ntree
.nodes
:
215 if i
.type == 'GROUP':
220 nodes_iterate(j
.node_tree
)
221 for i
in j
.node_tree
.nodes
:
222 if i
.type == 'GROUP':
227 # arrange nodes + this center nodes together
228 if context
.scene
.node_center
:
232 class NA_OT_ArrangeNodesOp(bpy
.types
.Operator
):
233 bl_idname
= 'node.arrange_nodetree'
234 bl_label
= 'Nodes Private Op'
236 mat_name
: bpy
.props
.StringProperty()
237 margin_x
: bpy
.props
.IntProperty(default
=120)
238 margin_y
: bpy
.props
.IntProperty(default
=120)
240 def nodemargin2(self
, context
):
242 mat_found
= bpy
.data
.materials
.get(self
.mat_name
)
243 if self
.mat_name
and mat_found
:
250 values
.mat_name
= self
.mat_name
252 scn
.nodemargin_x
= self
.margin_x
253 scn
.nodemargin_y
= self
.margin_y
258 def execute(self
, context
):
259 self
.nodemargin2(context
)
263 def outputnode_search(ntree
): # return node/None
266 for node
in ntree
.nodes
:
268 for input in node
.inputs
:
270 outputnodes
.append(node
)
274 print("No output node found")
279 ###############################################################
280 def nodes_iterate(ntree
, arrange
=True):
282 nodeoutput
= outputnode_search(ntree
)
283 if nodeoutput
is None:
284 #print ("nodeoutput is None")
297 for node
in a
[level
]:
298 inputlist
= [i
for i
in node
.inputs
if i
.is_linked
]
302 for input in inputlist
:
303 for nlinks
in input.links
:
304 node1
= nlinks
.from_node
305 a
[level
+ 1].append(node1
)
315 #remove duplicate nodes at the same level, first wins
316 for x
, nodes
in enumerate(a
):
317 a
[x
] = list(OrderedDict(zip(a
[x
], repeat(None))))
319 #remove duplicate nodes in all levels, last wins
321 for row1
in range(top
, 1, -1):
323 for row2
in range(row1
-1, 0, -1):
330 for x, i in enumerate(a):
337 #add node frames to nodelist
340 print ("level:", level)
342 for row in range(level, 0, -1):
344 for i, node in enumerate(a[row]):
346 print ("Frame found:", node.parent, node)
347 #if frame already added to the list ?
351 if frame not in frames:
353 #add frame to the same place than node was
354 a[row].insert(i, frame)
359 ########################################
364 nodelist
= [j
for i
in a
for j
in i
]
365 nodes_odd(ntree
, nodelist
=nodelist
)
368 ########################################
374 while level
< levelmax
:
377 nodes
= [x
for x
in a
[level
]]
378 #print ("level, nodes:", level, nodes)
379 nodes_arrange(nodes
, level
)
386 ###############################################################
387 def nodes_odd(ntree
, nodelist
):
393 a
= [x
for x
in nodes
if x
not in nodelist
]
394 # print ("odd nodes:",a)
399 def nodes_arrange(nodelist
, level
):
402 for node
in nodelist
:
403 parents
.append(node
.parent
)
405 bpy
.context
.space_data
.node_tree
.nodes
.update()
408 #print ("nodes arrange def")
411 widthmax
= max([x
.dimensions
.x
for x
in nodelist
])
412 xpos
= values
.x_last
- (widthmax
+ values
.margin_x
) if level
!= 0 else 0
413 #print ("nodelist, xpos", nodelist,xpos)
420 for node
in nodelist
:
423 hidey
= (node
.dimensions
.y
/ 2) - 8
429 y
= y
- values
.margin_y
- node
.dimensions
.y
+ hidey
431 node
.location
.x
= xpos
#if node.type != "FRAME" else xpos + 1200
433 y
= y
+ values
.margin_y
436 values
.average_y
= center
- values
.average_y
438 #for node in nodelist:
440 #node.location.y -= values.average_y
442 for i
, node
in enumerate(nodelist
):
443 node
.parent
= parents
[i
]
445 def nodetree_get(mat
):
447 return mat
.node_tree
.nodes
450 def nodes_center(ntree
):
457 for node
in ntree
.nodes
:
459 bboxminx
.append(node
.location
.x
)
460 bboxmaxx
.append(node
.location
.x
+ node
.dimensions
.x
)
461 bboxmaxy
.append(node
.location
.y
)
462 bboxminy
.append(node
.location
.y
- node
.dimensions
.y
)
464 # print ("bboxminy:",bboxminy)
465 bboxminx
= min(bboxminx
)
466 bboxmaxx
= max(bboxmaxx
)
467 bboxminy
= min(bboxminy
)
468 bboxmaxy
= max(bboxmaxy
)
469 center_x
= (bboxminx
+ bboxmaxx
) / 2
470 center_y
= (bboxminy
+ bboxmaxy
) / 2
472 print ("minx:",bboxminx)
473 print ("maxx:",bboxmaxx)
474 print ("miny:",bboxminy)
475 print ("maxy:",bboxmaxy)
477 print ("bboxes:", bboxminx, bboxmaxx, bboxmaxy, bboxminy)
478 print ("center x:",center_x)
479 print ("center y:",center_y)
485 for node
in ntree
.nodes
:
488 node
.location
.x
-= center_x
489 node
.location
.y
+= -center_y
495 NA_OT_NodeButtonCenter
,
496 NA_OT_ArrangeNodesOp
,
502 bpy
.utils
.register_class(c
)
504 bpy
.types
.Scene
.nodemargin_x
= bpy
.props
.IntProperty(default
=100, update
=nodemargin
)
505 bpy
.types
.Scene
.nodemargin_y
= bpy
.props
.IntProperty(default
=20, update
=nodemargin
)
506 bpy
.types
.Scene
.node_center
= bpy
.props
.BoolProperty(default
=True, update
=nodemargin
)
512 bpy
.utils
.unregister_class(c
)
514 del bpy
.types
.Scene
.nodemargin_x
515 del bpy
.types
.Scene
.nodemargin_y
516 del bpy
.types
.Scene
.node_center
518 if __name__
== "__main__":