node_kit/core/dumper.py

788 lines
22 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

import bpy
import mathutils
from pprint import pprint
import json
import itertools
from copy import copy
from os.path import abspath
def get_default(prop):
"""Get the default value of a bl property"""
if getattr(prop, 'is_array', False):
return list(prop.default_array)
elif hasattr(prop, 'default'):
return prop.default
def get_dumper(bl_object, fallback=None):
"""Find the right dumper type by checking inheritance"""
for dp in dumpers:
if isinstance(bl_object, dp.bl_type):
return dp
return fallback or Dumper
def get_bl_object(data):
"""Find the bl object for loading data into it depending on the type and the context"""
if data.get('_new', {}).get('type') == 'GeometryNodeTree':
return bpy.context.object.modifiers.active.node_group
def dump(ob):
"""Generic Recursive Dump, convert any object into a dict"""
Dumper.pointers.clear()
if isinstance(ob, (list, tuple)):
data = [get_dumper(o).dump(o) for o in ob]
else:
data = get_dumper(ob).dump(ob)
Dumper.pointers.clear()
return data
def load(data, bl_object=None):
"""Generic Load to create an object from a dict"""
Dumper.pointers.clear()
#print(Dumper.pointers)
if bl_object is None:
bl_object = get_bl_object(data)
dumper = get_dumper(bl_object)
dumper.load(data, bl_object)
Dumper.pointers.clear()
def set_attribute(bl_object, attr, value):
try:
setattr(bl_object, attr, value)
except Exception as e:
print(e)
class Dumper:
pointers = {}
includes = []
excludes = ["rna_type", "bl_rna", 'id_data', 'depsgraph']
@classmethod
def properties(cls, bl_object):
if cls.includes and not cls.excludes:
return [bl_object.bl_rna.properties[p] for p in cls.includes]
else:
return [ p for p in bl_object.bl_rna.properties if not
p.identifier.startswith('bl_') and p.identifier not in cls.excludes]
@classmethod
def new(cls, data):
print(f'New not implemented for data {data}')
@classmethod
def load(cls, data, bl_object=None):
if bl_object is None:
bl_object = cls.new(data)
if bl_object is None:
return
#pprint(data)
if bl_pointer := data.get('bl_pointer'):
cls.pointers[bl_pointer] = bl_object
props = cls.properties(bl_object)
for key, value in sorted(data.items(), key=lambda x: props.index(x[0]) if x[0] in props else 0):
if key.startswith('_') or key not in bl_object.bl_rna.properties:
continue
prop = bl_object.bl_rna.properties[key]
attr = getattr(bl_object, key)
if prop.type == 'COLLECTION':
dumper = PropCollection
if hasattr(attr, 'bl_rna'):
bl_type = attr.bl_rna.type_recast()
dumper = get_dumper(bl_type, fallback=PropCollection)
dumper.load(value, attr)
continue
elif prop.type == 'POINTER':
# if key == 'node_tree':
# print('--------------')
# print(bl_object, value)
# print(cls.pointers)
if isinstance(value, int): # It's a pointer
value = cls.pointers[value]
elif value is None:
set_attribute(bl_object, key, value)
else:
bl_type = prop.fixed_type.bl_rna.type_recast()
dumper = get_dumper(bl_type)
# If the pointer exist register the pointer then load data
#print('-----', value)
#pointer =
if attr is None:
attr = dumper.new(value)
dumper.load(value, attr)
#attr = getattr(bl_object, key)
#if not attr:
cls.pointers[value['bl_pointer']] = attr
if hasattr(attr, 'update'):
attr.update()
value = attr
if not prop.is_readonly:
set_attribute(bl_object, key, value)
# Some coll needs a manual update like curve mapping
if hasattr(attr, 'update'):
attr.update()
elif not prop.is_readonly:
#print(key, value)
set_attribute(bl_object, key, value)
continue
#return bl_object
@classmethod
def dump(cls, bl_object):
if isinstance(bl_object, (str, int, float, dict, list, type(None))):
return bl_object
#print('Dumping object', bl_object)
data = {"bl_pointer": bl_object.as_pointer()}
cls.pointers[bl_object.as_pointer()] = bl_object
for prop in cls.properties(bl_object):
if not hasattr(bl_object, prop.identifier):
print(f'{bl_object} has no attribute {prop.identifier}')
continue
#print(prop.identifier)
value = getattr(bl_object, prop.identifier)
# Not storing default value
if prop.identifier not in cls.includes:
if (array := getattr(prop, 'default_array', None)) and value == array:
continue
if isinstance(value, (str, int, float)) and value == prop.default:
continue
if getattr(prop, "is_array", False):
value = PropArray.dump(value)
elif prop.type == 'COLLECTION':
value = PropCollection.dump(value)
elif prop.type == 'POINTER' and value:
#if prop.identifier == 'image':
# print(bl_object, cls.pointers)
if value.as_pointer() in cls.pointers:
value = value.as_pointer()
else:
# print('Register Pointer', value.as_pointer(), value)
cls.pointers[value.as_pointer()] = value
# print(cls.pointers)
# print()
dumper = get_dumper(value)
value = dumper.dump(value)
elif bl_object.is_property_readonly(prop.identifier):
continue
else:
dumper = get_dumper(value)
value = dumper.dump(value)
data[prop.identifier] = value
return data
class PropCollection(Dumper):
bl_type = bpy.types.bpy_prop_collection
@classmethod
def dump(cls, coll):
if not len(coll):
return []
dumper = get_dumper(coll[0])
values = [dumper.dump(e) for e in coll]
# Value cannot be None
return [v for v in values if v is not None]
@classmethod
def load(cls, values, coll):
if not values:
return
dumper = None
if not hasattr(coll, 'new'): # Static collection
for item, value in zip(coll, values):
dumper = dumper or get_dumper(item)
dumper.load(value, item)
return
new_func = coll.bl_rna.functions['new']
for i, value in enumerate(values):
if value.get('_new'):
params = value['_new']
else:
params = {k: value.get(k, get_default(v)) for k, v in new_func.parameters.items()[:-1]}
# Replace arg pointer with bl object
valid_pointers = True
for param in coll.bl_rna.functions['new'].parameters:
if param.identifier not in params or param.type != 'POINTER':
continue
pointer_id = params[param.identifier]
if bl_object := cls.pointers.get(pointer_id):
params[param.identifier] = bl_object
else:
print(f'No Pointer found for param {param.identifier} of {coll}')
valid_pointers = False
if not valid_pointers:
continue
#print(param.identifier, cls.pointers[pointer_id])
try:
item = coll.new(**params)
except RuntimeError as e:
#print(e, coll.data)
#print()
try:
item = coll[i]
except IndexError as e:
#print(e, coll.data)
break
dumper = get_dumper(item)
dumper.load(value, item)#(item, value)
class PropArray(Dumper):
bl_type = bpy.types.bpy_prop_array
@classmethod
def dump(cls, array):
flat_array = []
for item in array:
if isinstance(item, (int, float)):
flat_array.append(item)
else:
flat_array.extend(cls.dump(item))
return flat_array
class NodeSocket(Dumper):
bl_type = bpy.types.NodeSocket
excludes = Dumper.excludes + ["node", "links", "display_shape", "rna_type", "link_limit"]
@classmethod
def dump(cls, socket):
if socket.is_unavailable:
return None
#cls.pointers[socket.as_pointer()] = socket
data = super().dump(socket)
#data["_id"] = socket.as_pointer()
#data.pop('name', '')
return data
class NodeGeometryRepeatOutputItems(PropCollection):
bl_type = bpy.types.NodeGeometryRepeatOutputItems
@classmethod
def load(cls, values, coll):
coll.clear()
super().load(values, coll)
class NodeLink(Dumper):
bl_type = bpy.types.NodeLink
@classmethod
def dump(cls, link):
return {"_new": {
"input": link.from_socket.as_pointer(),
"output": link.to_socket.as_pointer()
}
}
class NodeTreeInterfaceSocket(Dumper):
bl_type = bpy.types.NodeTreeInterfaceSocket
excludes = Dumper.excludes + ["parent", "interface_items"]
@classmethod
def dump(cls, socket):
#cls.pointers[socket.as_pointer()] = socket
data = super().dump(socket)
#data["_id"] = socket.as_pointer()
data['_new'] = {"name": data.get('name', '')}
if socket.item_type == 'SOCKET':
data['_new']["in_out"] = socket.in_out
# It's a real panel not the interface root
if socket.parent.parent:
data['parent'] = socket.parent.as_pointer()
return data
class NodeSockets(PropCollection):
@classmethod
def load(cls, values, coll):
#return
node_sockets = [s for s in coll if not s.is_unavailable]
for socket, value in zip(node_sockets, values):
cls.pointers[value['bl_pointer']] = socket
Dumper.load(value, socket)
# for k, v in value.items():
# if k not in socket.bl_rna.properties:
# continue
# setattr(socket, k, v)
"""
# Match Inputs Pointers
node_sockets = [s for s in coll if not s.is_unavailable]
if len(node_sockets) == len(inputs): # Match by index
super().load({"inputs": inputs}, node)
for socket, value in zip(node_sockets, coll):
cls.pointers[value['_id']] = socket
else: # Match by name
print(f'Match Inputs by Name for node {node}')
for socket in node_sockets:
index = next((i for i, v in enumerate(inputs) if v['name'] == socket.name), None)
if index is None:
continue
value = inputs[index]
print(socket, value)
cls.pointers[value['_id']] = socket
Dumper.load(value, socket)
del inputs[index]
"""
class NodeInputs(NodeSockets):
bl_type = bpy.types.NodeInputs
class NodeOutputs(NodeSockets):
bl_type = bpy.types.NodeOutputs
class Node(Dumper):
bl_type = bpy.types.Node
excludes = Dumper.excludes + ["dimensions", "height", "internal_links", "paired_output"]
@classmethod
def dump(cls, node=None):
#cls.pointers[node.as_pointer()] = node
data = super().dump(node)
#data["_id"] = node.as_pointer()
data["_new"] = {"type": node.bl_rna.identifier} # 'node_tree': node.id_data.as_pointer()
if paired_output := getattr(node, "paired_output", None):
data["_pair_with_output"] = paired_output.as_pointer()
#if node.parent:
# data['location'] -= Vector()node.parent.location
return data
@classmethod
def load(cls, data, node):
if node is None:
return
#cls.pointers[data['bl_pointer']] = node
inputs = copy(data.pop('inputs', []))
outputs = copy(data.pop('outputs', []))
super().load(data, node)
data['inputs'] = inputs
data['outputs'] = outputs
# Loading input and outputs after the properties
super().load({"inputs": inputs, "outputs": outputs}, node)
if node.parent:
node.location += node.parent.location
#if node.type != 'FRAME':
# node.location.y -= 500
class CompositorNodeGlare(Node):
bl_type = bpy.types.CompositorNodeGlare
includes = ["quality"]
class NodeTreeInterface(Dumper):
bl_type = bpy.types.NodeTreeInterface
@classmethod
def load(cls, data, interface):
print('Load Interface')
for value in data.get('items_tree', []):
item_type = value.get('item_type', 'SOCKET')
if item_type == 'SOCKET':
item = interface.new_socket(**value['_new'])
elif item_type == 'PANEL':
#print(value['_new'])
item = interface.new_panel(**value['_new'])
NodeTreeInterfaceSocket.load(value, item)
interface.active_index = data.get('active_index', 0)
class Nodes(PropCollection):
bl_type = bpy.types.Nodes
@classmethod
def load(cls, values, coll):
super().load(values, coll)
# Pair zone input and output
for node_data in values:
if paired_output_id := node_data.get('_pair_with_output', None):
node = cls.pointers[node_data['bl_pointer']]
node.pair_with_output(cls.pointers[paired_output_id])
#print(node, node_data['outputs'])
Dumper.load({"inputs": node_data['inputs'], "outputs": node_data['outputs']}, node)
class NodeTree(Dumper):
bl_type = bpy.types.NodeTree
excludes = []
includes = ["name", "interface", "nodes", "links"]
@classmethod
def new(cls, data):
if link := data.get('_link'):
with bpy.data.libraries.load(link['filepath'], link=True) as (data_from, data_to):
setattr(data_to, link['data_type'], [link['name']])
return getattr(data_to, link['data_type'])[0]
return bpy.data.node_groups.new(**data["_new"])
@classmethod
def dump(cls, node_tree):
if node_tree.library:
data = {'bl_pointer': node_tree.as_pointer()}
filepath = abspath(bpy.path.abspath(node_tree.library.filepath, library=node_tree.library.library))
data["_link"] = {"filepath": filepath, "data_type": 'node_groups', 'name': node_tree.name}
else:
data = super().dump(node_tree)
data["_new"] = {"type": node_tree.bl_rna.identifier, 'name': node_tree.name}
return data
class Points(PropCollection):
@classmethod
def load(cls, values, coll):
new_func = coll.bl_rna.functions['new']
params = {k: get_default(v)+1.1 for k, v in new_func.parameters.items()[:-1]}
# Match the same number of elements in collection
if len(values) > len(coll):
for _ in range(len(values) - len(coll)):
coll.new(**params)
for i, value in enumerate(values):
Dumper.load(value, coll[i])
#for k, v in value.items():
#setattr(coll[i], k, v)
class CurveMapPoints(Points):
bl_type = bpy.types.CurveMapPoints
class ColorRampElements(Points):
bl_type = bpy.types.ColorRampElements
class CompositorNodeOutputFileLayerSlots(PropCollection):
bl_type = bpy.types.CompositorNodeOutputFileLayerSlots
@classmethod
def load(cls, values, coll):
coll.clear()
super().load(values, coll)
class CompositorNodeOutputFileFileSlots(PropCollection):
@classmethod
def load(cls, values, coll):
coll.clear()
super().load(values, coll)
class AOVs(PropCollection):
bl_type = bpy.types.AOVs
@classmethod
def load(cls, values, coll):
for value in values:
aov = coll.get(value['name'])
if not aov:
aov = coll.add()
Dumper.load(value, aov)
class Image(Dumper):
bl_type = bpy.types.Image
excludes = []
includes = ['name', 'filepath']
@classmethod
def new(cls, data):
# image = next(( img for img in bpy.data.images if not img.library
# and img.filepath == data['filepath']), None)
# if image is None:
# image = bpy.data.images.load(data['filepath'])
return bpy.data.images.load(data['filepath'], check_existing=True)
class Material(Dumper):
bl_type = bpy.types.Material
excludes = Dumper.excludes + ['preview', "original"]
@classmethod
def new(cls, data):
material = bpy.data.materials.get(data.get('name', ''))
if material is None:
material = bpy.data.materials.new(data['name'])
return material
class Object(Dumper):
bl_type = bpy.types.Object
excludes = []
includes = ['name']
@classmethod
def new(cls, data):
if name := data.get('name'):
return bpy.data.objects.get(name)
class Scene(Dumper):
bl_type = bpy.types.Scene
excludes = []
includes = ['name']
@classmethod
def new(cls, data):
if scene := bpy.data.scenes.get(data.get('name', '')):
return scene
return bpy.data.scenes.new(name=data.get('name', ''))
"""
@classmethod
def dump(cls, scene):
view_layer = scene.view_layers[node.layer]
view_layer_data = ViewLayer.dump(view_layer)
return {
'bl_pointer': scene.as_pointer(),
'name': scene.name,
'render' : {'bl_pointer': scene.render.as_pointer(), "engine": scene.render.engine},
'view_layers': [view_layer_data]
}
"""
class Collection(Dumper):
bl_type = bpy.types.Collection
includes = ['name']
excludes = []
@classmethod
def new(cls, data):
if name := data.get('name'):
return bpy.data.collections.get(name)
# @classmethod
# def dump(cls, data):
# data = super().dump(data)
# data['render'] = {"engine": scene.render.engine}
# return data
class CompositorNodeRLayers(Node):
bl_type = bpy.types.CompositorNodeRLayers
excludes = Dumper.excludes + ['scene']
@classmethod
def load(cls, data, node):
#print('load CompositorNodeRLayers')
scene_data = data.pop('scene')
#print(scene_data)
layer = data.pop('layer')
scene = Scene.new(scene_data)
Scene.load(scene_data, scene)
node.scene = scene
node.layer = layer
super().load(data, node)
# Resetter the view_layer because it might have been created
# with the scene attr in the dictionnary and nor available yet
#print(bpy.)
@classmethod
def dump(cls, node):
# Add scene and viewlayer passes
data = super().dump(node)
#if
view_layer = node.scene.view_layers[node.layer]
view_layer_data = ViewLayer.dump(view_layer)
'''
view_layer_data = {
"name": view_layer.name}
properties = {p.name: p for p in view_layer.bl_rna.properties}
for prop in view_layer.bl_rna:
if prop.identifier.startswith('use_pass'):
view_layer_data[prop.identifier]
'''
#cls.pointers[bl_object.as_pointer()] = bl_object
data['scene'] = {
'bl_pointer': node.scene.as_pointer(),
'name': node.scene.name,
'render' : {'bl_pointer': node.scene.render.as_pointer(), "engine": node.scene.render.engine},
'view_layers': [view_layer_data]
}
return data
class ViewLayer(Dumper):
bl_type = bpy.types.ViewLayer
excludes = Dumper.excludes + ['freestyle_settings', 'eevee', 'cycles', 'active_layer_collection',
'active_aov', 'active_lightgroup_index', 'layer_collection', 'lightgroups', 'material_override',
'objects', 'use']
#includes = ['name']
class ViewLayers(PropCollection):
bl_type = bpy.types.ViewLayers
@classmethod
def load(cls, values, coll):
#print('LOAD VIEWLAYERS', values)
for value in values:
view_layer = coll.get(value['name'])
if view_layer is None:
view_layer = coll.new(value['name'])
Dumper.load(value, view_layer)
dumpers = [
CompositorNodeRLayers,
CompositorNodeGlare,
Node,
NodeSocket,
NodeTree,
NodeLink,
NodeTreeInterface,
NodeTreeInterfaceSocket,
NodeGeometryRepeatOutputItems,
Image,
Material,
Object,
Scene,
Collection,
ViewLayer,
CurveMapPoints,
ColorRampElements,
NodeInputs,
NodeOutputs,
Nodes,
ViewLayers,
PropCollection,
AOVs,
PropArray,
CompositorNodeOutputFileLayerSlots,
CompositorNodeOutputFileFileSlots,
]