diff options
Diffstat (limited to 'release')
-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() + |