diff options
author | Lukas Toenne <lukas.toenne@googlemail.com> | 2013-03-18 20:34:57 +0400 |
---|---|---|
committer | Lukas Toenne <lukas.toenne@googlemail.com> | 2013-03-18 20:34:57 +0400 |
commit | 4638e5f99a9ba59ad0b8a1fd52b12e876480b9e8 (patch) | |
tree | 2444f12b4612440f44cf02835cdf5951b6564e92 /release/scripts | |
parent | 7bfef29f2f2a1b262d28abdc6e30fcd9c1f1caad (diff) |
Merge of the PyNodes branch (aka "custom nodes") into trunk.
PyNodes opens up the node system in Blender to scripters and adds a number of UI-level improvements.
=== Dynamic node type registration ===
Node types can now be added at runtime, using the RNA registration mechanism from python. This enables addons such as render engines to create a complete user interface with nodes.
Examples of how such nodes can be defined can be found in my personal wiki docs atm [1] and as a script template in release/scripts/templates_py/custom_nodes.py [2].
=== Node group improvements ===
Each node editor now has a tree history of edited node groups, which allows opening and editing nested node groups. The node editor also supports pinning now, so that different spaces can be used to edit different node groups simultaneously. For more ramblings and rationale see (really old) blog post on code.blender.org [3].
The interface of node groups has been overhauled. Sockets of a node group are no longer displayed in columns on either side, but instead special input/output nodes are used to mirror group sockets inside a node tree. This solves the problem of long node lines in groups and allows more adaptable node layout. Internal sockets can be exposed from a group by either connecting to the extension sockets in input/output nodes (shown as empty circle) or by adding sockets from the node property bar in the "Interface" panel. Further details such as the socket name can also be changed there.
[1] http://wiki.blender.org/index.php/User:Phonybone/Python_Nodes
[2] http://projects.blender.org/scm/viewvc.php/trunk/blender/release/scripts/templates_py/custom_nodes.py?view=markup&root=bf-blender
[3] http://code.blender.org/index.php/2012/01/improving-node-group-interface-editing/
Diffstat (limited to 'release/scripts')
-rw-r--r-- | release/scripts/modules/bpy_types.py | 155 | ||||
-rw-r--r-- | release/scripts/startup/bl_operators/node.py | 194 | ||||
-rw-r--r-- | release/scripts/startup/bl_ui/space_node.py | 43 | ||||
-rw-r--r-- | release/scripts/templates_py/custom_nodes.py | 159 |
4 files changed, 434 insertions, 117 deletions
diff --git a/release/scripts/modules/bpy_types.py b/release/scripts/modules/bpy_types.py index 4398b1721f7..f42fd8e3107 100644 --- a/release/scripts/modules/bpy_types.py +++ b/release/scripts/modules/bpy_types.py @@ -485,17 +485,6 @@ class Text(bpy_types.ID): ) -class NodeSocket(StructRNA): # , metaclass=RNAMeta - __slots__ = () - - @property - def links(self): - """List of node links from or to this socket""" - return tuple(link for link in self.id_data.links - if (link.from_socket == self or - link.to_socket == self)) - - # values are module: [(cls, path, line), ...] TypeMap = {} @@ -757,3 +746,147 @@ class Region(StructRNA): return None + +class NodeTree(bpy_types.ID, metaclass=RNAMetaPropGroup): + __slots__ = () + + +class NodeSocketTemplate(): + type = 'UNDEFINED' + + # Default implementation: + # Create a single property using the socket template's 'value_property' attribute + # value_property should be created in the __init__ function + # + # If necessary this function can be overloaded in subclasses, e.g. to create multiple value properties + def define_node_properties(self, node_type, prefix): + if hasattr(self, "value_property"): + setattr(node_type, prefix+"value", self.value_property) + + def init_socket(self, socket): + socket.type = self.type + if hasattr(self, "value_property"): + socket.value_property = self.value_property[1]['attr'] + + +def gen_valid_identifier(seq): + # get an iterator + itr = iter(seq) + # pull characters until we get a legal one for first in identifer + for ch in itr: + if ch == '_' or ch.isalpha(): + yield ch + break + # pull remaining characters and yield legal ones for identifier + for ch in itr: + if ch == '_' or ch.isalpha() or ch.isdigit(): + yield ch + +def sanitize_identifier(name): + return ''.join(gen_valid_identifier(name)) + +def unique_identifier(name, identifier_list): + # First some basic sanitation, to make a usable identifier string from the name + base = sanitize_identifier(name) + # Now make a unique identifier by appending an unused index + identifier = base + index = 0 + while identifier in identifier_list: + index += 1 + identifier = base + str(index) + return identifier + +class RNAMetaNode(RNAMetaPropGroup): + def __new__(cls, name, bases, classdict, **args): + # Wrapper for node.init, to add sockets from templates + + def create_sockets(self): + inputs = getattr(self, 'input_templates', None) + if inputs: + for temp in inputs: + socket = self.inputs.new(type=temp.bl_socket_idname, name=temp.name, identifier=temp.identifier) + temp.init_socket(socket) + outputs = getattr(self, 'output_templates', None) + if outputs: + for temp in outputs: + socket = self.outputs.new(type=temp.bl_socket_idname, name=temp.name, identifier=temp.identifier) + temp.init_socket(socket) + + init_base = classdict.get('init', None) + if init_base: + def init_node(self, context): + create_sockets(self) + init_base(self, context) + else: + def init_node(self, context): + create_sockets(self) + + classdict['init'] = init_node + + # Create the regular class + result = RNAMetaPropGroup.__new__(cls, name, bases, classdict) + + # Add properties from socket templates + inputs = classdict.get('input_templates', None) + if inputs: + for i, temp in enumerate(inputs): + temp.identifier = unique_identifier(temp.name, [t.identifier for t in inputs[0:i]]) + temp.define_node_properties(result, "input_"+temp.identifier+"_") + outputs = classdict.get('output_templates', None) + if outputs: + for i, temp in enumerate(outputs): + temp.identifier = unique_identifier(temp.name, [t.identifier for t in outputs[0:i]]) + temp.define_node_properties(result, "output_"+temp.identifier+"_") + + return result + + +class Node(StructRNA, metaclass=RNAMetaNode): + __slots__ = () + + @classmethod + def poll(cls, ntree): + return True + + +class NodeSocket(StructRNA, metaclass=RNAMetaPropGroup): + __slots__ = () + + @property + def links(self): + """List of node links from or to this socket""" + return tuple(link for link in self.id_data.links + if (link.from_socket == self or + link.to_socket == self)) + + +class NodeSocketInterface(StructRNA, metaclass=RNAMetaPropGroup): + __slots__ = () + + +# These are intermediate subclasses, need a bpy type too +class CompositorNode(Node): + __slots__ = () + + @classmethod + def poll(cls, ntree): + return ntree.bl_idname == 'CompositorNodeTree' + + def update(self): + self.tag_need_exec() + +class ShaderNode(Node): + __slots__ = () + + @classmethod + def poll(cls, ntree): + return ntree.bl_idname == 'ShaderNodeTree' + + +class TextureNode(Node): + __slots__ = () + + @classmethod + def poll(cls, ntree): + return ntree.bl_idname == 'TextureNodeTree' + diff --git a/release/scripts/startup/bl_operators/node.py b/release/scripts/startup/bl_operators/node.py index bc0224db765..9839e0ee092 100644 --- a/release/scripts/startup/bl_operators/node.py +++ b/release/scripts/startup/bl_operators/node.py @@ -100,75 +100,55 @@ class NODE_OT_add_node(NodeAddOperator, Operator): return result -# XXX These node item lists should actually be generated by a callback at -# operator execution time (see node_type_items below), -# using the active node tree from the context. -# Due to a difficult bug in bpy this is not possible -# (item list memory gets freed too early), -# so for now just copy the static item lists to these global variables. -# -# In the custom_nodes branch, the static per-tree-type node items are replaced -# by a single independent type list anyway (with a poll function to limit node -# types to the respective trees). So this workaround is only temporary. - -# lazy init -node_type_items_dict = {} - -# Prefixes used to distinguish base node types and node groups -node_type_prefix = 'NODE_' -node_group_prefix = 'GROUP_' - - -# Generate a list of enum items for a given node class -# Copy existing type enum, adding a prefix to distinguish from node groups -# Skip the base node group type, -# node groups will be added below for all existing group trees -def node_type_items(node_class): - return [(node_type_prefix + item.identifier, item.name, item.description) - for item in node_class.bl_rna.properties['type'].enum_items - if item.identifier != 'GROUP'] - - -# Generate items for node group types -# Filter by the given tree_type -# Node group trees don't have a description property yet -# (could add this as a custom property though) -def node_group_items(tree_type): - return [(node_group_prefix + group.name, group.name, '') - for group in bpy.data.node_groups if group.type == tree_type] +def node_classes_iter(base=bpy.types.Node): + """ + Yields all true node classes by checking for the is_registered_node_type classmethod. + Node types can use specialized subtypes of bpy.types.Node, which are not usable + nodes themselves (e.g. CompositorNode). + """ + if base.is_registered_node_type(): + yield base + for subclass in base.__subclasses__(): + for node_class in node_classes_iter(subclass): + yield node_class + + +def node_class_items_iter(node_class, context): + identifier = node_class.bl_rna.identifier + # XXX Checking for explicit group node types is stupid. + # This should be replaced by a generic system of generating + # node items via callback. + # Group node_tree pointer should also use a poll function to filter the library list, + # but cannot do that without a node instance here. A node callback could just use the internal poll function. + if identifier in {'ShaderNodeGroup', 'CompositorNodeGroup', 'TextureNodeGroup'}: + tree_idname = context.space_data.edit_tree.bl_idname + for group in bpy.data.node_groups: + if group.bl_idname == tree_idname: + yield (group.name, "", {"node_tree":group}) # XXX empty string should be replaced by description from tree + else: + yield (node_class.bl_rna.name, node_class.bl_rna.description, {}) -# Returns the enum item list for the edited tree in the context -def node_type_items_cb(self, context): +def node_items_iter(context): snode = context.space_data if not snode: - return () + return tree = snode.edit_tree if not tree: - return () - - # Lists of basic node types for each - if not node_type_items_dict: - node_type_items_dict.update({ - 'SHADER': node_type_items(bpy.types.ShaderNode), - 'COMPOSITING': node_type_items(bpy.types.CompositorNode), - 'TEXTURE': node_type_items(bpy.types.TextureNode), - }) - - # XXX Does not work correctly, see comment above - ''' - return [(item.identifier, item.name, item.description, item.value) - for item in - tree.nodes.bl_rna.functions['new'].parameters['type'].enum_items] - ''' - - if tree.type in node_type_items_dict: - return node_type_items_dict[tree.type] + node_group_items(tree.type) - else: - return () + return + + for node_class in node_classes_iter(): + if node_class.poll(tree): + for item in node_class_items_iter(node_class, context): + yield (node_class,) + item + + +# Create an enum list from node class items +def node_type_items_cb(self, context): + return [(str(index), item[1], item[2]) for index, item in enumerate(node_items_iter(context))] -class NODE_OT_add_search(Operator): +class NODE_OT_add_search(NodeAddOperator, Operator): '''Add a node to the active tree''' bl_idname = "node.add_search" bl_label = "Search and Add Node" @@ -182,55 +162,48 @@ class NODE_OT_add_search(Operator): items=node_type_items_cb, ) - _node_type_items_dict = None + def execute(self, context): + for index, item in enumerate(node_items_iter(context)): + if str(index) == self.type: + node = self.create_node(context, item[0].bl_rna.identifier) + for prop,value in item[3].items(): + setattr(node, prop, value) + break + return {'FINISHED'} - def create_node(self, context): - space = context.space_data - tree = space.edit_tree + 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'} - # Enum item identifier has an additional prefix to - # distinguish base node types from node groups - item = self.type - if item.startswith(node_type_prefix): - # item means base node type - node = tree.nodes.new(type=item[len(node_type_prefix):]) - elif item.startswith(node_group_prefix): - # item means node group type - node = tree.nodes.new( - type='GROUP', - group=bpy.data.node_groups[item[len(node_group_prefix):]]) - else: - return None - for n in tree.nodes: - if n == node: - node.select = True - tree.nodes.active = node - else: - node.select = False - node.location = space.cursor_location - return node +# Simple basic operator for adding a node without further initialization +class NODE_OT_add_node(NodeAddOperator, bpy.types.Operator): + '''Add a node to the active tree''' + bl_idname = "node.add_node" + bl_label = "Add Node" - @classmethod - def poll(cls, context): - space = context.space_data - # needs active node editor and a tree to add nodes to - return (space.type == 'NODE_EDITOR' and space.edit_tree) + type = StringProperty(name="Node Type", description="Node type") def execute(self, context): - self.create_node(context) + node = self.create_node(context, self.type) return {'FINISHED'} - def invoke(self, context, event): - space = context.space_data - v2d = context.region.view2d - # convert mouse position to the View2D for later node placement - space.cursor_location = v2d.region_to_view(event.mouse_region_x, - event.mouse_region_y) +class NODE_OT_add_group_node(NodeAddOperator, bpy.types.Operator): + '''Add a group node to the active tree''' + bl_idname = "node.add_group_node" + bl_label = "Add Group Node" - context.window_manager.invoke_search_popup(self) - return {'CANCELLED'} + type = StringProperty(name="Node Type", description="Node type") + grouptree = StringProperty(name="Group tree", description="Group node tree name") + + def execute(self, context): + node = self.create_node(context, self.type) + node.node_tree = bpy.data.node_groups[self.grouptree] + + return {'FINISHED'} class NODE_OT_collapse_hide_unused_toggle(Operator): @@ -261,3 +234,24 @@ class NODE_OT_collapse_hide_unused_toggle(Operator): socket.hide = hide return {'FINISHED'} + + +class NODE_OT_tree_path_parent(Operator): + '''Go to parent node tree''' + bl_idname = "node.tree_path_parent" + bl_label = "Parent Node Tree" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + space = context.space_data + # needs active node editor and a tree + return (space.type == 'NODE_EDITOR' and len(space.path) > 1) + + def execute(self, context): + space = context.space_data + + space.path.pop() + + return {'FINISHED'} + diff --git a/release/scripts/startup/bl_ui/space_node.py b/release/scripts/startup/bl_ui/space_node.py index 1865b049a03..e739c5ea5d4 100644 --- a/release/scripts/startup/bl_ui/space_node.py +++ b/release/scripts/startup/bl_ui/space_node.py @@ -44,8 +44,8 @@ class NODE_HT_header(Header): row.menu("NODE_MT_node") layout.prop(snode, "tree_type", text="", expand=True) - - if snode.tree_type == 'SHADER': + + if snode.tree_type == 'ShaderNodeTree': if scene.render.use_shading_nodes: layout.prop(snode, "shader_type", text="", expand=True) @@ -65,7 +65,7 @@ class NODE_HT_header(Header): if snode_id: layout.prop(snode_id, "use_nodes") - elif snode.tree_type == 'TEXTURE': + elif snode.tree_type == 'TextureNodeTree': layout.prop(snode, "texture_type", text="", expand=True) if id_from: @@ -76,7 +76,7 @@ class NODE_HT_header(Header): if snode_id: layout.prop(snode_id, "use_nodes") - elif snode.tree_type == 'COMPOSITING': + elif snode.tree_type == 'CompositorNodeTree': layout.prop(snode_id, "use_nodes") layout.prop(snode_id.render, "use_free_unused_nodes", text="Free Unused") layout.prop(snode, "show_backdrop") @@ -84,6 +84,13 @@ class NODE_HT_header(Header): row = layout.row(align=True) row.prop(snode, "backdrop_channels", text="", expand=True) layout.prop(snode, "use_auto_render") + + else: + # Custom node tree is edited as independent ID block + layout.template_ID(snode, "node_tree", new="node.new_node_tree") + + layout.prop(snode, "pin", text="") + layout.operator("node.tree_path_parent", text="", icon='FILE_PARENT') layout.separator() @@ -182,6 +189,7 @@ class NODE_MT_node(Menu): layout.operator("node.group_edit") layout.operator("node.group_ungroup") layout.operator("node.group_make") + layout.operator("node.group_insert") layout.separator() @@ -208,7 +216,7 @@ class NODE_PT_properties(Panel): @classmethod def poll(cls, context): snode = context.space_data - return snode.tree_type == 'COMPOSITING' + return snode.tree_type == 'CompositorNodeTree' def draw_header(self, context): snode = context.space_data @@ -237,7 +245,7 @@ class NODE_PT_quality(bpy.types.Panel): @classmethod def poll(cls, context): snode = context.space_data - return snode.tree_type == 'COMPOSITING' and snode.node_tree is not None + return snode.tree_type == 'CompositorNodeTree' and snode.node_tree is not None def draw(self, context): layout = self.layout @@ -276,5 +284,28 @@ class NODE_MT_node_color_specials(Menu): layout.operator("node.node_copy_color", icon='COPY_ID') +class NODE_UL_interface_sockets(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + socket = item + color = socket.draw_color(context) + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row(align=True) + + # inputs get icon on the left + if socket.in_out == 'IN': + row.template_node_socket(color) + + row.label(text=socket.name, icon_value=icon) + + # outputs get icon on the right + if socket.in_out == 'OUT': + row.template_node_socket(color) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.template_node_socket(color) + + if __name__ == "__main__": # only for live edit. bpy.utils.register_module(__name__) diff --git a/release/scripts/templates_py/custom_nodes.py b/release/scripts/templates_py/custom_nodes.py new file mode 100644 index 00000000000..485ee0ebe05 --- /dev/null +++ b/release/scripts/templates_py/custom_nodes.py @@ -0,0 +1,159 @@ +import bpy +# XXX these don't work yet ... +#from bpy_types import NodeTree, Node, NodeSocket + +# Implementation of custom nodes from Python + + +# Shortcut for node type menu +def add_nodetype(layout, type): + layout.operator("node.add_node", text=type.bl_label).type = type.bl_rna.identifier + +# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc. +class MyCustomTree(bpy.types.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 + # NOTE: If no icon is defined, the node tree will not show up in the editor header! + # This can be used to make additional tree types for groups and similar nodes (see below) + # Only one base tree class is needed in the editor for selecting the general category + bl_icon = 'NODETREE' + + def draw_add_menu(self, context, layout): + layout.label("Hello World!") + add_nodetype(layout, bpy.types.CustomNodeType) + add_nodetype(layout, bpy.types.MyCustomGroup) + + +# Custom socket type +class MyCustomSocket(bpy.types.NodeSocket): + # Description string + '''Custom node socket type''' + # Optional identifier string. If not explicitly defined, the python class name is used. + bl_idname = 'CustomSocketType' + # Label for nice name display + bl_label = 'Custom Node Socket' + # Socket color + bl_color = (1.0, 0.4, 0.216, 0.5) + + # Enum items list + my_items = [ + ("DOWN", "Down", "Where your feet are"), + ("UP", "Up", "Where your head should be"), + ("LEFT", "Left", "Not right"), + ("RIGHT", "Right", "Not left") + ] + + myEnumProperty = bpy.props.EnumProperty(name="Direction", description="Just an example", items=my_items, default='UP') + + # Optional function for drawing the socket input value + def draw(self, context, layout, node): + layout.prop(self, "myEnumProperty", text=self.name) + + +# Base 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' + +# Derived from the Node base type. +class MyCustomNode(bpy.types.Node, MyCustomTreeNode): + # === Basics === + # Description string + '''A custom node''' + # Optional identifier string. If not explicitly defined, the python class name is used. + bl_idname = 'CustomNodeType' + # Label for nice name display + bl_label = 'Custom Node' + # Icon identifier + bl_icon = 'SOUND' + + # === Custom Properties === + # These work just like custom properties in ID data blocks + # Extensive information can be found under + # http://wiki.blender.org/index.php/Doc:2.6/Manual/Extensions/Python/Properties + myStringProperty = bpy.props.StringProperty() + myFloatProperty = bpy.props.FloatProperty(default=3.1415926) + + # === Optional Functions === + # Initialization function, called when a new node is created. + # This is the most common place to create the sockets for a node, as shown below. + # NOTE: this is not the same as the standard __init__ function in Python, which is + # a purely internal Python method and unknown to the node system! + def init(self, context): + self.inputs.new('CustomSocketType', "Hello") + self.inputs.new('NodeSocketFloat', "World") + self.inputs.new('NodeSocketVector', "!") + + self.outputs.new('NodeSocketColor', "How") + self.outputs.new('NodeSocketColor', "are") + self.outputs.new('NodeSocketFloat', "you") + + # Copy function to initialize a copied node from an existing one. + def copy(self, node): + print("Copying from node ", node) + + # Free function to clean up on removal. + def free(self): + print("Removing node ", self, ", Goodbye!") + + # Additional buttons displayed on the node. + def draw_buttons(self, context, layout): + layout.label("Node settings") + layout.prop(self, "myFloatProperty") + + # 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): + layout.prop(self, "myFloatProperty") + # myStringProperty button will only be visible in the sidebar + layout.prop(self, "myStringProperty") + + +# A customized group-like node. +class MyCustomGroup(bpy.types.NodeGroup, MyCustomTreeNode): + # === Basics === + # Description string + '''A custom group node''' + # Label for nice name display + bl_label = 'Custom Group Node' + bl_group_tree_idname = 'CustomTreeType' + + orks = bpy.props.IntProperty(default=3) + dwarfs = bpy.props.IntProperty(default=12) + wizards = bpy.props.IntProperty(default=1) + + # Additional buttons displayed on the node. + def draw_buttons(self, context, layout): + col = layout.column(align=True) + col.prop(self, "orks") + col.prop(self, "dwarfs") + col.prop(self, "wizards") + + layout.label("The Node Tree:") + layout.prop(self, "node_tree", text="") + + +def register(): + bpy.utils.register_class(MyCustomTree) + bpy.utils.register_class(MyCustomSocket) + bpy.utils.register_class(MyCustomNode) + bpy.utils.register_class(MyCustomGroup) + + +def unregister(): + bpy.utils.unregister_class(MyCustomTree) + bpy.utils.unregister_class(MyCustomSocket) + bpy.utils.unregister_class(MyCustomNode) + bpy.utils.unregister_class(MyCustomGroup) + + +if __name__ == "__main__": + register() + |