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 = ( modules = (
ui, ui,
operators, operators,
preferences
) )

View File

@ -1,61 +1,80 @@
import bpy from __future__ import annotations
import mathutils
from pprint import pprint
import json import json
import itertools
from copy import copy from copy import copy
from os.path import abspath from os.path import abspath
from pprint import pprint
import bpy
from .. import utils
def get_default(prop): format_token = "#FMT:NODE_KIT#"
"""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): if getattr(prop, "is_array", False):
return list(prop.default_array) return list(prop.default_array)
elif hasattr(prop, "default"): elif hasattr(prop, "default"):
return prop.default return prop.default
def get_dumper(bl_object, fallback=None): def get_dumper(bl_object: bpy.types.bpy_struct) -> type[Dumper]:
"""Find the right dumper type by checking inheritance""" """Get the corresponding dumper for a given Blender object, or its closest base type using its MRO"""
for dp in dumpers:
if isinstance(bl_object, dp.bl_type):
return dp
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): def get_current_node_tree(data):
"""Find the bl object for loading data into it depending on the type and the context"""
if data.get("_new", {}).get("type") == "GeometryNodeTree": if data.get("_new", {}).get("type") == "GeometryNodeTree":
return bpy.context.object.modifiers.active.node_group 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""" """Generic Recursive Dump, convert any object into a dict"""
Dumper.pointers.clear() Dumper.pointers.clear() # TODO: Bad global
if isinstance(ob, (list, tuple)): data = [dump_node(node) for node in nodes]
data = [get_dumper(o).dump(o) for o in ob]
else:
data = get_dumper(ob).dump(ob)
Dumper.pointers.clear() Dumper.pointers.clear()
return data 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""" """Generic Load to create an object from a dict"""
Dumper.pointers.clear() Dumper.pointers.clear()
# print(Dumper.pointers) # print(Dumper.pointers)
if bl_object is None: if node_tree is None:
bl_object = get_bl_object(data) node_tree = get_current_node_tree(data)
dumper = get_dumper(bl_object) dumper = get_dumper(node_tree)
dumper.load(data, bl_object) dumper.load(data, node_tree)
Dumper.pointers.clear() Dumper.pointers.clear()
@ -96,7 +115,6 @@ class Dumper:
if bl_object is None: if bl_object is None:
return return
# pprint(data)
if bl_pointer := data.get("bl_pointer"): if bl_pointer := data.get("bl_pointer"):
cls.pointers[bl_pointer] = bl_object cls.pointers[bl_pointer] = bl_object
@ -114,17 +132,12 @@ class Dumper:
dumper = PropCollection dumper = PropCollection
if hasattr(attr, "bl_rna"): if hasattr(attr, "bl_rna"):
bl_type = attr.bl_rna.type_recast() 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) dumper.load(value, attr)
continue continue
elif prop.type == "POINTER": 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 isinstance(value, int): # It's a pointer
if value not in cls.pointers: if value not in cls.pointers:
print(bl_object, "not loaded yet", prop) print(bl_object, "not loaded yet", prop)
@ -138,14 +151,10 @@ class Dumper:
dumper = get_dumper(bl_type) dumper = get_dumper(bl_type)
# If the pointer exist register the pointer then load data # If the pointer exist register the pointer then load data
# print('-----', value)
# pointer =
if attr is None: if attr is None:
attr = dumper.new(value) attr = dumper.new(value)
dumper.load(value, attr) dumper.load(value, attr)
# attr = getattr(bl_object, key)
# if not attr:
cls.pointers[value["bl_pointer"]] = attr cls.pointers[value["bl_pointer"]] = attr
if hasattr(attr, "update"): if hasattr(attr, "update"):
@ -161,7 +170,6 @@ class Dumper:
attr.update() attr.update()
elif not prop.is_readonly: elif not prop.is_readonly:
# print(key, value)
set_attribute(bl_object, key, value) set_attribute(bl_object, key, value)
continue continue
@ -172,8 +180,6 @@ class Dumper:
if isinstance(bl_object, (str, int, float, dict, list, type(None))): if isinstance(bl_object, (str, int, float, dict, list, type(None))):
return bl_object return bl_object
# print('Dumping object', bl_object)
data = {"bl_pointer": bl_object.as_pointer()} data = {"bl_pointer": bl_object.as_pointer()}
cls.pointers[bl_object.as_pointer()] = bl_object cls.pointers[bl_object.as_pointer()] = bl_object
@ -182,8 +188,6 @@ class Dumper:
print(f"{bl_object} has no attribute {prop.identifier}") print(f"{bl_object} has no attribute {prop.identifier}")
continue continue
# print(prop.identifier)
value = getattr(bl_object, prop.identifier) value = getattr(bl_object, prop.identifier)
# Not storing default value # Not storing default value
@ -200,15 +204,10 @@ class Dumper:
value = PropCollection.dump(value) value = PropCollection.dump(value)
elif prop.type == "POINTER" and value: elif prop.type == "POINTER" and value:
# if prop.identifier == 'image':
# print(bl_object, cls.pointers)
if value.as_pointer() in cls.pointers: if value.as_pointer() in cls.pointers:
value = value.as_pointer() value = value.as_pointer()
else: else:
# print('Register Pointer', value.as_pointer(), value)
cls.pointers[value.as_pointer()] = value cls.pointers[value.as_pointer()] = value
# print(cls.pointers)
# print()
dumper = get_dumper(value) dumper = get_dumper(value)
value = dumper.dump(value) value = dumper.dump(value)
@ -278,17 +277,12 @@ class PropCollection(Dumper):
if not valid_pointers: if not valid_pointers:
continue continue
# print(param.identifier, cls.pointers[pointer_id])
try: try:
item = coll.new(**params) item = coll.new(**params)
except RuntimeError as e: except RuntimeError as e:
# print(e, coll.data)
# print()
try: try:
item = coll[i] item = coll[i]
except IndexError as e: except IndexError as e:
# print(e, coll.data)
break break
dumper = get_dumper(item) dumper = get_dumper(item)
@ -515,8 +509,6 @@ class Nodes(PropCollection):
node = cls.pointers[node_data["bl_pointer"]] node = cls.pointers[node_data["bl_pointer"]]
node.pair_with_output(cls.pointers[paired_output_id]) node.pair_with_output(cls.pointers[paired_output_id])
# print(node, node_data['outputs'])
Dumper.load( Dumper.load(
{"inputs": node_data["inputs"], "outputs": node_data["outputs"]}, {"inputs": node_data["inputs"], "outputs": node_data["outputs"]},
node, node,
@ -794,31 +786,19 @@ class ViewLayers(PropCollection):
Dumper.load(value, view_layer) Dumper.load(value, view_layer)
dumpers = [ class DumperRegistry:
CompositorNodeRLayers, """Singleton-like class that holds a map of all parsers, constructed on first instantiation"""
CompositorNodeGlare, dumper_map = None
Node,
NodeSocket, def __init__(self):
NodeTree, if self.dumper_map is None:
NodeLink, self.construct_dumper_map()
NodeTreeInterface,
NodeTreeInterfaceSocket, @classmethod
NodeGeometryRepeatOutputItems, def construct_dumper_map(cls):
Image, cls.dumper_map = {}
Material,
Object, for subclass in utils.all_subclasses(Dumper):
Scene, assert hasattr(subclass, "bl_type")
Collection, cls.dumper_map[subclass.bl_type] = subclass
ViewLayer, print(cls.dumper_map)
CurveMapPoints,
ColorRampElements,
NodeInputs,
NodeOutputs,
Nodes,
ViewLayers,
PropCollection,
AOVs,
PropArray,
CompositorNodeOutputFileLayerSlots,
CompositorNodeOutputFileFileSlots,
]

View File

@ -1,5 +1,5 @@
""" """
This module contains all addons operators Node Kit Operators
:author: Autour de Minuit :author: Autour de Minuit
:maintainers: Florentin LUCE :maintainers: Florentin LUCE
@ -14,59 +14,71 @@ 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 node_kit.core.node_tree import NodeTree from .core.dumper import dump_nodes, load_nodes, dump_nkit_format, parse_nkit_format
from .core.dumper import dump_nodes, load_nodes
from .core.node_utils import remap_node_group_duplicates from .core.node_utils import remap_node_group_duplicates
from .core.pack_nodes import combine_objects, extract_objects from .core.pack_nodes import combine_objects, extract_objects
class NODEKIT_OT_copy(Operator): class NODEKIT_OT_copy(Operator):
bl_idname = "node_kit.copy_node_tree" bl_idname = "node_kit.copy_nodes"
bl_label = "Copy nodes" bl_label = "Copy Nodes"
bl_description = "Copy nodes to system clipboard" bl_description = "Copy nodes to system clipboard"
bl_options = {"REGISTER", "UNDO"} 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): def execute(self, context):
ntree = context.space_data.edit_tree ntree = context.space_data.edit_tree
if self.select_only: selected_nodes = [node for node in ntree.nodes if node.select]
ntree_data = {
"nodes": dump_nodes( ntree_data = {
[n for n in ntree.nodes if n.select] "nodes": dump_nodes(selected_nodes),
), # [dump(n) for n in ntree.nodes if n.select], "links": dump_nodes(
"links": dump_nodes( [l for l in ntree.links if l.from_node.select and l.to_node.select]
[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) 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"} return {"FINISHED"}
class NODEKIT_OT_paste(Operator): class NODEKIT_OT_paste(Operator):
bl_idname = "node_kit.paste_node_tree" bl_idname = "node_kit.paste_nodes"
bl_label = "Paste nodes" bl_label = "Paste Nodes"
bl_description = "Paste nodes from system clipboard" bl_description = "Paste nodes from system clipboard"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): 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) 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"} return {"FINISHED"}
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 = "Clean nodes" bl_label = "Remap Node Groups Duplicates"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty( selection: EnumProperty(
@ -127,7 +139,7 @@ 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 node" bl_label = "Update Nodes"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty( selection: EnumProperty(
@ -237,7 +249,7 @@ 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 = "Update node" bl_label = "Pack Nodes"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
@ -247,7 +259,7 @@ 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 = "Update node" bl_label = "Unpack Nodes"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
@ -257,6 +269,7 @@ class NODEKIT_OT_unpack_nodes(Operator):
classes = ( classes = (
NODEKIT_OT_copy, NODEKIT_OT_copy,
NODEKIT_OT_copy_tree,
NODEKIT_OT_paste, NODEKIT_OT_paste,
NODEKIT_OT_remap_node_group_duplicates, NODEKIT_OT_remap_node_group_duplicates,
NODEKIT_OT_update_nodes, 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 :author: Autour de Minuit
:maintainers: Florentin LUCE :maintainers: Florentin LUCE
@ -15,22 +15,22 @@ class NODEKIT_MT_node_kit(bpy.types.Menu):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator("node_kit.copy_node_tree", text="Copy Nodes", icon="COPYDOWN") layout.operator("node_kit.copy_nodes", icon="COPYDOWN")
layout.operator( layout.operator("node_kit.paste_nodes", icon="PASTEDOWN")
"node_kit.paste_node_tree", text="Paste Nodes", icon="PASTEDOWN"
) layout.separator()
layout.operator("node_kit.copy_node_tree", icon="NODETREE")
layout.separator() layout.separator()
layout.operator(
"node_kit.remap_node_group_duplicates", layout.operator("node_kit.remap_node_group_duplicates",icon="NODE_INSERT_OFF")
text="Remap Node Groups Duplicates", layout.operator("node_kit.update_nodes", icon="IMPORT")
icon="NODE_INSERT_OFF",
)
layout.operator("node_kit.update_nodes", text="Update Nodes", icon="IMPORT")
layout.separator() layout.separator()
layout.operator("node_kit.pack_nodes", text="Pack Nodes", icon="PACKAGE")
layout.operator( layout.operator("node_kit.pack_nodes", icon="PACKAGE")
"node_kit.unpack_nodes", text="UnPack Nodes", icon="UGLYPACKAGE" layout.operator("node_kit.unpack_nodes", icon="UGLYPACKAGE")
)
classes = (NODEKIT_MT_node_kit,) classes = (NODEKIT_MT_node_kit,)
@ -48,7 +48,7 @@ def register():
def unregister(): def unregister():
bpy.types.NODE_MT_editor_menus.remove(draw_menu)
for c in reversed(classes): for c in reversed(classes):
bpy.utils.unregister_class(c) 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)]
)