diff --git a/.gitignore b/.gitignore index 650638a..ea90230 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +env_managment/envs pynodes.zip .tmp.blend test.blend diff --git a/.tmp.blend1 b/.tmp.blend1 index 7067b4d..b310328 100644 Binary files a/.tmp.blend1 and b/.tmp.blend1 differ diff --git a/install-pynodes.py b/install-pynodes.py index d992436..b9ab036 100644 --- a/install-pynodes.py +++ b/install-pynodes.py @@ -2,25 +2,9 @@ import os sys.path.append(os.path.split(__file__)[0]) -print('installing environment into blender') - -# import env_managment.envs.pytorch_blender as pytorch_blender -# pytorch_blender.register() -import env_managment.envs.tensorflow_blender as tensorflow_blender -tensorflow_blender.register() - -print('installed environment into blender') - -import tensorflow -import tensorflow_datasets - -print('imported environment packages') - import pynodes import pynodes.nodes -print('registering pynodes') - pynodes.register() -print('pynodes was registered') +print('pynodes was imported successfully') diff --git a/pynodes/__init__.py b/pynodes/__init__.py index 184c306..7fcd52d 100644 --- a/pynodes/__init__.py +++ b/pynodes/__init__.py @@ -12,7 +12,17 @@ import bpy from bpy.types import NodeTree, Node, NodeSocket -import numpy as np +from bpy.types import ( + Operator, + PropertyGroup, +) +from bpy.props import ( + BoolProperty, + CollectionProperty, + EnumProperty, + FloatVectorProperty, + StringProperty, +) # TODO ideas: # 1. autorename sockets based off of return type, numpy array could show dtype, shape @@ -24,34 +34,10 @@ class PythonCompositorTree(NodeTree): # Optional identifier string. If not explicitly defined, the python class name is used. bl_idname = 'PythonCompositorTreeType' # Label for nice name display - bl_label = "Python Compositor Tree" + bl_label = "PyNodes Compositor Tree" # Icon identifier bl_icon = 'NODETREE' -def get_node_execution_scope(): - ''' - Returns the scope to be used when evaluating python node inputs. - Quickly get a bunch of constants, can be extracted and modified to - have more constants or packages later. Note that many python - builtins are already in the scope by default, such as int or str. - ''' - import numpy as np - import bpy - import os - import sys - # tensorflow, ffmpeg, gmic qt, osl, PIL... - return { - 'pi':np.pi, - 'tau':np.pi*2, - 'e':np.e, - 'np':np, - 'bpy':bpy, - 'os':os, - 'sys':sys - } - -node_execution_scope = get_node_execution_scope() - # Custom socket type class AbstractPyObjectSocket(NodeSocket): # Description string @@ -60,7 +46,7 @@ class AbstractPyObjectSocket(NodeSocket): # for storing the inputs and outputs of nodes without overriding default_value # we can't change value of _value_, but we can set what it points to if its a list - _value_ = [{},]#: bpy.props.PointerProperty(type=bpy.types.Object, name='value', description='Pointer to object contained by the socket') + # _value_ = [{},]#: bpy.props.PointerProperty(type=bpy.types.Object, name='value', description='Pointer to object contained by the socket') # blender properties have to be wrapped in this so they are inherited in a way that blender properties can access them class Properties: @@ -83,15 +69,6 @@ def argvalue_updated(self): def node_updated(self): pass - def get_value(self): - if (self.is_linked or self.is_output) and self.identifier in self._value_[0]: - return self._value_[0][self.identifier] - else: - return self.argvalue # eval(self.argvalue, node_execution_scope) - - def set_value(self, value): - self._value_[0][self.identifier] = value - def hide_text_input(self): self.argvalue_hidden = True @@ -119,200 +96,6 @@ class PyObjectSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): # Label for nice name display bl_label = "Python Object Socket" -class AbstractPyObjectVarArgSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): - # Description string - '''PyNodes node socket type for variable arguments (varargs, *args)''' - - class Properties: - socket_index : bpy.props.IntProperty( - name = 'socket_index', - default = -1 - ) - - socket_index_valid : bpy.props.BoolProperty( - name = 'socket_index_valid', - default = True - ) - - def __init__(self): - super().__init__() - self.name = self.identifier - if not self.socket_index_valid: - self.node.inputs.move(self.node.inputs.find(self.identifier), self.socket_index) - self.socket_index_valid = True - - # var args nodes automatically remove or add more of themselves as they are used - def node_updated(self): - self.update() - - def argvalue_updated(self): - self.update() - - def socket_init(self): - pass - - def update(self): - - if self.is_output: - socket_collection = self.node.outputs - else: - socket_collection = self.node.inputs - - emptypins = 0 - # count the number of non-linked, empty sibling vararg pins - for i in socket_collection: - if i.bl_idname == self.bl_idname: - if i.is_empty(): - emptypins+=1 - last_empty_socket = i - last_socket = i - - # there is at least one other empty non-linked one (other than self) - if emptypins > 1: - # remove self if empty and not linked - self.node.unsubscribe_to_update(last_empty_socket.node_updated) - socket_collection.remove(last_empty_socket) - # create new pin if there is not enough - elif emptypins < 1: - new_socket = socket_collection.new( - self.bl_idname, - '', - identifier=self.identifier - ) - - if last_socket.socket_index==-1: - new_socket.socket_index = socket_collection.find(last_socket.identifier)+1 - else: - new_socket.socket_index = last_socket.socket_index+1 - new_socket.socket_index_valid = False - - new_socket.socket_init() - -class PyObjectVarArgSocket(AbstractPyObjectVarArgSocket.Properties, AbstractPyObjectVarArgSocket): - # Description string - '''PyNodes node socket type for variable arguments (varargs, *args)''' - # Optional identifier string. If not explicitly defined, the python class name is used. - bl_idname = 'PyObjectVarArgSocketType' - # Label for nice name display - bl_label = 'Expanding Socket' - - def __init__(self): - super().__init__() - # pin shape - self.display_shape = 'DIAMOND' - -# class PyObjectVarArgSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): -# # Description string -# '''Python node socket type for variable arguments (varargs, *args)''' -# # Optional identifier string. If not explicitly defined, the python class name is used. -# bl_idname = 'PyObjectVarArgSocketType' -# # Label for nice name display -# bl_label = 'Python *args Object Socket' -# -# def __init__(self): -# super().__init__() -# # pin shape -# self.display_shape = 'DIAMOND' -# self.name = self.identifier -# # socket must be indexible using name. -# # therefore force name to be unique like identifier -# self.node.subscribe_to_update(self.node_updated) -# # subscribe socket to node update events -# -# # var args nodes automatically remove or add more of themselves as they are used -# def node_updated(self): -# # print('node_updated', self.node, self) -# self.update() -# -# def argvalue_updated(self): -# # print('argvalue_updated', self.node, self) -# self.update() -# -# def update(self): -# emptyvarargpins = 0 -# # count the number of non-linked, empty sibling vararg pins -# for input in self.node.inputs: -# if input.bl_idname == PyObjectVarArgSocket.bl_idname: -# if input.is_empty(): -# emptyvarargpins+=1 -# -# # there is at least one other empty non-linked one (other than self) -# if emptyvarargpins > 1: -# # remove self if empty and not linked -# self.node.unsubscribe_to_update(self) -# self.node.inputs.remove(self) -# # create new pin if there is not enough -# elif emptyvarargpins < 1: -# self.node.inputs.new(PyObjectVarArgSocket.bl_idname, '*arg') - -# class PyObjectKwArgSocket(AbstractPyObjectVarArgSocket.Properties, AbstractPyObjectVarArgSocket): -# # Description string -# '''PyNodes socket type for variable arguments (varargs, *args)''' -# # Optional identifier string. If not explicitly defined, the python class name is used. -# bl_idname = 'PyObjectKwArgSocket' -# # Label for nice name display -# bl_label = 'Expanding Socket for Optional Attributes' -# -# attribute : bpy.props.StringProperty( -# name='Attribute', -# description="An attribute to set.", -# update=lambda s,c:s.node.update() -# ) -# -# attribute_collection : bpy.props.CollectionProperty( -# name='Attribute Selector', -# type=bpy.types.PropertyGroup -# ) -# -# def update_attribute_collection(self): -# self.attribute_collection.clear() -# for a in self.get_display_attributes(): -# self.attribute_collection.add().name = a -# -# def node_updated(self): -# super().node_updated() -# self.update_attribute_collection() -# -# def socket_init(self): -# super().socket_init() -# self.update_attribute_collection() -# -# def get_display_attributes(self): -# unused_attributes = self.node.unused_attributes() -# if not self.attribute is None: -# unused_attributes+=[self.attribute,] -# unused_attributes = sorted(unused_attributes) -# return unused_attributes -# -# def draw(self, context, layout, node, text): -# if text!='': -# layout.label(text=text) -# layout.prop_search(self, 'attribute', self, 'attribute_collection', text='') -# if not self.is_linked and not self.is_output: -# layout.prop(self, 'argvalue', text='') -# -# def __init__(self): -# super().__init__() -# # pin shape -# self.display_shape = 'SQUARE' - -class PyObjectKwArgSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): - # Description string - '''Python node socket type for keyword argumnets''' - # Optional identifier string. If not explicitly defined, the python class name is used. - bl_idname = 'PyObjectKWArgSocketType' - # Label for nice name display - bl_label = 'Python *kwargs Object Socket' - - def __init__(self): - super().__init__() - # pin shape - self.display_shape = 'SQUARE' - - # method to set the default value defined by the kwargs - def set_default(self, value): - self.argvalue = value - ### Node Categories ### # Node categories are a python system for automatically # extending the Add menu, toolbar panels and search operator. @@ -334,43 +117,57 @@ class PythonCompositorOperator(bpy.types.Operator): def poll(cls, context): return context.space_data.tree_type == PythonCompositorTree.bl_idname -# Have to add grouping behavior manually: - -def group_make(self, new_group_name): - self.node_tree = bpy.data.node_groups.new(new_group_name, PythonCompositorTree.bl_idname) - self.group_name = self.node_tree.name - - nodes = self.node_tree.nodes - inputnode = nodes.new('PyNodesGroupInputsNode') - outputnode = nodes.new('PyNodesGroupOutputsNode') - inputnode.location = (-300, 0) - outputnode.location = (300, 0) - return self.node_tree - +def get_override(area_type): + for window in bpy.context.window_manager.windows: + screen = window.screen + + for area in screen.areas: + if area.type == area_type: + for region in area.regions: + if region.type == 'WINDOW': + override = {'window': window, + 'screen': screen, + 'area': area, + 'region': region, + 'blend_data': bpy.context.blend_data} + + return override + +# Have to add node grouping behavior manually: class PyNodesGroupEdit(PythonCompositorOperator): bl_idname = "node.pynodes_group_edit" - bl_label = "edits an pynodes node group" + bl_label = "edits a pynodes node group" group_name : bpy.props.StringProperty(default='Node Group') - + + def group_make(self, node, new_group_name): + self.node_tree = bpy.data.node_groups.new(new_group_name, PythonCompositorTree.bl_idname) + self.group_name = self.node_tree.name + + nodes = self.node_tree.nodes + inputnode = nodes.new('PyNodesGroupInputsNode') + outputnode = nodes.new('PyNodesGroupOutputsNode') + inputnode.location = (-300, 0) + outputnode.location = (300, 0) + return self.node_tree + def execute(self, context): node = context.active_node + parent_tree_name = node.id_data.name ng = bpy.data.node_groups print(self.group_name) - group_node = ng.get(self.group_name) - if not group_node: - group_node = group_make(node, new_group_name=self.group_name) - + node_group = ng.get(self.group_name) + if not node_group: + node_group = self.group_make(node, new_group_name=self.group_name) + bpy.ops.node.pynodes_switch_layout(layout_name=self.group_name) -# print(context.space_data, context.space_data.node_tree) -# context.space_data.node_tree = ng[self.group_name] # does the same # by switching, space_data is now different - parent_tree_name = node.id_data.name + # parent_tree_name = node.id_data.name path = context.space_data.path - path.clear() + path.clear() #? path.append(ng[parent_tree_name]) # below the green opacity layer path.append(ng[self.group_name]) # top level @@ -428,17 +225,17 @@ def execute(self, context): return {'CANCELLED'} return {'FINISHED'} -class NODE_MT_add_test_node_tree(PythonCompositorOperator): - """Programmatically create node tree for testing, if it dosen't already exist.""" - bl_idname = "node.add_test_node_tree" - bl_label = "Add test node tree." +class NODE_MT_add_python_node(PythonCompositorOperator): + """Allow user to search for the exact function they wish to run in a node.""" + bl_idname = "node.add_python_node" + bl_label = "Add python node by search" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): if bpy.data.node_groups.get('Python Node Tree Test', False)==False: bpy.ops.node.new_node_tree( - type='PythonCompositorTreeType', + type=PythonCompositorTree.bl_idname, name='Python Node Tree Test' ) test_node_tree = bpy.data.node_groups.get('Python Node Tree Test', False) @@ -456,6 +253,168 @@ def add_test_node_tree(self, context): NODE_MT_add_test_node_tree.bl_idname, text="Add Test Node Tree") +# NodeAddOperator, NodeSetting from https://raw.githubusercontent.com/blender/blender/main/scripts/startup/bl_operators/node.py + +class NodeSetting(PropertyGroup): + value: StringProperty( + name="Value", + description="Python expression to be evaluated " + "as the initial node setting", + default="", + ) + +# Base class for node "Add" operators. +class NodeAddOperator: + + use_transform: BoolProperty( + name="Use Transform", + description="Start transform operator after inserting the node", + default=False, + ) + settings: CollectionProperty( + name="Settings", + description="Settings to be applied on the newly created node", + type=NodeSetting, + options={'SKIP_SAVE'}, + ) + + @staticmethod + def store_mouse_cursor(context, event): + space = context.space_data + tree = space.edit_tree + + # convert mouse position to the View2D for later node placement + if context.region.type == 'WINDOW': + # convert mouse position to the View2D for later node placement + space.cursor_location_from_region( + event.mouse_region_x, event.mouse_region_y) + else: + space.cursor_location = tree.view_center + + # Deselect all nodes in the tree. + @staticmethod + def deselect_nodes(context): + space = context.space_data + tree = space.edit_tree + for n in tree.nodes: + n.select = False + + def create_node(self, context, node_type): + space = context.space_data + tree = space.edit_tree + + try: + node = tree.nodes.new(type=node_type) + except RuntimeError as ex: + self.report({'ERROR'}, str(ex)) + return None + + for setting in self.settings: + # XXX catch exceptions here? + value = eval(setting.value) + node_data = node + node_attr_name = setting.name + + # Support path to nested data. + if '.' in node_attr_name: + node_data_path, node_attr_name = node_attr_name.rsplit(".", 1) + node_data = node.path_resolve(node_data_path) + + try: + setattr(node_data, node_attr_name, value) + except AttributeError as ex: + self.report( + {'ERROR_INVALID_INPUT'}, + tip_("Node has no attribute %s") % setting.name) + print(str(ex)) + # Continue despite invalid attribute + + node.select = True + tree.nodes.active = node + node.location = space.cursor_location + return node + + @classmethod + def poll(cls, context): + space = context.space_data + # needs active node editor and a tree to add nodes to + return (space and (space.type == 'NODE_EDITOR') and + space.edit_tree and not space.edit_tree.library) + + # Default invoke stores the mouse position to place the node correctly + # and optionally invokes the transform operator + def invoke(self, context, event): + self.store_mouse_cursor(context, event) + result = self.execute(context) + + if self.use_transform and ('FINISHED' in result): + # removes the node again if transform is canceled + bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT') + + return result + +# NODE_OT_add_node from https://raw.githubusercontent.com/jesterKing/blender/143ccc8c44cbd1a630c4f02d8c6eb26e26c63757/blender/release/scripts/startup/bl_operators/node.py + +class NODE_OT_add_search_pynodes(NodeAddOperator, Operator): + '''Add a node to the active tree''' + bl_idname = "node.add_search_pynodes" + bl_label = "Search and Add Node" + bl_options = {'REGISTER', 'UNDO'} + bl_property = "node_item" + + _enum_item_hack = [] + + # Create an enum list from node items + def node_enum_items(self, context): + enum_items = NODE_OT_add_search._enum_item_hack + enum_items.clear() + + + + return enum_items + + # Look up the item based on index + def find_node_item(self, context): + node_item = int(self.node_item) + for index, item in enumerate(nodeitems_utils.node_items_iter(context)): + if index == node_item: + return item + return None + + node_item = EnumProperty( + name="Node Type", + description="Node type", + items=node_enum_items, + ) + + def execute(self, context): + item = self.find_node_item(context) + + # no need to keep + self._enum_item_hack.clear() + + if item: + # apply settings from the node item + for setting in item.settings.items(): + ops = self.settings.add() + ops.name = setting[0] + ops.value = setting[1] + + n = self.create_node(context, 'AnyNode') + n.api_endpoint_string = item.label + + if self.use_transform: + bpy.ops.transform.translate('INVOKE_DEFAULT', remove_on_cancel=True) + + return {'FINISHED'} + else: + return {'CANCELLED'} + + def invoke(self, context, event): + self.store_mouse_cursor(context, event) + # Delayed execution in the search popup + context.window_manager.invoke_search_popup(self) + return {'CANCELLED'} from pynodes import registry from pynodes import helpers @@ -468,24 +427,20 @@ def register(): # register the essentials to building a PythonNode register_class(PythonCompositorTree) register_class(PyObjectSocket) - register_class(PyObjectVarArgSocket) - register_class(PyObjectKwArgSocket) register_class(PyNodesGroupEdit) register_class(PyNodesTreePathParent) register_class(PyNodesSwitchToLayout) bpy.types.NODE_MT_node.append(pynodes_group_edit) - register_class(NODE_MT_add_test_node_tree) - bpy.types.NODE_MT_node.append(add_test_node_tree) + # register_class(NODE_MT_add_test_node_tree) + # bpy.types.NODE_MT_node.append(add_test_node_tree) def unregister(): registry.unregisterAll() unregister_class(PythonCompositorTree) unregister_class(PyObjectSocket) - unregister_class(PyObjectVarArgSocket) - unregister_class(PyObjectKwArgSocket) unregister_class(PyNodesGroupEdit) unregister_class(PyNodesTreePathParent) diff --git a/pynodes/nodes/AnyBaseNode.py b/pynodes/nodes/AnyBaseNode.py new file mode 100644 index 0000000..86a5113 --- /dev/null +++ b/pynodes/nodes/AnyBaseNode.py @@ -0,0 +1,64 @@ +import pynodes +from pynodes import nodes +import numpy as np +import bpy +import networkx as nx + +from pynodes.nodes.AnyNode import BlenderPythonAPIEndpoint + +def blender_python_kernel(code): + eval(code) + +class AnyBaseNode(nodes.PythonBaseNode.Properties, nodes.PythonBaseNode): + # === Basics === + # Description string + '''Execute connected AnyNodes''' + # Optional identifier string. If not explicitly defined, the python class name is used. + bl_idname = 'AnyBaseNode' + # Label for nice name display + bl_label = "Execute connected AnyNodes" + + _cache_ = [{}] # immutable lookup of node names to variables names in some kernel + + def init(self, context): + super().init(context) + self.inputs.new(pynodes.PyObjectSocket.bl_idname, "Result") + + def run(self): + G = nx.DiGraph() + + # BFS to find all connected nodes, add connectsion to dependency graph G + leaves = [] + branches = [self] + while len(branches)>0: + for branch in branches: + for input_socket in branch.inputs: + if len(input_socket.links)==0: + continue + node = input_socket.links[0].from_socket.node + G.add_edge(node.name, branch.name) + leaves.append(node) + branches = leaves + leaves = [] + print(len(branches), len(leaves)) + + # use the dependency graph to determine execution order of the nodes + print(G.edges) + # if nx.is_directed_acyclic_graph(G)>0: + # raise Exception('Cycles detected in node graph!') + + # should be a Directed Acyclic Graph + print(list(nx.topological_sort(G))) + + while len(G)>0: + + # ignoring all caching, and just recomputing whole graph upon every run + + leaves = [x for x in G.nodes() if G.out_degree(x)==0 and G.in_degree(x)==1] + + for leave in leaves: + api = BlenderPythonAPIEndpoint(leave.api_endpoint_string) + # get parameters, and composite an execution of the function with corresponding filled in variables from kernel + +from pynodes import registry +registry.registerNodeType(AnyBaseNode) diff --git a/pynodes/nodes/AnyNode.py b/pynodes/nodes/AnyNode.py new file mode 100644 index 0000000..6585932 --- /dev/null +++ b/pynodes/nodes/AnyNode.py @@ -0,0 +1,141 @@ +import pynodes +from pynodes import nodes +import time +import bpy +import inspect +import traceback + +class APIEndpointInterface: + + def get_signature(): + pass + + def get_documentation(): + pass + +class BlenderPythonAPIEndpoint(APIEndpointInterface): + + def __init__(self, api_endpoint_string): + self.api_endpoint_string = api_endpoint_string + self.api = eval(self.api_endpoint_string) + + def get_signature(self): + try: + return inspect.signature(self.api) + except: + return inspect.Signature( + [ + inspect.Parameter('unknown_posarg', inspect.Parameter.VAR_POSITIONAL, default=inspect.Parameter.empty, annotation=inspect.Parameter.empty), + inspect.Parameter('unknown_keyarg', inspect.Parameter.VAR_KEYWORD, default=inspect.Parameter.empty, annotation=inspect.Parameter.empty) + ], + return_annotation=inspect.Signature.empty + ) + + def get_documentation(self): + return inspect.getdoc(self.api) + +class AnyNode(nodes.PythonNode): + # === Basics === + # Description string + '''A node which takes on the form of the specified API.''' + # Optional identifier string. If not explicitly defined, the python class name is used. + bl_idname = 'AnyNode' + # Label for nice name display + bl_label = "Any Node" + + api_endpoint_string : bpy.props.StringProperty( + name='', + description="Callable API endpoint.", + update=lambda s,c:s.update_api(c) + ) + + api_documentation : bpy.props.StringProperty( + description='Documentation for the API endpoint.' + ) + + # Additional buttons displayed on the node. + def draw_buttons(self, context, layout): + layout.prop(self, "api_endpoint_string") + + # Detail buttons in the sidebar. + # If this function is not defined, the draw_buttons function is used instead + def draw_buttons_ext(self, context, layout): + # maybe add a label to say its the api string + + layout.prop(self, "api_endpoint_string") + + layout.label(text=self.api_documentation) # show the documentation for this api + # perhaps make this multiline + + def init(self, context): + super().init(context) + + def run(self): + super().run(eval(self.label)) + + def update(self): + print(f'node {self.name} updated') + + def update_api(self, context): + # can force the api string to be auto-corrected, etc. + self.label = self.api_endpoint_string + self.set_no_color() + + try: + api = BlenderPythonAPIEndpoint(self.api_endpoint_string) + except: + self.set_color([1,0,0]) + print(traceback.format_exc()) + return + + try: + self.api_documentation = api.get_documentation() + except: + self.api_documentation = 'No documentation found!' + print(traceback.format_exc()) + + self.inputs.clear() + self.outputs.clear() + + try: + sig = api.get_signature() + includes_varargs = False + includes_kwargs = False + for param_name in sig.parameters: + param = sig.parameters[param_name] + + if param.kind==inspect.Parameter.POSITIONAL_OR_KEYWORD or param.kind==inspect.Parameter.POSITIONAL_ONLY: + if param.annotation!=inspect.Parameter.empty: + key = param_name + ': ' + param.annotation + else: + key = param_name + + self.inputs.new(pynodes.PyObjectSocket.bl_idname, key) + + if param.default!=inspect.Parameter.empty: + self.inputs[key].argvalue = str(param.default) + + elif param.kind==inspect.Parameter.VAR_POSITIONAL: + includes_varargs = True + else: + includes_kwargs = True + + if includes_varargs: + self.inputs.new(pynodes.PyObjectSocket.bl_idname, '*args') + if includes_kwargs: + self.inputs.new(pynodes.PyObjectSocket.bl_idname, '**kwargs') + + if sig.return_annotation!=inspect.Signature.empty: + self.outputs.new(pynodes.PyObjectSocket.bl_idname, sig.return_annotation) + else: + self.outputs.new(pynodes.PyObjectSocket.bl_idname, 'Any') + except: + self.set_color([1,0,0]) + print(traceback.format_exc()) + # self.inputs.clear() + # self.outputs.clear() + return + + +from pynodes import registry +registry.registerNodeType(AnyNode) diff --git a/pynodes/nodes/PythonBaseNode.py b/pynodes/nodes/PythonBaseNode.py index 9656555..0fb8edb 100644 --- a/pynodes/nodes/PythonBaseNode.py +++ b/pynodes/nodes/PythonBaseNode.py @@ -15,7 +15,7 @@ def poll(cls, context): return space.type == 'NODE_EDITOR' def execute(self, context): - context.node.compute_output() + context.node.run() return {'FINISHED'} from pynodes import registry @@ -37,14 +37,6 @@ def mark_dirty(self): self.is_current = False super().mark_dirty() - # def compute_output(self): - # print('compute_output run') - # try: - # super().compute_output() - # except nodes.PythonNode.PythonNodeRunError as e: - # print('Python Nodes caught the following error:') - # traceback.print_exc() - def draw_buttons(self, context, layout): layout.operator('node.evaluate_python_node_tree', icon='PLAY') diff --git a/pynodes/nodes/PythonNode.py b/pynodes/nodes/PythonNode.py index 18a7595..d8bf0e6 100644 --- a/pynodes/nodes/PythonNode.py +++ b/pynodes/nodes/PythonNode.py @@ -3,6 +3,8 @@ import bpy import numpy as np +from pynodes import PythonCompositorTree + # Mix-in class for all custom nodes in this tree type. # Defines a poll function to enable instantiation. class PythonCompositorTreeNode: @@ -40,42 +42,6 @@ class PythonNode(ColorfulNode, PythonCompositorTreeNode): # Icon identifier bl_icon = 'SCRIPT' - # class Properties: - is_dirty = True - # bpy.props.BoolProperty( - # name='dirty', - # default=True - # ) - - class PythonNodeRunError(Exception): - ''' - raised when a node has an error. - ''' - def __init__(self, node, e): - self.node = node - self.ex = e - super().__init__( - f'Exception raised by node {node.bl_idname}:\n{e}' - ) - - def mark_dirty(self): - ''' - Propogate to all downstream nodes that this nodes is not up to date. - ''' - self.is_dirty = True - self.set_no_color() - for k in self.outputs.keys(): - out = self.outputs[k] - if out.is_linked: - for o in out.links: - node = o.to_socket.node - if not node is self and o.is_valid and not node.get_dirty(): - node.mark_dirty() - - - def get_dirty(self): - return self.is_dirty - def is_connected_to_base(self): ''' Return true if this node is connected to a node which is a "base" node. @@ -88,84 +54,10 @@ def is_connected_to_base(self): return True return False - def run(self): - ''' - Do whatever calculations this node does. Take input from self.get_input - and set outputs using self.set_output. - ''' - pass - - def get_input(self, k, default_func=lambda:None): - ''' - Called by run to get value stored behind socket. - ''' - v = self.inputs[k] - if v.is_linked and len(v.links)>0 and v.links[0].is_valid: - o = v.links[0].from_socket - value = o.get_value() - if value is None or o.node.get_dirty(): - o.node.compute_output() - value = o.get_value(); - return value - v.set_value(None) - return v.get_value() - - def set_output(self, k, v): - self.outputs[k].set_value(v) - - update_subscribers = [] - - def subscribe_to_update(self, callback): - self.update_subscribers.append(callback) - - def unsubscribe_to_update(self, callback): - if callback in self.update_subscribers: - self.update_subscribers.remove(callback) - def update(self): ''' Called when the node tree changes. ''' - if not self.get_dirty(): - self.mark_dirty() - # mark node, and downstream nodes as dirty/not current - for socket in self.inputs: + for socket in self.inputs: # TODO: check that socket is updatable socket.node_updated() - # call all socket callbacks and other subscribers - for callback in self.update_subscribers: - try: - callback() - except Exception as e: - traceback.print_exc() - self.unsubscribe_to_update(callback) - - - def interrupt_execution(self, e): - raise PythonNode.PythonNodeRunError(self, e) - - def compute_output(self): - # print(self, 'compute_output') - try: - if self.get_dirty(): - self.set_color([0.0, 0.0, 0.5]) - self.run() - self.is_dirty = False - self.propagate() - self.set_color([0.0, 0.5, 0.0]) - except Exception as e: - # self.mark_dirty() - self.set_color([0.5, 0.0, 0.0]) - traceback.print_exc() - self.interrupt_execution(e) - - def propagate(self): - ''' - Pass this nodes outputs to nodes linked to outputs. Call update_value on - them after passing. - ''' - for out in self.outputs: - if out.is_linked: - for o in out.links: - if o.is_valid: - o.to_socket.set_value(out.get_value()) diff --git a/pynodes/nodes/PythonNodeGroupNodes.py b/pynodes/nodes/PythonNodeGroupNodes.py index f8398ef..4fbe5ba 100644 --- a/pynodes/nodes/PythonNodeGroupNodes.py +++ b/pynodes/nodes/PythonNodeGroupNodes.py @@ -7,7 +7,7 @@ from pynodes import registry from bpy.utils import register_class -class PythonNodeGroupIOSocket(pynodes.AbstractPyObjectVarArgSocket.Properties, pynodes.AbstractPyObjectVarArgSocket): +class PythonNodeGroupIOSocket(pynodes.AbstractPyObjectSocket.Properties, pynodes.AbstractPyObjectSocket): ''' PyNodes node socket type for group input and output nodes''' # Optional identifier string. If not explicitly defined, the python class name is used. bl_idname = 'PythonNodeGroupIOSocket' diff --git a/pynodes/nodes/__init__.py b/pynodes/nodes/__init__.py index c537063..b33833b 100644 --- a/pynodes/nodes/__init__.py +++ b/pynodes/nodes/__init__.py @@ -9,6 +9,9 @@ from pynodes.nodes.PythonShowArrayShapeBaseNode import PythonShowArrayShapeBaseNode from pynodes.nodes.PythonNodeGroupNodes import * -from pynodes.nodes.AutoNodeTypeAdder import add_all_globals +from pynodes.nodes.AnyNode import AnyNode +from pynodes.nodes.AnyBaseNode import AnyBaseNode -add_all_globals() +# from pynodes.nodes.AutoNodeTypeAdder import add_all_globals + +# add_all_globals() diff --git a/pynodes/reflection.py b/pynodes/reflection.py new file mode 100644 index 0000000..e69de29