Compare commits

...

6 Commits

6 changed files with 150 additions and 128 deletions

View File

@ -10,11 +10,12 @@ bl_info = {
}
from . import ui, operators
from . import ui, operators, preferences
modules = (
ui,
operators,
preferences
)

View File

@ -1,61 +1,80 @@
import bpy
import mathutils
from pprint import pprint
from __future__ import annotations
import json
import itertools
from copy import copy
from os.path import abspath
from pprint import pprint
import bpy
from .. import utils
def get_default(prop):
"""Get the default value of a bl property"""
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, fallback=None):
"""Find the right dumper type by checking inheritance"""
for dp in dumpers:
if isinstance(bl_object, dp.bl_type):
return dp
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"""
return fallback or Dumper
for cls in bl_object.__class__.mro():
dumper_map = DumperRegistry().dumper_map
if cls in dumper_map:
return dumper_map[cls]
# Fallback to base Dumper if no matches are found
return Dumper
def get_bl_object(data):
"""Find the bl object for loading data into it depending on the type and the context"""
def get_current_node_tree(data):
if data.get("_new", {}).get("type") == "GeometryNodeTree":
return bpy.context.object.modifiers.active.node_group
def dump_nodes(ob):
def dump_nodes(nodes: list[bpy.types.Node]):
"""Generic Recursive Dump, convert any object into a dict"""
Dumper.pointers.clear()
Dumper.pointers.clear() # TODO: Bad global
if isinstance(ob, (list, tuple)):
data = [get_dumper(o).dump(o) for o in ob]
else:
data = get_dumper(ob).dump(ob)
data = [dump_node(node) for node in nodes]
Dumper.pointers.clear()
return data
def dump_node(node: bpy.types.Node):
dumper = get_dumper(node)
return dumper.dump(node) # TODO: Break the recursivity, clear things up
def load_nodes(data, bl_object=None):
def load_nodes(data, node_tree=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)
if node_tree is None:
node_tree = get_current_node_tree(data)
dumper = get_dumper(bl_object)
dumper.load(data, bl_object)
dumper = get_dumper(node_tree)
dumper.load(data, node_tree)
Dumper.pointers.clear()
@ -96,7 +115,6 @@ class Dumper:
if bl_object is None:
return
# pprint(data)
if bl_pointer := data.get("bl_pointer"):
cls.pointers[bl_pointer] = bl_object
@ -114,17 +132,12 @@ class Dumper:
dumper = PropCollection
if hasattr(attr, "bl_rna"):
bl_type = attr.bl_rna.type_recast()
dumper = get_dumper(bl_type, fallback=PropCollection)
dumper = PropCollection or get_dumper(bl_type)
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
if value not in cls.pointers:
print(bl_object, "not loaded yet", prop)
@ -138,14 +151,10 @@ class Dumper:
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"):
@ -161,7 +170,6 @@ class Dumper:
attr.update()
elif not prop.is_readonly:
# print(key, value)
set_attribute(bl_object, key, value)
continue
@ -172,8 +180,6 @@ class Dumper:
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
@ -182,8 +188,6 @@ class Dumper:
print(f"{bl_object} has no attribute {prop.identifier}")
continue
# print(prop.identifier)
value = getattr(bl_object, prop.identifier)
# Not storing default value
@ -200,15 +204,10 @@ class Dumper:
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)
@ -278,17 +277,12 @@ class PropCollection(Dumper):
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)
@ -515,8 +509,6 @@ class Nodes(PropCollection):
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,
@ -794,31 +786,19 @@ class ViewLayers(PropCollection):
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,
]
class DumperRegistry:
"""Singleton-like class that holds a map of all parsers, constructed on first instantiation"""
dumper_map = None
def __init__(self):
if self.dumper_map is None:
self.construct_dumper_map()
@classmethod
def construct_dumper_map(cls):
cls.dumper_map = {}
for subclass in utils.all_subclasses(Dumper):
assert hasattr(subclass, "bl_type")
cls.dumper_map[subclass.bl_type] = subclass
print(cls.dumper_map)

View File

@ -1,5 +1,5 @@
"""
This module contains all addons operators
Node Kit Operators
:author: Autour de Minuit
:maintainers: Florentin LUCE
@ -14,59 +14,71 @@ import bpy
from bpy.props import BoolProperty, EnumProperty
from bpy.types import Operator
# from node_kit.core.node_tree import NodeTree
from .core.dumper import dump_nodes, load_nodes
from .core.dumper import dump_nodes, load_nodes, dump_nkit_format, parse_nkit_format
from .core.node_utils import remap_node_group_duplicates
from .core.pack_nodes import combine_objects, extract_objects
class NODEKIT_OT_copy(Operator):
bl_idname = "node_kit.copy_node_tree"
bl_label = "Copy nodes"
bl_idname = "node_kit.copy_nodes"
bl_label = "Copy Nodes"
bl_description = "Copy nodes to system clipboard"
bl_options = {"REGISTER", "UNDO"}
select_only: BoolProperty(default=True) # TODO: Expose/rework this property properly - No F3 panel in Node Editor - Only F9
def execute(self, context):
ntree = context.space_data.edit_tree
if self.select_only:
ntree_data = {
"nodes": dump_nodes(
[n for n in ntree.nodes if n.select]
), # [dump(n) for n in ntree.nodes if n.select],
"links": dump_nodes(
[l for l in ntree.links if l.from_node.select and l.to_node.select]
),
}
else:
ntree_data = dump_nodes(ntree)
selected_nodes = [node for node in ntree.nodes if node.select]
ntree_data = {
"nodes": dump_nodes(selected_nodes),
"links": dump_nodes(
[l for l in ntree.links if l.from_node.select and l.to_node.select]
),
}
pprint(ntree_data)
context.window_manager.clipboard = json.dumps(ntree_data)
context.window_manager.clipboard = dump_nkit_format(ntree_data)
self.report({"INFO"}, f"Copied 5 selected nodes to system clipboard")
self.report({"INFO"}, f"Copied {len(selected_nodes)} selected nodes to system clipboard")
return {"FINISHED"}
class NODEKIT_OT_copy_tree(Operator):
bl_idname = "node_kit.copy_node_tree"
bl_label = "Copy Node Tree"
bl_description = "Copy node tree to system clipboard"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ntree = context.space_data.edit_tree
ntree_data = dump_nodes(ntree)
pprint(ntree_data)
context.window_manager.clipboard = dump_nkit_format(ntree_data)
self.report({"INFO"}, f"Copied {len(ntree.nodes)} selected nodes to system clipboard")
return {"FINISHED"}
class NODEKIT_OT_paste(Operator):
bl_idname = "node_kit.paste_node_tree"
bl_label = "Paste nodes"
bl_idname = "node_kit.paste_nodes"
bl_label = "Paste Nodes"
bl_description = "Paste nodes from system clipboard"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ntree_data = json.loads(context.window_manager.clipboard)
ntree_data = parse_nkit_format(context.window_manager.clipboard)
load_nodes(ntree_data, context.space_data.edit_tree)
self.report({"INFO"}, f"5 node(s) pasted from system clipboard")
self.report({"INFO"}, f"X node(s) pasted from system clipboard") # TODO: Ge the number of parsed nodes returned
return {"FINISHED"}
class NODEKIT_OT_remap_node_group_duplicates(Operator):
bl_idname = "node_kit.remap_node_group_duplicates"
bl_label = "Clean nodes"
bl_label = "Remap Node Groups Duplicates"
bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty(
@ -127,7 +139,7 @@ class NODEKIT_OT_remap_node_group_duplicates(Operator):
class NODEKIT_OT_update_nodes(Operator):
bl_idname = "node_kit.update_nodes"
bl_label = "Update node"
bl_label = "Update Nodes"
bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty(
@ -237,7 +249,7 @@ class NODEKIT_OT_update_nodes(Operator):
class NODEKIT_OT_pack_nodes(Operator):
bl_idname = "node_kit.pack_nodes"
bl_label = "Update node"
bl_label = "Pack Nodes"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@ -247,7 +259,7 @@ class NODEKIT_OT_pack_nodes(Operator):
class NODEKIT_OT_unpack_nodes(Operator):
bl_idname = "node_kit.unpack_nodes"
bl_label = "Update node"
bl_label = "Unpack Nodes"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@ -257,6 +269,7 @@ class NODEKIT_OT_unpack_nodes(Operator):
classes = (
NODEKIT_OT_copy,
NODEKIT_OT_copy_tree,
NODEKIT_OT_paste,
NODEKIT_OT_remap_node_group_duplicates,
NODEKIT_OT_update_nodes,

22
preferences.py Normal file
View File

@ -0,0 +1,22 @@
import bpy
from bpy.types import AddonPreferences
from bpy.props import BoolProperty
class NodeKitPreferences(AddonPreferences):
bl_idname = __package__
classes = (
NodeKitPreferences,
)
def register():
for c in classes:
bpy.utils.register_class(c)
def unregister():
for c in reversed(classes):
bpy.utils.unregister_class(c)

34
ui.py
View File

@ -1,5 +1,5 @@
"""
This module contains blender UI elements
Node Kit UI elements and menus.
:author: Autour de Minuit
:maintainers: Florentin LUCE
@ -15,22 +15,22 @@ class NODEKIT_MT_node_kit(bpy.types.Menu):
def draw(self, context):
layout = self.layout
layout.operator("node_kit.copy_node_tree", text="Copy Nodes", icon="COPYDOWN")
layout.operator(
"node_kit.paste_node_tree", text="Paste Nodes", icon="PASTEDOWN"
)
layout.operator("node_kit.copy_nodes", icon="COPYDOWN")
layout.operator("node_kit.paste_nodes", icon="PASTEDOWN")
layout.separator()
layout.operator(
"node_kit.remap_node_group_duplicates",
text="Remap Node Groups Duplicates",
icon="NODE_INSERT_OFF",
)
layout.operator("node_kit.update_nodes", text="Update Nodes", icon="IMPORT")
layout.operator("node_kit.copy_node_tree", icon="NODETREE")
layout.separator()
layout.operator("node_kit.pack_nodes", text="Pack Nodes", icon="PACKAGE")
layout.operator(
"node_kit.unpack_nodes", text="UnPack Nodes", icon="UGLYPACKAGE"
)
layout.operator("node_kit.remap_node_group_duplicates",icon="NODE_INSERT_OFF")
layout.operator("node_kit.update_nodes", icon="IMPORT")
layout.separator()
layout.operator("node_kit.pack_nodes", icon="PACKAGE")
layout.operator("node_kit.unpack_nodes", icon="UGLYPACKAGE")
classes = (NODEKIT_MT_node_kit,)
@ -48,7 +48,7 @@ def register():
def unregister():
bpy.types.NODE_MT_editor_menus.remove(draw_menu)
for c in reversed(classes):
bpy.utils.unregister_class(c)
bpy.types.NODE_MT_editor_menus.remove(draw_menu)

6
utils.py Normal file
View File

@ -0,0 +1,6 @@
def all_subclasses(cls):
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)]
)