Create addon with copy paste operators
This commit is contained in:
		
							parent
							
								
									9db9538172
								
							
						
					
					
						commit
						4fb9bb8c69
					
				
							
								
								
									
										40
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
bl_info = {
 | 
			
		||||
    "name": "Node Kit",
 | 
			
		||||
    "author": "Florentin Luce",
 | 
			
		||||
    "version": (0, 1),
 | 
			
		||||
    "blender": (4, 0, 2),
 | 
			
		||||
    "category": "Node"}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import importlib
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
# Ensure the name of the module in python import
 | 
			
		||||
module_name = Path(__file__).parent.name
 | 
			
		||||
sys.modules.update({'node_kit': importlib.import_module(module_name)})
 | 
			
		||||
 | 
			
		||||
from node_kit import ui, operators
 | 
			
		||||
 | 
			
		||||
modules = (
 | 
			
		||||
    ui,
 | 
			
		||||
    operators,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if "bpy" in locals():
 | 
			
		||||
    import importlib
 | 
			
		||||
 | 
			
		||||
    for mod in modules:
 | 
			
		||||
        importlib.reload(mod)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register():
 | 
			
		||||
    print('Register Noke kit')
 | 
			
		||||
    for mod in modules:
 | 
			
		||||
        mod.register()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def unregister():
 | 
			
		||||
    for mod in reversed(modules):
 | 
			
		||||
        mod.unregister()
 | 
			
		||||
							
								
								
									
										0
									
								
								core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										97
									
								
								core/node.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								core/node.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
			
		||||
from .sockets import Input, Output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Node:
 | 
			
		||||
    """Blender Node abstraction."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bl_node, parent):
 | 
			
		||||
 | 
			
		||||
        self.bl_node = bl_node
 | 
			
		||||
        self.tree = parent
 | 
			
		||||
        self.id = hex(id(self.bl_node))
 | 
			
		||||
 | 
			
		||||
        self.data = {}
 | 
			
		||||
        self.parameters = []
 | 
			
		||||
 | 
			
		||||
        for prop in self.bl_node.bl_rna.properties:
 | 
			
		||||
            if prop.is_readonly:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            prop_id = prop.identifier
 | 
			
		||||
 | 
			
		||||
            setattr(self, prop_id, getattr(self.bl_node, prop_id))
 | 
			
		||||
            self.parameters.append(prop_id)
 | 
			
		||||
 | 
			
		||||
        self.inputs = [Input(ipt, self.tree) for ipt in self.bl_node.inputs]
 | 
			
		||||
        self.outputs = [Output(opt, self.tree) for opt in self.bl_node.outputs]
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_dict(cls, data, tree):
 | 
			
		||||
        """Create all nodes from their dict representation.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            data (dict): dict nodes representation.
 | 
			
		||||
            tree (Tree): blender node tree abstraction.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Node: Create abstract node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        new_bl_node = tree.nodes.new(type=data['bl_idname'])
 | 
			
		||||
        node = cls(new_bl_node, parent=tree)
 | 
			
		||||
 | 
			
		||||
        for p in node.parameters:
 | 
			
		||||
            setattr(node, p, data[p])
 | 
			
		||||
            setattr(node.bl_node, p, data[p])
 | 
			
		||||
 | 
			
		||||
        node.inputs = [Input.from_dict(ipt_data, node) for ipt_data in data['inputs'].values()]
 | 
			
		||||
        node.outputs = [Output.from_dict(opt_data, node) for opt_data in data['outputs'].values()]
 | 
			
		||||
        return node
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        """Export currrent Node to its dict representation.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            dict: Node dict representation.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        for prop_id in self.parameters:
 | 
			
		||||
 | 
			
		||||
            if not hasattr(self, prop_id):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            attr_value = getattr(self, prop_id)
 | 
			
		||||
            if attr_value is None:
 | 
			
		||||
                attr_value = None
 | 
			
		||||
 | 
			
		||||
            elif not isinstance(attr_value, (str, int, float, list, tuple)):
 | 
			
		||||
                attr_value = list(attr_value)
 | 
			
		||||
 | 
			
		||||
            self.data[prop_id] = attr_value
 | 
			
		||||
 | 
			
		||||
        self.data['id'] = self.id
 | 
			
		||||
        self.data['inputs'] = {ipt.id: ipt.to_dict() for ipt in self.inputs}
 | 
			
		||||
        self.data['outputs'] = {opt.id: opt.to_dict() for opt in self.outputs}
 | 
			
		||||
 | 
			
		||||
        return self.data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Link:
 | 
			
		||||
    """Blender Link abstraction."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bl_link, parent):
 | 
			
		||||
 | 
			
		||||
        self.bl_link = bl_link
 | 
			
		||||
        self.tree = parent
 | 
			
		||||
        self.id = hex(id(self.bl_link))
 | 
			
		||||
 | 
			
		||||
        self.input = self.bl_link.to_socket
 | 
			
		||||
        self.output = self.bl_link.from_socket
 | 
			
		||||
 | 
			
		||||
        self.data = {}
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
 | 
			
		||||
        self.data['id'] = self.id
 | 
			
		||||
 | 
			
		||||
        return self.data
 | 
			
		||||
							
								
								
									
										66
									
								
								core/node_tree.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								core/node_tree.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
import json
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from .node import Node, Link
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeTree:
 | 
			
		||||
    """Blender node tree abstraction."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bl_node_tree):
 | 
			
		||||
 | 
			
		||||
        self.bl_node_tree = bl_node_tree
 | 
			
		||||
 | 
			
		||||
        self.data = {}
 | 
			
		||||
        self.tmp_file = Path('/home/florentin.luce/Bureau/copy_nodes.json')
 | 
			
		||||
 | 
			
		||||
        self.nodes = [Node(n, parent=self) for n in self.bl_node_tree.nodes]
 | 
			
		||||
        self.links = [Link(l, parent=self) for l in self.bl_node_tree.links]
 | 
			
		||||
 | 
			
		||||
    def to_dict(self, select_only=False):
 | 
			
		||||
        """Convert all blender nodes and links inside the tree into a dictionnary.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            select_only (bool, optional): True to convert only selected nodes.
 | 
			
		||||
                Defaults to False.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            dict: Nodes and links as dict. 
 | 
			
		||||
        """
 | 
			
		||||
        self.data['nodes'] = {n.id: n.to_dict() for n in self.nodes if not select_only or (select_only and n.select)}
 | 
			
		||||
        self.data['links'] = [l.id for l in self.links]
 | 
			
		||||
 | 
			
		||||
        return self.data
 | 
			
		||||
 | 
			
		||||
    def ingest_dict(self, data):
 | 
			
		||||
        """From a Tree dict representation, create new nodes with their attributes.
 | 
			
		||||
        Then create a connection dict by comparing link id from inputs and outputs of each nodes.
 | 
			
		||||
        Use this dict to link nodes between each others.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            data (dict): Tree dict representation to generate nodes and links from.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        connections = {}
 | 
			
		||||
 | 
			
		||||
        self.data = data
 | 
			
		||||
 | 
			
		||||
        for node_id, node_data in self.data['nodes'].items():
 | 
			
		||||
 | 
			
		||||
            new_node = Node.from_dict(node_data, self.bl_node_tree)
 | 
			
		||||
            new_node.bl_node.select = True
 | 
			
		||||
 | 
			
		||||
            for ipt in new_node.inputs:
 | 
			
		||||
                if ipt.is_linked:
 | 
			
		||||
                    connections.setdefault(ipt.link, {})['input'] = ipt.bl_input
 | 
			
		||||
 | 
			
		||||
            for opt in new_node.outputs:
 | 
			
		||||
                if opt.is_linked:
 | 
			
		||||
                    for link in opt.link:
 | 
			
		||||
                        connections.setdefault(link, {})['output'] = opt.bl_output
 | 
			
		||||
 | 
			
		||||
        for link_id in self.data['links']:
 | 
			
		||||
            ipt = connections[link_id]['input']
 | 
			
		||||
            opt = connections[link_id]['output']
 | 
			
		||||
 | 
			
		||||
            self.bl_node_tree.links.new(ipt, opt)
 | 
			
		||||
							
								
								
									
										107
									
								
								core/sockets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								core/sockets.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Socket:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bl_socket, tree):
 | 
			
		||||
 | 
			
		||||
        self.tree = tree
 | 
			
		||||
        self.bl_socket = bl_socket
 | 
			
		||||
        self.data = {}
 | 
			
		||||
 | 
			
		||||
        self.id = hex(id(bl_socket))
 | 
			
		||||
        self.identifier = bl_socket.identifier
 | 
			
		||||
        self.is_linked = bl_socket.is_linked
 | 
			
		||||
 | 
			
		||||
        self._value = bl_socket.default_value
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
 | 
			
		||||
        if not isinstance(self._value, (str, int, float, bool)):
 | 
			
		||||
            self._value = [v for v in self._value]
 | 
			
		||||
 | 
			
		||||
        return self._value
 | 
			
		||||
 | 
			
		||||
    @value.setter
 | 
			
		||||
    def value(self, v):
 | 
			
		||||
        self.bl_socket.default_value = v
 | 
			
		||||
        self._value = v
 | 
			
		||||
        return self._value
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        self.data['id'] = self.id
 | 
			
		||||
        self.data['value'] = self.value
 | 
			
		||||
        self.data['identifier'] = self.identifier
 | 
			
		||||
        self.data['is_linked'] = self.is_linked
 | 
			
		||||
        self.data['link'] = self.get_link()
 | 
			
		||||
        return self.data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Input(Socket):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bl_input, tree):
 | 
			
		||||
        super().__init__(bl_input, tree)
 | 
			
		||||
 | 
			
		||||
        self.bl_input = bl_input
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_dict(cls, data, node):
 | 
			
		||||
 | 
			
		||||
        for bl_ipt in node.bl_node.inputs:
 | 
			
		||||
 | 
			
		||||
            if bl_ipt.identifier != data['identifier']:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            new_ipt = cls(bl_ipt, node.tree)
 | 
			
		||||
 | 
			
		||||
            for k, v in data.items():
 | 
			
		||||
                setattr(new_ipt, k, v)
 | 
			
		||||
 | 
			
		||||
            return new_ipt
 | 
			
		||||
 | 
			
		||||
    def get_link(self):
 | 
			
		||||
 | 
			
		||||
        if not self.is_linked:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        for ipt_link in self.bl_input.links:
 | 
			
		||||
            for tree_link in self.tree.links:
 | 
			
		||||
                if ipt_link == tree_link.bl_link:
 | 
			
		||||
                    return tree_link.id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Output(Socket):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bl_output, tree):
 | 
			
		||||
        super().__init__(bl_output, tree)
 | 
			
		||||
 | 
			
		||||
        self.bl_output = bl_output
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_dict(cls, data, node):
 | 
			
		||||
 | 
			
		||||
        for bl_opt in node.bl_node.outputs:
 | 
			
		||||
 | 
			
		||||
            if bl_opt.identifier != data['identifier']:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            new_opt = cls(bl_opt, node.tree)
 | 
			
		||||
 | 
			
		||||
            for k, v in data.items():
 | 
			
		||||
                setattr(new_opt, k, v)
 | 
			
		||||
 | 
			
		||||
            return new_opt
 | 
			
		||||
 | 
			
		||||
    def get_link(self):
 | 
			
		||||
 | 
			
		||||
        links = []
 | 
			
		||||
 | 
			
		||||
        if not self.is_linked:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        for opt_link in self.bl_output.links:
 | 
			
		||||
            for tree_link in self.tree.links:
 | 
			
		||||
                if opt_link == tree_link.bl_link:
 | 
			
		||||
                    links.append(tree_link.id)
 | 
			
		||||
 | 
			
		||||
        return links
 | 
			
		||||
							
								
								
									
										55
									
								
								operators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								operators.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
