Compare commits

...

6 Commits

10 changed files with 58 additions and 444 deletions

View File

View File

@ -1,211 +0,0 @@
import bpy
from mathutils import Color, Vector
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 = []
self._parent = None
self._scene = None
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]
@property
def parent(self):
"""Get the Node from all the other nodes in the tree checking that the
parent of its blender node is the same as the blender node we are comparing.
Returns:
Node: Node parent.
"""
if self._parent:
return self._parent
# if blender node doesn't have a parent
if not self.bl_node.parent:
self._parent = None
return self._parent
for node in self.tree.nodes:
if node.bl_node == self.bl_node.parent:
self._parent = node
return self._parent
@parent.setter
def parent(self, value):
"""Set the Node parent, using the python object, it's id or the blender node.
Args:
value (Node|str|bpy.types.Node): Node, id or blender node to set as parent.
"""
# Node object case
if isinstance(value, Node):
self._parent = value
# Node id case
elif isinstance(value, str) and value.startswith("0x"):
for node in self.tree.nodes:
if node.id == value:
self._parent = node
else:
print("Cannot find parent")
# blender node case
elif isinstance(value, bpy.types.Node):
for node in self.tree.nodes:
if node.bl_node == value:
self._parent = node
if self._parent:
self.bl_node.parent = self._parent.bl_node
@classmethod
def from_blender_node(cls, bl_node, tree):
"""Instanciate an abstract class based of the blender node idname.
Args:
bl_node (bpy.types.Node): Blender Node To create abstraction from.
tree (NodeTree): Node tree object node belongs to.
Returns:
Node: Node abstract according to the blender node type.
"""
if bl_node.bl_idname == "CompositorNodeRLayers":
return RenderLayersNode(bl_node, tree)
elif bl_node.bl_idname == "CompositorNodeValToRGB":
return ColorRampNode(bl_node, tree)
else:
return cls(bl_node, tree)
@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.bl_node_tree.nodes.new(type=data["bl_idname"])
node = cls.from_blender_node(new_bl_node, tree)
node.id = data["id"]
for p in node.parameters:
setattr(node, p, data[p])
# set attribute on the blender node only if correct type is retrieve
if p not in ("parent", "scene"):
setattr(node.bl_node, p, getattr(node, 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 dump(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 isinstance(attr_value, Node):
attr_value = attr_value.id
elif isinstance(attr_value, (Color, Vector)):
attr_value = list(attr_value)
self.data[prop_id] = attr_value
self.data["id"] = self.id
self.data["inputs"] = {ipt.id: ipt.dump() for ipt in self.inputs}
self.data["outputs"] = {opt.id: opt.dump() for opt in self.outputs}
return self.data
class RenderLayersNode(Node):
"""Blender Render Layers Node abstraction"""
@property
def scene(self):
"""Get the name of the scene used by the node.
Returns:
str: scene name.
"""
if self._scene:
return self._scene.name
@scene.setter
def scene(self, value):
"""Set the blender scene using the bpy Scene object or its name.
Args:
value (str|bpy.types.Scene): scene name or scene object to set the node.
"""
if isinstance(value, str):
self._scene = bpy.data.scenes[value]
elif isinstance(value, bpy.types.Scene):
self._scene = value
if self._scene:
self.bl_node.scene = self._scene
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 dump(self):
self.data["id"] = self.id
return self.data

View File

@ -1,71 +0,0 @@
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.links = [Link(lnk, parent=self) for lnk in self.bl_node_tree.links]
self.nodes = []
for n in self.bl_node_tree.nodes:
self.nodes.append(Node.from_blender_node(n, self))
def dump(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.dump()
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 load(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)
self.nodes.append(new_node)
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)

View File

@ -1,97 +0,0 @@
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 = None
if hasattr(bl_socket, "default_value"):
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

View File

@ -1,41 +1,15 @@
from __future__ import annotations from __future__ import annotations
import json
from copy import copy from copy import copy
from os.path import abspath from os.path import abspath
from pprint import pprint
import bpy import bpy
from .. import utils from . import utils
format_token = "#FMT:NODE_KIT#"
def dump_nkit_format(data: str) -> str:
return format_token + json.dumps(data)
def parse_nkit_format(data: str) -> str | None:
if data.startswith(format_token):
print(data[len(format_token):])
return json.loads(data[len(format_token):])
return None
def get_default(prop: bpy.types.Property):
"""Get the default value of a Blender property"""
if getattr(prop, "is_array", False):
return list(prop.default_array)
elif hasattr(prop, "default"):
return prop.default
def get_dumper(bl_object: bpy.types.bpy_struct) -> type[Dumper]: def get_dumper(bl_object: bpy.types.bpy_struct) -> type[Dumper]:
"""Get the corresponding dumper for a given Blender object, or its closest base type using its MRO""" """Get the closest corresponding dumper for a given Blender object using its MRO"""
for cls in bl_object.__class__.mro(): for cls in bl_object.__class__.mro():
dumper_map = DumperRegistry().dumper_map dumper_map = DumperRegistry().dumper_map
if cls in dumper_map: if cls in dumper_map:
@ -44,13 +18,9 @@ def get_dumper(bl_object: bpy.types.bpy_struct) -> type[Dumper]:
# Fallback to base Dumper if no matches are found # Fallback to base Dumper if no matches are found
return Dumper return Dumper
def get_current_node_tree(data):
if data.get("_new", {}).get("type") == "GeometryNodeTree":
return bpy.context.object.modifiers.active.node_group
def dump_nodes(nodes: list[bpy.types.Node]): def dump_nodes(nodes: list[bpy.types.Node]):
"""Generic Recursive Dump, convert any object into a dict""" """Generic recursive dump, convert nodes into a dict"""
Dumper.pointers.clear() # TODO: Bad global Dumper.pointers.clear() # TODO: Bad global
data = [dump_node(node) for node in nodes] data = [dump_node(node) for node in nodes]
@ -59,19 +29,15 @@ def dump_nodes(nodes: list[bpy.types.Node]):
return data return data
def dump_node(node: bpy.types.Node): def dump_node(node: bpy.types.Node):
dumper = get_dumper(node) dumper = get_dumper(node)
return dumper.dump(node) # TODO: Break the recursivity, clear things up return dumper.dump(node)
def load_nodes(data, node_tree=None): def load_nodes(data, node_tree):
"""Generic Load to create an object from a dict""" """Load/Dump nodes into a specific node tree"""
Dumper.pointers.clear() Dumper.pointers.clear()
# print(Dumper.pointers)
if node_tree is None:
node_tree = get_current_node_tree(data)
dumper = get_dumper(node_tree) dumper = get_dumper(node_tree)
dumper.load(data, node_tree) dumper.load(data, node_tree)
@ -79,13 +45,6 @@ def load_nodes(data, node_tree=None):
Dumper.pointers.clear() Dumper.pointers.clear()
def set_attribute(bl_object, attr, value):
try:
setattr(bl_object, attr, value)
except Exception as e:
print(e)
class Dumper: class Dumper:
pointers = {} pointers = {}
includes = [] includes = []
@ -144,7 +103,7 @@ class Dumper:
value = cls.pointers[value] value = cls.pointers[value]
elif value is None: elif value is None:
set_attribute(bl_object, key, value) utils.set_bl_attribute(bl_object, key, value)
else: else:
bl_type = prop.fixed_type.bl_rna.type_recast() bl_type = prop.fixed_type.bl_rna.type_recast()
@ -163,14 +122,14 @@ class Dumper:
value = attr value = attr
if not prop.is_readonly: if not prop.is_readonly:
set_attribute(bl_object, key, value) utils.set_bl_attribute(bl_object, key, value)
# Some coll needs a manual update like curve mapping # Some coll needs a manual update like curve mapping
if hasattr(attr, "update"): if hasattr(attr, "update"):
attr.update() attr.update()
elif not prop.is_readonly: elif not prop.is_readonly:
set_attribute(bl_object, key, value) utils.set_bl_attribute(bl_object, key, value)
continue continue
# return bl_object # return bl_object
@ -257,7 +216,7 @@ class PropCollection(Dumper):
params = value["_new"] params = value["_new"]
else: else:
params = { params = {
k: value.get(k, get_default(v)) k: value.get(k, utils.get_bl_default(v))
for k, v in new_func.parameters.items()[:-1] for k, v in new_func.parameters.items()[:-1]
} }
@ -557,7 +516,7 @@ class Points(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
new_func = coll.bl_rna.functions["new"] new_func = coll.bl_rna.functions["new"]
params = {k: get_default(v) + 1.1 for k, v in new_func.parameters.items()[:-1]} params = {k: utils.get_bl_default(v) + 1.1 for k, v in new_func.parameters.items()[:-1]}
# Match the same number of elements in collection # Match the same number of elements in collection
if len(values) > len(coll): if len(values) > len(coll):

17
formats.py Normal file
View File

@ -0,0 +1,17 @@
import json
format_token = "#FMT:NODE_KIT#"
def dump_nkit_format(data: str) -> str:
return format_token + json.dumps(data)
def parse_nkit_format(data: str) -> str | None:
if data.startswith(format_token):
print(data[len(format_token):])
return json.loads(data[len(format_token):])
return None

View File

@ -14,9 +14,10 @@ import bpy
from bpy.props import BoolProperty, EnumProperty from bpy.props import BoolProperty, EnumProperty
from bpy.types import Operator from bpy.types import Operator
from .core.dumper import dump_nodes, load_nodes, dump_nkit_format, parse_nkit_format from .dumper import dump_nodes, load_nodes
from .core.node_utils import remap_node_group_duplicates from .node_utils import remap_node_group_duplicates
from .core.pack_nodes import combine_objects, extract_objects from .pack_nodes import combine_objects, extract_objects
from .formats import dump_nkit_format, parse_nkit_format
class NODEKIT_OT_copy(Operator): class NODEKIT_OT_copy(Operator):
@ -36,8 +37,6 @@ class NODEKIT_OT_copy(Operator):
), ),
} }
pprint(ntree_data)
context.window_manager.clipboard = dump_nkit_format(ntree_data) context.window_manager.clipboard = dump_nkit_format(ntree_data)
self.report({"INFO"}, f"Copied {len(selected_nodes)} selected nodes to system clipboard") self.report({"INFO"}, f"Copied {len(selected_nodes)} selected nodes to system clipboard")
@ -52,9 +51,7 @@ class NODEKIT_OT_copy_tree(Operator):
def execute(self, context): def execute(self, context):
ntree = context.space_data.edit_tree ntree = context.space_data.edit_tree
ntree_data = dump_nodes(ntree) ntree_data = dict(ntree)
pprint(ntree_data)
context.window_manager.clipboard = dump_nkit_format(ntree_data) context.window_manager.clipboard = dump_nkit_format(ntree_data)
@ -78,7 +75,8 @@ class NODEKIT_OT_paste(Operator):
class NODEKIT_OT_remap_node_group_duplicates(Operator): class NODEKIT_OT_remap_node_group_duplicates(Operator):
bl_idname = "node_kit.remap_node_group_duplicates" bl_idname = "node_kit.remap_node_group_duplicates"
bl_label = "Remap Node Groups Duplicates" bl_label = "Clean Node Groups Duplicates"
bl_description = "Remap Node Groups duplicates to the latest imported version"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty( selection: EnumProperty(
@ -139,7 +137,8 @@ class NODEKIT_OT_remap_node_group_duplicates(Operator):
class NODEKIT_OT_update_nodes(Operator): class NODEKIT_OT_update_nodes(Operator):
bl_idname = "node_kit.update_nodes" bl_idname = "node_kit.update_nodes"
bl_label = "Update Nodes" bl_label = "Update Nodes from Library"
bl_description = "Overrides node group using the latest version from Asset Library"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty( selection: EnumProperty(
@ -249,7 +248,8 @@ class NODEKIT_OT_update_nodes(Operator):
class NODEKIT_OT_pack_nodes(Operator): class NODEKIT_OT_pack_nodes(Operator):
bl_idname = "node_kit.pack_nodes" bl_idname = "node_kit.pack_nodes"
bl_label = "Pack Nodes" bl_label = "Pack Modifiers as Nodes"
bl_description = "Pack Geometry Nodes modifiers stack as a single node tree"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
@ -259,7 +259,8 @@ class NODEKIT_OT_pack_nodes(Operator):
class NODEKIT_OT_unpack_nodes(Operator): class NODEKIT_OT_unpack_nodes(Operator):
bl_idname = "node_kit.unpack_nodes" bl_idname = "node_kit.unpack_nodes"
bl_label = "Unpack Nodes" bl_label = "Unpack Nodes as Modifiers"
bl_description = "Unpack node tree as Geometry Nodes modifiers"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):

View File

@ -1,6 +1,22 @@
import bpy
def all_subclasses(cls): def all_subclasses(cls):
return set(cls.__subclasses__()).union( return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)] [s for c in cls.__subclasses__() for s in all_subclasses(c)]
) )
def get_bl_default(prop: bpy.types.Property):
"""Get the default value of a Blender property"""
if getattr(prop, "is_array", False):
return list(prop.default_array)
elif hasattr(prop, "default"):
return prop.default
def set_bl_attribute(bl_object, attr, value):
try:
setattr(bl_object, attr, value)
except Exception as e:
print(e)