I’m trying to define a custom node tree and some nodes. I’m getting stuck pretty early on. In the attached code, I’m trying to make a node that concatenates two input strings.
Assumption: I’m new to PyNodes, and based on my poking around the docs and Blender.SE feedback, I’ve come to the conclusion that the core PyNode system doesn’t really pass information through sockets for you; it’s up to each node to traverse backwards through the socket links, sucking data forward as required.
In the attached script, line 39 triggers a call to update(). Since my update method reaches back through all of the input connections (as per my above assumption) we have a problem - the node is only partially initialized, so not all of the input sockets exist yet!
It seems to me that a system like this ought to be a more automatic. For example, input sockets could be bound to instance attributes, which the system could automatically set. Since this isn’t the case, I’m guessing there’s something substantial in there that I’m misunderstanding.
Could someone explain where my code goes wrong, or where my assumption is wrong?
import bpy
from bpy.types import NodeTree, Node, NodeSocket
import pdb
# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc.
class MyCustomTree(NodeTree):
# Description string
'''A custom node tree type that will show up in the node editor header'''
# Optional identifier string. If not explicitly defined, the python class name is used.
bl_idname = 'CustomTreeType'
# Label for nice name display
bl_label = 'Custom Node Tree'
# Icon identifier
bl_icon = 'NODETREE'
# Mix-in class for all custom nodes in this tree type.
# Defines a poll function to enable instantiation.
class MyCustomTreeNode:
@classmethod
def poll(cls, ntree):
return ntree.bl_idname == 'CustomTreeType'
class MyNode(Node, MyCustomTreeNode):
# Description string
'''A node that add a prefix or a suffix to a string'''
# Optional identifier string. If not explicitly defined, the python class name is used.
bl_idname = 'MyNodeType'
# Label for nice name display
bl_label = 'PreSufFix Node'
# Icon identifier
bl_icon = 'SOUND'
in0_val = bpy.props.StringProperty()
in1_val = bpy.props.StringProperty()
def init(self, context):
self.inputs.new('NodeSocketString', "in0")
self.inputs.new('NodeSocketString', "in1")
self.outputs.new('NodeSocketString', "out")
self.inputs['in0'].default_value = 'foo'
self.inputs['in1'].default_value = 'bar'
self.outputs['out'].default_value = 'baz'
print("INIT DONE")
def draw_buttons(self, context, layout):
layout.label(self.outputs['out'].default_value)
def draw_label(self):
return "I am a custom text manipulator node"
def update(self):
print("Updating node: ", self.name)
# Update the input sockets
if self.inputs['in0'].is_linked and self.inputs['in0'].links[0].is_valid:
self.in0_val = self.inputs['in0'].links[0].from_socket.default_value
else:
self.in0_val = 'foo'
# Update the input sockets
if self.inputs['in1'].is_linked and self.inputs['in1'].links[0].is_valid:
self.in1_val = self.inputs['in1'].links[0].from_socket.default_value
else:
self.in1_val = 'bar'
# Update the output socket
self.outputs['out'].default_value = self.in0_val + self.in1_val
print(self.outputs['out'].default_value)
### Node Categories ###
# Node categories are a python system for automatically
# extending the Add menu, toolbar panels and search operator.
# For more examples see release/scripts/startup/nodeitems_builtins.py
import nodeitems_utils
from nodeitems_utils import NodeCategory, NodeItem
# our own base class with an appropriate poll function,
# so the categories only show up in our own tree type
class MyNodeCategory(NodeCategory):
@classmethod
def poll(cls, context):
return context.space_data.tree_type == 'CustomTreeType'
# all categories in a list
node_categories = [
# identifier, label, items list
MyNodeCategory("MYNODES", "Some Nodes", items=[
NodeItem("MyNodeType"),
]),
]
def register():
bpy.utils.register_class(MyCustomTree)
bpy.utils.register_class(MyNode)
nodeitems_utils.register_node_categories("CUSTOM_NODES", node_categories)
def unregister():
nodeitems_utils.unregister_node_categories("CUSTOM_NODES")
bpy.utils.unregister_class(MyCustomTree)
bpy.utils.unregister_class(MyNode)
if __name__ == "__main__":
register()
PyNodes is better designed for representation of relationships, rather than actually executing code. It limits your ability to separate function from interface, and creates heavily influences your code.
Update is triggered because you’re updating the node’s sockets(During init)
Though I would separate completely from the bpy API, at the very least you must guard your update function until you have a valid state
from contextlib import contextmanager
@contextmanager
def guard(self):
self.locked = True
yield
self.locked = False
def guarded(func):
def wrapper(self, *args, **kwargs):
if getattr(self, "locked", False):
return
return func.__get__(self, type(self))(*args, **kwargs)
return wrapper
class MyNode(Node, MyCustomTreeNode):
# Description string
'''A node that add a prefix or a suffix to a string'''
# Optional identifier string. If not explicitly defined, the python class name is used.
bl_idname = 'MyNodeType'
# Label for nice name display
bl_label = 'PreSufFix Node'
# Icon identifier
bl_icon = 'SOUND'
in0_val = bpy.props.StringProperty()
in1_val = bpy.props.StringProperty()
def init(self, context):
with guard(self):
self.inputs.new('NodeSocketString', "in0")
self.inputs.new('NodeSocketString', "in1")
self.outputs.new('NodeSocketString', "out")
self.inputs['in0'].default_value = 'foo'
self.inputs['in1'].default_value = 'bar'
self.outputs['out'].default_value = 'baz'
print("INIT DONE")
def draw_buttons(self, context, layout):
layout.label(self.outputs['out'].default_value)
def draw_label(self):
return "I am a custom text manipulator node"
@guarded
def update(self):
# print("Updating node: ", self.name)
print(self.inputs, self.outputs)
# Update the input sockets
if self.inputs['in0'].is_linked and self.inputs['in0'].links[0].is_valid:
self.in0_val = self.inputs['in0'].links[0].from_socket.default_value
else:
self.in0_val = 'foo'
# Update the input sockets
if self.inputs['in1'].is_linked and self.inputs['in1'].links[0].is_valid:
self.in1_val = self.inputs['in1'].links[0].from_socket.default_value
else:
self.in1_val = 'bar'
# Update the output socket
self.outputs['out'].default_value = self.in0_val + self.in1_val
print(self.outputs['out'].default_value)
I’ve seen other remarks similar to your “PyNodes is better designed for representation of relationships” but not sure I yet understand what it’s all about. I guess I can’t imagine how the representation of relationships could be at all useful unless the relationships are driving interesting code under the hood.
Perhaps if I described where I’d like to head with this, you could comment on it’s feasibility? I’d like to wrap most/all of scipy.ndimage in PyNodes (which I think will end up looking a lot like the Blender compositor, extended past 2D operations). On a scale from easy to crazy, how well suited is Blender’s PyNode for such a project?