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

View File

@ -1,80 +1,61 @@
from __future__ import annotations import bpy
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
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): 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: bpy.types.bpy_struct) -> type[Dumper]: def get_dumper(bl_object, fallback=None):
"""Get the corresponding dumper for a given Blender object, or its closest base type using its MRO""" """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(): return fallback or Dumper
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_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": 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(nodes: list[bpy.types.Node]): def dump_nodes(ob):
"""Generic Recursive Dump, convert any object into a dict""" """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() 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 node_tree is None: if bl_object is None:
node_tree = get_current_node_tree(data) bl_object = get_bl_object(data)
dumper = get_dumper(node_tree) dumper = get_dumper(bl_object)
dumper.load(data, node_tree) dumper.load(data, bl_object)
Dumper.pointers.clear() Dumper.pointers.clear()
@ -115,6 +96,7 @@ 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
@ -132,12 +114,17 @@ 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 = PropCollection or get_dumper(bl_type) dumper = get_dumper(bl_type, fallback=PropCollection)
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)
@ -151,10 +138,14 @@ 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"):
@ -170,6 +161,7 @@ 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
@ -180,6 +172,8 @@ 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
@ -188,6 +182,8 @@ 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
@ -204,10 +200,15 @@ 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)
@ -277,12 +278,17 @@ 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)
@ -509,6 +515,8 @@ 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,
@ -786,19 +794,31 @@ class ViewLayers(PropCollection):
Dumper.load(value, view_layer) Dumper.load(value, view_layer)
class DumperRegistry: dumpers = [
"""Singleton-like class that holds a map of all parsers, constructed on first instantiation""" CompositorNodeRLayers,
dumper_map = None CompositorNodeGlare,
Node,
def __init__(self): NodeSocket,
if self.dumper_map is None: NodeTree,
self.construct_dumper_map() NodeLink,
NodeTreeInterface,
@classmethod NodeTreeInterfaceSocket,
def construct_dumper_map(cls): NodeGeometryRepeatOutputItems,
cls.dumper_map = {} Image,
Material,
for subclass in utils.all_subclasses(Dumper): Object,
assert hasattr(subclass, "bl_type") Scene,
cls.dumper_map[subclass.bl_type] = subclass Collection,
print(cls.dumper_map) 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 :author: Autour de Minuit
:maintainers: Florentin LUCE :maintainers: Florentin LUCE
@ -14,71 +14,59 @@ 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 .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.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_nodes" bl_idname = "node_kit.copy_node_tree"
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"}
def execute(self, context): select_only: BoolProperty(default=True) # TODO: Expose/rework this property properly - No F3 panel in Node Editor - Only F9
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"}
def execute(self, context): def execute(self, context):
ntree = context.space_data.edit_tree 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) 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"} return {"FINISHED"}
class NODEKIT_OT_paste(Operator): class NODEKIT_OT_paste(Operator):
bl_idname = "node_kit.paste_nodes" bl_idname = "node_kit.paste_node_tree"
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 = parse_nkit_format(context.window_manager.clipboard) ntree_data = json.loads(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"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"} 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 = "Remap Node Groups Duplicates" bl_label = "Clean nodes"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty( selection: EnumProperty(
@ -139,7 +127,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 Nodes" bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection: EnumProperty( selection: EnumProperty(
@ -249,7 +237,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 = "Pack Nodes" bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
@ -259,7 +247,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 = "Unpack Nodes" bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
@ -269,7 +257,6 @@ 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,

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 :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_nodes", icon="COPYDOWN") layout.operator("node_kit.copy_node_tree", text="Copy Nodes", icon="COPYDOWN")
layout.operator("node_kit.paste_nodes", icon="PASTEDOWN") layout.operator(
"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(
layout.operator("node_kit.remap_node_group_duplicates",icon="NODE_INSERT_OFF") "node_kit.remap_node_group_duplicates",
layout.operator("node_kit.update_nodes", icon="IMPORT") text="Remap Node Groups Duplicates",
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("node_kit.pack_nodes", icon="PACKAGE") layout.operator(
layout.operator("node_kit.unpack_nodes", icon="UGLYPACKAGE") "node_kit.unpack_nodes", text="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)

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)]
)