1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 "name": "Node Arrange",
23 "blender": (2, 80, 4),
24 "location": "Node Editor > Properties > Trees",
25 "description": "Node Tree Arrangement Tools",
27 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
28 "node/node_arrange.html",
29 "tracker_url": "https://github.com/JuhaW/NodeArrange/issues",
36 from collections
import OrderedDict
37 from itertools
import repeat
40 from bpy
.types
import Operator
, Panel
41 from bpy
.props
import (
48 def get_nodes_linked(context
):
49 tree
= context
.space_data
.node_tree
51 # Get nodes from currently edited tree.
52 # If user is editing a group, space_data.node_tree is still the base level (outside group).
53 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
54 # the same as context.active_node, the user is in a group.
55 # Check recursively until we find the real active node_tree:
57 while tree
.nodes
.active
!= context
.active_node
:
58 tree
= tree
.nodes
.active
.node_tree
60 return tree
.nodes
, tree
.links
62 class NA_OT_AlignNodes(Operator
):
63 '''Align the selected nodes/Tidy loose nodes'''
64 bl_idname
= "node.na_align_nodes"
65 bl_label
= "Align Nodes"
66 bl_options
= {'REGISTER', 'UNDO'}
67 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
69 def execute(self
, context
):
70 nodes
, links
= get_nodes_linked(context
)
75 if node
.select
and node
.type != 'FRAME':
76 selection
.append(node
)
78 # If no nodes are selected, align all nodes
82 elif nodes
.active
in selection
:
83 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
85 # Check if nodes should be laid out horizontally or vertically
86 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
87 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
88 x_range
= max(x_locs
) - min(x_locs
)
89 y_range
= max(y_locs
) - min(y_locs
)
90 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
91 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
92 horizontal
= x_range
> y_range
94 # Sort selection by location of node mid-point
96 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
98 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
102 for node
in selection
:
103 current_margin
= margin
104 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
107 node
.location
.x
= current_pos
108 current_pos
+= current_margin
+ node
.dimensions
.x
109 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
111 node
.location
.y
= current_pos
112 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
113 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
115 # If active node is selected, center nodes around it
116 if active_loc
is not None:
117 active_loc_diff
= active_loc
- nodes
.active
.location
118 for node
in selection
:
119 node
.location
+= active_loc_diff
120 else: # Position nodes centered around where they used to be
121 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
])
122 new_mid
= (max(locs
) + min(locs
)) / 2
123 for node
in selection
:
125 node
.location
.x
+= (mid_x
- new_mid
)
127 node
.location
.y
+= (mid_y
- new_mid
)
139 class NA_PT_NodePanel(Panel
):
140 bl_label
= "Node Arrange"
141 bl_space_type
= "NODE_EDITOR"
142 bl_region_type
= "UI"
143 bl_category
= "Arrange"
145 def draw(self
, context
):
146 if context
.active_node
is not None:
150 row
.operator('node.button')
153 row
.prop(bpy
.context
.scene
, 'nodemargin_x', text
="Margin x")
155 row
.prop(bpy
.context
.scene
, 'nodemargin_y', text
="Margin y")
157 row
.prop(context
.scene
, 'node_center', text
="Center nodes")
160 row
.operator('node.na_align_nodes', text
="Align to Selected")
163 node
= context
.space_data
.node_tree
.nodes
.active
164 if node
and node
.select
:
165 row
.prop(node
, 'location', text
= "Node X", index
= 0)
166 row
.prop(node
, 'location', text
= "Node Y", index
= 1)
168 row
.prop(node
, 'width', text
= "Node width")
171 row
.operator('node.button_odd')
173 class NA_OT_NodeButton(Operator
):
175 '''Arrange Connected Nodes/Arrange All Nodes'''
176 bl_idname
= 'node.button'
177 bl_label
= 'Arrange All Nodes'
179 def execute(self
, context
):
180 nodemargin(self
, context
)
181 bpy
.context
.space_data
.node_tree
.nodes
.update()
182 bpy
.ops
.node
.view_all()
186 # not sure this is doing what you expect.
187 # blender.org/api/blender_python_api_current/bpy.types.Operator.html#invoke
188 def invoke(self
, context
, value
):
189 values
.mat_name
= bpy
.context
.space_data
.node_tree
190 nodemargin(self
, context
)
194 class NA_OT_NodeButtonOdd(Operator
):
196 'Show the nodes for this material'
197 bl_idname
= 'node.button_odd'
198 bl_label
= 'Select Unlinked'
200 def execute(self
, context
):
201 values
.mat_name
= bpy
.context
.space_data
.node_tree
202 #mat = bpy.context.object.active_material
203 nodes_iterate(context
.space_data
.node_tree
, False)
207 class NA_OT_NodeButtonCenter(Operator
):
209 'Show the nodes for this material'
210 bl_idname
= 'node.button_center'
211 bl_label
= 'Center nodes (0,0)'
213 def execute(self
, context
):
214 values
.mat_name
= "" # reset
215 mat
= bpy
.context
.object.active_material
220 def nodemargin(self
, context
):
222 values
.margin_x
= context
.scene
.nodemargin_x
223 values
.margin_y
= context
.scene
.nodemargin_y
225 ntree
= context
.space_data
.node_tree
227 #first arrange nodegroups
229 for i
in ntree
.nodes
:
230 if i
.type == 'GROUP':
235 nodes_iterate(j
.node_tree
)
236 for i
in j
.node_tree
.nodes
:
237 if i
.type == 'GROUP':
242 # arrange nodes + this center nodes together
243 if context
.scene
.node_center
:
247 class NA_OT_ArrangeNodesOp(bpy
.types
.Operator
):
248 bl_idname
= 'node.arrange_nodetree'
249 bl_label
= 'Nodes Private Op'
251 mat_name
: bpy
.props
.StringProperty()
252 margin_x
: bpy
.props
.IntProperty(default
=120)
253 margin_y
: bpy
.props
.IntProperty(default
=120)
255 def nodemargin2(self
, context
):
257 mat_found
= bpy
.data
.materials
.get(self
.mat_name
)
258 if self
.mat_name
and mat_found
:
265 values
.mat_name
= self
.mat_name
267 scn
.nodemargin_x
= self
.margin_x
268 scn
.nodemargin_y
= self
.margin_y
273 def execute(self
, context
):
274 self
.nodemargin2(context
)
278 def outputnode_search(ntree
): # return node/None
281 for node
in ntree
.nodes
:
283 for input in node
.inputs
:
285 outputnodes
.append(node
)
289 print("No output node found")
294 ###############################################################
295 def nodes_iterate(ntree
, arrange
=True):
297 nodeoutput
= outputnode_search(ntree
)
298 if nodeoutput
is None:
299 #print ("nodeoutput is None")
312 for node
in a
[level
]:
313 inputlist
= [i
for i
in node
.inputs
if i
.is_linked
]
317 for input in inputlist
:
318 for nlinks
in input.links
:
319 node1
= nlinks
.from_node
320 a
[level
+ 1].append(node1
)
330 #remove duplicate nodes at the same level, first wins
331 for x
, nodes
in enumerate(a
):
332 a
[x
] = list(OrderedDict(zip(a
[x
], repeat(None))))
334 #remove duplicate nodes in all levels, last wins
336 for row1
in range(top
, 1, -1):
338 for row2
in range(row1
-1, 0, -1):
345 for x, i in enumerate(a):
352 #add node frames to nodelist
355 print ("level:", level)
357 for row in range(level, 0, -1):
359 for i, node in enumerate(a[row]):
361 print ("Frame found:", node.parent, node)
362 #if frame already added to the list ?
366 if frame not in frames:
368 #add frame to the same place than node was
369 a[row].insert(i, frame)
374 ########################################
379 nodelist
= [j
for i
in a
for j
in i
]
380 nodes_odd(ntree
, nodelist
=nodelist
)
383 ########################################
389 while level
< levelmax
:
392 nodes
= [x
for x
in a
[level
]]
393 #print ("level, nodes:", level, nodes)
394 nodes_arrange(nodes
, level
)
401 ###############################################################
402 def nodes_odd(ntree
, nodelist
):
408 a
= [x
for x
in nodes
if x
not in nodelist
]
409 # print ("odd nodes:",a)
414 def nodes_arrange(nodelist
, level
):
417 for node
in nodelist
:
418 parents
.append(node
.parent
)
420 bpy
.context
.space_data
.node_tree
.nodes
.update()
423 #print ("nodes arrange def")
426 widthmax
= max([x
.dimensions
.x
for x
in nodelist
])
427 xpos
= values
.x_last
- (widthmax
+ values
.margin_x
) if level
!= 0 else 0
428 #print ("nodelist, xpos", nodelist,xpos)
435 for node
in nodelist
:
438 hidey
= (node
.dimensions
.y
/ 2) - 8
444 y
= y
- values
.margin_y
- node
.dimensions
.y
+ hidey
446 node
.location
.x
= xpos
#if node.type != "FRAME" else xpos + 1200
448 y
= y
+ values
.margin_y
451 values
.average_y
= center
- values
.average_y
453 #for node in nodelist:
455 #node.location.y -= values.average_y
457 for i
, node
in enumerate(nodelist
):
458 node
.parent
= parents
[i
]
460 def nodetree_get(mat
):
462 return mat
.node_tree
.nodes
465 def nodes_center(ntree
):
472 for node
in ntree
.nodes
:
474 bboxminx
.append(node
.location
.x
)
475 bboxmaxx
.append(node
.location
.x
+ node
.dimensions
.x
)
476 bboxmaxy
.append(node
.location
.y
)
477 bboxminy
.append(node
.location
.y
- node
.dimensions
.y
)
479 # print ("bboxminy:",bboxminy)
480 bboxminx
= min(bboxminx
)
481 bboxmaxx
= max(bboxmaxx
)
482 bboxminy
= min(bboxminy
)
483 bboxmaxy
= max(bboxmaxy
)
484 center_x
= (bboxminx
+ bboxmaxx
) / 2
485 center_y
= (bboxminy
+ bboxmaxy
) / 2
487 print ("minx:",bboxminx)
488 print ("maxx:",bboxmaxx)
489 print ("miny:",bboxminy)
490 print ("maxy:",bboxmaxy)
492 print ("bboxes:", bboxminx, bboxmaxx, bboxmaxy, bboxminy)
493 print ("center x:",center_x)
494 print ("center y:",center_y)
500 for node
in ntree
.nodes
:
503 node
.location
.x
-= center_x
504 node
.location
.y
+= -center_y
510 NA_OT_NodeButtonCenter
,
511 NA_OT_ArrangeNodesOp
,
517 bpy
.utils
.register_class(c
)
519 bpy
.types
.Scene
.nodemargin_x
= bpy
.props
.IntProperty(default
=100, update
=nodemargin
)
520 bpy
.types
.Scene
.nodemargin_y
= bpy
.props
.IntProperty(default
=20, update
=nodemargin
)
521 bpy
.types
.Scene
.node_center
= bpy
.props
.BoolProperty(default
=True, update
=nodemargin
)
527 bpy
.utils
.unregister_class(c
)
529 del bpy
.types
.Scene
.nodemargin_x
530 del bpy
.types
.Scene
.nodemargin_y
531 del bpy
.types
.Scene
.node_center
533 if __name__
== "__main__":