Blender 4.4.x
Testing how node editor works. Trying to make simple as possible but the register and unregister has some bug to handle remove nodes.
| # working... | |
| import bpy | |
| from bpy.types import NodeTree, Node, NodeSocketFloat, Operator | |
| from bpy.utils import register_class, unregister_class | |
| from nodeitems_utils import NodeCategory, register_node_categories, unregister_node_categories, NodeItem | |
| # Custom Node Tree | |
| class CustomNodeTree(NodeTree): | |
| bl_idname = "CustomNodeTree" | |
| bl_label = "Custom Node Tree" | |
| bl_icon = "NODETREE" | |
| # Custom Node (Multiplies input by a value) | |
| class CustomNode(Node): | |
| bl_idname = "CustomNodeType" | |
| bl_label = "Custom Multiply Node" | |
| bl_icon = "SOUND" | |
| my_float_prop: bpy.props.FloatProperty(name="Value", default=1.0, update=lambda self, context: self.update()) | |
| my_title_prop: bpy.props.StringProperty(name="Title", default="Multiply Node") | |
| def init(self, context): | |
| self.inputs.new("NodeSocketFloat", "Input") | |
| self.outputs.new("NodeSocketFloat", "Output") | |
| def draw_buttons(self, context, layout): | |
| layout.prop(self, "my_float_prop") | |
| layout.prop(self, "my_title_prop") | |
| def update(self): | |
| if self.inputs[0].is_linked and self.outputs[0].is_linked: | |
| input_value = self.inputs[0].default_value | |
| self.outputs[0].default_value = input_value * self.my_float_prop | |
| def draw_label(self): | |
| return self.my_title_prop | |
| # Custom Node 2 (Multiplies input by a value, similar to CustomNode) | |
| class CustomNode2(Node): | |
| bl_idname = "CustomNodeType2" | |
| bl_label = "Custom Multiply Node 2" | |
| bl_icon = "PLUS" | |
| my_float_prop: bpy.props.FloatProperty(name="Value", default=1.0, update=lambda self, context: self.update()) | |
| my_title_prop: bpy.props.StringProperty(name="Title", default="Multiply Node 2") | |
| def init(self, context): | |
| self.inputs.new("NodeSocketFloat", "Input") | |
| self.outputs.new("NodeSocketFloat", "Output") | |
| def draw_buttons(self, context, layout): | |
| layout.prop(self, "my_float_prop") | |
| layout.prop(self, "my_title_prop") | |
| def update(self): | |
| if self.inputs[0].is_linked and self.outputs[0].is_linked: | |
| input_value = self.inputs[0].default_value | |
| self.outputs[0].default_value = input_value * self.my_float_prop | |
| def draw_label(self): | |
| return self.my_title_prop | |
| # Operator to create a new CustomNodeTree | |
| class OBJECT_OT_CreateCustomNodeTree(Operator): | |
| bl_idname = "object.create_custom_node_tree" | |
| bl_label = "Create Custom Node Tree" | |
| def execute(self, context): | |
| node_tree = bpy.data.node_groups.new("CustomNodeTree", "CustomNodeTree") | |
| for area in context.screen.areas: | |
| if area.type == "NODE_EDITOR": | |
| area.spaces.active.tree_type = "CustomNodeTree" | |
| area.spaces.active.node_tree = node_tree | |
| break | |
| return {'FINISHED'} | |
| # Debug function to print node categories | |
| def print_node_categories(): | |
| from nodeitems_utils import node_categories_iter | |
| print("Registered Node Categories:") | |
| for cat in node_categories_iter(context=None): | |
| print(f" Category: {cat.identifier}") | |
| for item in cat.items(context=None): | |
| print(f" - Item: {item.label} ({item.nodetype})") | |
| # print(dir(item)) | |
| # print(f" - Item: {item.label} ({item.type})")# it has no type for NodeItem | |
| # Node Category for Menu | |
| class CustomNodeCategory(NodeCategory): | |
| @classmethod | |
| def poll(cls, context): | |
| return context.space_data.tree_type == "CustomNodeTree" | |
| node_categories = [ | |
| CustomNodeCategory("CUSTOM_NODES", "Custom Nodes", items=[ | |
| NodeItem("CustomNodeType"), | |
| NodeItem("CustomNodeType2"), | |
| # {"label": "Custom Multiply Node", "type": CustomNode.bl_idname}, | |
| ]) | |
| ] | |
| def cleanup_custom_node_trees(): | |
| """Remove all CustomNodeTree instances to ensure clean unregistration.""" | |
| for tree in bpy.data.node_groups: | |
| if tree.bl_idname == "CustomNodeTree": | |
| bpy.data.node_groups.remove(tree) | |
| def is_category_registered(category_id): | |
| """Check if a node category is already registered.""" | |
| from nodeitems_utils import node_categories_iter | |
| for cat in node_categories_iter(context=None): | |
| if cat.identifier == category_id: | |
| return True | |
| return False | |
| # List of classes for registration | |
| classes = [ | |
| CustomNodeTree, | |
| CustomNode, | |
| CustomNode2, | |
| OBJECT_OT_CreateCustomNodeTree | |
| ] | |
| def register(): | |
| # Register classes | |
| for cls in classes: | |
| if not hasattr(bpy.types, cls.bl_idname): | |
| register_class(cls) | |
| register_node_categories("CUSTOM_NODES", node_categories) | |
| # Register node categories | |
| # if not is_category_registered("CUSTOM_NODES"): | |
| # register_node_categories("CUSTOM_NODES", node_categories) | |
| # else: | |
| # print("Node categories 'CUSTOM_NODES' already registered, skipping registration.") | |
| # Print node categories for debugging | |
| print_node_categories() | |
| def unregister(): | |
| # testing see log | |
| print("======================") | |
| print("CLEAN UP...") | |
| print("======================") | |
| # Unregister node categories first | |
| if is_category_registered("CUSTOM_NODES"): | |
| try: | |
| print("CLEAN UP register_node_categories CUSTOM_NODES") | |
| unregister_node_categories("CUSTOM_NODES") | |
| except Exception as e: | |
| print(f"Failed to unregister node categories: {e}") | |
| # Remove any lingering CustomNodeTree instances | |
| cleanup_custom_node_trees() | |
| # Unregister classes in reverse order | |
| for cls in reversed(classes): | |
| if hasattr(bpy.types, cls.bl_idname): | |
| try: | |
| unregister_class(cls) | |
| except Exception as e: | |
| print(f"Failed to unregister {cls.__name__}: {e}") | |
| print("CUSTOM_NODES: ", is_category_registered("CUSTOM_NODES")) | |
| if __name__ == "__main__": | |
| # Clean up before registering to avoid conflicts | |
| unregister() | |
| register() |