Merge branch 'blender-v2.92-release'
[blender-addons.git] / node_arrange.py
blobbab8b333ecf94acb15b131595d6ff560805f3b5f
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 #####
19 bl_info = {
20 "name": "Node Arrange",
21 "author": "JuhaW",
22 "version": (0, 2, 2),
23 "blender": (2, 80, 4),
24 "location": "Node Editor > Properties > Trees",
25 "description": "Node Tree Arrangement Tools",
26 "warning": "",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_arrange.html",
28 "tracker_url": "https://github.com/JuhaW/NodeArrange/issues",
29 "category": "Node"
33 import sys
34 import bpy
35 from collections import OrderedDict
36 from itertools import repeat
37 import pprint
38 import pdb
39 from bpy.types import Operator, Panel
40 from bpy.props import (
41 IntProperty,
43 from copy import copy
46 #From Node Wrangler
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:
55 if tree.nodes.active:
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)
70 margin = self.margin
72 selection = []
73 for node in nodes:
74 if node.select and node.type != 'FRAME':
75 selection.append(node)
77 # If no nodes are selected, align all nodes
78 active_loc = None
79 if not selection:
80 selection = 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
94 if horizontal:
95 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
96 else:
97 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
99 # Alignment
100 current_pos = 0
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
105 if horizontal:
106 node.location.x = current_pos
107 current_pos += current_margin + node.dimensions.x
108 node.location.y = mid_y + (node.dimensions.y / 2)
109 else:
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:
123 if horizontal:
124 node.location.x += (mid_x - new_mid)
125 else:
126 node.location.y += (mid_y - new_mid)
128 return {'FINISHED'}
130 class values():
131 average_y = 0
132 x_last = 0
133 margin_x = 100
134 mat_name = ""
135 margin_y = 20
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:
146 layout = self.layout
147 row = layout.row()
148 col = layout.column
149 row.operator('node.button')
151 row = layout.row()
152 row.prop(bpy.context.scene, 'nodemargin_x', text="Margin x")
153 row = layout.row()
154 row.prop(bpy.context.scene, 'nodemargin_y', text="Margin y")
155 row = layout.row()
156 row.prop(context.scene, 'node_center', text="Center nodes")
158 row = layout.row()
159 row.operator('node.na_align_nodes', text="Align to Selected")
161 row = layout.row()
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)
166 row = layout.row()
167 row.prop(node, 'width', text = "Node width")
169 row = layout.row()
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()
183 return {'FINISHED'}
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)
190 return {'FINISHED'}
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)
203 return {'FINISHED'}
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
215 nodes_center(mat)
216 return {'FINISHED'}
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
227 n_groups = []
228 for i in ntree.nodes:
229 if i.type == 'GROUP':
230 n_groups.append(i)
232 while n_groups:
233 j = n_groups.pop(0)
234 nodes_iterate(j.node_tree)
235 for i in j.node_tree.nodes:
236 if i.type == 'GROUP':
237 n_groups.append(i)
239 nodes_iterate(ntree)
241 # arrange nodes + this center nodes together
242 if context.scene.node_center:
243 nodes_center(ntree)
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):
255 mat = None
256 mat_found = bpy.data.materials.get(self.mat_name)
257 if self.mat_name and mat_found:
258 mat = mat_found
259 #print(mat)
261 if not mat:
262 return
263 else:
264 values.mat_name = self.mat_name
265 scn = context.scene
266 scn.nodemargin_x = self.margin_x
267 scn.nodemargin_y = self.margin_y
268 nodes_iterate(mat)
269 if scn.node_center:
270 nodes_center(mat)
272 def execute(self, context):
273 self.nodemargin2(context)
274 return {'FINISHED'}
277 def outputnode_search(ntree): # return node/None
279 outputnodes = []
280 for node in ntree.nodes:
281 if not node.outputs:
282 for input in node.inputs:
283 if input.is_linked:
284 outputnodes.append(node)
285 break
287 if not outputnodes:
288 print("No output node found")
289 return None
290 return outputnodes
293 ###############################################################
294 def nodes_iterate(ntree, arrange=True):
296 nodeoutput = outputnode_search(ntree)
297 if nodeoutput is None:
298 #print ("nodeoutput is None")
299 return None
300 a = []
301 a.append([])
302 for i in nodeoutput:
303 a[0].append(i)
306 level = 0
308 while a[level]:
309 a.append([])
311 for node in a[level]:
312 inputlist = [i for i in node.inputs if i.is_linked]
314 if inputlist:
316 for input in inputlist:
317 for nlinks in input.links:
318 node1 = nlinks.from_node
319 a[level + 1].append(node1)
321 else:
322 pass
324 level += 1
326 del a[level]
327 level -= 1
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
334 top = level
335 for row1 in range(top, 1, -1):
336 for col1 in a[row1]:
337 for row2 in range(row1-1, 0, -1):
338 for col2 in a[row2]:
339 if col1 == col2:
340 a[row2].remove(col2)
341 break
344 for x, i in enumerate(a):
345 print (x)
346 for j in i:
347 print (j)
348 #print()
351 #add node frames to nodelist
352 frames = []
353 print ("Frames:")
354 print ("level:", level)
355 print ("a:",a)
356 for row in range(level, 0, -1):
358 for i, node in enumerate(a[row]):
359 if node.parent:
360 print ("Frame found:", node.parent, node)
361 #if frame already added to the list ?
362 frame = node.parent
363 #remove node
364 del a[row][i]
365 if frame not in frames:
366 frames.append(frame)
367 #add frame to the same place than node was
368 a[row].insert(i, frame)
370 pprint.pprint(a)
372 #return None
373 ########################################
377 if not arrange:
378 nodelist = [j for i in a for j in i]
379 nodes_odd(ntree, nodelist=nodelist)
380 return None
382 ########################################
384 levelmax = level + 1
385 level = 0
386 values.x_last = 0
388 while level < levelmax:
390 values.average_y = 0
391 nodes = [x for x in a[level]]
392 #print ("level, nodes:", level, nodes)
393 nodes_arrange(nodes, level)
395 level = level + 1
397 return None
400 ###############################################################
401 def nodes_odd(ntree, nodelist):
403 nodes = ntree.nodes
404 for i in nodes:
405 i.select = False
407 a = [x for x in nodes if x not in nodelist]
408 # print ("odd nodes:",a)
409 for i in a:
410 i.select = True
413 def nodes_arrange(nodelist, level):
415 parents = []
416 for node in nodelist:
417 parents.append(node.parent)
418 node.parent = None
419 bpy.context.space_data.node_tree.nodes.update()
422 #print ("nodes arrange def")
423 # node x positions
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)
428 values.x_last = xpos
430 # node y positions
431 x = 0
432 y = 0
434 for node in nodelist:
436 if node.hide:
437 hidey = (node.dimensions.y / 2) - 8
438 y = y - hidey
439 else:
440 hidey = 0
442 node.location.y = y
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
449 center = (0 + y) / 2
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):
466 bboxminx = []
467 bboxmaxx = []
468 bboxmaxy = []
469 bboxminy = []
471 for node in ntree.nodes:
472 if not node.parent:
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)
496 x = 0
497 y = 0
499 for node in ntree.nodes:
501 if not node.parent:
502 node.location.x -= center_x
503 node.location.y += -center_y
505 classes = [
506 NA_PT_NodePanel,
507 NA_OT_NodeButton,
508 NA_OT_NodeButtonOdd,
509 NA_OT_NodeButtonCenter,
510 NA_OT_ArrangeNodesOp,
511 NA_OT_AlignNodes
514 def register():
515 for c in classes:
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)
524 def unregister():
525 for c in classes:
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__":
533 register()