Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'release/scripts')
-rw-r--r--release/scripts/modules/bpy_types.py155
-rw-r--r--release/scripts/startup/bl_operators/node.py194
-rw-r--r--release/scripts/startup/bl_ui/space_node.py43
-rw-r--r--release/scripts/templates_py/custom_nodes.py159
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()
+