def check_reroutes_sockets(self): """ Fix reroute sockets type For now it does work properly in first update because all new sockets even if they have links have `is_linked` attribute with False value at next update events all works perfectly (skip first update?) There is hope this will be fixed https://developer.blender.org/T82390 """ tree = Tree(self) socket_job = [] Requirements = namedtuple('Requirements', [ 'left_n_i', 'left_s_i', 'left_t', 'reroute_n_i', 'right_n_is', 'right_s_is' ]) # analytical part, it's impossible to use Tree structure and modify the tree for node in tree.sorted_walk(tree.output_nodes): # walk should be sorted in case if reroute nodes are going one after other if node.bl_tween.bl_idname == 'NodeReroute': rer_in_s = node.inputs[0] rer_out_s = node.outputs[0] if rer_in_s.links: left_s = rer_in_s.linked_sockets[0] left_type = left_s.type if hasattr( left_s, 'type') else left_s.bl_tween.bl_idname if left_type != rer_in_s.bl_tween.bl_idname: rer_out_s.type = left_type socket_job.append( Requirements( left_s.node.index, left_s.index, left_type, node.index, [ s.node.index for s in rer_out_s.linked_sockets ], [s.index for s in rer_out_s.linked_sockets])) # regenerating sockets for props in socket_job: left_s = self.nodes[props.left_n_i].outputs[props.left_s_i] reroute = self.nodes[props.reroute_n_i] # handle input socket in_s = reroute.inputs.new(props.left_t, left_s.name) self.links.new(in_s, left_s) reroute.inputs.remove(reroute.inputs[0]) # handle output sockets out_s = reroute.outputs.new(props.left_t, left_s.name) for right_n_i, right_s_i in zip(props.right_n_is, props.right_s_is): left_s = self.nodes[right_n_i].inputs[right_s_i] self.links.new(left_s, out_s) reroute.outputs.remove(reroute.outputs[0])
def replace_nodes_id(cls, tree: Union[SvGroupTree, Tree], path: Path = ''): """ The idea is to replace nodes ID before evaluating the tree in this case sockets will get unique identifiers relative to base group node format of new nodes ID -> "group_node_id.node_id" ("group_node_id." is replaceable part unlike "node_id") but nodes which is not connected with input should not change their ID because the result of their process method will be constant between different group nodes group_node_id also can consist several paths -> "base_group_id.current_group_id" in case when the group is inside another group max length of path should be no more then number of base trees of most nested group node + 1 """ if hasattr(tree, 'bl_idname'): # it's Blender tree tree = Tree(tree) # todo should be cashed for optimization? input_linked_nodes = {n for n in tree.bfs_walk([tree.nodes.active_input] if tree.nodes.active_output else [])} for node in tree.nodes: node_id = cls.extract_node_id(node.bl_tween) if cut_mk_suffix(node.bl_tween.bl_idname) not in DEBUGGER_NODES and node in input_linked_nodes: node.bl_tween.n_id = path + '.' + node_id else: node.bl_tween.n_id = node_id
def get(cls, bl_tree: SvTree, path: Path): """Return caught tree with filled `is_updated` attribute according last statistic""" tree = cls._trees.get(bl_tree.tree_id) if tree is None: tree = Tree(bl_tree) cls._trees[bl_tree.tree_id] = tree for node in tree.nodes: node.is_updated = NodesStatuses.get(node.bl_tween, path).is_updated # good place to do this? return tree
def can_be_grouped(tree) -> bool: """True if selected nodes can be putted into group (does not produce cyclic links)""" # if there is one or more unselected nodes between nodes to be grouped # then current selection can't be grouped py_tree = Tree(tree) [ setattr(py_tree.nodes[n.name], 'select', n.select) for n in tree.nodes ] for node in py_tree.nodes: if not node.select: continue for neighbour_node in node.next_nodes: if neighbour_node.select: continue for next_node in py_tree.bfs_walk([neighbour_node]): if next_node.select: return False return True
def _update_tree(cls, bl_tree: SvTree): """ This method will generate new tree and update 'is_input_changed' node attribute according topological changes relatively previous call """ new_tree = Tree(bl_tree) # update is_input_changed attribute cls._update_topology_status(new_tree) return new_tree
def group_global_handler() -> Generator[Node]: """ It should find changes and update group nodes After that update system of main trees should update themselves meanwhile group nodes should be switched off because they already was updated """ for bl_tree in (t for t in bpy.data.node_groups if t.bl_idname == 'SvGroupTree'): # for now it always update all trees todo should be optimized later (keep in mind, trees can become outdated) ContextTrees.update_tree(bl_tree) for bl_tree in (t for t in bpy.data.node_groups if t.bl_idname == 'SverchCustomTreeType'): outdated_group_nodes = set() tree = Tree(bl_tree) for node in tree.sorted_walk(tree.output_nodes): if hasattr(node.bl_tween, 'updater'): group_updater = node.bl_tween.updater(is_input_changed=False) # just searching inner changes try: # it should return only nodes which should be updated while True: yield next(group_updater) except CancelError: group_updater.throw(CancelError) except StopIteration as stop_error: sub_tree_changed, error = stop_error.value if sub_tree_changed: outdated_group_nodes.add(node.bl_tween) # passing running to update system of main tree if outdated_group_nodes: outdated_group_nodes = list(outdated_group_nodes) active_states = [n.is_active for n in outdated_group_nodes] try: [n.toggle_active(False) for n in outdated_group_nodes] bl_tree.update_nodes(list(outdated_group_nodes)) except Exception: traceback.print_exc() finally: [n.toggle_active(s, to_update=False) for s, n in zip(active_states, outdated_group_nodes)]
def get(cls, bl_tree): """Return caught tree or new if the tree was not build yet""" tree = cls._trees.get(bl_tree.tree_id) # new tree, all nodes are outdated if tree is None: tree = Tree(bl_tree) cls._trees[bl_tree.tree_id] = tree # topology of the tree was changed and should be updated elif not tree.is_updated: tree = cls._update_tree(bl_tree) cls._trees[bl_tree.tree_id] = tree return tree
def _update_tree(cls, bl_tree): """ This method will generate new tree, copy is_updates status from previous tree and update 'is_input_changed' node attribute according topological changes relatively previous call Two reasons why always new tree is generated - it's simpler and new tree keeps fresh references to the nodes """ new_tree = Tree(bl_tree) # copy is_updated attribute if new_tree.id in cls._trees: old_tree = cls._trees[new_tree.id] for node in new_tree.nodes: if node.name in old_tree.nodes: node.is_updated = old_tree.nodes[node.name].is_updated # update is_input_changed attribute cls._update_topology_status(new_tree) return new_tree
def get(cls, bl_tree: SvTree, path: Path): """Return caught tree with filled `is_updated` attribute according last statistic""" tree = cls._trees.get(bl_tree.tree_id) # new tree, all nodes are outdated if tree is None: tree = Tree(bl_tree) cls._trees[bl_tree.tree_id] = tree # topology of the tree was changed and should be updated elif not tree.is_updated: tree = cls._update_tree(bl_tree) cls._trees[bl_tree.tree_id] = tree # we have to always update is_updated status because the tree does not keep them properly for node in tree.nodes: node.is_updated = NodesStatuses.get( node.bl_tween, path).is_updated # fill in actual is_updated state return tree
def execute(self, context): """Similar to AddGroupTreeFromSelected operator but in backward direction (from sub tree to tree)""" # go to sub tree, select all except input and output groups and mark nodes to be copied group_node = context.node sub_tree = group_node.node_tree bpy.ops.node.edit_group_tree({'node': group_node}) [setattr(n, 'select', False) for n in sub_tree.nodes] group_nodes_filter = filter( lambda n: n.bl_idname not in {'NodeGroupInput', 'NodeGroupOutput'}, sub_tree.nodes) for node in group_nodes_filter: node.select = True node[ 'sub_node_name'] = node.name # this will be copied within the nodes # the attribute should be empty in destination tree tree = context.space_data.path[-2].node_tree for node in tree.nodes: if 'sub_node_name' in node: del node['sub_node_name'] # Frames can't be just copied because they does not have absolute location, but they can be recreated frame_names = { n.name for n in sub_tree.nodes if n.select and n.bl_idname == 'NodeFrame' } [ setattr(n, 'select', False) for n in sub_tree.nodes if n.bl_idname == 'NodeFrame' ] with tree.throttle_update(): if any(n for n in sub_tree.nodes if n.select): # if no selection copy operator will raise error # copy and past nodes into group tree bpy.ops.node.clipboard_copy() context.space_data.path.pop() bpy.ops.node.clipboard_paste( ) # this will deselect all and select only pasted nodes # move nodes in group node center tree_select_nodes = [n for n in tree.nodes if n.select] center = reduce( lambda v1, v2: v1 + v2, [Vector(n.absolute_location) for n in tree_select_nodes]) / len(tree_select_nodes) [ setattr(n, 'location', n.location - (center - group_node.location)) for n in tree_select_nodes ] # recreate frames node_name_mapping = { n['sub_node_name']: n.name for n in tree.nodes if 'sub_node_name' in n } AddGroupTreeFromSelected.recreate_frames( sub_tree, tree, frame_names, node_name_mapping) else: context.space_data.path.pop( ) # should exit from sub tree anywhere # recreate py tree structure sub_py_tree = Tree(sub_tree) [ setattr(sub_py_tree.nodes[n.name], 'type', n.bl_idname) for n in sub_tree.nodes ] py_tree = Tree(tree) [ setattr(py_tree.nodes[n.name], 'select', n.select) for n in tree.nodes ] group_py_node = py_tree.nodes[group_node.name] for node in tree.nodes: if 'sub_node_name' in node: sub_py_tree.nodes[ node['sub_node_name']].twin = py_tree.nodes[node.name] py_tree.nodes[node.name].twin = sub_py_tree.nodes[ node['sub_node_name']] # create in links for group_input_py_node in [ n for n in sub_py_tree.nodes if n.type == 'NodeGroupInput' ]: for group_in_s, input_out_s in zip( group_py_node.inputs, group_input_py_node.outputs): if group_in_s.links and input_out_s.links: link_out_s = group_in_s.linked_sockets[0] for twin_in_s in input_out_s.linked_sockets: if twin_in_s.node.type == 'NodeGroupOutput': # node should be searched in above tree group_out_s = group_py_node.outputs[ twin_in_s.index] for link_in_s in group_out_s.linked_sockets: tree.links.new( link_in_s.get_bl_socket(tree), link_out_s.get_bl_socket(tree)) else: link_in_s = twin_in_s.node.twin.inputs[ twin_in_s.index] tree.links.new(link_in_s.get_bl_socket(tree), link_out_s.get_bl_socket(tree)) # create out links for group_output_py_node in [ n for n in sub_py_tree.nodes if n.type == 'NodeGroupOutput' ]: for group_out_s, output_in_s in zip( group_py_node.outputs, group_output_py_node.inputs): if group_out_s.links and output_in_s.links: twin_out_s = output_in_s.linked_sockets[0] if twin_out_s.node.type == 'NodeGroupInput': continue # we already added this link for link_in_s in group_out_s.linked_sockets: link_out_s = twin_out_s.node.twin.outputs[ twin_out_s.index] tree.links.new(link_in_s.get_bl_socket(tree), link_out_s.get_bl_socket(tree)) # delete group node tree.nodes.remove(group_node) for node in tree.nodes: if 'sub_node_name' in node: del node['sub_node_name'] tree.update() return {'FINISHED'}
def execute(self, context): """ Add group tree from selected: 01. Deselect group Input and Output nodes 02. Copy nodes into clipboard 03. Create group tree and move into one 04. Past nodes from clipboard 05. Move nodes into tree center 06. Add group "input" and "output" outside of bounding box of the nodes 07. Connect "input" and "output" sockets with group nodes 08. Add Group tree node in center of selected node in initial tree 09. Link the node with appropriate sockets 10. Cleaning """ base_tree = context.space_data.path[-1].node_tree if not self.can_be_grouped(base_tree): self.report({'WARNING'}, 'Current selection can not be converted to group') return {'CANCELLED'} sub_tree: SvGroupTree = bpy.data.node_groups.new( 'Sverchok group', SvGroupTree.bl_idname) # deselect group nodes if selected [ setattr(n, 'select', False) for n in base_tree.nodes if n.select and n.bl_idname in {'NodeGroupInput', 'NodeGroupOutput'} ] # Frames can't be just copied because they does not have absolute location, but they can be recreated frame_names = { n.name for n in base_tree.nodes if n.select and n.bl_idname == 'NodeFrame' } [ setattr(n, 'select', False) for n in base_tree.nodes if n.bl_idname == 'NodeFrame' ] with base_tree.throttle_update(): # copy and past nodes into group tree bpy.ops.node.clipboard_copy() context.space_data.path.append(sub_tree) bpy.ops.node.clipboard_paste() context.space_data.path.pop() # will enter later via operator # move nodes in tree center sub_tree_nodes = self.filter_selected_nodes(sub_tree) center = reduce(lambda v1, v2: v1 + v2, [n.location for n in sub_tree_nodes]) / len(sub_tree_nodes) [ setattr(n, 'location', n.location - center) for n in sub_tree_nodes ] # recreate frames node_name_mapping = { n.name: n.name for n in sub_tree.nodes } # all nodes have the same name as in base tree self.recreate_frames(base_tree, sub_tree, frame_names, node_name_mapping) # add group input and output nodes min_x = min(n.location[0] for n in sub_tree_nodes) max_x = max(n.location[0] for n in sub_tree_nodes) input_node = sub_tree.nodes.new('NodeGroupInput') input_node.location = (min_x - 250, 0) output_node = sub_tree.nodes.new('NodeGroupOutput') output_node.location = (max_x + 250, 0) # add group tree node initial_nodes = self.filter_selected_nodes(base_tree) center = reduce( lambda v1, v2: v1 + v2, [Vector(n.absolute_location) for n in initial_nodes]) / len(initial_nodes) group_node = base_tree.nodes.new(SvGroupTreeNode.bl_idname) group_node.select = False group_node.group_tree = sub_tree group_node.location = center sub_tree.group_node_name = group_node.name # linking, linking should be ordered from first socket to last (in case like `join list` nodes) py_base_tree = Tree(base_tree) [ setattr(py_base_tree.nodes[n.name], 'select', n.select) for n in base_tree.nodes ] input_node['connected_sockets'] = dict( ) # Dict[node.name + socket.identifier, socket index of input node] for py_node in py_base_tree.nodes: # is selected if not py_node.select: continue for in_s in py_node.inputs: for out_s in in_s.linked_sockets: # only one link always if out_s.node.select: continue out_s_key = out_s.node.name + out_s.identifier if out_s_key in input_node[ 'connected_sockets']: # protect from creating extra input sockets input_out_s_index = input_node[ 'connected_sockets'][out_s_key] sub_tree.links.new( in_s.get_bl_socket(sub_tree), input_node.outputs[input_out_s_index]) else: input_out_s_index = len(input_node.outputs) - 1 input_node['connected_sockets'][ out_s_key] = input_out_s_index sub_tree.links.new( in_s.get_bl_socket(sub_tree), input_node.outputs[input_out_s_index]) base_tree.links.new(group_node.inputs[-1], out_s.get_bl_socket(base_tree)) for out_py_socket in py_node.outputs: if any(not s.node.select for s in out_py_socket.linked_sockets): sub_tree.links.new( output_node.inputs[-1], out_py_socket.get_bl_socket(sub_tree)) for in_py_socket in out_py_socket.linked_sockets: if not in_py_socket.node.select: base_tree.links.new( in_py_socket.get_bl_socket(base_tree), group_node.outputs[-1]) # delete selected nodes and copied frames without children [ base_tree.nodes.remove(n) for n in self.filter_selected_nodes(base_tree) ] with_children_frames = { n.parent.name for n in base_tree.nodes if n.parent } [ base_tree.nodes.remove(n) for n in base_tree.nodes if n.name in frame_names and n.name not in with_children_frames ] base_tree.update_nodes([group_node]) bpy.ops.node.edit_group_tree({'node': group_node}) return {'FINISHED'}
def update_tree(cls, bl_tree: SvTree): """ This method will generate new tree and update 'link_changed' node attribute according topological changes relatively previous call """ cls._update_topology_status(Tree(bl_tree))