406 lines
14 KiB
Python
406 lines
14 KiB
Python
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
|
|
tree = src.node_group.interface.items_tree
|
|
else:
|
|
tree = src.node_tree.interface.items_tree
|
|
|
|
for param in tree:
|
|
if param.socket_type == 'NodeSocketGeometry':
|
|
continue
|
|
if param.in_out == 'OUTPUT':
|
|
continue
|
|
|
|
# seulement en extract mode, src est une node donc on check si des parametres sont dans le modifier
|
|
node_link_value = get_node_link_value(src, param.name, org_modifier)
|
|
identifier = tree.get(param.name).identifier
|
|
|
|
if mod_to_node:
|
|
tgt.inputs[identifier].default_value = src[identifier]
|
|
|
|
else:
|
|
if node_link_value:
|
|
tgt[identifier] = node_link_value
|
|
else:
|
|
tgt[identifier] = src.inputs[identifier].default_value
|
|
|
|
def set_group_inputs(target, objects, group):
|
|
mod = target.modifiers[0]
|
|
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"])
|
|
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 nkey == key:
|
|
input_node = add_input_node(group, node, param.identifier, sock)
|
|
|
|
# on efface les parametres par defaut qui sont connectes
|
|
# ( pour ne pas garder de trace des anciens params / collections /object)
|
|
node.inputs[param.identifier].default_value = None
|
|
|
|
# add to dict for cleanup
|
|
if not node in node_dct.keys():
|
|
node_dct[node] = [input_node]
|
|
else:
|
|
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"):
|
|
nkey = get_input_socket_key(node, param)
|
|
if not nkey: continue
|
|
if nkey == key:
|
|
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():
|
|
node_dct[node] = [input_node]
|
|
else:
|
|
node_dct[node].append(input_node)
|
|
|
|
# cleanup tree
|
|
for input_nodes in node_dct.values():
|
|
for offset, input_node in enumerate(input_nodes):
|
|
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:
|
|
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')
|
|
for param in node.node_tree.interface.items_tree:
|
|
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
|
|
return None
|
|
def get_input_socket_key(node, param):
|
|
if node.type == "GROUP":
|
|
if param.in_out != 'INPUT':
|
|
return False
|
|
if not param.socket_type in ['NodeSocketObject','NodeSocketCollection']:
|
|
return False
|
|
tgt = node.inputs[param.identifier].default_value
|
|
|
|
if not tgt:
|
|
return False
|
|
return f"{param.socket_type[10:][:3]} {tgt.name}"
|
|
|
|
if node.type == "OBJECT_INFO":
|
|
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"}
|
|
inputs = {}
|
|
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:
|
|
continue
|
|
tgt = node.inputs[param.identifier].default_value
|
|
inputs[key] = {'name': tgt.name, 'data': tgt, 'label': param.name , 'socket': param.socket_type}
|
|
|
|
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"}
|
|
|
|
return inputs
|
|
|
|
def get_node_bounds(objects, mode=0, x=0, y=0):
|
|
min_x = min_y = 10000000
|
|
max_x = max_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_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 ])
|
|
|
|
def get_collection(name):
|
|
scn = bpy.context.scene
|
|
col = None
|
|
link = False
|
|
|
|
# look for existing
|
|
for c in bpy.data.collections:
|
|
if c.name == name: col = c
|
|
|
|
# create if needed
|
|
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 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)
|
|
|
|
def get_frame_childrens(frame):
|
|
childrens = []
|
|
locs = {}
|
|
|
|
for node in frame.id_data.nodes:
|
|
if node.parent == frame:
|
|
locs[node.location[0]] = node
|
|
|
|
# sort nodes by their x location, je sais c'est mal ecris...
|
|
entries = sorted(locs.keys())
|
|
childrens = [locs[x] for x in entries]
|
|
return childrens
|
|
|
|
def parse_nodes(combined_nodes, type = "GROUP"):
|
|
nodes = []
|
|
for frame in combined_nodes:
|
|
for node in frame:
|
|
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
|
|
new_ob = None
|
|
if ob.type == "MESH" and len(ob.data.vertices) > 0:
|
|
new_ob = ob.copy()
|
|
new_ob.data = ob.data.copy()
|
|
|
|
if ob.type == "CURVE" and len(ob.data.splines) > 0:
|
|
new_ob = ob.copy()
|
|
new_ob.data = ob.data.copy()
|
|
|
|
if new_ob:
|
|
for mod in new_ob.modifiers:
|
|
new_ob.modifiers.remove(mod)
|
|
|
|
if new_ob and col:
|
|
col.objects.link(new_ob)
|
|
return new_ob
|
|
|
|
def hide_sockets(node,collapse = True):
|
|
for socket in node.outputs:
|
|
if not socket.links:
|
|
socket.hide = True
|
|
for socket in node.inputs:
|
|
if not socket.links:
|
|
socket.hide = True
|
|
if collapse:
|
|
node.hide = True
|
|
|
|
def add_input_node(group, node, param_id, socket):
|
|
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)
|
|
|
|
def add_material_node(ob, group, nodes):
|
|
if not ob.material_slots:
|
|
return 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.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):
|
|
if not prev:
|
|
prev = node
|
|
continue
|
|
geo_in = get_geo_socket(node)
|
|
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')
|
|
# frame = nodes.new(type='NodeFrame')
|
|
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.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
|
|
nodes.append(nd)
|
|
|
|
# ob modifiers
|
|
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.label = md.name
|
|
nd.width = 230
|
|
nd.location[0] = x * 300
|
|
nd.location[1] = y * 800
|
|
|
|
nd.node_tree = md.node_group
|
|
set_params(md, nd)
|
|
nodes.append(nd)
|
|
nodes = add_material_node(ob, group, nodes)
|
|
join_nodes(group, nodes)
|
|
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")
|
|
mod.node_group = group
|
|
|
|
ob.show_name = True
|
|
bpy.context.view_layer.objects.active = 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'
|
|
|
|
if not col:
|
|
scn.collection.objects.link(ob)
|
|
else:
|
|
col.objects.link(ob)
|
|
return(ob)
|
|
|
|
def assign_modifiers(ob, frame, org_modifier):
|
|
for node in get_frame_childrens(frame):
|
|
if node.type != "GROUP":
|
|
continue
|
|
mod = ob.modifiers.new(node.label, "NODES")
|
|
mod.node_group = node.node_tree
|
|
mod.show_expanded = False
|
|
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")
|
|
|
|
loc = get_node_bounds(objects, x=500)
|
|
join.location = loc
|
|
out.location = loc
|
|
out.location[0] += 700
|
|
|
|
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(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)
|
|
|
|
if target:
|
|
ob = target.copy()
|
|
ob.data = ob.data.copy()
|
|
col.objects.link(ob)
|
|
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
|
|
return ob
|
|
|
|
def combine_objects(objs):
|
|
name = f"NODEGROUP_combined"
|
|
col = get_collection(name)
|
|
group = bpy.data.node_groups.new(name=name, type='GeometryNodeTree')
|
|
|
|
objects = []
|
|
for y , ob in enumerate(objs):
|
|
objects.append(combine_ob(ob, group, y=y, 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
|
|
col = get_collection(grp.name)
|
|
|
|
for frame in get_mod_frames(grp):
|
|
name = f"{object.name} {frame.label}"
|
|
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)
|
|
"""
|
|
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 ! extract: si on trouve un noeud set material on l'assigne
|
|
OK ! extract: si un socket est connecté on recup la valeur de la connection plutot que du socket
|
|
OK ! combine: effacer tout les parametres par defaut qui sont connectes ( pour ne pas garder de trace des anciens params / collections /object)
|
|
OK ! combine: mettre tout les objets crees/copiés dans une collection
|
|
OK ! combine: si un objet source a des mesh/curve data, on en fait une copie, remove les modifiers, et assign dans le object node source
|
|
OK ! combine: si un noeud object info n'est pas vide on expose son contenu dans l'interface
|
|
OK ! extract: si un noeud object info n'est pas vide on duplique son contenu au lieu de creer un mesh vide
|
|
"""
|