diff --git a/__init__.py b/__init__.py index d979762..ed4e869 100644 --- a/__init__.py +++ b/__init__.py @@ -6,7 +6,7 @@ bl_info = { "location": "Node Editor -> Node Kit", "description": "Collection of node-related tools", "doc_url": "https://git.autourdeminuit.com/autour_de_minuit/node_kit", - "category": "Node" + "category": "Node", } diff --git a/core/_node.py b/core/_node.py index 7196bf6..c1a7c97 100644 --- a/core/_node.py +++ b/core/_node.py @@ -1,4 +1,3 @@ - import bpy from mathutils import Color, Vector @@ -9,7 +8,6 @@ class Node: """Blender Node abstraction.""" def __init__(self, bl_node, parent): - self.bl_node = bl_node self.tree = parent self.id = hex(id(self.bl_node)) @@ -67,12 +65,12 @@ class Node: self._parent = value # Node id case - elif isinstance(value, str) and value.startswith('0x'): + elif isinstance(value, str) and value.startswith("0x"): for node in self.tree.nodes: if node.id == value: self._parent = node else: - print('Cannot find parent') + print("Cannot find parent") # blender node case elif isinstance(value, bpy.types.Node): @@ -94,10 +92,10 @@ class Node: Returns: Node: Node abstract according to the blender node type. """ - if bl_node.bl_idname == 'CompositorNodeRLayers': + if bl_node.bl_idname == "CompositorNodeRLayers": return RenderLayersNode(bl_node, tree) - elif bl_node.bl_idname == 'CompositorNodeValToRGB': + elif bl_node.bl_idname == "CompositorNodeValToRGB": return ColorRampNode(bl_node, tree) else: @@ -115,19 +113,23 @@ class Node: Node: Create abstract node. """ - new_bl_node = tree.bl_node_tree.nodes.new(type=data['bl_idname']) + new_bl_node = tree.bl_node_tree.nodes.new(type=data["bl_idname"]) node = cls.from_blender_node(new_bl_node, tree) - node.id = data['id'] + node.id = data["id"] for p in node.parameters: setattr(node, p, data[p]) # set attribute on the blender node only if correct type is retrieve - if p not in ('parent', 'scene'): + if p not in ("parent", "scene"): setattr(node.bl_node, p, getattr(node, p)) - node.inputs = [Input.from_dict(ipt_data, node) for ipt_data in data['inputs'].values()] - node.outputs = [Output.from_dict(opt_data, node) for opt_data in data['outputs'].values()] + node.inputs = [ + Input.from_dict(ipt_data, node) for ipt_data in data["inputs"].values() + ] + node.outputs = [ + Output.from_dict(opt_data, node) for opt_data in data["outputs"].values() + ] return node def dump(self): @@ -138,7 +140,6 @@ class Node: """ for prop_id in self.parameters: - if not hasattr(self, prop_id): continue @@ -154,9 +155,9 @@ class Node: self.data[prop_id] = attr_value - self.data['id'] = self.id - self.data['inputs'] = {ipt.id: ipt.dump() for ipt in self.inputs} - self.data['outputs'] = {opt.id: opt.dump() for opt in self.outputs} + self.data["id"] = self.id + self.data["inputs"] = {ipt.id: ipt.dump() for ipt in self.inputs} + self.data["outputs"] = {opt.id: opt.dump() for opt in self.outputs} return self.data @@ -195,7 +196,6 @@ class Link: """Blender Link abstraction.""" def __init__(self, bl_link, parent): - self.bl_link = bl_link self.tree = parent self.id = hex(id(self.bl_link)) @@ -206,7 +206,6 @@ class Link: self.data = {} def dump(self): - - self.data['id'] = self.id + self.data["id"] = self.id return self.data diff --git a/core/_node_tree.py b/core/_node_tree.py index c042a9a..65ef8d4 100644 --- a/core/_node_tree.py +++ b/core/_node_tree.py @@ -8,7 +8,6 @@ class NodeTree: """Blender node tree abstraction.""" def __init__(self, bl_node_tree): - self.bl_node_tree = bl_node_tree self.data = {} @@ -26,10 +25,14 @@ class NodeTree: Defaults to False. Returns: - dict: Nodes and links as dict. + dict: Nodes and links as dict. """ - self.data['nodes'] = {n.id: n.dump() for n in self.nodes if not select_only or (select_only and n.select)} - self.data['links'] = [l.id for l in self.links] + self.data["nodes"] = { + n.id: n.dump() + for n in self.nodes + if not select_only or (select_only and n.select) + } + self.data["links"] = [l.id for l in self.links] return self.data @@ -46,8 +49,7 @@ class NodeTree: self.data = data - for node_id, node_data in self.data['nodes'].items(): - + for node_id, node_data in self.data["nodes"].items(): new_node = Node.from_dict(node_data, self) self.nodes.append(new_node) @@ -55,15 +57,15 @@ class NodeTree: for ipt in new_node.inputs: if ipt.is_linked: - connections.setdefault(ipt.link, {})['input'] = ipt.bl_input + connections.setdefault(ipt.link, {})["input"] = ipt.bl_input for opt in new_node.outputs: if opt.is_linked: for link in opt.link: - connections.setdefault(link, {})['output'] = opt.bl_output + connections.setdefault(link, {})["output"] = opt.bl_output - for link_id in self.data['links']: - ipt = connections[link_id]['input'] - opt = connections[link_id]['output'] + for link_id in self.data["links"]: + ipt = connections[link_id]["input"] + opt = connections[link_id]["output"] self.bl_node_tree.links.new(ipt, opt) diff --git a/core/_sockets.py b/core/_sockets.py index 32ab4b8..9ca2646 100644 --- a/core/_sockets.py +++ b/core/_sockets.py @@ -1,9 +1,5 @@ - - class Socket: - def __init__(self, bl_socket, tree): - self.tree = tree self.bl_socket = bl_socket self.data = {} @@ -14,12 +10,11 @@ class Socket: self._value = None - if hasattr(bl_socket, 'default_value'): + if hasattr(bl_socket, "default_value"): self._value = bl_socket.default_value @property def value(self): - if not isinstance(self._value, (str, int, float, bool)): self._value = [v for v in self._value] @@ -32,16 +27,15 @@ class Socket: return self._value def to_dict(self): - self.data['id'] = self.id - self.data['value'] = self.value - self.data['identifier'] = self.identifier - self.data['is_linked'] = self.is_linked - self.data['link'] = self.get_link() + self.data["id"] = self.id + self.data["value"] = self.value + self.data["identifier"] = self.identifier + self.data["is_linked"] = self.is_linked + self.data["link"] = self.get_link() return self.data class Input(Socket): - def __init__(self, bl_input, tree): super().__init__(bl_input, tree) @@ -49,10 +43,8 @@ class Input(Socket): @classmethod def from_dict(cls, data, node): - for bl_ipt in node.bl_node.inputs: - - if bl_ipt.identifier != data['identifier']: + if bl_ipt.identifier != data["identifier"]: continue new_ipt = cls(bl_ipt, node.tree) @@ -63,7 +55,6 @@ class Input(Socket): return new_ipt def get_link(self): - if not self.is_linked: return None @@ -74,7 +65,6 @@ class Input(Socket): class Output(Socket): - def __init__(self, bl_output, tree): super().__init__(bl_output, tree) @@ -82,10 +72,8 @@ class Output(Socket): @classmethod def from_dict(cls, data, node): - for bl_opt in node.bl_node.outputs: - - if bl_opt.identifier != data['identifier']: + if bl_opt.identifier != data["identifier"]: continue new_opt = cls(bl_opt, node.tree) @@ -96,7 +84,6 @@ class Output(Socket): return new_opt def get_link(self): - links = [] if not self.is_linked: diff --git a/core/dumper.py b/core/dumper.py index 73a3705..9260228 100644 --- a/core/dumper.py +++ b/core/dumper.py @@ -7,54 +7,53 @@ 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): + if getattr(prop, "is_array", False): return list(prop.default_array) - elif hasattr(prop, 'default'): + 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: + 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': + 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) + # print(Dumper.pointers) if bl_object is None: bl_object = get_bl_object(data) - + dumper = get_dumper(bl_object) dumper.load(data, bl_object) @@ -71,56 +70,62 @@ def set_attribute(bl_object, attr, value): class Dumper: pointers = {} includes = [] - excludes = ["rna_type", "bl_rna", 'id_data', 'depsgraph'] - - @classmethod + 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] - + 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}') + print(f"New not implemented for data {data}") - @classmethod + @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'): + + # 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: + 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': + + if prop.type == "COLLECTION": dumper = PropCollection - if hasattr(attr, 'bl_rna'): + 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': + + 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: print(bl_object, "not loaded yet", prop) value = cls.pointers[value] @@ -133,70 +138,69 @@ class Dumper: dumper = get_dumper(bl_type) # If the pointer exist register the pointer then load data - #print('-----', value) - #pointer = + # 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 + # attr = getattr(bl_object, key) + # if not attr: + cls.pointers[value["bl_pointer"]] = attr - if hasattr(attr, 'update'): + 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'): + if hasattr(attr, "update"): attr.update() - + elif not prop.is_readonly: - #print(key, value) + # print(key, value) set_attribute(bl_object, key, value) continue - - #return bl_object + + # 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) + + # 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): + for prop in cls.properties(bl_object): if not hasattr(bl_object, prop.identifier): - print(f'{bl_object} has no attribute {prop.identifier}') + print(f"{bl_object} has no attribute {prop.identifier}") continue - - #print(prop.identifier) + + # 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: + 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': + + elif prop.type == "COLLECTION": value = PropCollection.dump(value) - - elif prop.type == 'POINTER' and value: - #if prop.identifier == 'image': + + 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() @@ -207,93 +211,93 @@ class Dumper: # 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 + + 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 + return - new_func = coll.bl_rna.functions['new'] + new_func = coll.bl_rna.functions["new"] for i, value in enumerate(values): - - if value.get('_new'): - params = value['_new'] + 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]} - + 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': + 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}') + 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]) + # print(param.identifier, cls.pointers[pointer_id]) try: - item = coll.new(**params) except RuntimeError as e: - #print(e, coll.data) - #print() + # print(e, coll.data) + # print() try: item = coll[i] except IndexError as e: - #print(e, coll.data) + # print(e, coll.data) break - - dumper = get_dumper(item) - dumper.load(value, item)#(item, value) - - + + 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 = [] @@ -307,20 +311,26 @@ class PropArray(Dumper): class NodeSocket(Dumper): bl_type = bpy.types.NodeSocket - excludes = Dumper.excludes + ["node", "links", "display_shape", "rna_type", "link_limit"] + excludes = Dumper.excludes + [ + "node", + "links", + "display_shape", + "rna_type", + "link_limit", + ] - @classmethod + @classmethod def dump(cls, socket): if socket.is_unavailable: return None - #cls.pointers[socket.as_pointer()] = socket - + # cls.pointers[socket.as_pointer()] = socket + data = super().dump(socket) - - #data["_id"] = socket.as_pointer() - #data.pop('name', '') - + + # data["_id"] = socket.as_pointer() + # data.pop('name', '') + return data @@ -336,50 +346,48 @@ class NodeGeometryRepeatOutputItems(PropCollection): class NodeLink(Dumper): bl_type = bpy.types.NodeLink - - @classmethod + + @classmethod def dump(cls, link): - return {"_new": { - "input": link.from_socket.as_pointer(), - "output": link.to_socket.as_pointer() - } - } - + 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 + + @classmethod def dump(cls, socket): - #cls.pointers[socket.as_pointer()] = socket - + # cls.pointers[socket.as_pointer()] = socket + data = super().dump(socket) - #data["_id"] = socket.as_pointer() - - data['_new'] = {"name": data.get('name', '')} + # data["_id"] = socket.as_pointer() - if socket.item_type == 'SOCKET': - data['_new']["in_out"] = socket.in_out + 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() - + if socket.parent.parent: + data["parent"] = socket.parent.as_pointer() + return data - + class NodeSockets(PropCollection): - - @classmethod + @classmethod def load(cls, values, coll): + # return - #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 + cls.pointers[value["bl_pointer"]] = socket Dumper.load(value, socket) # for k, v in value.items(): # if k not in socket.bl_rna.properties: @@ -419,47 +427,54 @@ class NodeOutputs(NodeSockets): class Node(Dumper): bl_type = bpy.types.Node - excludes = Dumper.excludes + ["dimensions", "height", "internal_links", "paired_output"] + excludes = Dumper.excludes + [ + "dimensions", + "height", + "internal_links", + "paired_output", + ] - @classmethod + @classmethod def dump(cls, node=None): - #cls.pointers[node.as_pointer()] = node - + # 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() + # 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: + + # if node.parent: # data['location'] -= Vector()node.parent.location return data - @classmethod + @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', [])) + # 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 + 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': + + # if node.type != 'FRAME': # node.location.y -= 500 - + class CompositorNodeGlare(Node): bl_type = bpy.types.CompositorNodeGlare @@ -469,23 +484,22 @@ class CompositorNodeGlare(Node): class NodeTreeInterface(Dumper): bl_type = bpy.types.NodeTreeInterface - - @classmethod - def load(cls, data, interface): - print('Load Interface') + @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"]) - 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) + + interface.active_index = data.get("active_index", 0) class Nodes(PropCollection): @@ -497,13 +511,16 @@ class Nodes(PropCollection): # 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']] + 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']) + # print(node, node_data['outputs']) - Dumper.load({"inputs": node_data['inputs'], "outputs": node_data['outputs']}, node) + Dumper.load( + {"inputs": node_data["inputs"], "outputs": node_data["outputs"]}, + node, + ) class NodeTree(Dumper): @@ -511,49 +528,59 @@ class NodeTree(Dumper): excludes = [] includes = ["name", "interface", "nodes", "links"] - @classmethod + @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] + 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 + @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} + 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): + 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]} - + 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) - + # 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 @@ -583,11 +610,11 @@ class AOVs(PropCollection): @classmethod def load(cls, values, coll): for value in values: - aov = coll.get(value['name']) + aov = coll.get(value["name"]) if not aov: aov = coll.add() - + Dumper.load(value, aov) @@ -595,57 +622,56 @@ class Image(Dumper): bl_type = bpy.types.Image excludes = [] - includes = ['name', 'filepath'] + includes = ["name", "filepath"] @classmethod def new(cls, data): - # image = next(( img for img in bpy.data.images if not img.library + # 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) + return bpy.data.images.load(data["filepath"], check_existing=True) class Material(Dumper): bl_type = bpy.types.Material - excludes = Dumper.excludes + ['preview', "original"] + excludes = Dumper.excludes + ["preview", "original"] @classmethod def new(cls, data): - material = bpy.data.materials.get(data.get('name', '')) + material = bpy.data.materials.get(data.get("name", "")) if material is None: - material = bpy.data.materials.new(data['name']) - + material = bpy.data.materials.new(data["name"]) + return material class Object(Dumper): bl_type = bpy.types.Object excludes = [] - includes = ['name'] + includes = ["name"] @classmethod def new(cls, data): - if name := data.get('name'): + if name := data.get("name"): return bpy.data.objects.get(name) class Scene(Dumper): bl_type = bpy.types.Scene excludes = [] - includes = ['name'] - + includes = ["name"] @classmethod def new(cls, data): - if scene := bpy.data.scenes.get(data.get('name', '')): + if scene := bpy.data.scenes.get(data.get("name", "")): return scene - - return bpy.data.scenes.new(name=data.get('name', '')) + + return bpy.data.scenes.new(name=data.get("name", "")) """ @classmethod @@ -661,14 +687,15 @@ class Scene(Dumper): } """ + class Collection(Dumper): bl_type = bpy.types.Collection - includes = ['name'] + includes = ["name"] excludes = [] @classmethod def new(cls, data): - if name := data.get('name'): + if name := data.get("name"): return bpy.data.collections.get(name) # @classmethod @@ -682,19 +709,18 @@ class Collection(Dumper): class CompositorNodeRLayers(Node): bl_type = bpy.types.CompositorNodeRLayers - excludes = Dumper.excludes + ['scene'] + excludes = Dumper.excludes + ["scene"] - @classmethod + @classmethod def load(cls, data, node): + # print('load CompositorNodeRLayers') - #print('load CompositorNodeRLayers') - - scene_data = data.pop('scene') - #print(scene_data) - layer = data.pop('layer') + 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 @@ -703,36 +729,37 @@ class CompositorNodeRLayers(Node): # Resetter the view_layer because it might have been created # with the scene attr in the dictionnary and nor available yet - #print(bpy.) + # print(bpy.) - - - @classmethod + @classmethod def dump(cls, node): # Add scene and viewlayer passes data = super().dump(node) - #if + # 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 + # 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] + 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 @@ -740,23 +767,30 @@ class CompositorNodeRLayers(Node): class ViewLayer(Dumper): bl_type = bpy.types.ViewLayer - excludes = Dumper.excludes + ['freestyle_settings', 'eevee', 'cycles', 'active_layer_collection', - 'active_aov', 'active_lightgroup_index', 'active_lightgroup'] - #includes = ['name'] - + excludes = Dumper.excludes + [ + "freestyle_settings", + "eevee", + "cycles", + "active_layer_collection", + "active_aov", + "active_lightgroup_index", + "active_lightgroup", + ] + # includes = ['name'] + class ViewLayers(PropCollection): bl_type = bpy.types.ViewLayers @classmethod def load(cls, values, coll): - #print('LOAD VIEWLAYERS', values) + # print('LOAD VIEWLAYERS', values) for value in values: - view_layer = coll.get(value['name']) + view_layer = coll.get(value["name"]) if view_layer is None: - view_layer = coll.new(value['name']) - + view_layer = coll.new(value["name"]) + Dumper.load(value, view_layer) @@ -787,4 +821,4 @@ dumpers = [ PropArray, CompositorNodeOutputFileLayerSlots, CompositorNodeOutputFileFileSlots, -] \ No newline at end of file +] diff --git a/core/node_utils.py b/core/node_utils.py index 693c6cf..9377cac 100644 --- a/core/node_utils.py +++ b/core/node_utils.py @@ -1,24 +1,24 @@ - import bpy import re def clean_name(name): - if re.match(r'(.*)\.\d{3}$', name): + if re.match(r"(.*)\.\d{3}$", name): return name[:-4] return name def is_node_groups_duplicate(node_groups): node_group_types = sorted([n.type for n in node_groups[0].nodes]) - return all( sorted([n.type for n in ng.nodes]) == - node_group_types for ng in node_groups[1:]) + return all( + sorted([n.type for n in ng.nodes]) == node_group_types for ng in node_groups[1:] + ) def remap_node_group_duplicates(nodes=None, force=False): if nodes is None: nodes = list(bpy.data.node_groups) - + nodes = [n for n in nodes if not n.library] failed = [] @@ -31,7 +31,7 @@ def remap_node_group_duplicates(nodes=None, force=False): for node in bpy.data.node_groups: name = clean_name(node.name) - if name in groups and node not in groups[name]: + if name in groups and node not in groups[name]: groups[name].append(node) print("\nMerge Duplicate NodeGroup...") @@ -41,28 +41,30 @@ def remap_node_group_duplicates(nodes=None, force=False): continue if not force: - node_groups.sort(key=lambda x : x.name, reverse=True) - + node_groups.sort(key=lambda x: x.name, reverse=True) + print(node_groups) for node_group in node_groups[1:]: is_duplicate = is_node_groups_duplicate((node_group, node_groups[0])) - + if not is_duplicate and not force: failed.append((node_group.name, node_groups[0].name)) - print(f'Cannot merge Nodegroup {node_group.name} with {node_groups[0].name} they are different') + print( + f"Cannot merge Nodegroup {node_group.name} with {node_groups[0].name} they are different" + ) continue merged.append((node_group.name, node_groups[0].name)) - print(f'Merge Nodegroup {node_group.name} into {node_groups[0].name}') - + print(f"Merge Nodegroup {node_group.name} into {node_groups[0].name}") + node_group.user_remap(node_groups[0]) bpy.data.node_groups.remove(node_group) node_groups.remove(node_group) - + # Rename groups if it has no duplicate left for node_groups in groups.values(): if len(node_groups) == 1 and not node_groups[0].library: node_groups[0].name = clean_name(node_groups[0].name) - - return merged, failed \ No newline at end of file + + return merged, failed diff --git a/core/pack_nodes.py b/core/pack_nodes.py index 620336e..4003bb9 100644 --- a/core/pack_nodes.py +++ b/core/pack_nodes.py @@ -1,16 +1,17 @@ import bpy + def set_params(src, tgt, mod_to_node=True, org_modifier=None): # mod to node: est-ce qu'on copie les valeurs d'un modifier a une node, ou l'inverse - if mod_to_node: # syntax for node and modifier are slightly different + if mod_to_node: # syntax for node and modifier are slightly different tree = src.node_group.interface.items_tree else: tree = src.node_tree.interface.items_tree for param in tree: - if param.socket_type == 'NodeSocketGeometry': + if param.socket_type == "NodeSocketGeometry": continue - if param.in_out == 'OUTPUT': + if param.in_out == "OUTPUT": continue # seulement en extract mode, src est une node donc on check si des parametres sont dans le modifier @@ -26,19 +27,23 @@ def set_params(src, tgt, mod_to_node=True, org_modifier=None): else: tgt[identifier] = src.inputs[identifier].default_value + def set_group_inputs(target, objects, group): mod = target.modifiers[0] - node_dct = {} # used for cleanup + node_dct = {} # used for cleanup for key, inp in get_node_inputs(objects).items(): # add the socket to the node group / modifier pannel - sock = group.interface.new_socket(inp["label"],in_out="INPUT",socket_type=inp["socket"]) + sock = group.interface.new_socket( + inp["label"], in_out="INPUT", socket_type=inp["socket"] + ) mod[sock.identifier] = inp["data"] # inspect all nodes and add a group input node when that socket is used for node in parse_nodes(objects): for param in node.node_tree.interface.items_tree: nkey = get_input_socket_key(node, param) - if not nkey: continue + if not nkey: + continue if nkey == key: input_node = add_input_node(group, node, param.identifier, sock) @@ -53,12 +58,13 @@ def set_group_inputs(target, objects, group): node_dct[node].append(input_node) # on refait la meme chose pour les object info nodes car leur syntaxe est un peu differente - for node in parse_nodes(objects, type = "OBJECT_INFO"): + for node in parse_nodes(objects, type="OBJECT_INFO"): nkey = get_input_socket_key(node, param) - if not nkey: continue + if not nkey: + continue if nkey == key: - input_node = add_input_node(group, node, 'Object', sock) - node.inputs['Object'].default_value = None + input_node = add_input_node(group, node, "Object", sock) + node.inputs["Object"].default_value = None # add to dict for cleanup if not node in node_dct.keys(): @@ -72,30 +78,36 @@ def set_group_inputs(target, objects, group): input_node.location[1] += 50 * offset hide_sockets(input_node) + def get_node_link_value(node, param_name, org_mod): if not org_mod: return # est-ce que le param est connecté a une autre node ? - if not node.inputs[param_name].links: + if not node.inputs[param_name].links: return socket_id = node.inputs[param_name].links[0].from_socket.identifier return org_mod[socket_id] + def get_geo_socket(node, input=True): - if node.type != "GROUP": - return('Geometry') + if node.type != "GROUP": + return "Geometry" for param in node.node_tree.interface.items_tree: - if param.socket_type != 'NodeSocketGeometry': + if param.socket_type != "NodeSocketGeometry": continue - if input and param.in_out == 'INPUT' : return param.identifier - if not input and param.in_out == 'OUTPUT' : return param.identifier + if input and param.in_out == "INPUT": + return param.identifier + if not input and param.in_out == "OUTPUT": + return param.identifier return None + + def get_input_socket_key(node, param): if node.type == "GROUP": - if param.in_out != 'INPUT': + if param.in_out != "INPUT": return False - if not param.socket_type in ['NodeSocketObject','NodeSocketCollection']: + if not param.socket_type in ["NodeSocketObject", "NodeSocketCollection"]: return False tgt = node.inputs[param.identifier].default_value @@ -104,11 +116,12 @@ def get_input_socket_key(node, param): return f"{param.socket_type[10:][:3]} {tgt.name}" if node.type == "OBJECT_INFO": - tgt = node.inputs['Object'].default_value + tgt = node.inputs["Object"].default_value if not tgt: return False return f"Object {tgt.name}" + def get_node_inputs(combined_nodes): # inputs["Col COL.name"] = {name = COL.name, data = COL, socket = "COLLECTION"} # inputs["Obj OBJ.name"] = {name = OBJ.name, data = OBJ, socket = "OBJECT"} @@ -116,20 +129,31 @@ def get_node_inputs(combined_nodes): for node in parse_nodes(combined_nodes): for param in node.node_tree.interface.items_tree: key = get_input_socket_key(node, param) - if not key: + if not key: continue tgt = node.inputs[param.identifier].default_value - inputs[key] = {'name': tgt.name, 'data': tgt, 'label': param.name , 'socket': param.socket_type} + inputs[key] = { + "name": tgt.name, + "data": tgt, + "label": param.name, + "socket": param.socket_type, + } - for node in parse_nodes(combined_nodes, type = "OBJECT_INFO"): + for node in parse_nodes(combined_nodes, type="OBJECT_INFO"): key = get_input_socket_key(node, None) if not key: continue - tgt = node.inputs['Object'].default_value - inputs[key] = {'name': tgt.name, 'data': tgt, 'label': 'Source OB' , 'socket': "NodeSocketObject"} + tgt = node.inputs["Object"].default_value + inputs[key] = { + "name": tgt.name, + "data": tgt, + "label": "Source OB", + "socket": "NodeSocketObject", + } return inputs + def get_node_bounds(objects, mode=0, x=0, y=0): min_x = min_y = 10000000 max_x = max_y = 0 @@ -137,13 +161,14 @@ def get_node_bounds(objects, mode=0, x=0, y=0): for ob in objects: for node in ob: co = node.location - min_x = min(co[0],min_x) - max_x = max(co[0],max_x) + min_x = min(co[0], min_x) + max_x = max(co[0], max_x) - min_y = min(co[1],min_y) - max_y = max(co[1],max_y) + min_y = min(co[1], min_y) + max_y = max(co[1], max_y) if mode == 0: - return([max_x+x, (min_y+max_y)/2 ]) + return [max_x + x, (min_y + max_y) / 2] + def get_collection(name): scn = bpy.context.scene @@ -152,23 +177,29 @@ def get_collection(name): # look for existing for c in bpy.data.collections: - if c.name == name: col = c + if c.name == name: + col = c # create if needed - if not col: col = bpy.data.collections.new(name) + if not col: + col = bpy.data.collections.new(name) # link to scene if needed for c in scn.collection.children_recursive: - if c.name == col.name: link = True + if c.name == col.name: + link = True if not link: scn.collection.children.link(col) return col + def get_mod_frames(grp): frames = [] for node in grp.nodes: - if node.type == "FRAME": frames.append(node) - return(frames) + if node.type == "FRAME": + frames.append(node) + return frames + def get_frame_childrens(frame): childrens = [] @@ -183,13 +214,16 @@ def get_frame_childrens(frame): childrens = [locs[x] for x in entries] return childrens -def parse_nodes(combined_nodes, type = "GROUP"): + +def parse_nodes(combined_nodes, type="GROUP"): nodes = [] for frame in combined_nodes: for node in frame: - if node.type == type: nodes.append(node) + if node.type == type: + nodes.append(node) return nodes + def copy_source_ob(ob, col): # est-ce que l'objet a des data ? si oui on cree une copie , # si non on renvois None @@ -210,24 +244,26 @@ def copy_source_ob(ob, col): col.objects.link(new_ob) return new_ob -def hide_sockets(node,collapse = True): + +def hide_sockets(node, collapse=True): for socket in node.outputs: - if not socket.links: + if not socket.links: socket.hide = True for socket in node.inputs: - if not socket.links: + if not socket.links: socket.hide = True - if collapse: + if collapse: node.hide = True + def add_input_node(group, node, param_id, socket): - group_input_node = group.nodes.new('NodeGroupInput') + group_input_node = group.nodes.new("NodeGroupInput") group_input_node.location = node.location group_input_node.location[1] += 70 group_input_node.label = socket.name - group.links.new(group_input_node.outputs[socket.identifier], - node.inputs[param_id]) - return(group_input_node) + group.links.new(group_input_node.outputs[socket.identifier], node.inputs[param_id]) + return group_input_node + def add_material_node(ob, group, nodes): if not ob.material_slots: @@ -235,54 +271,59 @@ def add_material_node(ob, group, nodes): if not ob.material_slots[0].material: return nodes last_node = nodes[-1:][0] - node = group.nodes.new('GeometryNodeSetMaterial') - node.inputs['Material'].default_value = ob.material_slots[0].material + node = group.nodes.new("GeometryNodeSetMaterial") + node.inputs["Material"].default_value = ob.material_slots[0].material node.location = last_node.location node.location[0] += 300 nodes.append(node) return nodes + def join_nodes(group, nodes): prev = None - for i , node in enumerate(nodes): + for i, node in enumerate(nodes): if not prev: prev = node continue geo_in = get_geo_socket(node) - geo_out = get_geo_socket(prev, input = False) + geo_out = get_geo_socket(prev, input=False) if not geo_in or not geo_out: continue group.links.new(prev.outputs[geo_out], node.inputs[geo_in]) prev = node + def frame_nodes(group, nodes, ob): - nd = group.nodes.new('NodeFrame') + nd = group.nodes.new("NodeFrame") # frame = nodes.new(type='NodeFrame') - for n in nodes: + for n in nodes: n.parent = nd nd.label = ob.name + def combine_ob(ob, group, y=0, col=None): nodes = [] # object info node - nd = group.nodes.new('GeometryNodeObjectInfo') + nd = group.nodes.new("GeometryNodeObjectInfo") nd.location[0] -= 300 nd.location[1] = y * 800 nd.transform_space = "RELATIVE" - nd.inputs['Object'].default_value = copy_source_ob(ob, col) # si l'objet contient des data on crée une copie + nd.inputs["Object"].default_value = copy_source_ob( + ob, col + ) # si l'objet contient des data on crée une copie nodes.append(nd) # ob modifiers - for x,md in enumerate(ob.modifiers): - if md.type != "NODES" : + for x, md in enumerate(ob.modifiers): + if md.type != "NODES": print(abordage) if md.node_group == group: continue - nd = group.nodes.new('GeometryNodeGroup') + nd = group.nodes.new("GeometryNodeGroup") nd.label = md.name nd.width = 230 nd.location[0] = x * 300 @@ -296,6 +337,7 @@ def combine_ob(ob, group, y=0, col=None): frame_nodes(group, nodes, ob) return nodes + def gen_target_ob(group, col=None): ob = gen_empty_ob(group.name, col=col) mod = ob.modifiers.new(group.name, "NODES") @@ -303,20 +345,22 @@ def gen_target_ob(group, col=None): ob.show_name = True bpy.context.view_layer.objects.active = ob - return(ob) + return ob + def gen_empty_ob(name, col=None): scn = bpy.context.scene ob = bpy.data.objects.new(name, object_data=bpy.data.meshes.new(name)) ob.data.materials.append(None) - ob.material_slots[0].link = 'OBJECT' + ob.material_slots[0].link = "OBJECT" if not col: scn.collection.objects.link(ob) else: col.objects.link(ob) - return(ob) + return ob + def assign_modifiers(ob, frame, org_modifier): for node in get_frame_childrens(frame): @@ -328,11 +372,14 @@ def assign_modifiers(ob, frame, org_modifier): set_params(node, mod, mod_to_node=False, org_modifier=org_modifier) mod.node_group.interface_update(bpy.context) + def join_branches(objects, group): # join all trees and add an output node - join = group.nodes.new('GeometryNodeJoinGeometry') - out = group.nodes.new('NodeGroupOutput') - out_sock = group.interface.new_socket("Geometry",in_out="OUTPUT",socket_type="NodeSocketGeometry") + join = group.nodes.new("GeometryNodeJoinGeometry") + out = group.nodes.new("NodeGroupOutput") + out_sock = group.interface.new_socket( + "Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry" + ) loc = get_node_bounds(objects, x=500) join.location = loc @@ -341,45 +388,52 @@ def join_branches(objects, group): for ob in objects: node = ob[-1:][0] - group.links.new(node.outputs[get_geo_socket(node, input=False)], - join.inputs[get_geo_socket(join)]) + group.links.new( + node.outputs[get_geo_socket(node, input=False)], + join.inputs[get_geo_socket(join)], + ) + + group.links.new( + join.outputs[get_geo_socket(join, input=False)], out.inputs[out_sock.identifier] + ) - group.links.new(join.outputs[get_geo_socket(join, input=False)], - out.inputs[out_sock.identifier]) def gen_extracted_ob(name, frame, col, mod): ob = None for node in get_frame_childrens(frame): if node.type != "OBJECT_INFO": continue - target = get_node_link_value(node, 'Object', mod) + target = get_node_link_value(node, "Object", mod) if target: ob = target.copy() ob.data = ob.data.copy() col.objects.link(ob) - if not ob: ob = gen_empty_ob(name , col = col) + if not ob: + ob = gen_empty_ob(name, col=col) # assign material for node in get_frame_childrens(frame): if node.type != "SET_MATERIAL": continue - ob.material_slots[0].material = node.inputs['Material'].default_value + ob.material_slots[0].material = node.inputs["Material"].default_value return ob + def combine_objects(objs): name = f"NODEGROUP_combined" col = get_collection(name) - group = bpy.data.node_groups.new(name=name, type='GeometryNodeTree') + group = bpy.data.node_groups.new(name=name, type="GeometryNodeTree") objects = [] - for y , ob in enumerate(objs): + for y, ob in enumerate(objs): objects.append(combine_ob(ob, group, y=y, col=col)) - target = gen_target_ob(group, col = col) + target = gen_target_ob(group, col=col) set_group_inputs(target, objects, group) join_branches(objects, group) + def extract_objects(object): mod = object.modifiers[0] grp = mod.node_group @@ -390,8 +444,9 @@ def extract_objects(object): ob = gen_extracted_ob(name, frame, col, mod) assign_modifiers(ob, frame, mod) -#combine_objects(bpy.context.selected_objects) -#extract_objects(bpy.context.active_object) + +# combine_objects(bpy.context.selected_objects) +# extract_objects(bpy.context.active_object) """ TODO: extract copier les transform de l'objet original ... OK ! combine: si un objet a un materiel on cree un node set material en fin de liste diff --git a/operators.py b/operators.py index fbb7984..4aa63f5 100644 --- a/operators.py +++ b/operators.py @@ -14,26 +14,29 @@ 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, load +# from node_kit.core.node_tree import NodeTree +from .core.dumper import dump, load 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_options = {'REGISTER', 'UNDO'} + bl_idname = "node_kit.copy_node_tree" + bl_label = "Copy nodes" + bl_options = {"REGISTER", "UNDO"} select_only: BoolProperty(default=True) def execute(self, context): - ntree = context.space_data.edit_tree if self.select_only: ntree_data = { - "nodes" : dump([n for n in ntree.nodes if n.select]) ,#[dump(n) for n in ntree.nodes if n.select], - "links" : dump([l for l in ntree.links if l.from_node.select and l.to_node.select]) + "nodes": dump( + [n for n in ntree.nodes if n.select] + ), # [dump(n) for n in ntree.nodes if n.select], + "links": dump( + [l for l in ntree.links if l.from_node.select and l.to_node.select] + ), } else: ntree_data = dump(ntree) @@ -42,76 +45,91 @@ class NODEKIT_OT_copy(Operator): context.window_manager.clipboard = json.dumps(ntree_data) - return {'FINISHED'} + return {"FINISHED"} class NODEKIT_OT_paste(Operator): - bl_idname = 'node_kit.paste_node_tree' - bl_label = 'Paste nodes' + bl_idname = "node_kit.paste_node_tree" + bl_label = "Paste nodes" def execute(self, context): - ntree_data = json.loads(context.window_manager.clipboard) load(ntree_data, context.space_data.edit_tree) - return {'FINISHED'} + return {"FINISHED"} class NODEKIT_OT_remap_node_group_duplicates(Operator): - bl_idname = 'node_kit.remap_node_group_duplicates' - bl_label = 'Clean nodes' + bl_idname = "node_kit.remap_node_group_duplicates" + bl_label = "Clean nodes" bl_options = {"REGISTER", "UNDO"} - selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'CURRENT')], default="CURRENT", name='All Nodes') - force : BoolProperty(default=False, description='Remap nodes even if there are different', name='Force') + selection: EnumProperty( + items=[(s, s.title(), "") for s in ("ALL", "SELECTED", "CURRENT")], + default="CURRENT", + name="All Nodes", + ) + force: BoolProperty( + default=False, + description="Remap nodes even if there are different", + name="Force", + ) def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def execute(self, context): nodes = None - if self.selection == 'SELECTED': - nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes - if n.type == "GROUP" and n.select] - elif self.selection == 'ACTIVE': + if self.selection == "SELECTED": + nodes = [ + n.node_tree + for n in context.space_data.edit_tree.nodes + if n.type == "GROUP" and n.select + ] + elif self.selection == "ACTIVE": active_node = context.space_data.edit_tree nodes = [active_node] merged, failed = remap_node_group_duplicates(nodes=nodes, force=self.force) if failed and not merged: - self.report({"ERROR"}, 'No duplicates remapped, Node Group are differents') + self.report({"ERROR"}, "No duplicates remapped, Node Group are differents") return {"CANCELLED"} - self.report({"INFO"}, f'{len(merged)} Node Groups Remapped, {len(failed)} Node Groups failed') + self.report( + {"INFO"}, + f"{len(merged)} Node Groups Remapped, {len(failed)} Node Groups failed", + ) + + return {"FINISHED"} - return {'FINISHED'} - def draw(self, context): - - layout = self.layout layout.prop(self, "selection", expand=True) layout.prop(self, "force") - if self.force and self.selection == 'CURRENT': + if self.force and self.selection == "CURRENT": ntree = context.space_data.edit_tree - layout.label(text=f'Remap node {ntree.name} to others') - elif self.force and self.selection == 'SELECTED': - layout.label(text='Selected nodes will override others') - elif self.selection == 'SELECTED': - layout.label(text='Remap last .*** nodes') - layout.label(text='Ex: Node.001 will override Node') - elif self.selection in ('CURRENT', 'ALL'): - layout.label(text='Remap last .*** nodes') - layout.label(text='Ex: Node.001 will override Node') + layout.label(text=f"Remap node {ntree.name} to others") + elif self.force and self.selection == "SELECTED": + layout.label(text="Selected nodes will override others") + elif self.selection == "SELECTED": + layout.label(text="Remap last .*** nodes") + layout.label(text="Ex: Node.001 will override Node") + elif self.selection in ("CURRENT", "ALL"): + layout.label(text="Remap last .*** nodes") + layout.label(text="Ex: Node.001 will override Node") class NODEKIT_OT_update_nodes(Operator): - bl_idname = 'node_kit.update_nodes' - bl_label = 'Update node' + bl_idname = "node_kit.update_nodes" + bl_label = "Update node" bl_options = {"REGISTER", "UNDO"} - selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'ACTIVE')], default="ACTIVE", name='All Nodes') + selection: EnumProperty( + items=[(s, s.title(), "") for s in ("ALL", "SELECTED", "ACTIVE")], + default="ACTIVE", + name="All Nodes", + ) def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -123,61 +141,75 @@ class NODEKIT_OT_update_nodes(Operator): ntree_name = ntree.name new_ntree = None - if self.selection == 'SELECTED': - nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes - if n.type == "GROUP" and n.select] - elif self.selection == 'ACTIVE': + if self.selection == "SELECTED": + nodes = [ + n.node_tree + for n in context.space_data.edit_tree.nodes + if n.type == "GROUP" and n.select + ] + elif self.selection == "ACTIVE": active_node = context.space_data.edit_tree nodes = [active_node] else: nodes = list(bpy.data.node_groups) - - - node_names = set(n.name for n in nodes) - #new_node_groups = [] - #print("node_names", node_names) + node_names = set(n.name for n in nodes) + # new_node_groups = [] + + # print("node_names", node_names) for asset_library in asset_libraries: library_path = Path(asset_library.path) blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()] - node_groups = list(bpy.data.node_groups)# Storing original node_geoup to compare with imported + node_groups = list( + bpy.data.node_groups + ) # Storing original node_geoup to compare with imported - link = (asset_library.import_method == 'LINK') + link = asset_library.import_method == "LINK" for blend_file in blend_files: print(blend_file) - with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to): - + with bpy.data.libraries.load( + str(blend_file), assets_only=True, link=link + ) as (data_from, data_to): print(data_from.node_groups) - import_node_groups = [n for n in data_from.node_groups if n in node_names] + import_node_groups = [ + n for n in data_from.node_groups if n in node_names + ] print("import_node_groups", import_node_groups) data_to.node_groups = import_node_groups - #print(data_from.node_groups) - #print("data_to.node_groups", data_to.node_groups) - node_names -= set(import_node_groups) # Store already updated nodes - - #new_ntree = data_to.node_groups[0] + # print(data_from.node_groups) + # print("data_to.node_groups", data_to.node_groups) + node_names -= set(import_node_groups) # Store already updated nodes + + # new_ntree = data_to.node_groups[0] new_node_groups = [n for n in bpy.data.node_groups if n not in node_groups] - #break - - #if new_ntree: - # break + # break + + # if new_ntree: + # break new_node_groups = list(set(new_node_groups)) - #print(new_node_groups) + # print(new_node_groups) # if new_node_groups: for new_node_group in new_node_groups: new_node_group_name = new_node_group.library_weak_reference.id_name[2:] - local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None) + local_node_group = next( + ( + n + for n in bpy.data.node_groups + if n.name == new_node_group_name and n != new_node_group + ), + None, + ) if not local_node_group: - print(f'No local node_group {new_node_group_name}') + print(f"No local node_group {new_node_group_name}") continue - - print(f'Merge node {local_node_group.name} into {new_node_group.name}') + + print(f"Merge node {local_node_group.name} into {new_node_group.name}") local_node_group.user_remap(new_node_group) new_node_group.interface_update(context) @@ -186,8 +218,8 @@ class NODEKIT_OT_update_nodes(Operator): new_node_group.name = new_node_group_name new_node_group.asset_clear() - #self.report({"INFO"}, f"Node updated from {blend_file}") - return {'FINISHED'} + # self.report({"INFO"}, f"Node updated from {blend_file}") + return {"FINISHED"} # else: # self.report({"ERROR"}, f'No Node Group named "{ntree_name}" in the library') @@ -199,23 +231,23 @@ class NODEKIT_OT_update_nodes(Operator): class NODEKIT_OT_pack_nodes(Operator): - bl_idname = 'node_kit.pack_nodes' - bl_label = 'Update node' + bl_idname = "node_kit.pack_nodes" + bl_label = "Update node" bl_options = {"REGISTER", "UNDO"} def execute(self, context): combine_objects(context.selected_objects) - return {'FINISHED'} + return {"FINISHED"} class NODEKIT_OT_unpack_nodes(Operator): - bl_idname = 'node_kit.unpack_nodes' - bl_label = 'Update node' + bl_idname = "node_kit.unpack_nodes" + bl_label = "Update node" bl_options = {"REGISTER", "UNDO"} - + def execute(self, context): extract_objects(context.active_object) - return {'FINISHED'} + return {"FINISHED"} classes = ( @@ -224,7 +256,7 @@ classes = ( NODEKIT_OT_remap_node_group_duplicates, NODEKIT_OT_update_nodes, NODEKIT_OT_pack_nodes, - NODEKIT_OT_unpack_nodes + NODEKIT_OT_unpack_nodes, ) diff --git a/ui.py b/ui.py index 90461e2..deea35c 100644 --- a/ui.py +++ b/ui.py @@ -15,22 +15,29 @@ 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_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', text='Remap Node Groups Duplicates', icon='NODE_INSERT_OFF') - layout.operator('node_kit.update_nodes', text='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', text='Pack Nodes', icon='PACKAGE') - layout.operator('node_kit.unpack_nodes', text='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, -) + +classes = (NODEKIT_MT_node_kit,) def draw_menu(self, context): - self.layout.menu('NODEKIT_MT_node_kit') + self.layout.menu("NODEKIT_MT_node_kit") def register():