node_kit/core/pack_nodes.py

406 lines
14 KiB
Python
Raw Normal View History

2024-11-06 11:26:28 +01:00
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
"""