Compare commits

..

No commits in common. "94627debc6580513136b18bffd327ed41511a8d1" and "4e029c59a291ace0104850869a0dc64d69c6b832" have entirely different histories.

6 changed files with 127 additions and 149 deletions

View File

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

View File

@ -1,80 +1,61 @@
from __future__ import annotations
import bpy
import mathutils
from pprint import pprint
import json
import itertools
from copy import copy
from os.path import abspath
from pprint import pprint
import bpy
from .. import utils
format_token = "#FMT:NODE_KIT#"
def get_default(prop):
"""Get the default value of a bl property"""
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]:
"""Get the corresponding dumper for a given Blender object, or its closest base type using its MRO"""
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
for cls in bl_object.__class__.mro():
dumper_map = DumperRegistry().dumper_map
if cls in dumper_map:
return dumper_map[cls]
return fallback or Dumper
# Fallback to base Dumper if no matches are found
return Dumper
def get_current_node_tree(data):
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_nodes(nodes: list[bpy.types.Node]):
def dump_nodes(ob):
"""Generic Recursive Dump, convert any object into a dict"""
Dumper.pointers.clear() # TODO: Bad global
Dumper.pointers.clear()
data = [dump_node(node) for node in nodes]
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 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, node_tree=None):
def load_nodes(data, bl_object=None):
"""Generic Load to create an object from a dict"""
Dumper.pointers.clear()
# print(Dumper.pointers)
if node_tree is None:
node_tree = get_current_node_tree(data)
if bl_object is None:
bl_object = get_bl_object(data)
dumper = get_dumper(node_tree)
dumper.load(data, node_tree)
dumper = get_dumper(bl_object)
dumper.load(data, bl_object)
Dumper.pointers.clear()
@ -115,6 +96,7 @@ class Dumper:
if bl_object is None:
return
# pprint(data)
if bl_pointer := data.get("bl_pointer"):
cls.pointers[bl_pointer] = bl_object
@ -132,12 +114,17 @@ class Dumper:
dumper = PropCollection
if hasattr(attr, "bl_rna"):
bl_type = attr.bl_rna.type_recast()
dumper = PropCollection or get_dumper(bl_type)
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
if value not in cls.pointers:
print(bl_object, "not loaded yet", prop)
@ -151,10 +138,14 @@ 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"):
@ -170,6 +161,7 @@ class Dumper:
attr.update()
elif not prop.is_readonly:
# print(key, value)
set_attribute(bl_object, key, value)
continue
@ -180,6 +172,8 @@ 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
@ -188,6 +182,8 @@ 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
@ -204,10 +200,15 @@ 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)
@ -277,12 +278,17 @@ 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)
@ -509,6 +515,8 @@ 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,
@ -786,19 +794,31 @@ class ViewLayers(PropCollection):
Dumper.load(value, view_layer)
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)
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,
]

View File

@ -1,5 +1,5 @@
"""
Node Kit Operators
This module contains all addons operators
:author: Autour de Minuit
:maintainers: Florentin LUCE
@ -14,71 +14,59 @@ import bpy
from bpy.props import BoolProperty, EnumProperty
from bpy.types import Operator
from .core.dumper import dump_nodes, load_nodes, dump_nkit_format, parse_nkit_format
# from node_kit.core.node_tree import NodeTree
from .core.dumper import dump_nodes, load_nodes
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_nodes"
bl_label = "Copy Nodes"
bl_idname = "node_kit.copy_node_tree"
bl_label = "Copy nodes"
bl_description = "Copy nodes to system clipboard"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ntree = context.space_data.edit_tree
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 = dump_nkit_format(ntree_data)
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"}
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
ntree_data = dump_nodes(ntree)
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)
pprint(ntree_data)
context.window_manager.clipboard = dump_nkit_format(ntree_data)
context.window_manager.clipboard = json.dumps(ntree_data)
self.report({"INFO"}, f"Copied {len(ntree.nodes)} selected nodes to system clipboard")
self.report({"INFO"}, f"Copied 5 selected nodes to system clipboard")
return {"FINISHED"}
class NODEKIT_OT_paste(Operator):
bl_idname = "node_kit.paste_nodes"
bl_label = "Paste Nodes"
bl_idname = "node_kit.paste_node_tree"
bl_label = "Paste nodes"
bl_description = "Paste nodes from system clipboard"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ntree_data = parse_nkit_format(context.window_manager.clipboard)
ntree_data = json.loads(context.window_manager.clipboard)
load_nodes(ntree_data, context.space_data.edit_tree)
self.report({"INFO"}, f"X node(s) pasted from system clipboard") # TODO: Ge the number of parsed nodes returned
self.report({"INFO"}, f"5 node(s) pasted from system clipboard")
return {"FINISHED"}
class NODEKIT_OT_remap_node_group_duplicates(Operator):
bl_idname = "node_kit.remap_node_group_duplicates"
bl_label = "Remap Node Groups Duplicates"
bl_label = "Clean nodes"
bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty(
@ -139,7 +127,7 @@ class NODEKIT_OT_remap_node_group_duplicates(Operator):
class NODEKIT_OT_update_nodes(Operator):
bl_idname = "node_kit.update_nodes"
bl_label = "Update Nodes"
bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty(
@ -249,7 +237,7 @@ class NODEKIT_OT_update_nodes(Operator):
class NODEKIT_OT_pack_nodes(Operator):
bl_idname = "node_kit.pack_nodes"
bl_label = "Pack Nodes"
bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@ -259,7 +247,7 @@ class NODEKIT_OT_pack_nodes(Operator):
class NODEKIT_OT_unpack_nodes(Operator):
bl_idname = "node_kit.unpack_nodes"
bl_label = "Unpack Nodes"
bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@ -269,7 +257,6 @@ 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,

View File

@ -1,22 +0,0 @@
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 @@
"""
Node Kit UI elements and menus.
This module contains blender UI elements
: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_nodes", icon="COPYDOWN")
layout.operator("node_kit.paste_nodes", icon="PASTEDOWN")
layout.separator()
layout.operator("node_kit.copy_node_tree", icon="NODETREE")
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.separator()
layout.operator("node_kit.remap_node_group_duplicates",icon="NODE_INSERT_OFF")
layout.operator("node_kit.update_nodes", icon="IMPORT")
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.separator()
layout.operator("node_kit.pack_nodes", icon="PACKAGE")
layout.operator("node_kit.unpack_nodes", icon="UGLYPACKAGE")
layout.operator("node_kit.pack_nodes", text="Pack Nodes", icon="PACKAGE")
layout.operator(
"node_kit.unpack_nodes", text="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)

View File

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