"""
 | 
			
		||||
This module contains all addons operators
 | 
			
		||||
 | 
			
		||||
:author: Autour de Minuit
 | 
			
		||||
:maintainers: Florentin LUCE
 | 
			
		||||
:date: 2024
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import bpy
 | 
			
		||||
 | 
			
		||||
from node_kit.core.node_tree import NodeTree
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NODEKIT_OT_copy(bpy.types.Operator):
 | 
			
		||||
    bl_idname = 'node_kit.copy_node_tree'
 | 
			
		||||
    bl_label = 'Copy node tree'
 | 
			
		||||
    bl_options = {'REGISTER', 'UNDO'}
 | 
			
		||||
 | 
			
		||||
    select_only: bpy.props.BoolProperty(default=False)
 | 
			
		||||
 | 
			
		||||
    def execute(self, context):
 | 
			
		||||
 | 
			
		||||
        tree = NodeTree(context.space_data.node_tree)
 | 
			
		||||
        context.window_manager.clipboard = json.dumps(tree.to_dict(select_only=self.select_only))
 | 
			
		||||
 | 
			
		||||
        return {'FINISHED'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NODEKIT_OT_paste(bpy.types.Operator):
 | 
			
		||||
    bl_idname = 'node_kit.paste_node_tree'
 | 
			
		||||
    bl_label = 'Paste node tree'
 | 
			
		||||
 | 
			
		||||
    def execute(self, context):
 | 
			
		||||
 | 
			
		||||
        tree = NodeTree(context.space_data.node_tree)
 | 
			
		||||
        tree.ingest_dict(json.loads(context.window_manager.clipboard))
 | 
			
		||||
 | 
			
		||||
        return {'FINISHED'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
classes = (
 | 
			
		||||
    NODEKIT_OT_copy,
 | 
			
		||||
    NODEKIT_OT_paste,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register():
 | 
			
		||||
    for c in classes:
 | 
			
		||||
        bpy.utils.register_class(c)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def unregister():
 | 
			
		||||
    for c in reversed(classes):
 | 
			
		||||
        bpy.utils.unregister_class(c)
 | 
			
		||||
							
								
								
									
										43
									
								
								ui.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								ui.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
			
		||||
"""
 | 
			
		||||
