node_kit/core/pack_nodes.py

461 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
"""