ruff format

refactor
Jonas Holzman 2025-03-19 17:40:17 +01:00
parent 30ae14f9bb
commit 9940a9c8ca
9 changed files with 582 additions and 464 deletions

View File

@ -6,7 +6,7 @@ bl_info = {
"location": "Node Editor -> Node Kit", "location": "Node Editor -> Node Kit",
"description": "Collection of node-related tools", "description": "Collection of node-related tools",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/node_kit", "doc_url": "https://git.autourdeminuit.com/autour_de_minuit/node_kit",
"category": "Node" "category": "Node",
} }

View File

@ -1,4 +1,3 @@
import bpy import bpy
from mathutils import Color, Vector from mathutils import Color, Vector
@ -9,7 +8,6 @@ class Node:
"""Blender Node abstraction.""" """Blender Node abstraction."""
def __init__(self, bl_node, parent): def __init__(self, bl_node, parent):
self.bl_node = bl_node self.bl_node = bl_node
self.tree = parent self.tree = parent
self.id = hex(id(self.bl_node)) self.id = hex(id(self.bl_node))
@ -67,12 +65,12 @@ class Node:
self._parent = value self._parent = value
# Node id case # 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: for node in self.tree.nodes:
if node.id == value: if node.id == value:
self._parent = node self._parent = node
else: else:
print('Cannot find parent') print("Cannot find parent")
# blender node case # blender node case
elif isinstance(value, bpy.types.Node): elif isinstance(value, bpy.types.Node):
@ -94,10 +92,10 @@ class Node:
Returns: Returns:
Node: Node abstract according to the blender node type. 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) return RenderLayersNode(bl_node, tree)
elif bl_node.bl_idname == 'CompositorNodeValToRGB': elif bl_node.bl_idname == "CompositorNodeValToRGB":
return ColorRampNode(bl_node, tree) return ColorRampNode(bl_node, tree)
else: else:
@ -115,19 +113,23 @@ class Node:
Node: Create abstract 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 = cls.from_blender_node(new_bl_node, tree)
node.id = data['id'] node.id = data["id"]
for p in node.parameters: for p in node.parameters:
setattr(node, p, data[p]) setattr(node, p, data[p])
# set attribute on the blender node only if correct type is retrieve # 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)) setattr(node.bl_node, p, getattr(node, p))
node.inputs = [Input.from_dict(ipt_data, node) for ipt_data in data['inputs'].values()] node.inputs = [
node.outputs = [Output.from_dict(opt_data, node) for opt_data in data['outputs'].values()] 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 return node
def dump(self): def dump(self):
@ -138,7 +140,6 @@ class Node:
""" """
for prop_id in self.parameters: for prop_id in self.parameters:
if not hasattr(self, prop_id): if not hasattr(self, prop_id):
continue continue
@ -154,9 +155,9 @@ class Node:
self.data[prop_id] = attr_value self.data[prop_id] = attr_value
self.data['id'] = self.id self.data["id"] = self.id
self.data['inputs'] = {ipt.id: ipt.dump() for ipt in self.inputs} 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["outputs"] = {opt.id: opt.dump() for opt in self.outputs}
return self.data return self.data
@ -195,7 +196,6 @@ class Link:
"""Blender Link abstraction.""" """Blender Link abstraction."""
def __init__(self, bl_link, parent): def __init__(self, bl_link, parent):
self.bl_link = bl_link self.bl_link = bl_link
self.tree = parent self.tree = parent
self.id = hex(id(self.bl_link)) self.id = hex(id(self.bl_link))
@ -206,7 +206,6 @@ class Link:
self.data = {} self.data = {}
def dump(self): def dump(self):
self.data["id"] = self.id
self.data['id'] = self.id
return self.data return self.data

View File

@ -8,7 +8,6 @@ class NodeTree:
"""Blender node tree abstraction.""" """Blender node tree abstraction."""
def __init__(self, bl_node_tree): def __init__(self, bl_node_tree):
self.bl_node_tree = bl_node_tree self.bl_node_tree = bl_node_tree
self.data = {} self.data = {}
@ -28,8 +27,12 @@ class NodeTree:
Returns: 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["nodes"] = {
self.data['links'] = [l.id for l in self.links] 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 return self.data
@ -46,8 +49,7 @@ class NodeTree:
self.data = data 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) new_node = Node.from_dict(node_data, self)
self.nodes.append(new_node) self.nodes.append(new_node)
@ -55,15 +57,15 @@ class NodeTree:
for ipt in new_node.inputs: for ipt in new_node.inputs:
if ipt.is_linked: 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: for opt in new_node.outputs:
if opt.is_linked: if opt.is_linked:
for link in opt.link: 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']: for link_id in self.data["links"]:
ipt = connections[link_id]['input'] ipt = connections[link_id]["input"]
opt = connections[link_id]['output'] opt = connections[link_id]["output"]
self.bl_node_tree.links.new(ipt, opt) self.bl_node_tree.links.new(ipt, opt)

View File

@ -1,9 +1,5 @@
class Socket: class Socket:
def __init__(self, bl_socket, tree): def __init__(self, bl_socket, tree):
self.tree = tree self.tree = tree
self.bl_socket = bl_socket self.bl_socket = bl_socket
self.data = {} self.data = {}
@ -14,12 +10,11 @@ class Socket:
self._value = None self._value = None
if hasattr(bl_socket, 'default_value'): if hasattr(bl_socket, "default_value"):
self._value = bl_socket.default_value self._value = bl_socket.default_value
@property @property
def value(self): def value(self):
if not isinstance(self._value, (str, int, float, bool)): if not isinstance(self._value, (str, int, float, bool)):
self._value = [v for v in self._value] self._value = [v for v in self._value]
@ -32,16 +27,15 @@ class Socket:
return self._value return self._value
def to_dict(self): def to_dict(self):
self.data['id'] = self.id self.data["id"] = self.id
self.data['value'] = self.value self.data["value"] = self.value
self.data['identifier'] = self.identifier self.data["identifier"] = self.identifier
self.data['is_linked'] = self.is_linked self.data["is_linked"] = self.is_linked
self.data['link'] = self.get_link() self.data["link"] = self.get_link()
return self.data return self.data
class Input(Socket): class Input(Socket):
def __init__(self, bl_input, tree): def __init__(self, bl_input, tree):
super().__init__(bl_input, tree) super().__init__(bl_input, tree)
@ -49,10 +43,8 @@ class Input(Socket):
@classmethod @classmethod
def from_dict(cls, data, node): def from_dict(cls, data, node):
for bl_ipt in node.bl_node.inputs: for bl_ipt in node.bl_node.inputs:
if bl_ipt.identifier != data["identifier"]:
if bl_ipt.identifier != data['identifier']:
continue continue
new_ipt = cls(bl_ipt, node.tree) new_ipt = cls(bl_ipt, node.tree)
@ -63,7 +55,6 @@ class Input(Socket):
return new_ipt return new_ipt
def get_link(self): def get_link(self):
if not self.is_linked: if not self.is_linked:
return None return None
@ -74,7 +65,6 @@ class Input(Socket):
class Output(Socket): class Output(Socket):
def __init__(self, bl_output, tree): def __init__(self, bl_output, tree):
super().__init__(bl_output, tree) super().__init__(bl_output, tree)
@ -82,10 +72,8 @@ class Output(Socket):
@classmethod @classmethod
def from_dict(cls, data, node): def from_dict(cls, data, node):
for bl_opt in node.bl_node.outputs: for bl_opt in node.bl_node.outputs:
if bl_opt.identifier != data["identifier"]:
if bl_opt.identifier != data['identifier']:
continue continue
new_opt = cls(bl_opt, node.tree) new_opt = cls(bl_opt, node.tree)
@ -96,7 +84,6 @@ class Output(Socket):
return new_opt return new_opt
def get_link(self): def get_link(self):
links = [] links = []
if not self.is_linked: if not self.is_linked:

View File

@ -7,13 +7,12 @@ from copy import copy
from os.path import abspath from os.path import abspath
def get_default(prop): def get_default(prop):
"""Get the default value of a bl property""" """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) return list(prop.default_array)
elif hasattr(prop, 'default'): elif hasattr(prop, "default"):
return prop.default return prop.default
@ -28,7 +27,7 @@ def get_dumper(bl_object, fallback=None):
def get_bl_object(data): def get_bl_object(data):
"""Find the bl object for loading data into it depending on the type and the context""" """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
@ -50,7 +49,7 @@ def load(data, bl_object=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 bl_object is None:
bl_object = get_bl_object(data) bl_object = get_bl_object(data)
@ -71,19 +70,23 @@ def set_attribute(bl_object, attr, value):
class Dumper: class Dumper:
pointers = {} pointers = {}
includes = [] includes = []
excludes = ["rna_type", "bl_rna", 'id_data', 'depsgraph'] excludes = ["rna_type", "bl_rna", "id_data", "depsgraph"]
@classmethod @classmethod
def properties(cls, bl_object): def properties(cls, bl_object):
if cls.includes and not cls.excludes: if cls.includes and not cls.excludes:
return [bl_object.bl_rna.properties[p] for p in cls.includes] return [bl_object.bl_rna.properties[p] for p in cls.includes]
else: else:
return [ p for p in bl_object.bl_rna.properties if not return [
p.identifier.startswith('bl_') and p.identifier not in cls.excludes] p
for p in bl_object.bl_rna.properties
if not p.identifier.startswith("bl_")
and p.identifier not in cls.excludes
]
@classmethod @classmethod
def new(cls, data): 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): def load(cls, data, bl_object=None):
@ -93,34 +96,36 @@ class Dumper:
if bl_object is None: if bl_object is None:
return return
#pprint(data) # 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
props = cls.properties(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): for key, value in sorted(
if key.startswith('_') or key not in bl_object.bl_rna.properties: 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 continue
prop = bl_object.bl_rna.properties[key] prop = bl_object.bl_rna.properties[key]
attr = getattr(bl_object, key) attr = getattr(bl_object, key)
if prop.type == 'COLLECTION': if prop.type == "COLLECTION":
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 = 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': # if key == 'node_tree':
# print('--------------') # print('--------------')
# print(bl_object, value) # print(bl_object, value)
# print(cls.pointers) # 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)
value = cls.pointers[value] value = cls.pointers[value]
@ -133,17 +138,17 @@ 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) # print('-----', value)
#pointer = # 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) # attr = getattr(bl_object, key)
#if not attr: # if not attr:
cls.pointers[value['bl_pointer']] = attr cls.pointers[value["bl_pointer"]] = attr
if hasattr(attr, 'update'): if hasattr(attr, "update"):
attr.update() attr.update()
value = attr value = attr
@ -152,39 +157,38 @@ class Dumper:
set_attribute(bl_object, key, value) set_attribute(bl_object, key, value)
# Some coll needs a manual update like curve mapping # Some coll needs a manual update like curve mapping
if hasattr(attr, 'update'): if hasattr(attr, "update"):
attr.update() attr.update()
elif not prop.is_readonly: elif not prop.is_readonly:
#print(key, value) # print(key, value)
set_attribute(bl_object, key, value) set_attribute(bl_object, key, value)
continue continue
#return bl_object # return bl_object
@classmethod @classmethod
def dump(cls, bl_object): def dump(cls, bl_object):
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) # 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
for prop in cls.properties(bl_object): for prop in cls.properties(bl_object):
if not hasattr(bl_object, prop.identifier): 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 continue
#print(prop.identifier) # print(prop.identifier)
value = getattr(bl_object, prop.identifier) value = getattr(bl_object, prop.identifier)
# Not storing default value # Not storing default value
if prop.identifier not in cls.includes: 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 continue
if isinstance(value, (str, int, float)) and value == prop.default: if isinstance(value, (str, int, float)) and value == prop.default:
continue continue
@ -192,11 +196,11 @@ class Dumper:
if getattr(prop, "is_array", False): if getattr(prop, "is_array", False):
value = PropArray.dump(value) value = PropArray.dump(value)
elif prop.type == 'COLLECTION': elif prop.type == "COLLECTION":
value = PropCollection.dump(value) value = PropCollection.dump(value)
elif prop.type == 'POINTER' and value: elif prop.type == "POINTER" and value:
#if prop.identifier == 'image': # if prop.identifier == 'image':
# print(bl_object, cls.pointers) # 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()
@ -234,7 +238,6 @@ class PropCollection(Dumper):
# Value cannot be None # Value cannot be None
return [v for v in values if v is not None] return [v for v in values if v is not None]
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
if not values: if not values:
@ -242,53 +245,54 @@ class PropCollection(Dumper):
dumper = None dumper = None
if not hasattr(coll, 'new'): # Static collection if not hasattr(coll, "new"): # Static collection
for item, value in zip(coll, values): for item, value in zip(coll, values):
dumper = dumper or get_dumper(item) dumper = dumper or get_dumper(item)
dumper.load(value, 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): for i, value in enumerate(values):
if value.get("_new"):
if value.get('_new'): params = value["_new"]
params = value['_new']
else: 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 # Replace arg pointer with bl object
valid_pointers = True valid_pointers = True
for param in coll.bl_rna.functions['new'].parameters: for param in coll.bl_rna.functions["new"].parameters:
if param.identifier not in params or param.type != 'POINTER': if param.identifier not in params or param.type != "POINTER":
continue continue
pointer_id = params[param.identifier] pointer_id = params[param.identifier]
if bl_object := cls.pointers.get(pointer_id): if bl_object := cls.pointers.get(pointer_id):
params[param.identifier] = bl_object params[param.identifier] = bl_object
else: 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 valid_pointers = False
if not valid_pointers: if not valid_pointers:
continue continue
#print(param.identifier, cls.pointers[pointer_id]) # 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(e, coll.data)
#print() # print()
try: try:
item = coll[i] item = coll[i]
except IndexError as e: except IndexError as e:
#print(e, coll.data) # print(e, coll.data)
break break
dumper = get_dumper(item) dumper = get_dumper(item)
dumper.load(value, item)#(item, value) dumper.load(value, item) # (item, value)
class PropArray(Dumper): class PropArray(Dumper):
@ -307,19 +311,25 @@ class PropArray(Dumper):
class NodeSocket(Dumper): class NodeSocket(Dumper):
bl_type = bpy.types.NodeSocket 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): def dump(cls, socket):
if socket.is_unavailable: if socket.is_unavailable:
return None return None
#cls.pointers[socket.as_pointer()] = socket # cls.pointers[socket.as_pointer()] = socket
data = super().dump(socket) data = super().dump(socket)
#data["_id"] = socket.as_pointer() # data["_id"] = socket.as_pointer()
#data.pop('name', '') # data.pop('name', '')
return data return data
@ -339,11 +349,12 @@ class NodeLink(Dumper):
@classmethod @classmethod
def dump(cls, link): def dump(cls, link):
return {"_new": { return {
"input": link.from_socket.as_pointer(), "_new": {
"output": link.to_socket.as_pointer() "input": link.from_socket.as_pointer(),
} "output": link.to_socket.as_pointer(),
} }
}
class NodeTreeInterfaceSocket(Dumper): class NodeTreeInterfaceSocket(Dumper):
@ -352,34 +363,31 @@ class NodeTreeInterfaceSocket(Dumper):
@classmethod @classmethod
def dump(cls, socket): def dump(cls, socket):
#cls.pointers[socket.as_pointer()] = socket # cls.pointers[socket.as_pointer()] = socket
data = super().dump(socket) data = super().dump(socket)
#data["_id"] = socket.as_pointer() # data["_id"] = socket.as_pointer()
data['_new'] = {"name": data.get('name', '')} data["_new"] = {"name": data.get("name", "")}
if socket.item_type == 'SOCKET':
data['_new']["in_out"] = socket.in_out
if socket.item_type == "SOCKET":
data["_new"]["in_out"] = socket.in_out
# It's a real panel not the interface root # It's a real panel not the interface root
if socket.parent.parent: if socket.parent.parent:
data['parent'] = socket.parent.as_pointer() data["parent"] = socket.parent.as_pointer()
return data return data
class NodeSockets(PropCollection): class NodeSockets(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
# return
#return
node_sockets = [s for s in coll if not s.is_unavailable] node_sockets = [s for s in coll if not s.is_unavailable]
for socket, value in zip(node_sockets, values): for socket, value in zip(node_sockets, values):
cls.pointers[value['bl_pointer']] = socket cls.pointers[value["bl_pointer"]] = socket
Dumper.load(value, socket) Dumper.load(value, socket)
# for k, v in value.items(): # for k, v in value.items():
# if k not in socket.bl_rna.properties: # if k not in socket.bl_rna.properties:
@ -419,20 +427,27 @@ class NodeOutputs(NodeSockets):
class Node(Dumper): class Node(Dumper):
bl_type = bpy.types.Node 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): def dump(cls, node=None):
#cls.pointers[node.as_pointer()] = node # cls.pointers[node.as_pointer()] = node
data = super().dump(node) data = super().dump(node)
#data["_id"] = node.as_pointer() # data["_id"] = node.as_pointer()
data["_new"] = {"type": node.bl_rna.identifier} # 'node_tree': node.id_data.as_pointer() data["_new"] = {
"type": node.bl_rna.identifier
} # 'node_tree': node.id_data.as_pointer()
if paired_output := getattr(node, "paired_output", None): if paired_output := getattr(node, "paired_output", None):
data["_pair_with_output"] = paired_output.as_pointer() data["_pair_with_output"] = paired_output.as_pointer()
#if node.parent: # if node.parent:
# data['location'] -= Vector()node.parent.location # data['location'] -= Vector()node.parent.location
return data return data
@ -441,15 +456,15 @@ class Node(Dumper):
def load(cls, data, node): def load(cls, data, node):
if node is None: if node is None:
return return
#cls.pointers[data['bl_pointer']] = node # cls.pointers[data['bl_pointer']] = node
inputs = copy(data.pop('inputs', [])) inputs = copy(data.pop("inputs", []))
outputs = copy(data.pop('outputs', [])) outputs = copy(data.pop("outputs", []))
super().load(data, node) super().load(data, node)
data['inputs'] = inputs data["inputs"] = inputs
data['outputs'] = outputs data["outputs"] = outputs
# Loading input and outputs after the properties # Loading input and outputs after the properties
super().load({"inputs": inputs, "outputs": outputs}, node) super().load({"inputs": inputs, "outputs": outputs}, node)
@ -457,7 +472,7 @@ class Node(Dumper):
if node.parent: if node.parent:
node.location += node.parent.location node.location += node.parent.location
#if node.type != 'FRAME': # if node.type != 'FRAME':
# node.location.y -= 500 # node.location.y -= 500
@ -472,20 +487,19 @@ class NodeTreeInterface(Dumper):
@classmethod @classmethod
def load(cls, data, interface): def load(cls, data, interface):
print("Load Interface")
print('Load Interface') for value in data.get("items_tree", []):
item_type = value.get("item_type", "SOCKET")
for value in data.get('items_tree', []): if item_type == "SOCKET":
item_type = value.get('item_type', 'SOCKET') item = interface.new_socket(**value["_new"])
if item_type == 'SOCKET': elif item_type == "PANEL":
item = interface.new_socket(**value['_new']) # print(value['_new'])
elif item_type == 'PANEL': item = interface.new_panel(**value["_new"])
#print(value['_new'])
item = interface.new_panel(**value['_new'])
NodeTreeInterfaceSocket.load(value, item) NodeTreeInterfaceSocket.load(value, item)
interface.active_index = data.get('active_index', 0) interface.active_index = data.get("active_index", 0)
class Nodes(PropCollection): class Nodes(PropCollection):
@ -497,13 +511,16 @@ class Nodes(PropCollection):
# Pair zone input and output # Pair zone input and output
for node_data in values: for node_data in values:
if paired_output_id := node_data.get('_pair_with_output', None): if paired_output_id := node_data.get("_pair_with_output", None):
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']) # 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): class NodeTree(Dumper):
@ -513,32 +530,42 @@ class NodeTree(Dumper):
@classmethod @classmethod
def new(cls, data): def new(cls, data):
if link := data.get('_link'): if link := data.get("_link"):
with bpy.data.libraries.load(link['filepath'], link=True) as (data_from, data_to): with bpy.data.libraries.load(link["filepath"], link=True) as (
setattr(data_to, link['data_type'], [link['name']]) data_from,
return getattr(data_to, link['data_type'])[0] 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"]) return bpy.data.node_groups.new(**data["_new"])
@classmethod @classmethod
def dump(cls, node_tree): def dump(cls, node_tree):
if node_tree.library: if node_tree.library:
data = {'bl_pointer': node_tree.as_pointer()} data = {"bl_pointer": node_tree.as_pointer()}
filepath = abspath(bpy.path.abspath(node_tree.library.filepath, library=node_tree.library.library)) filepath = abspath(
data["_link"] = {"filepath": filepath, "data_type": 'node_groups', 'name': node_tree.name} 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: else:
data = super().dump(node_tree) data = super().dump(node_tree)
data["_new"] = {"type": node_tree.bl_rna.identifier, 'name': node_tree.name} data["_new"] = {"type": node_tree.bl_rna.identifier, "name": node_tree.name}
return data return data
class Points(PropCollection): class Points(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
new_func = coll.bl_rna.functions['new'] new_func = coll.bl_rna.functions["new"]
params = {k: get_default(v)+1.1 for k, v in new_func.parameters.items()[:-1]} params = {k: get_default(v) + 1.1 for k, v in new_func.parameters.items()[:-1]}
# Match the same number of elements in collection # Match the same number of elements in collection
if len(values) > len(coll): if len(values) > len(coll):
@ -547,8 +574,8 @@ class Points(PropCollection):
for i, value in enumerate(values): for i, value in enumerate(values):
Dumper.load(value, coll[i]) Dumper.load(value, coll[i])
#for k, v in value.items(): # for k, v in value.items():
#setattr(coll[i], k, v) # setattr(coll[i], k, v)
class CurveMapPoints(Points): class CurveMapPoints(Points):
@ -583,7 +610,7 @@ class AOVs(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
for value in values: for value in values:
aov = coll.get(value['name']) aov = coll.get(value["name"])
if not aov: if not aov:
aov = coll.add() aov = coll.add()
@ -595,7 +622,7 @@ class Image(Dumper):
bl_type = bpy.types.Image bl_type = bpy.types.Image
excludes = [] excludes = []
includes = ['name', 'filepath'] includes = ["name", "filepath"]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
@ -605,20 +632,20 @@ class Image(Dumper):
# if image is None: # if image is None:
# image = bpy.data.images.load(data['filepath']) # 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): class Material(Dumper):
bl_type = bpy.types.Material bl_type = bpy.types.Material
excludes = Dumper.excludes + ['preview', "original"] excludes = Dumper.excludes + ["preview", "original"]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
material = bpy.data.materials.get(data.get('name', '')) material = bpy.data.materials.get(data.get("name", ""))
if material is None: if material is None:
material = bpy.data.materials.new(data['name']) material = bpy.data.materials.new(data["name"])
return material return material
@ -626,26 +653,25 @@ class Material(Dumper):
class Object(Dumper): class Object(Dumper):
bl_type = bpy.types.Object bl_type = bpy.types.Object
excludes = [] excludes = []
includes = ['name'] includes = ["name"]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
if name := data.get('name'): if name := data.get("name"):
return bpy.data.objects.get(name) return bpy.data.objects.get(name)
class Scene(Dumper): class Scene(Dumper):
bl_type = bpy.types.Scene bl_type = bpy.types.Scene
excludes = [] excludes = []
includes = ['name'] includes = ["name"]
@classmethod @classmethod
def new(cls, data): 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 scene
return bpy.data.scenes.new(name=data.get('name', '')) return bpy.data.scenes.new(name=data.get("name", ""))
""" """
@classmethod @classmethod
@ -661,14 +687,15 @@ class Scene(Dumper):
} }
""" """
class Collection(Dumper): class Collection(Dumper):
bl_type = bpy.types.Collection bl_type = bpy.types.Collection
includes = ['name'] includes = ["name"]
excludes = [] excludes = []
@classmethod @classmethod
def new(cls, data): def new(cls, data):
if name := data.get('name'): if name := data.get("name"):
return bpy.data.collections.get(name) return bpy.data.collections.get(name)
# @classmethod # @classmethod
@ -682,16 +709,15 @@ class Collection(Dumper):
class CompositorNodeRLayers(Node): class CompositorNodeRLayers(Node):
bl_type = bpy.types.CompositorNodeRLayers bl_type = bpy.types.CompositorNodeRLayers
excludes = Dumper.excludes + ['scene'] excludes = Dumper.excludes + ["scene"]
@classmethod @classmethod
def load(cls, data, node): def load(cls, data, node):
# print('load CompositorNodeRLayers')
#print('load CompositorNodeRLayers') scene_data = data.pop("scene")
# print(scene_data)
scene_data = data.pop('scene') layer = data.pop("layer")
#print(scene_data)
layer = data.pop('layer')
scene = Scene.new(scene_data) scene = Scene.new(scene_data)
Scene.load(scene_data, scene) Scene.load(scene_data, scene)
@ -703,36 +729,37 @@ class CompositorNodeRLayers(Node):
# Resetter the view_layer because it might have been created # Resetter the view_layer because it might have been created
# with the scene attr in the dictionnary and nor available yet # with the scene attr in the dictionnary and nor available yet
#print(bpy.) # print(bpy.)
@classmethod @classmethod
def dump(cls, node): def dump(cls, node):
# Add scene and viewlayer passes # Add scene and viewlayer passes
data = super().dump(node) data = super().dump(node)
#if # if
view_layer = node.scene.view_layers[node.layer] view_layer = node.scene.view_layers[node.layer]
view_layer_data = ViewLayer.dump(view_layer) view_layer_data = ViewLayer.dump(view_layer)
''' """
view_layer_data = { view_layer_data = {
"name": view_layer.name} "name": view_layer.name}
properties = {p.name: p for p in view_layer.bl_rna.properties} properties = {p.name: p for p in view_layer.bl_rna.properties}
for prop in view_layer.bl_rna: for prop in view_layer.bl_rna:
if prop.identifier.startswith('use_pass'): if prop.identifier.startswith('use_pass'):
view_layer_data[prop.identifier] view_layer_data[prop.identifier]
''' """
#cls.pointers[bl_object.as_pointer()] = bl_object # cls.pointers[bl_object.as_pointer()] = bl_object
data['scene'] = { data["scene"] = {
'bl_pointer': node.scene.as_pointer(), "bl_pointer": node.scene.as_pointer(),
'name': node.scene.name, "name": node.scene.name,
'render' : {'bl_pointer': node.scene.render.as_pointer(), "engine": node.scene.render.engine}, "render": {
'view_layers': [view_layer_data] "bl_pointer": node.scene.render.as_pointer(),
"engine": node.scene.render.engine,
},
"view_layers": [view_layer_data],
} }
return data return data
@ -740,9 +767,16 @@ class CompositorNodeRLayers(Node):
class ViewLayer(Dumper): class ViewLayer(Dumper):
bl_type = bpy.types.ViewLayer bl_type = bpy.types.ViewLayer
excludes = Dumper.excludes + ['freestyle_settings', 'eevee', 'cycles', 'active_layer_collection', excludes = Dumper.excludes + [
'active_aov', 'active_lightgroup_index', 'active_lightgroup'] "freestyle_settings",
#includes = ['name'] "eevee",
"cycles",
"active_layer_collection",
"active_aov",
"active_lightgroup_index",
"active_lightgroup",
]
# includes = ['name']
class ViewLayers(PropCollection): class ViewLayers(PropCollection):
@ -750,12 +784,12 @@ class ViewLayers(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
#print('LOAD VIEWLAYERS', values) # print('LOAD VIEWLAYERS', values)
for value in values: for value in values:
view_layer = coll.get(value['name']) view_layer = coll.get(value["name"])
if view_layer is None: if view_layer is None:
view_layer = coll.new(value['name']) view_layer = coll.new(value["name"])
Dumper.load(value, view_layer) Dumper.load(value, view_layer)

View File

@ -1,18 +1,18 @@
import bpy import bpy
import re import re
def clean_name(name): def clean_name(name):
if re.match(r'(.*)\.\d{3}$', name): if re.match(r"(.*)\.\d{3}$", name):
return name[:-4] return name[:-4]
return name return name
def is_node_groups_duplicate(node_groups): def is_node_groups_duplicate(node_groups):
node_group_types = sorted([n.type for n in node_groups[0].nodes]) node_group_types = sorted([n.type for n in node_groups[0].nodes])
return all( sorted([n.type for n in ng.nodes]) == return all(
node_group_types for ng in node_groups[1:]) 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): def remap_node_group_duplicates(nodes=None, force=False):
@ -41,7 +41,7 @@ def remap_node_group_duplicates(nodes=None, force=False):
continue continue
if not force: 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) print(node_groups)
@ -50,11 +50,13 @@ def remap_node_group_duplicates(nodes=None, force=False):
if not is_duplicate and not force: if not is_duplicate and not force:
failed.append((node_group.name, node_groups[0].name)) 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 continue
merged.append((node_group.name, node_groups[0].name)) 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]) node_group.user_remap(node_groups[0])
bpy.data.node_groups.remove(node_group) bpy.data.node_groups.remove(node_group)

View File

@ -1,16 +1,17 @@
import bpy import bpy
def set_params(src, tgt, mod_to_node=True, org_modifier=None): 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 # 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 tree = src.node_group.interface.items_tree
else: else:
tree = src.node_tree.interface.items_tree tree = src.node_tree.interface.items_tree
for param in tree: for param in tree:
if param.socket_type == 'NodeSocketGeometry': if param.socket_type == "NodeSocketGeometry":
continue continue
if param.in_out == 'OUTPUT': if param.in_out == "OUTPUT":
continue continue
# seulement en extract mode, src est une node donc on check si des parametres sont dans le modifier # 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: else:
tgt[identifier] = src.inputs[identifier].default_value tgt[identifier] = src.inputs[identifier].default_value
def set_group_inputs(target, objects, group): def set_group_inputs(target, objects, group):
mod = target.modifiers[0] mod = target.modifiers[0]
node_dct = {} # used for cleanup node_dct = {} # used for cleanup
for key, inp in get_node_inputs(objects).items(): for key, inp in get_node_inputs(objects).items():
# add the socket to the node group / modifier pannel # 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"] mod[sock.identifier] = inp["data"]
# inspect all nodes and add a group input node when that socket is used # inspect all nodes and add a group input node when that socket is used
for node in parse_nodes(objects): for node in parse_nodes(objects):
for param in node.node_tree.interface.items_tree: for param in node.node_tree.interface.items_tree:
nkey = get_input_socket_key(node, param) nkey = get_input_socket_key(node, param)
if not nkey: continue if not nkey:
continue
if nkey == key: if nkey == key:
input_node = add_input_node(group, node, param.identifier, sock) 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) node_dct[node].append(input_node)
# on refait la meme chose pour les object info nodes car leur syntaxe est un peu differente # 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) nkey = get_input_socket_key(node, param)
if not nkey: continue if not nkey:
continue
if nkey == key: if nkey == key:
input_node = add_input_node(group, node, 'Object', sock) input_node = add_input_node(group, node, "Object", sock)
node.inputs['Object'].default_value = None node.inputs["Object"].default_value = None
# add to dict for cleanup # add to dict for cleanup
if not node in node_dct.keys(): if not node in node_dct.keys():
@ -72,6 +78,7 @@ def set_group_inputs(target, objects, group):
input_node.location[1] += 50 * offset input_node.location[1] += 50 * offset
hide_sockets(input_node) hide_sockets(input_node)
def get_node_link_value(node, param_name, org_mod): def get_node_link_value(node, param_name, org_mod):
if not org_mod: if not org_mod:
return return
@ -82,20 +89,25 @@ def get_node_link_value(node, param_name, org_mod):
return org_mod[socket_id] return org_mod[socket_id]
def get_geo_socket(node, input=True): def get_geo_socket(node, input=True):
if node.type != "GROUP": if node.type != "GROUP":
return('Geometry') return "Geometry"
for param in node.node_tree.interface.items_tree: for param in node.node_tree.interface.items_tree:
if param.socket_type != 'NodeSocketGeometry': if param.socket_type != "NodeSocketGeometry":
continue continue
if input and param.in_out == 'INPUT' : return param.identifier if input and param.in_out == "INPUT":
if not input and param.in_out == 'OUTPUT' : return param.identifier return param.identifier
if not input and param.in_out == "OUTPUT":
return param.identifier
return None return None
def get_input_socket_key(node, param): def get_input_socket_key(node, param):
if node.type == "GROUP": if node.type == "GROUP":
if param.in_out != 'INPUT': if param.in_out != "INPUT":
return False return False
if not param.socket_type in ['NodeSocketObject','NodeSocketCollection']: if not param.socket_type in ["NodeSocketObject", "NodeSocketCollection"]:
return False return False
tgt = node.inputs[param.identifier].default_value 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}" return f"{param.socket_type[10:][:3]} {tgt.name}"
if node.type == "OBJECT_INFO": if node.type == "OBJECT_INFO":
tgt = node.inputs['Object'].default_value tgt = node.inputs["Object"].default_value
if not tgt: if not tgt:
return False return False
return f"Object {tgt.name}" return f"Object {tgt.name}"
def get_node_inputs(combined_nodes): def get_node_inputs(combined_nodes):
# inputs["Col COL.name"] = {name = COL.name, data = COL, socket = "COLLECTION"} # inputs["Col COL.name"] = {name = COL.name, data = COL, socket = "COLLECTION"}
# inputs["Obj OBJ.name"] = {name = OBJ.name, data = OBJ, socket = "OBJECT"} # inputs["Obj OBJ.name"] = {name = OBJ.name, data = OBJ, socket = "OBJECT"}
@ -119,17 +132,28 @@ def get_node_inputs(combined_nodes):
if not key: if not key:
continue continue
tgt = node.inputs[param.identifier].default_value 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) key = get_input_socket_key(node, None)
if not key: if not key:
continue continue
tgt = node.inputs['Object'].default_value tgt = node.inputs["Object"].default_value
inputs[key] = {'name': tgt.name, 'data': tgt, 'label': 'Source OB' , 'socket': "NodeSocketObject"} inputs[key] = {
"name": tgt.name,
"data": tgt,
"label": "Source OB",
"socket": "NodeSocketObject",
}
return inputs return inputs
def get_node_bounds(objects, mode=0, x=0, y=0): def get_node_bounds(objects, mode=0, x=0, y=0):
min_x = min_y = 10000000 min_x = min_y = 10000000
max_x = max_y = 0 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 ob in objects:
for node in ob: for node in ob:
co = node.location co = node.location
min_x = min(co[0],min_x) min_x = min(co[0], min_x)
max_x = max(co[0],max_x) max_x = max(co[0], max_x)
min_y = min(co[1],min_y) min_y = min(co[1], min_y)
max_y = max(co[1],max_y) max_y = max(co[1], max_y)
if mode == 0: 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): def get_collection(name):
scn = bpy.context.scene scn = bpy.context.scene
@ -152,23 +177,29 @@ def get_collection(name):
# look for existing # look for existing
for c in bpy.data.collections: for c in bpy.data.collections:
if c.name == name: col = c if c.name == name:
col = c
# create if needed # 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 # link to scene if needed
for c in scn.collection.children_recursive: for c in scn.collection.children_recursive:
if c.name == col.name: link = True if c.name == col.name:
link = True
if not link: if not link:
scn.collection.children.link(col) scn.collection.children.link(col)
return col return col
def get_mod_frames(grp): def get_mod_frames(grp):
frames = [] frames = []
for node in grp.nodes: for node in grp.nodes:
if node.type == "FRAME": frames.append(node) if node.type == "FRAME":
return(frames) frames.append(node)
return frames
def get_frame_childrens(frame): def get_frame_childrens(frame):
childrens = [] childrens = []
@ -183,13 +214,16 @@ def get_frame_childrens(frame):
childrens = [locs[x] for x in entries] childrens = [locs[x] for x in entries]
return childrens return childrens
def parse_nodes(combined_nodes, type = "GROUP"):
def parse_nodes(combined_nodes, type="GROUP"):
nodes = [] nodes = []
for frame in combined_nodes: for frame in combined_nodes:
for node in frame: for node in frame:
if node.type == type: nodes.append(node) if node.type == type:
nodes.append(node)
return nodes return nodes
def copy_source_ob(ob, col): def copy_source_ob(ob, col):
# est-ce que l'objet a des data ? si oui on cree une copie , # est-ce que l'objet a des data ? si oui on cree une copie ,
# si non on renvois None # si non on renvois None
@ -210,7 +244,8 @@ def copy_source_ob(ob, col):
col.objects.link(new_ob) col.objects.link(new_ob)
return new_ob return new_ob
def hide_sockets(node,collapse = True):
def hide_sockets(node, collapse=True):
for socket in node.outputs: for socket in node.outputs:
if not socket.links: if not socket.links:
socket.hide = True socket.hide = True
@ -220,14 +255,15 @@ def hide_sockets(node,collapse = True):
if collapse: if collapse:
node.hide = True node.hide = True
def add_input_node(group, node, param_id, socket): 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 = node.location
group_input_node.location[1] += 70 group_input_node.location[1] += 70
group_input_node.label = socket.name group_input_node.label = socket.name
group.links.new(group_input_node.outputs[socket.identifier], group.links.new(group_input_node.outputs[socket.identifier], node.inputs[param_id])
node.inputs[param_id]) return group_input_node
return(group_input_node)
def add_material_node(ob, group, nodes): def add_material_node(ob, group, nodes):
if not ob.material_slots: if not ob.material_slots:
@ -235,54 +271,59 @@ def add_material_node(ob, group, nodes):
if not ob.material_slots[0].material: if not ob.material_slots[0].material:
return nodes return nodes
last_node = nodes[-1:][0] last_node = nodes[-1:][0]
node = group.nodes.new('GeometryNodeSetMaterial') node = group.nodes.new("GeometryNodeSetMaterial")
node.inputs['Material'].default_value = ob.material_slots[0].material node.inputs["Material"].default_value = ob.material_slots[0].material
node.location = last_node.location node.location = last_node.location
node.location[0] += 300 node.location[0] += 300
nodes.append(node) nodes.append(node)
return nodes return nodes
def join_nodes(group, nodes): def join_nodes(group, nodes):
prev = None prev = None
for i , node in enumerate(nodes): for i, node in enumerate(nodes):
if not prev: if not prev:
prev = node prev = node
continue continue
geo_in = get_geo_socket(node) 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: if not geo_in or not geo_out:
continue continue
group.links.new(prev.outputs[geo_out], node.inputs[geo_in]) group.links.new(prev.outputs[geo_out], node.inputs[geo_in])
prev = node prev = node
def frame_nodes(group, nodes, ob): def frame_nodes(group, nodes, ob):
nd = group.nodes.new('NodeFrame') nd = group.nodes.new("NodeFrame")
# frame = nodes.new(type='NodeFrame') # frame = nodes.new(type='NodeFrame')
for n in nodes: for n in nodes:
n.parent = nd n.parent = nd
nd.label = ob.name nd.label = ob.name
def combine_ob(ob, group, y=0, col=None): def combine_ob(ob, group, y=0, col=None):
nodes = [] nodes = []
# object info node # object info node
nd = group.nodes.new('GeometryNodeObjectInfo') nd = group.nodes.new("GeometryNodeObjectInfo")
nd.location[0] -= 300 nd.location[0] -= 300
nd.location[1] = y * 800 nd.location[1] = y * 800
nd.transform_space = "RELATIVE" 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) nodes.append(nd)
# ob modifiers # ob modifiers
for x,md in enumerate(ob.modifiers): for x, md in enumerate(ob.modifiers):
if md.type != "NODES" : if md.type != "NODES":
print(abordage) print(abordage)
if md.node_group == group: if md.node_group == group:
continue continue
nd = group.nodes.new('GeometryNodeGroup') nd = group.nodes.new("GeometryNodeGroup")
nd.label = md.name nd.label = md.name
nd.width = 230 nd.width = 230
nd.location[0] = x * 300 nd.location[0] = x * 300
@ -296,6 +337,7 @@ def combine_ob(ob, group, y=0, col=None):
frame_nodes(group, nodes, ob) frame_nodes(group, nodes, ob)
return nodes return nodes
def gen_target_ob(group, col=None): def gen_target_ob(group, col=None):
ob = gen_empty_ob(group.name, col=col) ob = gen_empty_ob(group.name, col=col)
mod = ob.modifiers.new(group.name, "NODES") mod = ob.modifiers.new(group.name, "NODES")
@ -303,20 +345,22 @@ def gen_target_ob(group, col=None):
ob.show_name = True ob.show_name = True
bpy.context.view_layer.objects.active = ob bpy.context.view_layer.objects.active = ob
return(ob) return ob
def gen_empty_ob(name, col=None): def gen_empty_ob(name, col=None):
scn = bpy.context.scene scn = bpy.context.scene
ob = bpy.data.objects.new(name, object_data=bpy.data.meshes.new(name)) ob = bpy.data.objects.new(name, object_data=bpy.data.meshes.new(name))
ob.data.materials.append(None) ob.data.materials.append(None)
ob.material_slots[0].link = 'OBJECT' ob.material_slots[0].link = "OBJECT"
if not col: if not col:
scn.collection.objects.link(ob) scn.collection.objects.link(ob)
else: else:
col.objects.link(ob) col.objects.link(ob)
return(ob) return ob
def assign_modifiers(ob, frame, org_modifier): def assign_modifiers(ob, frame, org_modifier):
for node in get_frame_childrens(frame): 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) set_params(node, mod, mod_to_node=False, org_modifier=org_modifier)
mod.node_group.interface_update(bpy.context) mod.node_group.interface_update(bpy.context)
def join_branches(objects, group): def join_branches(objects, group):
# join all trees and add an output node # join all trees and add an output node
join = group.nodes.new('GeometryNodeJoinGeometry') join = group.nodes.new("GeometryNodeJoinGeometry")
out = group.nodes.new('NodeGroupOutput') out = group.nodes.new("NodeGroupOutput")
out_sock = group.interface.new_socket("Geometry",in_out="OUTPUT",socket_type="NodeSocketGeometry") out_sock = group.interface.new_socket(
"Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry"
)
loc = get_node_bounds(objects, x=500) loc = get_node_bounds(objects, x=500)
join.location = loc join.location = loc
@ -341,45 +388,52 @@ def join_branches(objects, group):
for ob in objects: for ob in objects:
node = ob[-1:][0] node = ob[-1:][0]
group.links.new(node.outputs[get_geo_socket(node, input=False)], group.links.new(
join.inputs[get_geo_socket(join)]) 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): def gen_extracted_ob(name, frame, col, mod):
ob = None ob = None
for node in get_frame_childrens(frame): for node in get_frame_childrens(frame):
if node.type != "OBJECT_INFO": if node.type != "OBJECT_INFO":
continue continue
target = get_node_link_value(node, 'Object', mod) target = get_node_link_value(node, "Object", mod)
if target: if target:
ob = target.copy() ob = target.copy()
ob.data = ob.data.copy() ob.data = ob.data.copy()
col.objects.link(ob) 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 # assign material
for node in get_frame_childrens(frame): for node in get_frame_childrens(frame):
if node.type != "SET_MATERIAL": if node.type != "SET_MATERIAL":
continue continue
ob.material_slots[0].material = node.inputs['Material'].default_value ob.material_slots[0].material = node.inputs["Material"].default_value
return ob return ob
def combine_objects(objs): def combine_objects(objs):
name = f"NODEGROUP_combined" name = f"NODEGROUP_combined"
col = get_collection(name) 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 = [] objects = []
for y , ob in enumerate(objs): for y, ob in enumerate(objs):
objects.append(combine_ob(ob, group, y=y, col=col)) 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) set_group_inputs(target, objects, group)
join_branches(objects, group) join_branches(objects, group)
def extract_objects(object): def extract_objects(object):
mod = object.modifiers[0] mod = object.modifiers[0]
grp = mod.node_group grp = mod.node_group
@ -390,8 +444,9 @@ def extract_objects(object):
ob = gen_extracted_ob(name, frame, col, mod) ob = gen_extracted_ob(name, frame, col, mod)
assign_modifiers(ob, frame, 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 ... 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 OK ! combine: si un objet a un materiel on cree un node set material en fin de liste

View File

@ -14,26 +14,29 @@ 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 node_kit.core.node_tree import NodeTree
from . core.dumper import dump, load from .core.dumper import dump, load
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_node_tree"
bl_label = 'Copy nodes' bl_label = "Copy nodes"
bl_options = {'REGISTER', 'UNDO'} bl_options = {"REGISTER", "UNDO"}
select_only: BoolProperty(default=True) select_only: BoolProperty(default=True)
def execute(self, context): def execute(self, context):
ntree = context.space_data.edit_tree ntree = context.space_data.edit_tree
if self.select_only: if self.select_only:
ntree_data = { ntree_data = {
"nodes" : dump([n for n in ntree.nodes if n.select]) ,#[dump(n) for n in ntree.nodes if n.select], "nodes": dump(
"links" : dump([l for l in ntree.links if l.from_node.select and l.to_node.select]) [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: else:
ntree_data = dump(ntree) ntree_data = dump(ntree)
@ -42,76 +45,91 @@ class NODEKIT_OT_copy(Operator):
context.window_manager.clipboard = json.dumps(ntree_data) context.window_manager.clipboard = json.dumps(ntree_data)
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_node_tree"
bl_label = 'Paste nodes' bl_label = "Paste nodes"
def execute(self, context): def execute(self, context):
ntree_data = json.loads(context.window_manager.clipboard) ntree_data = json.loads(context.window_manager.clipboard)
load(ntree_data, context.space_data.edit_tree) load(ntree_data, context.space_data.edit_tree)
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 = "Clean nodes"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'CURRENT')], default="CURRENT", name='All Nodes') selection: EnumProperty(
force : BoolProperty(default=False, description='Remap nodes even if there are different', name='Force') 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): def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def execute(self, context): def execute(self, context):
nodes = None nodes = None
if self.selection == 'SELECTED': if self.selection == "SELECTED":
nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes nodes = [
if n.type == "GROUP" and n.select] n.node_tree
elif self.selection == 'ACTIVE': 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 active_node = context.space_data.edit_tree
nodes = [active_node] nodes = [active_node]
merged, failed = remap_node_group_duplicates(nodes=nodes, force=self.force) merged, failed = remap_node_group_duplicates(nodes=nodes, force=self.force)
if failed and not merged: 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"} 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): def draw(self, context):
layout = self.layout layout = self.layout
layout.prop(self, "selection", expand=True) layout.prop(self, "selection", expand=True)
layout.prop(self, "force") layout.prop(self, "force")
if self.force and self.selection == 'CURRENT': if self.force and self.selection == "CURRENT":
ntree = context.space_data.edit_tree ntree = context.space_data.edit_tree
layout.label(text=f'Remap node {ntree.name} to others') layout.label(text=f"Remap node {ntree.name} to others")
elif self.force and self.selection == 'SELECTED': elif self.force and self.selection == "SELECTED":
layout.label(text='Selected nodes will override others') layout.label(text="Selected nodes will override others")
elif self.selection == 'SELECTED': elif self.selection == "SELECTED":
layout.label(text='Remap last .*** nodes') layout.label(text="Remap last .*** nodes")
layout.label(text='Ex: Node.001 will override Node') layout.label(text="Ex: Node.001 will override Node")
elif self.selection in ('CURRENT', 'ALL'): elif self.selection in ("CURRENT", "ALL"):
layout.label(text='Remap last .*** nodes') layout.label(text="Remap last .*** nodes")
layout.label(text='Ex: Node.001 will override Node') layout.label(text="Ex: Node.001 will override Node")
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 node"
bl_options = {"REGISTER", "UNDO"} 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): def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
@ -123,61 +141,75 @@ class NODEKIT_OT_update_nodes(Operator):
ntree_name = ntree.name ntree_name = ntree.name
new_ntree = None new_ntree = None
if self.selection == 'SELECTED': if self.selection == "SELECTED":
nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes nodes = [
if n.type == "GROUP" and n.select] n.node_tree
elif self.selection == 'ACTIVE': 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 active_node = context.space_data.edit_tree
nodes = [active_node] nodes = [active_node]
else: else:
nodes = list(bpy.data.node_groups) nodes = list(bpy.data.node_groups)
node_names = set(n.name for n in nodes) node_names = set(n.name for n in nodes)
#new_node_groups = [] # new_node_groups = []
#print("node_names", node_names) # print("node_names", node_names)
for asset_library in asset_libraries: for asset_library in asset_libraries:
library_path = Path(asset_library.path) library_path = Path(asset_library.path)
blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()] 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: for blend_file in blend_files:
print(blend_file) 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) 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) print("import_node_groups", import_node_groups)
data_to.node_groups = import_node_groups data_to.node_groups = import_node_groups
#print(data_from.node_groups) # print(data_from.node_groups)
#print("data_to.node_groups", data_to.node_groups) # print("data_to.node_groups", data_to.node_groups)
node_names -= set(import_node_groups) # Store already updated nodes node_names -= set(import_node_groups) # Store already updated nodes
#new_ntree = data_to.node_groups[0] # new_ntree = data_to.node_groups[0]
new_node_groups = [n for n in bpy.data.node_groups if n not in node_groups] new_node_groups = [n for n in bpy.data.node_groups if n not in node_groups]
#break # break
#if new_ntree: # if new_ntree:
# break # break
new_node_groups = list(set(new_node_groups)) new_node_groups = list(set(new_node_groups))
#print(new_node_groups) # print(new_node_groups)
# if new_node_groups: # if new_node_groups:
for new_node_group in new_node_groups: for new_node_group in new_node_groups:
new_node_group_name = new_node_group.library_weak_reference.id_name[2:] 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: 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 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) local_node_group.user_remap(new_node_group)
new_node_group.interface_update(context) 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.name = new_node_group_name
new_node_group.asset_clear() new_node_group.asset_clear()
#self.report({"INFO"}, f"Node updated from {blend_file}") # self.report({"INFO"}, f"Node updated from {blend_file}")
return {'FINISHED'} return {"FINISHED"}
# else: # else:
# self.report({"ERROR"}, f'No Node Group named "{ntree_name}" in the library') # 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): class NODEKIT_OT_pack_nodes(Operator):
bl_idname = 'node_kit.pack_nodes' bl_idname = "node_kit.pack_nodes"
bl_label = 'Update node' bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
combine_objects(context.selected_objects) combine_objects(context.selected_objects)
return {'FINISHED'} return {"FINISHED"}
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 = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
extract_objects(context.active_object) extract_objects(context.active_object)
return {'FINISHED'} return {"FINISHED"}
classes = ( classes = (
@ -224,7 +256,7 @@ classes = (
NODEKIT_OT_remap_node_group_duplicates, NODEKIT_OT_remap_node_group_duplicates,
NODEKIT_OT_update_nodes, NODEKIT_OT_update_nodes,
NODEKIT_OT_pack_nodes, NODEKIT_OT_pack_nodes,
NODEKIT_OT_unpack_nodes NODEKIT_OT_unpack_nodes,
) )

27
ui.py
View File

@ -15,22 +15,29 @@ 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_node_tree", text="Copy Nodes", icon="COPYDOWN")
layout.operator('node_kit.paste_node_tree', text='Paste Nodes', icon='PASTEDOWN') layout.operator(
"node_kit.paste_node_tree", text="Paste Nodes", icon="PASTEDOWN"
)
layout.separator() layout.separator()
layout.operator('node_kit.remap_node_group_duplicates', text='Remap Node Groups Duplicates', icon='NODE_INSERT_OFF') layout.operator(
layout.operator('node_kit.update_nodes', text='Update Nodes', icon='IMPORT') "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.separator()
layout.operator('node_kit.pack_nodes', text='Pack Nodes', icon='PACKAGE') 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.unpack_nodes", text="UnPack Nodes", icon="UGLYPACKAGE"
)
classes = (
NODEKIT_MT_node_kit, classes = (NODEKIT_MT_node_kit,)
)
def draw_menu(self, context): def draw_menu(self, context):
self.layout.menu('NODEKIT_MT_node_kit') self.layout.menu("NODEKIT_MT_node_kit")
def register(): def register():