This module contains blender UI elements
 | 
			
		||||
 | 
			
		||||
:author: Autour de Minuit
 | 
			
		||||
:maintainers: Florentin LUCE
 | 
			
		||||
:date: 2024
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import bpy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NODEKIT_MT_node_kit(bpy.types.Menu):
 | 
			
		||||
    bl_label = "Node kit"
 | 
			
		||||
 | 
			
		||||
    def draw(self, context):
 | 
			
		||||
        layout = self.layout
 | 
			
		||||
 | 
			
		||||
        layout.operator('node_kit.copy_node_tree', text='Copy node tree', icon='COPYDOWN')
 | 
			
		||||
        layout.operator('node_kit.paste_node_tree', text='Paste node tree', icon='PASTEDOWN')
 | 
			
		||||
        layout.separator()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
classes = (
 | 
			
		||||
    NODEKIT_MT_node_kit,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def draw_menu(self, context):
 | 
			
		||||
    self.layout.menu('NODEKIT_MT_node_kit')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register():
 | 
			
		||||
    for c in classes:
 | 
			
		||||
        bpy.utils.register_class(c)
 | 
			
		||||
 | 
			
		||||
    bpy.types.NODE_MT_editor_menus.append(draw_menu)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def unregister():
 | 
			
		||||
    for c in reversed(classes):
 | 
			
		||||
        bpy.utils.unregister_class(c)
 | 
			
		||||
 | 
			
		||||
    bpy.types.NODE_MT_editor_menus.remove(draw_menu)
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user