From 4fb9bb8c69f91ce8fa19bbf90e9ad8bdafc0bea3 Mon Sep 17 00:00:00 2001 From: "florentin.luce" Date: Thu, 22 Feb 2024 10:09:34 +0100 Subject: [PATCH] Create addon with copy paste operators --- __init__.py | 40 +++++++++++++++++ core/__init__.py | 0 core/node.py | 97 +++++++++++++++++++++++++++++++++++++++++ core/node_tree.py | 66 ++++++++++++++++++++++++++++ core/sockets.py | 107 ++++++++++++++++++++++++++++++++++++++++++++++ operators.py | 55 ++++++++++++++++++++++++ ui.py | 43 +++++++++++++++++++ 7 files changed, 408 insertions(+) create mode 100644 __init__.py create mode 100644 core/__init__.py create mode 100644 core/node.py create mode 100644 core/node_tree.py create mode 100644 core/sockets.py create mode 100644 operators.py create mode 100644 ui.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d7f0758 --- /dev/null +++ b/__init__.py @@ -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() diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/node.py b/core/node.py new file mode 100644 index 0000000..c07e638 --- /dev/null +++ b/core/node.py @@ -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 diff --git a/core/node_tree.py b/core/node_tree.py new file mode 100644 index 0000000..7337baf --- /dev/null +++ b/core/node_tree.py @@ -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) diff --git a/core/sockets.py b/core/sockets.py new file mode 100644 index 0000000..1d6408a --- /dev/null +++ b/core/sockets.py @@ -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 diff --git a/operators.py b/operators.py new file mode 100644 index 0000000..1489b17 --- /dev/null +++ b/operators.py @@ -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) diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..8f610c3 --- /dev/null +++ b/ui.py @@ -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)