350 lines
14 KiB
Python
350 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from copy import copy
|
|
from dataclasses import dataclass
|
|
from os.path import abspath
|
|
from typing import Any
|
|
|
|
import bpy
|
|
|
|
from . import utils
|
|
from . utils import BlenderProperty
|
|
|
|
|
|
def serialize_selected_nodes_from_node_tree(node_tree: bpy.types.NodeTree):
|
|
"""Serialize the selected nodes from a node tree"""
|
|
selected_nodes = [node for node in node_tree.nodes if node.select]
|
|
selected_links = [link for link in node_tree.links if link.from_node.select
|
|
and link.to_node.select]
|
|
|
|
bl_pointers = {}
|
|
nodes_data = [Serializer.serialize(node, bl_pointers) for node in selected_nodes]
|
|
links_data = [Serializer.serialize(link, bl_pointers) for link in selected_links]
|
|
|
|
# Only serialize selected nodes and their links
|
|
# Data format corresponds to the bpy.types.NodeTree properties that we want to (de)serialize
|
|
ntree_data = {
|
|
"nodes": nodes_data,
|
|
"links": links_data,
|
|
}
|
|
|
|
return ntree_data
|
|
|
|
|
|
def deserialize_nodes_into_node_tree(data: dict, node_tree: bpy.types.NodeTree):
|
|
"""Deserialize node data into a specific node tree"""
|
|
bl_pointers = {}
|
|
Serializer.deserialize(data, node_tree, bl_pointers)
|
|
|
|
|
|
# TODO: Sub serialize function where the isinstance is set to the default number of things.
|
|
|
|
# TODO: Collection is not handled as a class anymore, handle it manually
|
|
|
|
class Serializer(ABC):
|
|
"""
|
|
Base Serializer class.
|
|
`bl_pointers_ref` corresponds to a mutable dict passed through serialize/deserialize
|
|
functions, containing a map of Blender pointers IDs and their corresponding objects.
|
|
"""
|
|
# Whitelisted properties, applied after the blacklist
|
|
prop_whitelist = None
|
|
# Properties that are excluded from (de)serialization, in addition to any bl_* properties
|
|
prop_blacklist = ("rna_type", "id_data", "depsgraph")
|
|
|
|
serializer_map = {}
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def construct_bl_object(cls, data: dict):
|
|
"""Abstract method to construct a Serializer's specific Blender Object"""
|
|
print("DEBUG: construct_bl_object called on Base Serializer, shouldn't happen")
|
|
return None
|
|
|
|
# --- Serialization ---
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: bpy.types.bpy_struct | Any, bl_pointers_ref: dict) -> Any:
|
|
if not isinstance(obj, bpy.types.bpy_struct):
|
|
# Primitive type, return directly
|
|
return obj
|
|
|
|
"""Resolve which Serializer class to use"""
|
|
serializer = cls.get_serializer(obj)
|
|
|
|
return serializer.serialize_obj(obj, bl_pointers_ref)
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def serialize_obj(cls, obj: bpy.types.bpy_struct | Any, bl_pointers_ref: dict) -> dict:
|
|
"""Base serialization method, overridden by subclasses"""
|
|
|
|
# Early recursive return case
|
|
# TODO: Ported as is, check the heuristics, (if there are more or attributes type to add)
|
|
if isinstance(obj, (str, int, float, dict, list, type(None))):
|
|
return obj
|
|
|
|
# Returned data, tracks the pointer for later re-assignments during deserialization
|
|
data = {"_kit_ptr": obj.as_pointer()}
|
|
bl_pointers_ref[obj.as_pointer()] = obj
|
|
|
|
# Iterate over all *filtered* properties found in the object
|
|
for bl_prop in cls.get_serialized_properties(obj):
|
|
# Do not store default values nor read-only properties
|
|
if (array := getattr(bl_prop.rep, "default_array", None)) and bl_prop.attr == array:
|
|
continue
|
|
if isinstance(bl_prop.attr, (str, int, float)) and bl_prop.attr == bl_prop.rep.default:
|
|
continue
|
|
if obj.is_property_readonly(bl_prop.rep.identifier):
|
|
continue
|
|
|
|
print(type(bl_prop.attr))
|
|
|
|
# Serialize each property
|
|
data[bl_prop.rep.identifier] = cls.serialize_property(bl_prop, bl_pointers_ref)
|
|
|
|
return data
|
|
|
|
@classmethod
|
|
def serialize_property(cls, bl_prop: BlenderProperty, bl_pointers_ref: dict) -> Any:
|
|
"""Serialize Blender property, special cases for arrays/collections/pointers"""
|
|
# Property array case
|
|
if getattr(bl_prop.rep, "is_array", False):
|
|
# Contained in BoolProperty, IntProperty and FloatProperty
|
|
prop_array = []
|
|
for item in bl_prop.attr:
|
|
assert isinstance(item, (bool, int, float)) # TODO: For development, replace by list comprehension later
|
|
prop_array.append(item)
|
|
|
|
return prop_array
|
|
|
|
# Collection case
|
|
if isinstance(bl_prop.attr, bpy.types.bpy_prop_collection):
|
|
collection = bl_prop.attr
|
|
if not collection:
|
|
return []
|
|
|
|
values = [cls.serialize(sub_prop) for sub_prop in collection]
|
|
|
|
# TODO: Check why the original code has a None check
|
|
return [v for v in values if v is not None]
|
|
|
|
# Pointer case
|
|
if bl_prop.rep.type == "POINTER" and bl_prop.attr:
|
|
# Property points to another object, stores it ptr/deref value in our pointer table
|
|
ptr = bl_prop.attr.as_pointer()
|
|
|
|
if ptr in bl_pointers_ref:
|
|
return ptr
|
|
|
|
bl_pointers_ref[ptr] = bl_prop.attr
|
|
|
|
return cls.serialize(bl_prop.rep, bl_pointers_ref)
|
|
|
|
# --- Deserialization ---
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def deserialize(cls, data: dict, target_obj: bpy.types.bpy_struct, bl_pointers_ref: dict):
|
|
"""
|
|
Base deserialization method.
|
|
Deserialize data into a specific Blender object, creating sub-objects as needed.
|
|
Partial data may be provided, in which case, fields not specified will be left to default.
|
|
"""
|
|
|
|
if (kit_ptr := data.get("_kit_ptr", None)):
|
|
bl_pointers_ref[kit_ptr] = target_obj
|
|
|
|
data_to_deserialize = cls.get_data_to_deserialize(data, target_obj)
|
|
|
|
for stored_key, stored_value in data_to_deserialize:
|
|
if stored_key.startswith("_kit") or stored_key not in target_obj.bl_rna.properties:
|
|
continue
|
|
|
|
target_bl_prop = BlenderProperty(rep=target_obj.bl_rna.properties[stored_key],
|
|
attr=getattr(target_obj, stored_key))
|
|
|
|
# Collection case
|
|
# Unlike serialization, there's no property array case, as they are just directly assigned
|
|
if isinstance(target_bl_prop.attr, bpy.types.bpy_prop_collection):
|
|
cls.deserialize_collection(stored_value, target_bl_prop.attr, bl_pointers_ref)
|
|
continue
|
|
|
|
value_to_set = stored_value
|
|
|
|
# Pointer case
|
|
# Dereference the value if its already present in the pointers_ref map
|
|
if target_bl_prop.rep.type == "POINTER":
|
|
value_to_set = cls.deserialize_pointer(stored_value, target_bl_prop.attr, bl_pointers_ref)
|
|
|
|
# Skip setting the property if it's read-only
|
|
if target_bl_prop.rep.is_readonly:
|
|
continue
|
|
|
|
# Assign the property
|
|
setattr(target_obj, stored_key, value_to_set)
|
|
|
|
# If supported, update the Blender property after setting it
|
|
if hasattr(target_bl_prop.attr, "update"):
|
|
target_bl_prop.attr.update()
|
|
|
|
@classmethod
|
|
def deserialize_collection(cls, stored_value: Any, bl_coll: bpy.types.bpy_prop_collection, bl_pointers_ref: dict):
|
|
# Static collection case
|
|
if not hasattr(bl_coll, "new"):
|
|
cls.sub_deserialize(stored_value, bl_coll, bl_pointers_ref)
|
|
return
|
|
|
|
# We need to call the collection "new" function, parse and construct its parameters
|
|
new_func = bl_coll.bl_rna.functions["new"]
|
|
|
|
for i, value in enumerate(stored_value):
|
|
# Using a dictionary of {parameter: parameter_value}
|
|
default_new_func_params = {
|
|
k: value.get(k, utils.get_bl_default(v))
|
|
for k, v in new_func.parameters.items()[:-1]
|
|
}
|
|
new_func_params = value.get("_kit_new_params", default_new_func_params)
|
|
|
|
solved_all_pointers = True
|
|
for param in bl_coll.bl_rna.functions["new"].parameters:
|
|
if param.identifier not in new_func_params or param.type != "POINTER":
|
|
continue
|
|
|
|
pointer_id = param[param.identifier]
|
|
if bl_object := bl_pointers_ref.get(pointer_id):
|
|
new_func_params[param.identifier] = bl_object
|
|
else:
|
|
print(f"No pointer found for param {param.identifier} of new function of {bl_coll}")
|
|
solved_all_pointers = False
|
|
|
|
# Bail out if we fail to solve all pointers (TODO: I'm guessing this causes a runtimerror, but double check)
|
|
if not solved_all_pointers:
|
|
continue
|
|
|
|
print("Calling BL collection new with the following parameters")
|
|
print(new_func_params)
|
|
|
|
# Creates a collection item, type from the collection type, no need to manually construct
|
|
collection_item = bl_coll.new(**new_func_params)
|
|
deserializer = cls.get_serializer(collection_item)
|
|
|
|
# Recursively deserialize into the newly constructured object
|
|
cls.deserialize(value, collection_item, bl_pointers_ref)
|
|
|
|
# Old code guarded by a RuntimeError before, investigate later
|
|
# Static collection case, would need to check how to detect this.
|
|
collection_item = bl_coll[i]
|
|
|
|
# TODO: The target_bl_prop_attr terminology is unclear
|
|
@classmethod
|
|
def deserialize_pointer(cls, stored_value: Any, target_bl_prop_attr: bpy.types.bpy_struct, bl_pointers_ref: dict):
|
|
if stored_value is None:
|
|
return None
|
|
|
|
# Actual existing pointer, dereference and return
|
|
if isinstance(stored_value, int):
|
|
if stored_value not in bl_pointers_ref:
|
|
print("DEBUG: Pointer reference hasn't been loaded yet")
|
|
# Obtain a reference to a previously dereferenced object
|
|
return bl_pointers_ref[stored_value]
|
|
|
|
# Pointer doesn't exist yet, create it if it doesn't exist yet, store it, and return its object
|
|
deserializer = cls.get_serializer(target_bl_prop_attr)
|
|
|
|
# Create the Blender object if it doesn't exist yet
|
|
if target_bl_prop_attr is None:
|
|
target_bl_prop_attr = cls.construct_bl_object(stored_value)
|
|
|
|
# Recursively deserialize into the target object
|
|
deserializer.deserialize(stored_value, target_bl_prop_attr, bl_pointers_ref)
|
|
|
|
bl_pointers_ref[stored_value["_kit_ptr"]] = target_bl_prop_attr
|
|
|
|
return target_bl_prop_attr
|
|
|
|
@classmethod
|
|
def get_data_to_deserialize(cls, data: dict, target_obj: bpy.types.bpy_struct=None):
|
|
props_to_deserialize = cls.get_serialized_properties(target_obj)
|
|
|
|
sorted_data = sorted(
|
|
data.items(), key=lambda x: props_to_deserialize.index(x[0]) if x[0] in props_to_deserialize else 0
|
|
)
|
|
|
|
return sorted_data
|
|
|
|
# --- Getters for sub-serializers ---
|
|
|
|
@classmethod
|
|
def get_serializer_map(cls) -> dict[type[bpy.types.bpy_struct], type[Serializer]]:
|
|
"""Get the serializer map, stored in a class variable for simple caching"""
|
|
if not cls.serializer_map:
|
|
for subclass in utils.all_subclasses(Serializer):
|
|
assert hasattr(subclass, "bl_type")
|
|
cls.serializer_map[subclass.bl_type] = subclass
|
|
|
|
return cls.serializer_map
|
|
|
|
@classmethod
|
|
def get_serializer(cls, bl_object: bpy.types.bpy_struct) -> type[Serializer]:
|
|
"""Get the closest corresponding serializer for a given Blender object using its MRO"""
|
|
serializer_map = cls.get_serializer_map()
|
|
|
|
bl_type = type(bl_object.bl_rna.type_recast())
|
|
|
|
for bl_parents in bl_type.mro():
|
|
if bl_parents in serializer_map:
|
|
return serializer_map[bl_parents]
|
|
|
|
# Fallback to base Serializer if no matches are found
|
|
return Serializer
|
|
|
|
# --- Properties to (de)serialize ---
|
|
|
|
@classmethod
|
|
def get_serialized_properties(cls, obj: bpy.types.bpy_struct | Any):
|
|
serialized_properties: list[BlenderProperty] = [
|
|
BlenderProperty(rep=prop, attr=getattr(obj, prop.identifier))
|
|
for prop in obj.bl_rna.properties
|
|
if not prop.identifier.startswith("bl_") # Exclude internal Blender properties
|
|
and prop.identifier not in cls.prop_blacklist # Additional blacklist filtering
|
|
]
|
|
|
|
if cls.prop_whitelist: # Additional whitelist, applied after the blacklist
|
|
serialized_properties: list[BlenderProperty] = [
|
|
prop for prop in serialized_properties
|
|
if prop.rep.identifier in cls.prop_whitelist
|
|
]
|
|
|
|
return serialized_properties
|
|
|
|
# class NodeSocket(Serializer):
|
|
# bl_type = bpy.types.NodeSocket
|
|
# prop_blacklist = Serializer.prop_blacklist + (
|
|
# "node",
|
|
# "links",
|
|
# "display_shape",
|
|
# "link_limit",
|
|
# )
|
|
|
|
# @classmethod
|
|
# def serialize(cls, socket_obj: bpy.types.NodeSocket, _: dict) -> dict:
|
|
# if socket_obj.is_unavailable:
|
|
# return None
|
|
|
|
# return super().serialize(socket_obj)
|
|
|
|
class NodeSerializer(Serializer):
|
|
bl_type = bpy.types.Node
|
|
|
|
@classmethod
|
|
def construct_bl_object(cls, data: dict):
|
|
|
|
return super().construct_bl_object(data)
|
|
|
|
@classmethod
|
|
def serialize(cls, obj, bl_pointers_ref):
|
|
|
|
return super().serialize(obj, bl_pointers_ref)
|