Module sverchok.core.node_group
Expand source code
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE
# from __future__ import annotations <- Don't use it here, `group node` will loose its `group tree` attribute
import time
from collections import namedtuple, defaultdict
from functools import reduce
from typing import Tuple, List, Set, Dict, Iterator, Optional
import bpy
from bpy.props import BoolProperty, EnumProperty
from sverchok.core.event_system import handle_event
from sverchok.data_structure import extend_blender_class
from mathutils import Vector
from sverchok.core.sockets import socket_type_names
import sverchok.core.events as ev
import sverchok.core.group_update_system as gus
from sverchok.core.update_system import ERROR_KEY
from sverchok.utils.tree_structure import Tree
from sverchok.utils.sv_node_utils import recursive_framed_location_finder
from sverchok.utils.handle_blender_data import BlTrees, BlSockets
from sverchok.node_tree import SvNodeTreeCommon, SverchCustomTreeNode
class SvGroupTree(SvNodeTreeCommon, bpy.types.NodeTree):
"""Separate tree class for sub trees"""
bl_idname = 'SvGroupTree'
bl_icon = 'NODETREE'
bl_label = 'Group tree'
# should be updated by "Go to edit group tree" operator
group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'})
# Always False, does not have sense to have for nested trees, sine of draft mode refactoring
sv_draft: bpy.props.BoolProperty(options={'SKIP_SAVE'})
sv_show_time_nodes: BoolProperty(default=False, options={'SKIP_SAVE'})
show_time_mode: EnumProperty(
items=[(n, n, '') for n in ["Per node", "Cumulative"]],
options={'SKIP_SAVE'},
)
@classmethod
def poll(cls, context):
return False # only for inner usage
sv_show: bpy.props.BoolProperty(name="Show", default=True, description='Show group tree')
description: bpy.props.StringProperty(
name="Tree description",
default="Hover over question mark to read tooltip\n"
"It's alpha version of group nodes use with caution\n"
"At this moment only 3 output nodes are supported (Group output, Stethoscope, Debug print)\n"
"Any node connected to them will be evaluated\n"
"Viewer nodes are not supported\n"
"Import into JSON is not supported\n"
"but it is possible to import group trees via standard Blender append functionality\n"
"Group trees are using its own update system\n"
"This system supports canceling processing next nodes by pressing escape during group tree editing")
@property
def sv_show_socket_menus(self):
"""It searches root tree and returns its eponymous attribute"""
for area in bpy.context.screen.areas:
# this is not Sverchok editor
if area.ui_type != BlTrees.MAIN_TREE_ID:
continue
# editor does not have any active tree
if not area.spaces[0].node_tree:
continue
# this editor edits another trees, What!?
if self not in (p.node_tree for p in area.spaces[0].path):
continue
return area.spaces[0].path[0].node_tree.sv_show_socket_menus
return False
def upstream_trees(self) -> List['SvGroupTree']:
"""
It will try to return all the tree sub trees (in case if there is group nodes)
and sub trees of sub trees and so on
The method can help to predict if linking new sub tree can lead to cyclic linking
"""
next_group_nodes = [node for node in self.nodes if node.bl_idname == 'SvGroupTreeNode']
trees = [self]
safe_counter = 0
while next_group_nodes:
next_node = next_group_nodes.pop()
if next_node.node_tree:
trees.append(next_node.node_tree)
next_group_nodes.extend([
node for node in next_node.node_tree.nodes if node.bl_idname == 'SvGroupTreeNode'])
safe_counter += 1
if safe_counter > 1000:
raise RecursionError(f'Looks like group tree "{self}" has links to itself from other groups')
return trees
def can_be_linked(self):
"""trying to avoid creating loops of group trees to each other"""
# upstream trees of tested treed should nad share trees with downstream trees of current tree
tested_tree_upstream_trees = {t.name for t in self.upstream_trees()}
current_tree_downstream_trees = {p.node_tree.name for p in bpy.context.space_data.path}
shared_trees = tested_tree_upstream_trees & current_tree_downstream_trees
return not shared_trees
def update(self):
"""trigger on links or nodes collections changes, on assigning tree to a group node
also it is triggered when a tree, next in the path, was changed (even if this tree was not effected)"""
# When group input or output nodes are connected some extra work should be done
if 'init_tree' in self.id_data: # tree is building by a script - let it do this
return
self.check_last_socket() # Should not be too expensive to call it each update
if self.name not in bpy.data.node_groups: # load new file event
return
if not hasattr(bpy.context.space_data, 'path'): # 3D panel also can call update method O_o
return
if not self.group_node_name: # initialization tree
return
group_node: SvGroupTreeNode = None
# update tree can lead to calling update of previous tree too, so should find position tree in the path
for i, path in zip(range(-1, -1000, -1), reversed(bpy.context.space_data.path)):
if path.node_tree == self:
group_node = bpy.context.space_data.path[i - 1].node_tree.nodes[self.group_node_name]
break
if group_node is None:
# the tree was assigned to a group node, it does not have sense to update
return
self.check_reroutes_sockets()
self.update_sockets() # probably more precise trigger could be found for calling this method
handle_event(ev.GroupTreeEvent(self, self.get_update_path()))
def update_sockets(self): # todo it lets simplify sockets API
"""Set properties of sockets of parent nodes and of output modes"""
for node in self.parent_nodes():
for n_in_s, t_in_s in zip(node.inputs, self.inputs):
# also before getting data from socket `socket.use_prop` property should be set
if hasattr(n_in_s, 'default_property'):
n_in_s.use_prop = not t_in_s.hide_value
if hasattr(t_in_s, 'default_type'):
n_in_s.default_property_type = t_in_s.default_type
for out_node in (n for n in self.nodes if n.bl_idname == 'NodeGroupOutput'):
for n_in_s, t_out_s in zip(out_node.inputs, self.outputs):
if hasattr(n_in_s, 'default_property'):
n_in_s.use_prop = not t_out_s.hide_value
if hasattr(t_out_s, 'default_type'):
n_in_s.default_property_type = t_out_s.default_type
else:
n_in_s.use_prop = False
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 check_last_socket(self):
"""Override socket creation of standard operator in Node interface menu"""
if self.inputs:
if self.inputs[-1].bl_socket_idname == 'NodeSocketFloat':
# This is wrong socket type -> fixing
self.inputs.remove(self.inputs[-1])
self.inputs.new('SvStringsSocket', 'Value')
if self.outputs:
if self.outputs[-1].bl_socket_idname == 'NodeSocketFloat':
self.outputs.remove(self.outputs[-1])
self.outputs.new('SvStringsSocket', 'Value')
def update_nodes(self, nodes: list):
"""
This method expect to get list of its nodes which should be updated
Execution won't be immediately, use cases -
1. Node property of was changed
2. ???
"""
# the method can be called during tree reconstruction from JSON file
# in this case we does not intend doing any updates
if not self.group_node_name: # initialization tree
return
handle_event(ev.GroupPropertyEvent(self, self.get_update_path(), nodes))
def parent_nodes(self) -> Iterator['SvGroupTreeNode']:
"""Returns all parent nodes"""
# todo optimisation?
for tree in (t for t in bpy.data.node_groups if t.bl_idname in {'SverchCustomTreeType', 'SvGroupTree'}):
for node in tree.nodes:
if hasattr(node, 'node_tree') and node.node_tree and node.node_tree.name == self.name:
yield node
def get_update_path(self) -> List['SvGroupTreeNode']:
"""
Should be called only when the tree is opened in one of tree editors
returns list of group nodes in path of current screen
"""
for area in bpy.context.screen.areas:
# this is not Sverchok editor
if area.ui_type != BlTrees.MAIN_TREE_ID:
continue
# editor does not have any active tree
if not area.spaces[0].node_tree:
continue
# this editor edits another tree, What!?
if self not in (p.node_tree for p in area.spaces[0].path):
continue
group_nodes = []
paths = area.spaces[0].path
for path, next_path in zip(paths[:-1], paths[1:]):
group_nodes.append(path.node_tree.nodes[next_path.node_tree.group_node_name])
if next_path.node_tree == self:
break # the tree is no last in the path
return group_nodes
raise LookupError(f'Path the group tree: {self} was not found')
if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash
@classmethod
def valid_socket_type(cls, socket_type: str):
# https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
return socket_type in socket_type_names()
class BaseNode:
n_id: bpy.props.StringProperty(options={'SKIP_SAVE'})
dependency_error = None
@property
def node_id(self):
"""Identifier of the node"""
if not self.n_id:
self.n_id = str(hash(self) ^ hash(time.monotonic()))
return self.n_id
def process_node(self, context):
"""update properties of socket of the node trigger this method"""
self.id_data.update_nodes([self])
def copy(self, original):
self.n_id = ''
sv_default_color = SverchCustomTreeNode.sv_default_color
set_temp_color = SverchCustomTreeNode.set_temp_color
@property
def absolute_location(self):
return recursive_framed_location_finder(self, self.location[:])
class SvGroupTreeNode(SverchCustomTreeNode, bpy.types.NodeCustomGroup):
"""Node for keeping sub trees"""
bl_idname = 'SvGroupTreeNode'
bl_label = 'Group node (Alpha)'
# todo add methods: switch_on_off
def nested_tree_filter(self, context):
"""Define which tree we would like to use as nested trees."""
tested_tree = context
if tested_tree.bl_idname == SvGroupTree.bl_idname: # It should be our dedicated to this class
return tested_tree.can_be_linked()
else:
return False
def update_group_tree(self, context):
"""Apply filtered tree to `node_tree` attribute.
By this attribute Blender is aware of linking between the node and nested tree."""
handle_event(ev.TreesGraphEvent())
self.node_tree: SvGroupTree = self.group_tree
# also default values should be fixed
if self.node_tree:
self.node_tree.use_fake_user = True
for node_sock, interface_sock in zip(self.inputs, self.node_tree.inputs):
if hasattr(interface_sock, 'default_value') and hasattr(node_sock, 'default_property'):
node_sock.default_property = interface_sock.default_value
self.node_tree.update_sockets() # properties of input socket properties should be updated
else: # in case if None is assigned to node_tree
self.inputs.clear()
self.outputs.clear()
group_tree: bpy.props.PointerProperty(type=SvGroupTree, poll=nested_tree_filter, update=update_group_tree)
def toggle_active(self, state: bool, to_update: bool = True):
"""This function can change state of `is_active` attribute without node updating"""
if 'toggle_active' in self:
# avoiding recursion
del self['toggle_active']
return
else:
self['toggle_active'] = True
self.is_active = state # it will call the method again and will delete 'toggle_active' key
if state and to_update:
self.id_data.update_nodes([self])
is_active: bpy.props.BoolProperty(name="Live", description='Update realtime if active', default=True,
update=lambda s, c: s.toggle_active(s.is_active))
def switch_viewers(self, context):
"""Turn on/off displaying objects in viewport generated by viewer nodes inside group tree"""
for node in self.node_tree.nodes:
try:
node.show_viewport(self.show)
except AttributeError:
pass
show: bpy.props.BoolProperty(default=True, description="On/off viewer nodes inside", update=switch_viewers)
def draw_buttons(self, context, layout):
if self.node_tree:
row_description = layout.row()
row = row_description.row(align=True)
row.prop(self, 'is_active', toggle=True)
row = row.row(align=True)
# row.prop(self, 'show', text="", icon=f'RESTRICT_VIEW_{"OFF" if self.show else "ON"}')
row.prop(self.node_tree, 'use_fake_user', text='')
add_description = row_description.operator('node.add_tree_description', text='', icon='QUESTION')
add_description.tree_name = self.node_tree.name
add_description.description = self.node_tree.description
col = layout.column()
# col.template_ID(self, 'group_tree')
row_name = col.row()
row_ops = col.row()
row_search = row_ops.row(align=True)
row_search.operator('node.search_group_tree', text='', icon='VIEWZOOM')
if self.group_tree:
row_name.prop(self.group_tree, 'name', text='')
row_search.operator('node.edit_group_tree', text='Edit', icon='FILE_PARENT')
row_ops.operator('node.ungroup_group_tree', text='', icon='MOD_PHYSICS')
else:
row_search.operator('node.add_group_tree', text='New', icon='ADD')
def process(self):
"""
This method is going to be called only by update system of main tree
Calling this method means that input group node should fetch data from group node
"""
# it's better process the node even if it is switched off in case when tree is just opened
should_update_output_data = False
if self.outputs:
try:
self.outputs[0].sv_get(deepcopy=False)
except LookupError:
should_update_output_data = True
if not self.node_tree or (not self.is_active and not should_update_output_data):
return
self.node_tree: SvGroupTree
# most simple way to pass data about whether node group should show timings
self.node_tree.sv_show_time_nodes = self.id_data.sv_show_time_nodes
self.node_tree.show_time_mode = self.id_data.show_time_mode
input_node = self.active_input()
output_node = self.active_output()
if not input_node or not output_node:
return
for in_s, out_s in zip(self.inputs, input_node.outputs):
if out_s.identifier == '__extend__': # virtual socket
break
out_s.sv_set(in_s.sv_get(deepcopy=False))
tree = gus.GroupUpdateTree.get(self.node_tree, refresh_tree=True)
tree.add_outdated([input_node])
tree.update(self)
for node in self.node_tree.nodes:
if err := node.get(ERROR_KEY):
raise Exception(err)
else:
for in_s, out_s in zip(output_node.inputs, self.outputs):
if in_s.identifier == '__extend__': # virtual socket
break
out_s.sv_set(in_s.sv_get(deepcopy=False))
def active_input(self) -> Optional[bpy.types.Node]:
# https://developer.blender.org/T82350
for node in reversed(self.node_tree.nodes):
if node.bl_idname == 'NodeGroupInput':
return node
def active_output(self) -> Optional[bpy.types.Node]:
for node in reversed(self.node_tree.nodes):
if node.bl_idname == 'NodeGroupOutput':
return node
def sv_update(self):
"""This method is also called when interface of the subtree is changed"""
def copy_socket_names(from_sockets, to_sockets):
for from_s, to_s in zip(from_sockets, to_sockets):
to_s.name = from_s.name
if bpy.app.version >= (3, 5): # sockets should be generated manually
BlSockets(self.inputs).copy_sockets(self.node_tree.inputs)
copy_socket_names(self.node_tree.inputs, self.inputs)
BlSockets(self.outputs).copy_sockets(self.node_tree.outputs)
copy_socket_names(self.node_tree.outputs, self.outputs)
# this code should work only first time a socket was added
if self.node_tree:
for n_in_s, t_in_s in zip(self.inputs, self.node_tree.inputs):
# also before getting data from socket `socket.use_prop` property should be set
if hasattr(n_in_s, 'default_property'):
n_in_s.use_prop = not t_in_s.hide_value
if hasattr(t_in_s, 'default_type'):
n_in_s.default_property_type = t_in_s.default_type
def sv_copy(self, original):
handle_event(ev.TreesGraphEvent())
def sv_free(self):
handle_event(ev.TreesGraphEvent())
class PlacingNodeOperator:
"""Helper class for locating nodes in a node tree"""
# quite basic operator can be moved to some more general module
@staticmethod
def placing_node(context, node_type: str):
tree = context.space_data.path[-1].node_tree
bpy.ops.node.select_all(action='DESELECT')
group_node = tree.nodes.new(node_type)
group_node.location = context.space_data.cursor_location
@staticmethod
def store_mouse_cursor(context, event):
# convert mouse position to the View2D for later node placement
space = context.space_data
space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
# Default invoke stores the mouse position to place the node correctly
# and invokes the transform operator
def invoke(self, context, event):
self.store_mouse_cursor(context, event)
result = self.execute(context)
if 'FINISHED' in result:
# removes the node again if transform is canceled
bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT')
return result
class AddGroupNode(PlacingNodeOperator, bpy.types.Operator):
"""Creating just group node without linking any sub tree to it"""
bl_idname = "node.add_group_node"
bl_label = "Add group node"
def execute(self, context):
self.placing_node(context, 'SvGroupTreeNode')
return {'FINISHED'}
@classmethod
def poll(cls, context):
return cls.can_be_added(context)[0]
@classmethod
def description(cls, context, properties):
return cls.can_be_added(context)[1]
@classmethod
def can_be_added(cls, context) -> Tuple[bool, str]:
if not hasattr(context.space_data, 'path'):
return False, ''
path = context.space_data.path[-1] if len(context.space_data.path) else None
if not path:
return False, ''
tree = path.node_tree
if tree.bl_idname == 'SverchCustomTreeType':
return True, 'Add group node'
elif tree.bl_idname == 'SvGroupTree':
return True, "Add group node"
else:
return False, f"Can't add in '{tree.bl_idname}' type"
class AddNodeOutputInput(PlacingNodeOperator, bpy.types.Operator):
"""Operator for creating output and input nodes in sub trees"""
bl_idname = "node.add_node_output_input"
bl_label = "Add output input nodes"
bl_options = {'INTERNAL'}
node_type: bpy.props.EnumProperty(items=[(i, i, '') for i in ['input', 'output']])
def execute(self, context):
if self.node_type == 'input':
node_type = 'NodeGroupInput'
else:
node_type = 'NodeGroupOutput'
self.placing_node(context, node_type)
return {'FINISHED'}
@classmethod
def poll(cls, context):
path = context.space_data.path
if len(path):
if path[-1].node_tree.bl_idname == SvGroupTree.bl_idname:
return True
return False
@classmethod
def description(cls, context, properties):
return f'Add group {properties.node_type} node'
class AddGroupTree(bpy.types.Operator):
"""Create empty sub tree for group node"""
bl_idname = "node.add_group_tree"
bl_label = "Add group tree"
def execute(self, context):
"""Link new sub tree to group node, create input and output nodes in sub tree and go to edit one"""
sub_tree = bpy.data.node_groups.new('Sverchok group', 'SvGroupTree') # creating sub tree
sub_tree.use_fake_user = True
context.node.group_tree = sub_tree # link sub tree to group node
sub_tree.nodes.new('NodeGroupInput').location = (-250, 0) # create node for putting data into sub tree
sub_tree.nodes.new('NodeGroupOutput').location = (250, 0) # create node for getting data from sub tree
return bpy.ops.node.edit_group_tree({'node': context.node})
class AddGroupTreeFromSelected(bpy.types.Operator):
"""Create instead of select nodes group node and placing them into sub tree"""
bl_idname = "node.add_group_tree_from_selected"
bl_label = "Add group tree from selected"
@classmethod
def poll(cls, context):
path = getattr(context.space_data, 'path', [])
if len(path):
if path[-1].node_tree.bl_idname in {'SverchCustomTreeType', SvGroupTree.bl_idname}:
return bool(cls.filter_selected_nodes(path[-1].node_tree))
return False
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.init_tree(), sub_tree.init_tree():
# 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
# generate new sockets
py_base_tree = Tree(base_tree)
[setattr(py_base_tree.nodes[n.name], 'select', n.select) for n in base_tree.nodes]
from_sockets, to_sockets = defaultdict(set), defaultdict(set)
for py_node in py_base_tree.nodes:
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 not out_s.node.select:
from_sockets[out_s.bl_tween].add(in_s.get_bl_socket(sub_tree))
for out_py_socket in py_node.outputs:
for in_py_socket in out_py_socket.linked_sockets:
if not in_py_socket.node.select:
to_sockets[in_py_socket.bl_tween].add(out_py_socket.get_bl_socket(sub_tree))
for fs in from_sockets.keys():
sub_tree.inputs.new(fs.bl_idname, fs.name)
for ts in to_sockets.keys():
sub_tree.outputs.new(ts.bl_idname, ts.name)
if bpy.app.version >= (3, 5): # generate also sockets of group nodes
for fs in sub_tree.inputs:
group_node.inputs.new(fs.bl_socket_idname, fs.name, identifier=fs.identifier)
for ts in sub_tree.outputs:
group_node.outputs.new(ts.bl_socket_idname, ts.name, identifier=ts.identifier)
# linking, linking should be ordered from first socket to last (in case like `join list` nodes)
for i, (from_s, first_ss) in enumerate(from_sockets.items()):
base_tree.links.new(group_node.inputs[i], from_s)
for first_s in first_ss:
sub_tree.links.new(first_s, input_node.outputs[i])
for i, (to_s, last_ss) in enumerate(to_sockets.items()):
base_tree.links.new(to_s, group_node.outputs[i])
for last_s in last_ss:
sub_tree.links.new(output_node.inputs[i], last_s)
# 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]
# todo one ui update (useless) will be done by the operator and another with update system of main handler
bpy.ops.node.edit_group_tree({'node': group_node}, is_new_group=True)
return {'FINISHED'}
@staticmethod
def filter_selected_nodes(tree) -> list:
"""Avoiding selecting nodes which should not be copied into sub tree"""
return [n for n in tree.nodes if n.select and n.bl_idname not in {'NodeGroupInput', 'NodeGroupOutput'}]
@staticmethod
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
@staticmethod
def recreate_frames(from_tree: bpy.types.NodeTree,
to_tree: bpy.types.NodeTree,
frame_names: Set[str],
from_to_node_names: Dict[str, str]):
"""
It will copy frames from one tree to another
from_to_node_names - mapping of node names between two trees
"""
new_frame_names = {n: to_tree.nodes.new('NodeFrame').name for n in frame_names}
frame_attributes = ['label', 'use_custom_color', 'color', 'label_size', 'text']
for frame_name in frame_names:
old_frame = from_tree.nodes[frame_name]
new_frame = to_tree.nodes[new_frame_names[frame_name]]
for attr in frame_attributes:
setattr(new_frame, attr, getattr(old_frame, attr))
for from_node in from_tree.nodes:
if from_node.name not in from_to_node_names:
continue
if from_node.parent and from_node.parent.name in new_frame_names:
if from_node.bl_idname == 'NodeFrame':
to_node = to_tree.nodes[new_frame_names[from_node.name]]
else:
to_node = to_tree.nodes[from_to_node_names[from_node.name]]
to_node.parent = to_tree.nodes[new_frame_names[from_node.parent.name]]
class UngroupGroupTree(bpy.types.Operator):
"""Put sub nodes into current layout and delete current group node"""
bl_idname = 'node.ungroup_group_tree'
bl_label = "Ungroup group tree"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
if context.active_node and hasattr(context.active_node, 'node_tree'):
return True
elif context.node:
return True
return False
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']
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'}
class EditGroupTree(bpy.types.Operator):
"""Go into sub tree to edit"""
bl_idname = 'node.edit_group_tree'
bl_label = 'Edit group tree'
is_new_group: BoolProperty(
description="True when group to edit was just created by Ctrl + G")
def execute(self, context):
group_node = context.node
sub_tree: SvGroupTree = context.node.node_tree
context.space_data.path.append(sub_tree, node=group_node)
sub_tree.group_node_name = group_node.name
if self.is_new_group:
event = ev.NewGroupTreeEvent(
sub_tree, sub_tree.get_update_path(), group_node.id_data)
else:
event = ev.GroupTreeEvent(sub_tree, sub_tree.get_update_path())
handle_event(event)
# todo make protection from editing the same trees in more then one area
# todo add the same logic to exit from tree operator
return {'FINISHED'}
class AddTreeDescription(bpy.types.Operator):
"""UI for filling Group tree description"""
bl_idname = 'node.add_tree_description'
bl_label = "Tree description"
tree_name: bpy.props.StringProperty(options={'HIDDEN'})
from_file: bpy.props.BoolProperty()
text_name: bpy.props.StringProperty(description="Text with description of the node")
description: bpy.props.StringProperty()
@classmethod
def description(cls, context, properties):
return properties.description
def execute(self, context):
tree = bpy.data.node_groups[self.tree_name]
if self.from_file:
tree.description = bpy.data.texts[self.text_name].as_string()
else:
tree.description = self.description
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
col = self.layout.column()
col.use_property_split = True
row = col.row()
row.active = not self.from_file
row.prop(self, 'description')
col.prop(self, 'from_file')
row = col.row()
row.active = self.from_file
row.prop_search(self, 'text_name', bpy.data, 'texts', text="Description")
class SearchGroupTree(bpy.types.Operator):
"""Browse group trees to be linked"""
bl_idname = 'node.search_group_tree'
bl_label = 'Search group tree'
bl_property = 'tree_name'
def available_trees(self, context):
linkable_trees = filter(lambda t: hasattr(t, 'can_be_linked') and t.can_be_linked(), bpy.data.node_groups)
return [(t.name, t.name, '') for t in linkable_trees]
tree_name: bpy.props.EnumProperty(items=available_trees)
group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'})
def execute(self, context):
tree = context.space_data.path[-1].node_tree
tree_to_link = bpy.data.node_groups[self.tree_name]
group_node = tree.nodes[self.group_node_name]
group_node.group_tree = tree_to_link
return {'FINISHED'}
def invoke(self, context, event):
self.group_node_name = context.node.name # execute context does not have the attribute -_-
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
classes = [SvGroupTree, SvGroupTreeNode, AddGroupNode, AddGroupTree, EditGroupTree, AddTreeDescription,
AddNodeOutputInput, AddGroupTreeFromSelected, SearchGroupTree, UngroupGroupTree]
@extend_blender_class
class NodeGroupOutput(BaseNode): # todo copy node id problem
def process(self):
return
@extend_blender_class
class NodeGroupInput(BaseNode):
def process(self):
return
@extend_blender_class
class NodeReroute(BaseNode):
"""Add sv logic"""
# `copy` attribute can't be overridden for this class
@extend_blender_class
class NodeFrame(BaseNode):
# for API consistency, it's much simpler way then create extra conditions everywhere
pass
register, unregister = bpy.utils.register_classes_factory(classes)
Classes
class AddGroupNode (...)
-
Creating just group node without linking any sub tree to it
Expand source code
class AddGroupNode(PlacingNodeOperator, bpy.types.Operator): """Creating just group node without linking any sub tree to it""" bl_idname = "node.add_group_node" bl_label = "Add group node" def execute(self, context): self.placing_node(context, 'SvGroupTreeNode') return {'FINISHED'} @classmethod def poll(cls, context): return cls.can_be_added(context)[0] @classmethod def description(cls, context, properties): return cls.can_be_added(context)[1] @classmethod def can_be_added(cls, context) -> Tuple[bool, str]: if not hasattr(context.space_data, 'path'): return False, '' path = context.space_data.path[-1] if len(context.space_data.path) else None if not path: return False, '' tree = path.node_tree if tree.bl_idname == 'SverchCustomTreeType': return True, 'Add group node' elif tree.bl_idname == 'SvGroupTree': return True, "Add group node" else: return False, f"Can't add in '{tree.bl_idname}' type"
Ancestors
- PlacingNodeOperator
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_rna
Static methods
def can_be_added(context) ‑> Tuple[bool, str]
-
Expand source code
@classmethod def can_be_added(cls, context) -> Tuple[bool, str]: if not hasattr(context.space_data, 'path'): return False, '' path = context.space_data.path[-1] if len(context.space_data.path) else None if not path: return False, '' tree = path.node_tree if tree.bl_idname == 'SverchCustomTreeType': return True, 'Add group node' elif tree.bl_idname == 'SvGroupTree': return True, "Add group node" else: return False, f"Can't add in '{tree.bl_idname}' type"
def description(context, properties)
-
Expand source code
@classmethod def description(cls, context, properties): return cls.can_be_added(context)[1]
def poll(context)
-
Expand source code
@classmethod def poll(cls, context): return cls.can_be_added(context)[0]
Methods
def execute(self, context)
-
Expand source code
def execute(self, context): self.placing_node(context, 'SvGroupTreeNode') return {'FINISHED'}
class AddGroupTree (...)
-
Create empty sub tree for group node
Expand source code
class AddGroupTree(bpy.types.Operator): """Create empty sub tree for group node""" bl_idname = "node.add_group_tree" bl_label = "Add group tree" def execute(self, context): """Link new sub tree to group node, create input and output nodes in sub tree and go to edit one""" sub_tree = bpy.data.node_groups.new('Sverchok group', 'SvGroupTree') # creating sub tree sub_tree.use_fake_user = True context.node.group_tree = sub_tree # link sub tree to group node sub_tree.nodes.new('NodeGroupInput').location = (-250, 0) # create node for putting data into sub tree sub_tree.nodes.new('NodeGroupOutput').location = (250, 0) # create node for getting data from sub tree return bpy.ops.node.edit_group_tree({'node': context.node})
Ancestors
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_rna
Methods
def execute(self, context)
-
Link new sub tree to group node, create input and output nodes in sub tree and go to edit one
Expand source code
def execute(self, context): """Link new sub tree to group node, create input and output nodes in sub tree and go to edit one""" sub_tree = bpy.data.node_groups.new('Sverchok group', 'SvGroupTree') # creating sub tree sub_tree.use_fake_user = True context.node.group_tree = sub_tree # link sub tree to group node sub_tree.nodes.new('NodeGroupInput').location = (-250, 0) # create node for putting data into sub tree sub_tree.nodes.new('NodeGroupOutput').location = (250, 0) # create node for getting data from sub tree return bpy.ops.node.edit_group_tree({'node': context.node})
class AddGroupTreeFromSelected (...)
-
Create instead of select nodes group node and placing them into sub tree
Expand source code
class AddGroupTreeFromSelected(bpy.types.Operator): """Create instead of select nodes group node and placing them into sub tree""" bl_idname = "node.add_group_tree_from_selected" bl_label = "Add group tree from selected" @classmethod def poll(cls, context): path = getattr(context.space_data, 'path', []) if len(path): if path[-1].node_tree.bl_idname in {'SverchCustomTreeType', SvGroupTree.bl_idname}: return bool(cls.filter_selected_nodes(path[-1].node_tree)) return False 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.init_tree(), sub_tree.init_tree(): # 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 # generate new sockets py_base_tree = Tree(base_tree) [setattr(py_base_tree.nodes[n.name], 'select', n.select) for n in base_tree.nodes] from_sockets, to_sockets = defaultdict(set), defaultdict(set) for py_node in py_base_tree.nodes: 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 not out_s.node.select: from_sockets[out_s.bl_tween].add(in_s.get_bl_socket(sub_tree)) for out_py_socket in py_node.outputs: for in_py_socket in out_py_socket.linked_sockets: if not in_py_socket.node.select: to_sockets[in_py_socket.bl_tween].add(out_py_socket.get_bl_socket(sub_tree)) for fs in from_sockets.keys(): sub_tree.inputs.new(fs.bl_idname, fs.name) for ts in to_sockets.keys(): sub_tree.outputs.new(ts.bl_idname, ts.name) if bpy.app.version >= (3, 5): # generate also sockets of group nodes for fs in sub_tree.inputs: group_node.inputs.new(fs.bl_socket_idname, fs.name, identifier=fs.identifier) for ts in sub_tree.outputs: group_node.outputs.new(ts.bl_socket_idname, ts.name, identifier=ts.identifier) # linking, linking should be ordered from first socket to last (in case like `join list` nodes) for i, (from_s, first_ss) in enumerate(from_sockets.items()): base_tree.links.new(group_node.inputs[i], from_s) for first_s in first_ss: sub_tree.links.new(first_s, input_node.outputs[i]) for i, (to_s, last_ss) in enumerate(to_sockets.items()): base_tree.links.new(to_s, group_node.outputs[i]) for last_s in last_ss: sub_tree.links.new(output_node.inputs[i], last_s) # 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] # todo one ui update (useless) will be done by the operator and another with update system of main handler bpy.ops.node.edit_group_tree({'node': group_node}, is_new_group=True) return {'FINISHED'} @staticmethod def filter_selected_nodes(tree) -> list: """Avoiding selecting nodes which should not be copied into sub tree""" return [n for n in tree.nodes if n.select and n.bl_idname not in {'NodeGroupInput', 'NodeGroupOutput'}] @staticmethod 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 @staticmethod def recreate_frames(from_tree: bpy.types.NodeTree, to_tree: bpy.types.NodeTree, frame_names: Set[str], from_to_node_names: Dict[str, str]): """ It will copy frames from one tree to another from_to_node_names - mapping of node names between two trees """ new_frame_names = {n: to_tree.nodes.new('NodeFrame').name for n in frame_names} frame_attributes = ['label', 'use_custom_color', 'color', 'label_size', 'text'] for frame_name in frame_names: old_frame = from_tree.nodes[frame_name] new_frame = to_tree.nodes[new_frame_names[frame_name]] for attr in frame_attributes: setattr(new_frame, attr, getattr(old_frame, attr)) for from_node in from_tree.nodes: if from_node.name not in from_to_node_names: continue if from_node.parent and from_node.parent.name in new_frame_names: if from_node.bl_idname == 'NodeFrame': to_node = to_tree.nodes[new_frame_names[from_node.name]] else: to_node = to_tree.nodes[from_to_node_names[from_node.name]] to_node.parent = to_tree.nodes[new_frame_names[from_node.parent.name]]
Ancestors
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_rna
Static methods
def can_be_grouped(tree) ‑> bool
-
True if selected nodes can be putted into group (does not produce cyclic links)
Expand source code
@staticmethod 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 filter_selected_nodes(tree) ‑> list
-
Avoiding selecting nodes which should not be copied into sub tree
Expand source code
@staticmethod def filter_selected_nodes(tree) -> list: """Avoiding selecting nodes which should not be copied into sub tree""" return [n for n in tree.nodes if n.select and n.bl_idname not in {'NodeGroupInput', 'NodeGroupOutput'}]
def poll(context)
-
Expand source code
@classmethod def poll(cls, context): path = getattr(context.space_data, 'path', []) if len(path): if path[-1].node_tree.bl_idname in {'SverchCustomTreeType', SvGroupTree.bl_idname}: return bool(cls.filter_selected_nodes(path[-1].node_tree)) return False
def recreate_frames(from_tree: bpy_types.NodeTree, to_tree: bpy_types.NodeTree, frame_names: Set[str], from_to_node_names: Dict[str, str])
-
It will copy frames from one tree to another from_to_node_names - mapping of node names between two trees
Expand source code
@staticmethod def recreate_frames(from_tree: bpy.types.NodeTree, to_tree: bpy.types.NodeTree, frame_names: Set[str], from_to_node_names: Dict[str, str]): """ It will copy frames from one tree to another from_to_node_names - mapping of node names between two trees """ new_frame_names = {n: to_tree.nodes.new('NodeFrame').name for n in frame_names} frame_attributes = ['label', 'use_custom_color', 'color', 'label_size', 'text'] for frame_name in frame_names: old_frame = from_tree.nodes[frame_name] new_frame = to_tree.nodes[new_frame_names[frame_name]] for attr in frame_attributes: setattr(new_frame, attr, getattr(old_frame, attr)) for from_node in from_tree.nodes: if from_node.name not in from_to_node_names: continue if from_node.parent and from_node.parent.name in new_frame_names: if from_node.bl_idname == 'NodeFrame': to_node = to_tree.nodes[new_frame_names[from_node.name]] else: to_node = to_tree.nodes[from_to_node_names[from_node.name]] to_node.parent = to_tree.nodes[new_frame_names[from_node.parent.name]]
Methods
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
Expand source code
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.init_tree(), sub_tree.init_tree(): # 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 # generate new sockets py_base_tree = Tree(base_tree) [setattr(py_base_tree.nodes[n.name], 'select', n.select) for n in base_tree.nodes] from_sockets, to_sockets = defaultdict(set), defaultdict(set) for py_node in py_base_tree.nodes: 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 not out_s.node.select: from_sockets[out_s.bl_tween].add(in_s.get_bl_socket(sub_tree)) for out_py_socket in py_node.outputs: for in_py_socket in out_py_socket.linked_sockets: if not in_py_socket.node.select: to_sockets[in_py_socket.bl_tween].add(out_py_socket.get_bl_socket(sub_tree)) for fs in from_sockets.keys(): sub_tree.inputs.new(fs.bl_idname, fs.name) for ts in to_sockets.keys(): sub_tree.outputs.new(ts.bl_idname, ts.name) if bpy.app.version >= (3, 5): # generate also sockets of group nodes for fs in sub_tree.inputs: group_node.inputs.new(fs.bl_socket_idname, fs.name, identifier=fs.identifier) for ts in sub_tree.outputs: group_node.outputs.new(ts.bl_socket_idname, ts.name, identifier=ts.identifier) # linking, linking should be ordered from first socket to last (in case like `join list` nodes) for i, (from_s, first_ss) in enumerate(from_sockets.items()): base_tree.links.new(group_node.inputs[i], from_s) for first_s in first_ss: sub_tree.links.new(first_s, input_node.outputs[i]) for i, (to_s, last_ss) in enumerate(to_sockets.items()): base_tree.links.new(to_s, group_node.outputs[i]) for last_s in last_ss: sub_tree.links.new(output_node.inputs[i], last_s) # 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] # todo one ui update (useless) will be done by the operator and another with update system of main handler bpy.ops.node.edit_group_tree({'node': group_node}, is_new_group=True) return {'FINISHED'}
class AddNodeOutputInput (...)
-
Operator for creating output and input nodes in sub trees
Expand source code
class AddNodeOutputInput(PlacingNodeOperator, bpy.types.Operator): """Operator for creating output and input nodes in sub trees""" bl_idname = "node.add_node_output_input" bl_label = "Add output input nodes" bl_options = {'INTERNAL'} node_type: bpy.props.EnumProperty(items=[(i, i, '') for i in ['input', 'output']]) def execute(self, context): if self.node_type == 'input': node_type = 'NodeGroupInput' else: node_type = 'NodeGroupOutput' self.placing_node(context, node_type) return {'FINISHED'} @classmethod def poll(cls, context): path = context.space_data.path if len(path): if path[-1].node_tree.bl_idname == SvGroupTree.bl_idname: return True return False @classmethod def description(cls, context, properties): return f'Add group {properties.node_type} node'
Ancestors
- PlacingNodeOperator
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_options
var bl_rna
var node_type : <_PropertyDeferred,
, {'items': [('input', 'input', ''), ('output', 'output', '')], 'attr': 'node_type'}>
Static methods
def description(context, properties)
-
Expand source code
@classmethod def description(cls, context, properties): return f'Add group {properties.node_type} node'
def poll(context)
-
Expand source code
@classmethod def poll(cls, context): path = context.space_data.path if len(path): if path[-1].node_tree.bl_idname == SvGroupTree.bl_idname: return True return False
Methods
def execute(self, context)
-
Expand source code
def execute(self, context): if self.node_type == 'input': node_type = 'NodeGroupInput' else: node_type = 'NodeGroupOutput' self.placing_node(context, node_type) return {'FINISHED'}
class AddTreeDescription (...)
-
UI for filling Group tree description
Expand source code
class AddTreeDescription(bpy.types.Operator): """UI for filling Group tree description""" bl_idname = 'node.add_tree_description' bl_label = "Tree description" tree_name: bpy.props.StringProperty(options={'HIDDEN'}) from_file: bpy.props.BoolProperty() text_name: bpy.props.StringProperty(description="Text with description of the node") description: bpy.props.StringProperty() @classmethod def description(cls, context, properties): return properties.description def execute(self, context): tree = bpy.data.node_groups[self.tree_name] if self.from_file: tree.description = bpy.data.texts[self.text_name].as_string() else: tree.description = self.description return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): col = self.layout.column() col.use_property_split = True row = col.row() row.active = not self.from_file row.prop(self, 'description') col.prop(self, 'from_file') row = col.row() row.active = self.from_file row.prop_search(self, 'text_name', bpy.data, 'texts', text="Description")
Ancestors
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_rna
var from_file : <_PropertyDeferred,
, {'attr': 'from_file'}> var text_name : <_PropertyDeferred,
, {'description': 'Text with description of the node', 'attr': 'text_name'}> var tree_name : <_PropertyDeferred,
, {'options': {'HIDDEN'}, 'attr': 'tree_name'}>
Static methods
def description(context, properties) ‑> <_PropertyDeferred,
, {'attr': 'description'}> -
Expand source code
@classmethod def description(cls, context, properties): return properties.description
Methods
def draw(self, context)
-
Expand source code
def draw(self, context): col = self.layout.column() col.use_property_split = True row = col.row() row.active = not self.from_file row.prop(self, 'description') col.prop(self, 'from_file') row = col.row() row.active = self.from_file row.prop_search(self, 'text_name', bpy.data, 'texts', text="Description")
def execute(self, context)
-
Expand source code
def execute(self, context): tree = bpy.data.node_groups[self.tree_name] if self.from_file: tree.description = bpy.data.texts[self.text_name].as_string() else: tree.description = self.description return {'FINISHED'}
def invoke(self, context, event)
-
Expand source code
def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)
class BaseNode
-
Expand source code
class BaseNode: n_id: bpy.props.StringProperty(options={'SKIP_SAVE'}) dependency_error = None @property def node_id(self): """Identifier of the node""" if not self.n_id: self.n_id = str(hash(self) ^ hash(time.monotonic())) return self.n_id def process_node(self, context): """update properties of socket of the node trigger this method""" self.id_data.update_nodes([self]) def copy(self, original): self.n_id = '' sv_default_color = SverchCustomTreeNode.sv_default_color set_temp_color = SverchCustomTreeNode.set_temp_color @property def absolute_location(self): return recursive_framed_location_finder(self, self.location[:])
Subclasses
Class variables
var dependency_error
var n_id : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'n_id'}>
Instance variables
var absolute_location
-
Expand source code
@property def absolute_location(self): return recursive_framed_location_finder(self, self.location[:])
var node_id
-
Identifier of the node
Expand source code
@property def node_id(self): """Identifier of the node""" if not self.n_id: self.n_id = str(hash(self) ^ hash(time.monotonic())) return self.n_id
var sv_default_color
-
Returns default color of the node which can be changed in add-on settings.
Expand source code
@property def sv_default_color(self): """Returns default color of the node which can be changed in add-on settings.""" return color_def.get_color(self.bl_idname)
Methods
def copy(self, original)
-
Expand source code
def copy(self, original): self.n_id = ''
def process_node(self, context)
-
update properties of socket of the node trigger this method
Expand source code
def process_node(self, context): """update properties of socket of the node trigger this method""" self.id_data.update_nodes([self])
def set_temp_color(self, color=None)
-
This method memorize its initial color and override it with given one if given color is None it tries to return its initial color or do nothing
Expand source code
def set_temp_color(self, color=None): """This method memorize its initial color and override it with given one if given color is None it tries to return its initial color or do nothing""" if color is None: # looks like the node should return its initial color (user choice) if 'user_color' in self: self.use_custom_color = self['use_user_color'] del self['use_user_color'] self.color = self['user_color'] del self['user_color'] # set temporary color else: # save overridden color (only once) if 'user_color' not in self: self['use_user_color'] = self.use_custom_color self['user_color'] = self.color self.use_custom_color = True self.color = color
class EditGroupTree (...)
-
Go into sub tree to edit
Expand source code
class EditGroupTree(bpy.types.Operator): """Go into sub tree to edit""" bl_idname = 'node.edit_group_tree' bl_label = 'Edit group tree' is_new_group: BoolProperty( description="True when group to edit was just created by Ctrl + G") def execute(self, context): group_node = context.node sub_tree: SvGroupTree = context.node.node_tree context.space_data.path.append(sub_tree, node=group_node) sub_tree.group_node_name = group_node.name if self.is_new_group: event = ev.NewGroupTreeEvent( sub_tree, sub_tree.get_update_path(), group_node.id_data) else: event = ev.GroupTreeEvent(sub_tree, sub_tree.get_update_path()) handle_event(event) # todo make protection from editing the same trees in more then one area # todo add the same logic to exit from tree operator return {'FINISHED'}
Ancestors
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_rna
var is_new_group : <_PropertyDeferred,
, {'description': 'True when group to edit was just created by Ctrl + G', 'attr': 'is_new_group'}>
Methods
def execute(self, context)
-
Expand source code
def execute(self, context): group_node = context.node sub_tree: SvGroupTree = context.node.node_tree context.space_data.path.append(sub_tree, node=group_node) sub_tree.group_node_name = group_node.name if self.is_new_group: event = ev.NewGroupTreeEvent( sub_tree, sub_tree.get_update_path(), group_node.id_data) else: event = ev.GroupTreeEvent(sub_tree, sub_tree.get_update_path()) handle_event(event) # todo make protection from editing the same trees in more then one area # todo add the same logic to exit from tree operator return {'FINISHED'}
class NodeFrame
-
Expand source code
@extend_blender_class class NodeFrame(BaseNode): # for API consistency, it's much simpler way then create extra conditions everywhere pass
Ancestors
Class variables
var n_id : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'n_id'}>
Inherited members
class NodeGroupInput
-
Expand source code
@extend_blender_class class NodeGroupInput(BaseNode): def process(self): return
Ancestors
Class variables
var n_id : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'n_id'}>
Methods
def process(self)
-
Expand source code
def process(self): return
Inherited members
class NodeGroupOutput
-
Expand source code
@extend_blender_class class NodeGroupOutput(BaseNode): # todo copy node id problem def process(self): return
Ancestors
Class variables
var n_id : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'n_id'}>
Methods
def process(self)
-
Expand source code
def process(self): return
Inherited members
class NodeReroute
-
Add sv logic
Expand source code
@extend_blender_class class NodeReroute(BaseNode): """Add sv logic""" # `copy` attribute can't be overridden for this class
Ancestors
Class variables
var n_id : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'n_id'}>
Inherited members
class PlacingNodeOperator
-
Helper class for locating nodes in a node tree
Expand source code
class PlacingNodeOperator: """Helper class for locating nodes in a node tree""" # quite basic operator can be moved to some more general module @staticmethod def placing_node(context, node_type: str): tree = context.space_data.path[-1].node_tree bpy.ops.node.select_all(action='DESELECT') group_node = tree.nodes.new(node_type) group_node.location = context.space_data.cursor_location @staticmethod def store_mouse_cursor(context, event): # convert mouse position to the View2D for later node placement space = context.space_data space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y) # Default invoke stores the mouse position to place the node correctly # and invokes the transform operator def invoke(self, context, event): self.store_mouse_cursor(context, event) result = self.execute(context) if 'FINISHED' in result: # removes the node again if transform is canceled bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT') return result
Subclasses
Static methods
def placing_node(context, node_type: str)
-
Expand source code
@staticmethod def placing_node(context, node_type: str): tree = context.space_data.path[-1].node_tree bpy.ops.node.select_all(action='DESELECT') group_node = tree.nodes.new(node_type) group_node.location = context.space_data.cursor_location
def store_mouse_cursor(context, event)
-
Expand source code
@staticmethod def store_mouse_cursor(context, event): # convert mouse position to the View2D for later node placement space = context.space_data space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
Methods
def invoke(self, context, event)
-
Expand source code
def invoke(self, context, event): self.store_mouse_cursor(context, event) result = self.execute(context) if 'FINISHED' in result: # removes the node again if transform is canceled bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT') return result
class SearchGroupTree (...)
-
Browse group trees to be linked
Expand source code
class SearchGroupTree(bpy.types.Operator): """Browse group trees to be linked""" bl_idname = 'node.search_group_tree' bl_label = 'Search group tree' bl_property = 'tree_name' def available_trees(self, context): linkable_trees = filter(lambda t: hasattr(t, 'can_be_linked') and t.can_be_linked(), bpy.data.node_groups) return [(t.name, t.name, '') for t in linkable_trees] tree_name: bpy.props.EnumProperty(items=available_trees) group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'}) def execute(self, context): tree = context.space_data.path[-1].node_tree tree_to_link = bpy.data.node_groups[self.tree_name] group_node = tree.nodes[self.group_node_name] group_node.group_tree = tree_to_link return {'FINISHED'} def invoke(self, context, event): self.group_node_name = context.node.name # execute context does not have the attribute -_- context.window_manager.invoke_search_popup(self) return {'FINISHED'}
Ancestors
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_property
var bl_rna
var group_node_name : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'group_node_name'}> var tree_name : <_PropertyDeferred,
, {'items': SearchGroupTree.available_trees() at 0x7f2f1d89a700>, 'attr': 'tree_name'}>
Methods
def available_trees(self, context)
-
Expand source code
def available_trees(self, context): linkable_trees = filter(lambda t: hasattr(t, 'can_be_linked') and t.can_be_linked(), bpy.data.node_groups) return [(t.name, t.name, '') for t in linkable_trees]
def execute(self, context)
-
Expand source code
def execute(self, context): tree = context.space_data.path[-1].node_tree tree_to_link = bpy.data.node_groups[self.tree_name] group_node = tree.nodes[self.group_node_name] group_node.group_tree = tree_to_link return {'FINISHED'}
def invoke(self, context, event)
-
Expand source code
def invoke(self, context, event): self.group_node_name = context.node.name # execute context does not have the attribute -_- context.window_manager.invoke_search_popup(self) return {'FINISHED'}
class SvGroupTree (...)
-
Separate tree class for sub trees
Expand source code
class SvGroupTree(SvNodeTreeCommon, bpy.types.NodeTree): """Separate tree class for sub trees""" bl_idname = 'SvGroupTree' bl_icon = 'NODETREE' bl_label = 'Group tree' # should be updated by "Go to edit group tree" operator group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'}) # Always False, does not have sense to have for nested trees, sine of draft mode refactoring sv_draft: bpy.props.BoolProperty(options={'SKIP_SAVE'}) sv_show_time_nodes: BoolProperty(default=False, options={'SKIP_SAVE'}) show_time_mode: EnumProperty( items=[(n, n, '') for n in ["Per node", "Cumulative"]], options={'SKIP_SAVE'}, ) @classmethod def poll(cls, context): return False # only for inner usage sv_show: bpy.props.BoolProperty(name="Show", default=True, description='Show group tree') description: bpy.props.StringProperty( name="Tree description", default="Hover over question mark to read tooltip\n" "It's alpha version of group nodes use with caution\n" "At this moment only 3 output nodes are supported (Group output, Stethoscope, Debug print)\n" "Any node connected to them will be evaluated\n" "Viewer nodes are not supported\n" "Import into JSON is not supported\n" "but it is possible to import group trees via standard Blender append functionality\n" "Group trees are using its own update system\n" "This system supports canceling processing next nodes by pressing escape during group tree editing") @property def sv_show_socket_menus(self): """It searches root tree and returns its eponymous attribute""" for area in bpy.context.screen.areas: # this is not Sverchok editor if area.ui_type != BlTrees.MAIN_TREE_ID: continue # editor does not have any active tree if not area.spaces[0].node_tree: continue # this editor edits another trees, What!? if self not in (p.node_tree for p in area.spaces[0].path): continue return area.spaces[0].path[0].node_tree.sv_show_socket_menus return False def upstream_trees(self) -> List['SvGroupTree']: """ It will try to return all the tree sub trees (in case if there is group nodes) and sub trees of sub trees and so on The method can help to predict if linking new sub tree can lead to cyclic linking """ next_group_nodes = [node for node in self.nodes if node.bl_idname == 'SvGroupTreeNode'] trees = [self] safe_counter = 0 while next_group_nodes: next_node = next_group_nodes.pop() if next_node.node_tree: trees.append(next_node.node_tree) next_group_nodes.extend([ node for node in next_node.node_tree.nodes if node.bl_idname == 'SvGroupTreeNode']) safe_counter += 1 if safe_counter > 1000: raise RecursionError(f'Looks like group tree "{self}" has links to itself from other groups') return trees def can_be_linked(self): """trying to avoid creating loops of group trees to each other""" # upstream trees of tested treed should nad share trees with downstream trees of current tree tested_tree_upstream_trees = {t.name for t in self.upstream_trees()} current_tree_downstream_trees = {p.node_tree.name for p in bpy.context.space_data.path} shared_trees = tested_tree_upstream_trees & current_tree_downstream_trees return not shared_trees def update(self): """trigger on links or nodes collections changes, on assigning tree to a group node also it is triggered when a tree, next in the path, was changed (even if this tree was not effected)""" # When group input or output nodes are connected some extra work should be done if 'init_tree' in self.id_data: # tree is building by a script - let it do this return self.check_last_socket() # Should not be too expensive to call it each update if self.name not in bpy.data.node_groups: # load new file event return if not hasattr(bpy.context.space_data, 'path'): # 3D panel also can call update method O_o return if not self.group_node_name: # initialization tree return group_node: SvGroupTreeNode = None # update tree can lead to calling update of previous tree too, so should find position tree in the path for i, path in zip(range(-1, -1000, -1), reversed(bpy.context.space_data.path)): if path.node_tree == self: group_node = bpy.context.space_data.path[i - 1].node_tree.nodes[self.group_node_name] break if group_node is None: # the tree was assigned to a group node, it does not have sense to update return self.check_reroutes_sockets() self.update_sockets() # probably more precise trigger could be found for calling this method handle_event(ev.GroupTreeEvent(self, self.get_update_path())) def update_sockets(self): # todo it lets simplify sockets API """Set properties of sockets of parent nodes and of output modes""" for node in self.parent_nodes(): for n_in_s, t_in_s in zip(node.inputs, self.inputs): # also before getting data from socket `socket.use_prop` property should be set if hasattr(n_in_s, 'default_property'): n_in_s.use_prop = not t_in_s.hide_value if hasattr(t_in_s, 'default_type'): n_in_s.default_property_type = t_in_s.default_type for out_node in (n for n in self.nodes if n.bl_idname == 'NodeGroupOutput'): for n_in_s, t_out_s in zip(out_node.inputs, self.outputs): if hasattr(n_in_s, 'default_property'): n_in_s.use_prop = not t_out_s.hide_value if hasattr(t_out_s, 'default_type'): n_in_s.default_property_type = t_out_s.default_type else: n_in_s.use_prop = False 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 check_last_socket(self): """Override socket creation of standard operator in Node interface menu""" if self.inputs: if self.inputs[-1].bl_socket_idname == 'NodeSocketFloat': # This is wrong socket type -> fixing self.inputs.remove(self.inputs[-1]) self.inputs.new('SvStringsSocket', 'Value') if self.outputs: if self.outputs[-1].bl_socket_idname == 'NodeSocketFloat': self.outputs.remove(self.outputs[-1]) self.outputs.new('SvStringsSocket', 'Value') def update_nodes(self, nodes: list): """ This method expect to get list of its nodes which should be updated Execution won't be immediately, use cases - 1. Node property of was changed 2. ??? """ # the method can be called during tree reconstruction from JSON file # in this case we does not intend doing any updates if not self.group_node_name: # initialization tree return handle_event(ev.GroupPropertyEvent(self, self.get_update_path(), nodes)) def parent_nodes(self) -> Iterator['SvGroupTreeNode']: """Returns all parent nodes""" # todo optimisation? for tree in (t for t in bpy.data.node_groups if t.bl_idname in {'SverchCustomTreeType', 'SvGroupTree'}): for node in tree.nodes: if hasattr(node, 'node_tree') and node.node_tree and node.node_tree.name == self.name: yield node def get_update_path(self) -> List['SvGroupTreeNode']: """ Should be called only when the tree is opened in one of tree editors returns list of group nodes in path of current screen """ for area in bpy.context.screen.areas: # this is not Sverchok editor if area.ui_type != BlTrees.MAIN_TREE_ID: continue # editor does not have any active tree if not area.spaces[0].node_tree: continue # this editor edits another tree, What!? if self not in (p.node_tree for p in area.spaces[0].path): continue group_nodes = [] paths = area.spaces[0].path for path, next_path in zip(paths[:-1], paths[1:]): group_nodes.append(path.node_tree.nodes[next_path.node_tree.group_node_name]) if next_path.node_tree == self: break # the tree is no last in the path return group_nodes raise LookupError(f'Path the group tree: {self} was not found') if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash @classmethod def valid_socket_type(cls, socket_type: str): # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type return socket_type in socket_type_names()
Ancestors
- SvNodeTreeCommon
- bpy_types.NodeTree
- bpy.types.ID
- builtins.bpy_struct
Class variables
var bl_icon
var bl_idname
var bl_label
var bl_rna
var description : <_PropertyDeferred,
, {'name': 'Tree description', 'default': "Hover over question mark to read tooltip\nIt's alpha version of group nodes use with caution\nAt this moment only 3 output nodes are supported (Group output, Stethoscope, Debug print)\nAny node connected to them will be evaluated\nViewer nodes are not supported\nImport into JSON is not supported\nbut it is possible to import group trees via standard Blender append functionality\nGroup trees are using its own update system\nThis system supports canceling processing next nodes by pressing escape during group tree editing", 'attr': 'description'}> var group_node_name : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'group_node_name'}> var show_time_mode : <_PropertyDeferred,
, {'items': [('Per node', 'Per node', ''), ('Cumulative', 'Cumulative', '')], 'options': {'SKIP_SAVE'}, 'attr': 'show_time_mode'}> var sv_draft : <_PropertyDeferred,
, {'options': {'SKIP_SAVE'}, 'attr': 'sv_draft'}> var sv_show : <_PropertyDeferred,
, {'name': 'Show', 'default': True, 'description': 'Show group tree', 'attr': 'sv_show'}> var sv_show_time_nodes : <_PropertyDeferred,
, {'default': False, 'options': {'SKIP_SAVE'}, 'attr': 'sv_show_time_nodes'}>
Static methods
def poll(context)
-
Expand source code
@classmethod def poll(cls, context): return False # only for inner usage
Instance variables
-
It searches root tree and returns its eponymous attribute
Expand source code
@property def sv_show_socket_menus(self): """It searches root tree and returns its eponymous attribute""" for area in bpy.context.screen.areas: # this is not Sverchok editor if area.ui_type != BlTrees.MAIN_TREE_ID: continue # editor does not have any active tree if not area.spaces[0].node_tree: continue # this editor edits another trees, What!? if self not in (p.node_tree for p in area.spaces[0].path): continue return area.spaces[0].path[0].node_tree.sv_show_socket_menus return False
Methods
def can_be_linked(self)
-
trying to avoid creating loops of group trees to each other
Expand source code
def can_be_linked(self): """trying to avoid creating loops of group trees to each other""" # upstream trees of tested treed should nad share trees with downstream trees of current tree tested_tree_upstream_trees = {t.name for t in self.upstream_trees()} current_tree_downstream_trees = {p.node_tree.name for p in bpy.context.space_data.path} shared_trees = tested_tree_upstream_trees & current_tree_downstream_trees return not shared_trees
def check_last_socket(self)
-
Override socket creation of standard operator in Node interface menu
Expand source code
def check_last_socket(self): """Override socket creation of standard operator in Node interface menu""" if self.inputs: if self.inputs[-1].bl_socket_idname == 'NodeSocketFloat': # This is wrong socket type -> fixing self.inputs.remove(self.inputs[-1]) self.inputs.new('SvStringsSocket', 'Value') if self.outputs: if self.outputs[-1].bl_socket_idname == 'NodeSocketFloat': self.outputs.remove(self.outputs[-1]) self.outputs.new('SvStringsSocket', 'Value')
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
Expand source code
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 get_update_path(self) ‑> List[SvGroupTreeNode]
-
Should be called only when the tree is opened in one of tree editors returns list of group nodes in path of current screen
Expand source code
def get_update_path(self) -> List['SvGroupTreeNode']: """ Should be called only when the tree is opened in one of tree editors returns list of group nodes in path of current screen """ for area in bpy.context.screen.areas: # this is not Sverchok editor if area.ui_type != BlTrees.MAIN_TREE_ID: continue # editor does not have any active tree if not area.spaces[0].node_tree: continue # this editor edits another tree, What!? if self not in (p.node_tree for p in area.spaces[0].path): continue group_nodes = [] paths = area.spaces[0].path for path, next_path in zip(paths[:-1], paths[1:]): group_nodes.append(path.node_tree.nodes[next_path.node_tree.group_node_name]) if next_path.node_tree == self: break # the tree is no last in the path return group_nodes raise LookupError(f'Path the group tree: {self} was not found')
def parent_nodes(self) ‑> Iterator[SvGroupTreeNode]
-
Returns all parent nodes
Expand source code
def parent_nodes(self) -> Iterator['SvGroupTreeNode']: """Returns all parent nodes""" # todo optimisation? for tree in (t for t in bpy.data.node_groups if t.bl_idname in {'SverchCustomTreeType', 'SvGroupTree'}): for node in tree.nodes: if hasattr(node, 'node_tree') and node.node_tree and node.node_tree.name == self.name: yield node
def update(self)
-
trigger on links or nodes collections changes, on assigning tree to a group node also it is triggered when a tree, next in the path, was changed (even if this tree was not effected)
Expand source code
def update(self): """trigger on links or nodes collections changes, on assigning tree to a group node also it is triggered when a tree, next in the path, was changed (even if this tree was not effected)""" # When group input or output nodes are connected some extra work should be done if 'init_tree' in self.id_data: # tree is building by a script - let it do this return self.check_last_socket() # Should not be too expensive to call it each update if self.name not in bpy.data.node_groups: # load new file event return if not hasattr(bpy.context.space_data, 'path'): # 3D panel also can call update method O_o return if not self.group_node_name: # initialization tree return group_node: SvGroupTreeNode = None # update tree can lead to calling update of previous tree too, so should find position tree in the path for i, path in zip(range(-1, -1000, -1), reversed(bpy.context.space_data.path)): if path.node_tree == self: group_node = bpy.context.space_data.path[i - 1].node_tree.nodes[self.group_node_name] break if group_node is None: # the tree was assigned to a group node, it does not have sense to update return self.check_reroutes_sockets() self.update_sockets() # probably more precise trigger could be found for calling this method handle_event(ev.GroupTreeEvent(self, self.get_update_path()))
def update_nodes(self, nodes: list)
-
This method expect to get list of its nodes which should be updated Execution won't be immediately, use cases - 1. Node property of was changed 2. ???
Expand source code
def update_nodes(self, nodes: list): """ This method expect to get list of its nodes which should be updated Execution won't be immediately, use cases - 1. Node property of was changed 2. ??? """ # the method can be called during tree reconstruction from JSON file # in this case we does not intend doing any updates if not self.group_node_name: # initialization tree return handle_event(ev.GroupPropertyEvent(self, self.get_update_path(), nodes))
def update_sockets(self)
-
Set properties of sockets of parent nodes and of output modes
Expand source code
def update_sockets(self): # todo it lets simplify sockets API """Set properties of sockets of parent nodes and of output modes""" for node in self.parent_nodes(): for n_in_s, t_in_s in zip(node.inputs, self.inputs): # also before getting data from socket `socket.use_prop` property should be set if hasattr(n_in_s, 'default_property'): n_in_s.use_prop = not t_in_s.hide_value if hasattr(t_in_s, 'default_type'): n_in_s.default_property_type = t_in_s.default_type for out_node in (n for n in self.nodes if n.bl_idname == 'NodeGroupOutput'): for n_in_s, t_out_s in zip(out_node.inputs, self.outputs): if hasattr(n_in_s, 'default_property'): n_in_s.use_prop = not t_out_s.hide_value if hasattr(t_out_s, 'default_type'): n_in_s.default_property_type = t_out_s.default_type else: n_in_s.use_prop = False
def upstream_trees(self) ‑> List[SvGroupTree]
-
It will try to return all the tree sub trees (in case if there is group nodes) and sub trees of sub trees and so on The method can help to predict if linking new sub tree can lead to cyclic linking
Expand source code
def upstream_trees(self) -> List['SvGroupTree']: """ It will try to return all the tree sub trees (in case if there is group nodes) and sub trees of sub trees and so on The method can help to predict if linking new sub tree can lead to cyclic linking """ next_group_nodes = [node for node in self.nodes if node.bl_idname == 'SvGroupTreeNode'] trees = [self] safe_counter = 0 while next_group_nodes: next_node = next_group_nodes.pop() if next_node.node_tree: trees.append(next_node.node_tree) next_group_nodes.extend([ node for node in next_node.node_tree.nodes if node.bl_idname == 'SvGroupTreeNode']) safe_counter += 1 if safe_counter > 1000: raise RecursionError(f'Looks like group tree "{self}" has links to itself from other groups') return trees
Inherited members
class SvGroupTreeNode (...)
-
Node for keeping sub trees
Expand source code
class SvGroupTreeNode(SverchCustomTreeNode, bpy.types.NodeCustomGroup): """Node for keeping sub trees""" bl_idname = 'SvGroupTreeNode' bl_label = 'Group node (Alpha)' # todo add methods: switch_on_off def nested_tree_filter(self, context): """Define which tree we would like to use as nested trees.""" tested_tree = context if tested_tree.bl_idname == SvGroupTree.bl_idname: # It should be our dedicated to this class return tested_tree.can_be_linked() else: return False def update_group_tree(self, context): """Apply filtered tree to `node_tree` attribute. By this attribute Blender is aware of linking between the node and nested tree.""" handle_event(ev.TreesGraphEvent()) self.node_tree: SvGroupTree = self.group_tree # also default values should be fixed if self.node_tree: self.node_tree.use_fake_user = True for node_sock, interface_sock in zip(self.inputs, self.node_tree.inputs): if hasattr(interface_sock, 'default_value') and hasattr(node_sock, 'default_property'): node_sock.default_property = interface_sock.default_value self.node_tree.update_sockets() # properties of input socket properties should be updated else: # in case if None is assigned to node_tree self.inputs.clear() self.outputs.clear() group_tree: bpy.props.PointerProperty(type=SvGroupTree, poll=nested_tree_filter, update=update_group_tree) def toggle_active(self, state: bool, to_update: bool = True): """This function can change state of `is_active` attribute without node updating""" if 'toggle_active' in self: # avoiding recursion del self['toggle_active'] return else: self['toggle_active'] = True self.is_active = state # it will call the method again and will delete 'toggle_active' key if state and to_update: self.id_data.update_nodes([self]) is_active: bpy.props.BoolProperty(name="Live", description='Update realtime if active', default=True, update=lambda s, c: s.toggle_active(s.is_active)) def switch_viewers(self, context): """Turn on/off displaying objects in viewport generated by viewer nodes inside group tree""" for node in self.node_tree.nodes: try: node.show_viewport(self.show) except AttributeError: pass show: bpy.props.BoolProperty(default=True, description="On/off viewer nodes inside", update=switch_viewers) def draw_buttons(self, context, layout): if self.node_tree: row_description = layout.row() row = row_description.row(align=True) row.prop(self, 'is_active', toggle=True) row = row.row(align=True) # row.prop(self, 'show', text="", icon=f'RESTRICT_VIEW_{"OFF" if self.show else "ON"}') row.prop(self.node_tree, 'use_fake_user', text='') add_description = row_description.operator('node.add_tree_description', text='', icon='QUESTION') add_description.tree_name = self.node_tree.name add_description.description = self.node_tree.description col = layout.column() # col.template_ID(self, 'group_tree') row_name = col.row() row_ops = col.row() row_search = row_ops.row(align=True) row_search.operator('node.search_group_tree', text='', icon='VIEWZOOM') if self.group_tree: row_name.prop(self.group_tree, 'name', text='') row_search.operator('node.edit_group_tree', text='Edit', icon='FILE_PARENT') row_ops.operator('node.ungroup_group_tree', text='', icon='MOD_PHYSICS') else: row_search.operator('node.add_group_tree', text='New', icon='ADD') def process(self): """ This method is going to be called only by update system of main tree Calling this method means that input group node should fetch data from group node """ # it's better process the node even if it is switched off in case when tree is just opened should_update_output_data = False if self.outputs: try: self.outputs[0].sv_get(deepcopy=False) except LookupError: should_update_output_data = True if not self.node_tree or (not self.is_active and not should_update_output_data): return self.node_tree: SvGroupTree # most simple way to pass data about whether node group should show timings self.node_tree.sv_show_time_nodes = self.id_data.sv_show_time_nodes self.node_tree.show_time_mode = self.id_data.show_time_mode input_node = self.active_input() output_node = self.active_output() if not input_node or not output_node: return for in_s, out_s in zip(self.inputs, input_node.outputs): if out_s.identifier == '__extend__': # virtual socket break out_s.sv_set(in_s.sv_get(deepcopy=False)) tree = gus.GroupUpdateTree.get(self.node_tree, refresh_tree=True) tree.add_outdated([input_node]) tree.update(self) for node in self.node_tree.nodes: if err := node.get(ERROR_KEY): raise Exception(err) else: for in_s, out_s in zip(output_node.inputs, self.outputs): if in_s.identifier == '__extend__': # virtual socket break out_s.sv_set(in_s.sv_get(deepcopy=False)) def active_input(self) -> Optional[bpy.types.Node]: # https://developer.blender.org/T82350 for node in reversed(self.node_tree.nodes): if node.bl_idname == 'NodeGroupInput': return node def active_output(self) -> Optional[bpy.types.Node]: for node in reversed(self.node_tree.nodes): if node.bl_idname == 'NodeGroupOutput': return node def sv_update(self): """This method is also called when interface of the subtree is changed""" def copy_socket_names(from_sockets, to_sockets): for from_s, to_s in zip(from_sockets, to_sockets): to_s.name = from_s.name if bpy.app.version >= (3, 5): # sockets should be generated manually BlSockets(self.inputs).copy_sockets(self.node_tree.inputs) copy_socket_names(self.node_tree.inputs, self.inputs) BlSockets(self.outputs).copy_sockets(self.node_tree.outputs) copy_socket_names(self.node_tree.outputs, self.outputs) # this code should work only first time a socket was added if self.node_tree: for n_in_s, t_in_s in zip(self.inputs, self.node_tree.inputs): # also before getting data from socket `socket.use_prop` property should be set if hasattr(n_in_s, 'default_property'): n_in_s.use_prop = not t_in_s.hide_value if hasattr(t_in_s, 'default_type'): n_in_s.default_property_type = t_in_s.default_type def sv_copy(self, original): handle_event(ev.TreesGraphEvent()) def sv_free(self): handle_event(ev.TreesGraphEvent())
Ancestors
- SverchCustomTreeNode
- UpdateNodes
- NodeUtils
- NodeDependencies
- NodeDocumentation
- bpy.types.NodeCustomGroup
- bpy_types.Node
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_rna
var group_tree : <_PropertyDeferred,
, {'type': SvGroupTree'>, 'poll': SvGroupTreeNode.nested_tree_filter() at 0x7f2f1d880310>, 'update': SvGroupTreeNode.update_group_tree() at 0x7f2f1d8803a0>, 'attr': 'group_tree'}> var is_active : <_PropertyDeferred,
, {'name': 'Live', 'description': 'Update realtime if active', 'default': True, 'update': SvGroupTreeNode. at 0x7f2f1d8804c0>, 'attr': 'is_active'}> var is_registered_node_type
var show : <_PropertyDeferred,
, {'default': True, 'description': 'On/off viewer nodes inside', 'update': SvGroupTreeNode.switch_viewers() at 0x7f2f1d880550>, 'attr': 'show'}>
Methods
def active_input(self) ‑> Optional[bpy_types.Node]
-
Expand source code
def active_input(self) -> Optional[bpy.types.Node]: # https://developer.blender.org/T82350 for node in reversed(self.node_tree.nodes): if node.bl_idname == 'NodeGroupInput': return node
def active_output(self) ‑> Optional[bpy_types.Node]
-
Expand source code
def active_output(self) -> Optional[bpy.types.Node]: for node in reversed(self.node_tree.nodes): if node.bl_idname == 'NodeGroupOutput': return node
def nested_tree_filter(self, context)
-
Define which tree we would like to use as nested trees.
Expand source code
def nested_tree_filter(self, context): """Define which tree we would like to use as nested trees.""" tested_tree = context if tested_tree.bl_idname == SvGroupTree.bl_idname: # It should be our dedicated to this class return tested_tree.can_be_linked() else: return False
def process(self)
-
This method is going to be called only by update system of main tree Calling this method means that input group node should fetch data from group node
Expand source code
def process(self): """ This method is going to be called only by update system of main tree Calling this method means that input group node should fetch data from group node """ # it's better process the node even if it is switched off in case when tree is just opened should_update_output_data = False if self.outputs: try: self.outputs[0].sv_get(deepcopy=False) except LookupError: should_update_output_data = True if not self.node_tree or (not self.is_active and not should_update_output_data): return self.node_tree: SvGroupTree # most simple way to pass data about whether node group should show timings self.node_tree.sv_show_time_nodes = self.id_data.sv_show_time_nodes self.node_tree.show_time_mode = self.id_data.show_time_mode input_node = self.active_input() output_node = self.active_output() if not input_node or not output_node: return for in_s, out_s in zip(self.inputs, input_node.outputs): if out_s.identifier == '__extend__': # virtual socket break out_s.sv_set(in_s.sv_get(deepcopy=False)) tree = gus.GroupUpdateTree.get(self.node_tree, refresh_tree=True) tree.add_outdated([input_node]) tree.update(self) for node in self.node_tree.nodes: if err := node.get(ERROR_KEY): raise Exception(err) else: for in_s, out_s in zip(output_node.inputs, self.outputs): if in_s.identifier == '__extend__': # virtual socket break out_s.sv_set(in_s.sv_get(deepcopy=False))
def sv_update(self)
-
This method is also called when interface of the subtree is changed
Expand source code
def sv_update(self): """This method is also called when interface of the subtree is changed""" def copy_socket_names(from_sockets, to_sockets): for from_s, to_s in zip(from_sockets, to_sockets): to_s.name = from_s.name if bpy.app.version >= (3, 5): # sockets should be generated manually BlSockets(self.inputs).copy_sockets(self.node_tree.inputs) copy_socket_names(self.node_tree.inputs, self.inputs) BlSockets(self.outputs).copy_sockets(self.node_tree.outputs) copy_socket_names(self.node_tree.outputs, self.outputs) # this code should work only first time a socket was added if self.node_tree: for n_in_s, t_in_s in zip(self.inputs, self.node_tree.inputs): # also before getting data from socket `socket.use_prop` property should be set if hasattr(n_in_s, 'default_property'): n_in_s.use_prop = not t_in_s.hide_value if hasattr(t_in_s, 'default_type'): n_in_s.default_property_type = t_in_s.default_type
def switch_viewers(self, context)
-
Turn on/off displaying objects in viewport generated by viewer nodes inside group tree
Expand source code
def switch_viewers(self, context): """Turn on/off displaying objects in viewport generated by viewer nodes inside group tree""" for node in self.node_tree.nodes: try: node.show_viewport(self.show) except AttributeError: pass
def toggle_active(self, state: bool, to_update: bool = True)
-
This function can change state of
is_active
attribute without node updatingExpand source code
def toggle_active(self, state: bool, to_update: bool = True): """This function can change state of `is_active` attribute without node updating""" if 'toggle_active' in self: # avoiding recursion del self['toggle_active'] return else: self['toggle_active'] = True self.is_active = state # it will call the method again and will delete 'toggle_active' key if state and to_update: self.id_data.update_nodes([self])
def update_group_tree(self, context)
-
Apply filtered tree to
node_tree
attribute. By this attribute Blender is aware of linking between the node and nested tree.Expand source code
def update_group_tree(self, context): """Apply filtered tree to `node_tree` attribute. By this attribute Blender is aware of linking between the node and nested tree.""" handle_event(ev.TreesGraphEvent()) self.node_tree: SvGroupTree = self.group_tree # also default values should be fixed if self.node_tree: self.node_tree.use_fake_user = True for node_sock, interface_sock in zip(self.inputs, self.node_tree.inputs): if hasattr(interface_sock, 'default_value') and hasattr(node_sock, 'default_property'): node_sock.default_property = interface_sock.default_value self.node_tree.update_sockets() # properties of input socket properties should be updated else: # in case if None is assigned to node_tree self.inputs.clear() self.outputs.clear()
Inherited members
SverchCustomTreeNode
:absolute_location
copy
dependency_error
draw_buttons
draw_buttons_ext
free
get_bpy_data_from_name
get_doc_link
init
insert_link
is_animation_dependent
is_scene_dependent
migrate_from
migrate_links_from
node_id
node_replacement_menu
poll
prefs_over_sized_buttons
process_node
rclick_menu
refresh_node
set_temp_color
sv_category
sv_copy
sv_default_color
sv_dependencies
sv_draw_buttons
sv_draw_buttons_ext
sv_free
sv_init
sv_internal_links
sv_new_input
update
update_interactive_mode
update_ui
wrapper_tracked_ui_draw_op
UpdateNodes
:
class UngroupGroupTree (...)
-
Put sub nodes into current layout and delete current group node
Expand source code
class UngroupGroupTree(bpy.types.Operator): """Put sub nodes into current layout and delete current group node""" bl_idname = 'node.ungroup_group_tree' bl_label = "Ungroup group tree" bl_options = {'INTERNAL'} @classmethod def poll(cls, context): if context.active_node and hasattr(context.active_node, 'node_tree'): return True elif context.node: return True return False 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'] 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'}
Ancestors
- bpy_types.Operator
- builtins.bpy_struct
Class variables
var bl_idname
var bl_label
var bl_options
var bl_rna
Static methods
def poll(context)
-
Expand source code
@classmethod def poll(cls, context): if context.active_node and hasattr(context.active_node, 'node_tree'): return True elif context.node: return True return False
Methods
def execute(self, context)
-
Similar to AddGroupTreeFromSelected operator but in backward direction (from sub tree to tree)
Expand source code
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'] 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'}