Create addon with copy paste operators
parent
9db9538172
commit
4fb9bb8c69
|
@ -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,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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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…
Reference in New Issue