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",
28 "tracker_url": "https://github.com/JuhaW/NodeArrange/issues",
35 from collections
import OrderedDict
36 from itertools
import repeat
39 from bpy
.types
import Operator
, Panel
40 from bpy
.props
import (
47 def get_nodes_linked(context
):
48 tree
= context
.space_data
.node_tree
50 # Get nodes from currently edited tree.
51 # If user is editing a group, space_data.node_tree is still the base level (outside group).
52 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
53 # the same as context.active_node, the user is in a group.
54 # Check recursively until we find the real active node_tree:
56 while tree
.nodes
.active
!= context
.active_node
:
57 tree
= tree
.nodes
.active
.node_tree
59 return tree
.nodes
, tree
.links
61 class NA_OT_AlignNodes(Operator
):
62 '''Align the selected nodes/Tidy loose nodes'''
63 bl_idname
= "node.na_align_nodes"
64 bl_label
= "Align Nodes"
65 bl_options
= {'REGISTER', 'UNDO'}
66 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
68 def execute(self
, context
):
69 nodes
, links
= get_nodes_linked(context
)
74 if node
.select
and node
.type != 'FRAME':
75 selection
.append(node
)
77 # If no nodes are selected, align all nodes
81 elif nodes
.active
in selection
:
82 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
84 # Check if nodes should be laid out horizontally or vertically
85 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
86 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
87 x_range
= max(x_locs
) - min(x_locs
)
88 y_range
= max(y_locs
) - min(y_locs
)
89 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
90 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
91 horizontal
= x_range
> y_range
93 # Sort selection by location of node mid-point
95 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
97 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
101 for node
in selection
:
102 current_margin
= margin
103 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
106 node
.location
.x
= current_pos
107 current_pos
+= current_margin
+ node
.dimensions
.x
108 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
110 node
.location
.y
= current_pos
111 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
112 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
114 # If active node is selected, center nodes around it
115 if active_loc
is not None:
116 active_loc_diff
= active_loc
- nodes
.active
.location
117 for node
in selection
:
118 node
.location
+= active_loc_diff
119 else: # Position nodes centered around where they used to be
120 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
])
121 new_mid
= (max(locs
) + min(locs
)) / 2
122 for node
in selection
:
124 node
.location
.x
+= (mid_x
- new_mid
)
126 node
.location
.y
+= (mid_y
- new_mid
)
138 class NA_PT_NodePanel(Panel
):
139 bl_label
= "Node Arrange"
140 bl_space_type
= "NODE_EDITOR"
141 bl_region_type
= "UI"
142 bl_category
= "Arrange"
144 def draw(self
, context
):
145 if context
.active_node
is not None:
149 row
.operator('node.button')
152 row
.prop(bpy
.context
.scene
, 'nodemargin_x', text
="Margin x")
154 row
.prop(bpy
.context
.scene
, 'nodemargin_y', text
="Margin y")
156 row
.prop(context
.scene
, 'node_center', text
="Center nodes")
159 row
.operator('node.na_align_nodes', text
="Align to Selected")
162 node
= context
.space_data
.node_tree
.nodes
.active
163 if node
and node
.select
:
164 row
.prop(node
, 'location', text
= "Node X", index
= 0)
165 row
.prop(node
, 'location', text
= "Node Y", index
= 1)
167 row
.prop(node
, 'width', text
= "Node width")
170 row
.operator('node.button_odd')
172 class NA_OT_NodeButton(Operator
):
174 '''Arrange Connected Nodes/Arrange All Nodes'''
175 bl_idname
= 'node.button'
176 bl_label
= 'Arrange All Nodes'
178 def execute(self
, context
):
179 nodemargin(self
, context
)
180 bpy
.context
.space_data
.node_tree
.nodes
.update()
181 bpy
.ops
.node
.view_all()
185 # not sure this is doing what you expect.
186 # blender.org/api/blender_python_api_current/bpy.types.Operator.html#invoke
187 def invoke(self
, context
, value
):
188 values
.mat_name
= bpy
.context
.space_data
.node_tree
189 nodemargin(self
, context
)
193 class NA_OT_NodeButtonOdd(Operator
):
195 'Show the nodes for this material'
196 bl_idname
= 'node.button_odd'
197 bl_label
= 'Select Unlinked'
199 def execute(self
, context
):
200 values
.mat_name
= bpy
.context
.space_data
.node_tree
201 #mat = bpy.context.object.active_material
202 nodes_iterate(context
.space_data
.node_tree
, False)
206 class NA_OT_NodeButtonCenter(Operator
):
208 'Show the nodes for this material'
209 bl_idname
= 'node.button_center'
210 bl_label
= 'Center nodes (0,0)'
212 def execute(self
, context
):
213 values
.mat_name
= "" # reset
214 mat
= bpy
.context
.object.active_material
219 def nodemargin(self
, context
):
221 values
.margin_x
= context
.scene
.nodemargin_x
222 values
.margin_y
= context
.scene
.nodemargin_y
224 ntree
= context
.space_data
.node_tree
226 #first arrange nodegroups
228 for i
in ntree
.nodes
:
229 if i
.type == 'GROUP':
234 nodes_iterate(j
.node_tree
)
235 for i
in j
.node_tree
.nodes
:
236 if i
.type == 'GROUP':
241 # arrange nodes + this center nodes together
242 if context
.scene
.node_center
:
246 class NA_OT_ArrangeNodesOp(bpy
.types
.Operator
):
247 bl_idname
= 'node.arrange_nodetree'
248 bl_label
= 'Nodes Private Op'
250 mat_name
: bpy
.props
.StringProperty()
251 margin_x
: bpy
.props
.IntProperty(default
=120)
252 margin_y
: bpy
.props
.IntProperty(default
=120)
254 def nodemargin2(self
, context
):
256 mat_found
= bpy
.data
.materials
.get(self
.mat_name
)
257 if self
.mat_name
and mat_found
:
264 values
.mat_name
= self
.mat_name
266 scn
.nodemargin_x
= self
.margin_x
267 scn
.nodemargin_y
= self
.margin_y
272 def execute(self
, context
):
273 self
.nodemargin2(context
)
277 def outputnode_search(ntree
): # return node/None
280 for node
in ntree
.nodes
:
282 for input in node
.inputs
:
284 outputnodes
.append(node
)
288 print("No output node found")
293 ###############################################################
294 def nodes_iterate(ntree
, arrange
=True):
296 nodeoutput
= outputnode_search(ntree
)
297 if nodeoutput
is None:
298 #print ("nodeoutput is None")
311 for node
in a
[level
]:
312 inputlist
= [i
for i
in node
.inputs
if i
.is_linked
]
316 for input in inputlist
:
317 for nlinks
in input.links
:
318 node1
= nlinks
.from_node
319 a
[level
+ 1].append(node1
)
329 #remove duplicate nodes at the same level, first wins
330 for x
, nodes
in enumerate(a
):
331 a
[x
] = list(OrderedDict(zip(a
[x
], repeat(None))))
333 #remove duplicate nodes in all levels, last wins
335 for row1
in range(top
, 1, -1):
337 for row2
in range(row1
-1, 0, -1):
344 for x, i in enumerate(a):
351 #add node frames to nodelist
354 print ("level:", level)
356 for row in range(level, 0, -1):
358 for i, node in enumerate(a[row]):
360 print ("Frame found:", node.parent, node)
361 #if frame already added to the list ?
365 if frame not in frames:
367 #add frame to the same place than node was
368 a[row].insert(i, frame)
373 ########################################
378 nodelist
= [j
for i
in a
for j
in i
]
379 nodes_odd(ntree
, nodelist
=nodelist
)
382 ########################################
388 while level
< levelmax
:
391 nodes
= [x
for x
in a
[level
]]
392 #print ("level, nodes:", level, nodes)
393 nodes_arrange(nodes
, level
)
400 ###############################################################
401 def nodes_odd(ntree
, nodelist
):
407 a
= [x
for x
in nodes
if x
not in nodelist
]
408 # print ("odd nodes:",a)
413 def nodes_arrange(nodelist
, level
):
416 for node
in nodelist
:
417 parents
.append(node
.parent
)
419 bpy
.context
.space_data
.node_tree
.nodes
.update()
422 #print ("nodes arrange def")
425 widthmax
= max([x
.dimensions
.x
for x
in nodelist
])
426 xpos
= values
.x_last
- (widthmax
+ values
.margin_x
) if level
!= 0 else 0
427 #print ("nodelist, xpos", nodelist,xpos)
434 for node
in nodelist
:
437 hidey
= (node
.dimensions
.y
/ 2) - 8
443 y
= y
- values
.margin_y
- node
.dimensions
.y
+ hidey
445 node
.location
.x
= xpos
#if node.type != "FRAME" else xpos + 1200
447 y
= y
+ values
.margin_y
450 values
.average_y
= center
- values
.average_y
452 #for node in nodelist:
454 #node.location.y -= values.average_y
456 for i
, node
in enumerate(nodelist
):
457 node
.parent
= parents
[i
]
459 def nodetree_get(mat
):
461 return mat
.node_tree
.nodes
464 def nodes_center(ntree
):
471 for node
in ntree
.nodes
:
473 bboxminx
.append(node
.location
.x
)
474 bboxmaxx
.append(node
.location
.x
+ node
.dimensions
.x
)
475 bboxmaxy
.append(node
.location
.y
)
476 bboxminy
.append(node
.location
.y
- node
.dimensions
.y
)
478 # print ("bboxminy:",bboxminy)
479 bboxminx
= min(bboxminx
)
480 bboxmaxx
= max(bboxmaxx
)
481 bboxminy
= min(bboxminy
)
482 bboxmaxy
= max(bboxmaxy
)
483 center_x
= (bboxminx
+ bboxmaxx
) / 2
484 center_y
= (bboxminy
+ bboxmaxy
) / 2
486 print ("minx:",bboxminx)
487 print ("maxx:",bboxmaxx)
488 print ("miny:",bboxminy)
489 print ("maxy:",bboxmaxy)
491 print ("bboxes:", bboxminx, bboxmaxx, bboxmaxy, bboxminy)
492 print ("center x:",center_x)
493 print ("center y:",center_y)
499 for node
in ntree
.nodes
:
502 node
.location
.x
-= center_x
503 node
.location
.y
+= -center_y
509 NA_OT_NodeButtonCenter
,
510 NA_OT_ArrangeNodesOp
,
516 bpy
.utils
.register_class(c
)
518 bpy
.types
.Scene
.nodemargin_x
= bpy
.props
.IntProperty(default
=100, update
=nodemargin
)
519 bpy
.types
.Scene
.nodemargin_y
= bpy
.props
.IntProperty(default
=20, update
=nodemargin
)
520 bpy
.types
.Scene
.node_center
= bpy
.props
.BoolProperty(default
=True, update
=nodemargin
)
526 bpy
.utils
.unregister_class(c
)
528 del bpy
.types
.Scene
.nodemargin_x
529 del bpy
.types
.Scene
.nodemargin_y
530 del bpy
.types
.Scene
.node_center
532 if __name__
== "__main__":