2025-12-01
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -55,11 +56,34 @@ def append_brush(file_name, brushname=None, link=False, fake_user=True):
|
||||
|
||||
|
||||
def append_nodegroup(
|
||||
file_name, nodegroupname=None, link=False, fake_user=True, node_x=0, node_y=0
|
||||
file_name,
|
||||
nodegroupname=None,
|
||||
link=False,
|
||||
fake_user=True,
|
||||
node_x=0,
|
||||
node_y=0,
|
||||
target_object=None,
|
||||
nodegroup_mode="",
|
||||
model_location=(0, 0, 0),
|
||||
model_rotation=(0, 0, 0),
|
||||
**kwargs,
|
||||
):
|
||||
"""Append selected node group. If nodegroupname is None, first node group is appended.
|
||||
If node group with the same name is already in the scene, it is not appended again.
|
||||
Try to look for a suitable node editor and insert the node group there, in the middle of the area.
|
||||
Try to look for a suitable node editor and insert the node group there, or create/use modifier based on mode.
|
||||
For geometry nodegroups, if no target object is provided, a target object will be created automatically.
|
||||
|
||||
Args:
|
||||
file_name: Path to the .blend file containing the nodegroup
|
||||
nodegroupname: Name of the nodegroup to append
|
||||
link: Whether to link or append
|
||||
fake_user: Whether to set fake user
|
||||
node_x: X position for node placement in editor
|
||||
node_y: Y position for node placement in editor
|
||||
target_object: Target object for modifier mode (name string). If None and nodegroup is geometry type, a target object will be created
|
||||
nodegroup_mode: How to add the nodegroup - "MODIFIER" for new modifier, "NODE" for node in editor, "" for default behavior
|
||||
model_location: Location for the target object (used when creating new target)
|
||||
model_rotation: Rotation for the target object (used when creating new target)
|
||||
|
||||
Returns:
|
||||
tuple: (nodegroup, added_to_editor) - The nodegroup and whether it was added to an editor
|
||||
@@ -76,6 +100,22 @@ def append_nodegroup(
|
||||
nodegroup = bpy.data.node_groups[nodegroupname]
|
||||
nodegroup.use_fake_user = fake_user
|
||||
|
||||
# Create target object automatically for geometry nodegroups when no target is provided
|
||||
auto_created_target: Optional[bpy.types.Object] = None
|
||||
if nodegroup.bl_rna.identifier == "GeometryNodeTree" and not target_object:
|
||||
# Create a default mesh cube
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
size=2, location=model_location, rotation=model_rotation
|
||||
)
|
||||
target_obj = bpy.context.active_object
|
||||
target_obj.name = "GeometryNodeTarget"
|
||||
target_object = target_obj.name
|
||||
auto_created_target = target_obj
|
||||
|
||||
# Make sure it's selected and active
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
target_obj.select_set(True)
|
||||
|
||||
# Mapping dict for node editor tree types to node group node types
|
||||
sdict = {
|
||||
"GeometryNodeTree": "GeometryNodeGroup",
|
||||
@@ -86,25 +126,119 @@ def append_nodegroup(
|
||||
# Get the nodegroup type
|
||||
nodegroup_type = nodegroup.bl_rna.identifier
|
||||
|
||||
# Find a suitable node editor
|
||||
# If no explicit mode is set, try to detect if we should add to an existing editor first
|
||||
# This allows drag-drop into existing node editors to work properly
|
||||
if not nodegroup_mode:
|
||||
# Find a suitable node editor
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type != "NODE_EDITOR":
|
||||
continue
|
||||
|
||||
if area.spaces.active.tree_type == nodegroup_type:
|
||||
nt = area.spaces.active.edit_tree
|
||||
if nt is None:
|
||||
continue
|
||||
|
||||
# Add node to this editor
|
||||
for n in nt.nodes:
|
||||
n.select = False
|
||||
|
||||
node_type = sdict.get(nodegroup_type)
|
||||
if node_type:
|
||||
node = nt.nodes.new(node_type)
|
||||
node.node_tree = nodegroup
|
||||
node.location = (node_x, node_y)
|
||||
node.select = True
|
||||
nt.nodes.active = node
|
||||
return (nodegroup, True)
|
||||
|
||||
# Handle modifier mode for geometry nodegroups
|
||||
if nodegroup_mode == "MODIFIER" and target_object:
|
||||
target_obj = bpy.data.objects.get(target_object)
|
||||
if target_obj and nodegroup.bl_rna.identifier == "GeometryNodeTree":
|
||||
# Create a new geometry nodes modifier with this nodegroup
|
||||
gn_mod = target_obj.modifiers.new(name=nodegroup.name, type="NODES")
|
||||
gn_mod.node_group = nodegroup
|
||||
|
||||
# Select the target object to make the change visible
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
if target_obj not in bpy.context.selected_objects:
|
||||
target_obj.select_set(True)
|
||||
|
||||
return (
|
||||
nodegroup,
|
||||
True,
|
||||
) # Return True as we "added" it successfully to the modifier
|
||||
|
||||
# Handle node mode for geometry nodegroups with target object
|
||||
# Create a modifier setup and then add the nodegroup as a node to the tree
|
||||
if (
|
||||
nodegroup_mode == "NODE"
|
||||
and target_object
|
||||
and nodegroup.bl_rna.identifier == "GeometryNodeTree"
|
||||
):
|
||||
target_obj = bpy.data.objects.get(target_object)
|
||||
if target_obj:
|
||||
# Select the target object to make it active
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
if target_obj not in bpy.context.selected_objects:
|
||||
target_obj.select_set(True)
|
||||
# look for the geometry nodes modifier
|
||||
gn_mod = None
|
||||
for mod in target_obj.modifiers:
|
||||
if mod.type == "NODES" and mod.node_group:
|
||||
gn_mod = mod
|
||||
break
|
||||
if not gn_mod:
|
||||
# create a new geometry nodes modifier
|
||||
gn_mod = target_obj.modifiers.new(name="GeometryNodes", type="NODES")
|
||||
if not gn_mod.node_group:
|
||||
# create a new node group
|
||||
bpy.ops.node.new_geometry_node_group_assign()
|
||||
|
||||
node_tree = gn_mod.node_group
|
||||
|
||||
if node_tree:
|
||||
# Add the nodegroup as a node to the tree
|
||||
group_node = node_tree.nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = nodegroup
|
||||
group_node.location = (node_x, node_y)
|
||||
group_node.select = True
|
||||
node_tree.nodes.active = group_node
|
||||
|
||||
return (nodegroup, True)
|
||||
|
||||
# If not added yet through modes or if no mode specified, try to find any compatible editor
|
||||
added_to_editor = False
|
||||
|
||||
# First try: exact match for tree type
|
||||
# Try any compatible editor
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type != "NODE_EDITOR":
|
||||
continue
|
||||
|
||||
if area.spaces.active.tree_type == nodegroup_type:
|
||||
nt = area.spaces.active.edit_tree
|
||||
if nt is None:
|
||||
continue
|
||||
nt = area.spaces.active.edit_tree
|
||||
if nt is None:
|
||||
continue
|
||||
|
||||
# Check if this editor type is compatible
|
||||
if area.spaces.active.tree_type in sdict:
|
||||
# Add node to this editor
|
||||
for n in nt.nodes:
|
||||
n.select = False
|
||||
|
||||
node_type = sdict.get(nodegroup_type)
|
||||
node_type = sdict.get(area.spaces.active.tree_type)
|
||||
if node_type:
|
||||
# Check if nodegroup is compatible with this editor
|
||||
# For example, don't add shader nodegroups to geometry node editor
|
||||
if (
|
||||
nodegroup_type == "ShaderNodeTree"
|
||||
and area.spaces.active.tree_type != "ShaderNodeTree"
|
||||
) or (
|
||||
nodegroup_type == "GeometryNodeTree"
|
||||
and area.spaces.active.tree_type != "GeometryNodeTree"
|
||||
):
|
||||
continue
|
||||
|
||||
node = nt.nodes.new(node_type)
|
||||
node.node_tree = nodegroup
|
||||
node.location = (node_x, node_y)
|
||||
@@ -113,42 +247,20 @@ def append_nodegroup(
|
||||
added_to_editor = True
|
||||
break
|
||||
|
||||
# If not added yet, try any compatible editor
|
||||
if not added_to_editor:
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type != "NODE_EDITOR":
|
||||
continue
|
||||
|
||||
nt = area.spaces.active.edit_tree
|
||||
if nt is None:
|
||||
continue
|
||||
|
||||
# Check if this editor type is compatible
|
||||
if area.spaces.active.tree_type in sdict:
|
||||
# Add node to this editor
|
||||
for n in nt.nodes:
|
||||
n.select = False
|
||||
|
||||
node_type = sdict.get(area.spaces.active.tree_type)
|
||||
if node_type:
|
||||
# Check if nodegroup is compatible with this editor
|
||||
# For example, don't add shader nodegroups to geometry node editor
|
||||
if (
|
||||
nodegroup_type == "ShaderNodeTree"
|
||||
and area.spaces.active.tree_type != "ShaderNodeTree"
|
||||
) or (
|
||||
nodegroup_type == "GeometryNodeTree"
|
||||
and area.spaces.active.tree_type != "GeometryNodeTree"
|
||||
):
|
||||
continue
|
||||
|
||||
node = nt.nodes.new(node_type)
|
||||
node.node_tree = nodegroup
|
||||
node.location = (node_x, node_y)
|
||||
node.select = True
|
||||
nt.nodes.active = node
|
||||
added_to_editor = True
|
||||
break
|
||||
# Ensure automatically created targets receive the nodegroup as modifier
|
||||
if auto_created_target:
|
||||
gn_mod = None
|
||||
for mod in auto_created_target.modifiers:
|
||||
if mod.type == "NODES":
|
||||
gn_mod = mod
|
||||
break
|
||||
if not gn_mod:
|
||||
gn_mod = auto_created_target.modifiers.new(
|
||||
name=nodegroup.name, type="NODES"
|
||||
)
|
||||
gn_mod.node_group = nodegroup
|
||||
auto_created_target.select_set(True)
|
||||
bpy.context.view_layer.objects.active = auto_created_target
|
||||
|
||||
return nodegroup, added_to_editor
|
||||
|
||||
@@ -246,15 +358,18 @@ def hdr_swap(name, hdr):
|
||||
:return: None
|
||||
"""
|
||||
w = bpy.context.scene.world
|
||||
if w:
|
||||
if not w:
|
||||
new_hdr_world(name, hdr)
|
||||
|
||||
if bpy.app.version < (5, 0, 0):
|
||||
w.use_nodes = True
|
||||
w.name = name
|
||||
nt = w.node_tree
|
||||
for n in nt.nodes:
|
||||
if "ShaderNodeTexEnvironment" == n.bl_rna.identifier:
|
||||
env_node = n
|
||||
env_node.image = hdr
|
||||
return
|
||||
w.name = name
|
||||
nt = w.node_tree
|
||||
for n in nt.nodes:
|
||||
if "ShaderNodeTexEnvironment" == n.bl_rna.identifier:
|
||||
env_node = n
|
||||
env_node.image = hdr
|
||||
return
|
||||
new_hdr_world(name, hdr)
|
||||
|
||||
|
||||
@@ -266,7 +381,8 @@ def new_hdr_world(name, hdr):
|
||||
:return: None
|
||||
"""
|
||||
w = bpy.data.worlds.new(name=name)
|
||||
w.use_nodes = True
|
||||
if bpy.app.version < (5, 0, 0):
|
||||
w.use_nodes = True
|
||||
bpy.context.scene.world = w
|
||||
|
||||
nt = w.node_tree
|
||||
@@ -302,11 +418,11 @@ def load_HDR(file_name, name):
|
||||
|
||||
def link_collection(
|
||||
file_name,
|
||||
obnames=None,
|
||||
obnames: Optional[list] = None,
|
||||
location=(0, 0, 0),
|
||||
link=False,
|
||||
parent=None,
|
||||
collection="",
|
||||
link: bool = False,
|
||||
parent: Optional[str] = None,
|
||||
collection: str = "",
|
||||
**kwargs,
|
||||
):
|
||||
"""link an instanced group - model type asset"""
|
||||
@@ -314,7 +430,7 @@ def link_collection(
|
||||
obnames = []
|
||||
sel = utils.selection_get()
|
||||
# Store the original active collection
|
||||
orig_active_collection = bpy.context.view_layer.active_layer_collection
|
||||
orig_active_collection = bpy.context.view_layer.active_layer_collection # type: ignore[union-attr]
|
||||
|
||||
# Activate target collection if specified
|
||||
if collection:
|
||||
@@ -322,10 +438,10 @@ def link_collection(
|
||||
if target_collection:
|
||||
# Find and activate the layer collection
|
||||
layer_collection = find_layer_collection(
|
||||
bpy.context.view_layer.layer_collection, collection
|
||||
bpy.context.view_layer.layer_collection, collection # type: ignore[union-attr]
|
||||
)
|
||||
if layer_collection:
|
||||
bpy.context.view_layer.active_layer_collection = layer_collection
|
||||
bpy.context.view_layer.active_layer_collection = layer_collection # type: ignore[union-attr]
|
||||
|
||||
with bpy.data.libraries.load(file_name, link=link, relative=True) as (
|
||||
data_from,
|
||||
@@ -340,33 +456,33 @@ def link_collection(
|
||||
rotation = kwargs["rotation"]
|
||||
|
||||
bpy.ops.object.empty_add(type="PLAIN_AXES", location=location, rotation=rotation)
|
||||
main_object = bpy.context.view_layer.objects.active
|
||||
main_object.instance_type = "COLLECTION"
|
||||
main_object = bpy.context.view_layer.objects.active # type: ignore[union-attr]
|
||||
main_object.instance_type = "COLLECTION" # type: ignore[union-attr]
|
||||
|
||||
if parent is not None:
|
||||
main_object.parent = bpy.data.objects.get(parent)
|
||||
if parent is not None and parent != "":
|
||||
main_object.parent = bpy.data.objects.get(parent) # type: ignore[union-attr]
|
||||
|
||||
main_object.matrix_world.translation = location
|
||||
main_object.matrix_world.translation = location # type: ignore[union-attr]
|
||||
|
||||
for col in bpy.data.collections:
|
||||
if col.library is not None:
|
||||
fp = bpy.path.abspath(col.library.filepath)
|
||||
fp1 = bpy.path.abspath(file_name)
|
||||
if fp == fp1:
|
||||
main_object.instance_collection = col
|
||||
main_object.instance_collection = col # type: ignore[union-attr]
|
||||
break
|
||||
|
||||
# sometimes, the lib might already be without the actual link.
|
||||
if not main_object.instance_collection and kwargs["name"]:
|
||||
if not main_object.instance_collection and kwargs["name"]: # type: ignore[union-attr]
|
||||
col = bpy.data.collections.get(kwargs["name"])
|
||||
if col:
|
||||
main_object.instance_collection = col
|
||||
main_object.instance_collection = col # type: ignore[union-attr]
|
||||
|
||||
main_object.name = main_object.instance_collection.name
|
||||
main_object.name = main_object.instance_collection.name # type: ignore[union-attr]
|
||||
|
||||
# Restore original active collection
|
||||
if orig_active_collection:
|
||||
bpy.context.view_layer.active_layer_collection = orig_active_collection
|
||||
bpy.context.view_layer.active_layer_collection = orig_active_collection # type: ignore[union-attr]
|
||||
|
||||
utils.selection_set(sel)
|
||||
return main_object, []
|
||||
@@ -449,7 +565,13 @@ def append_particle_system(
|
||||
|
||||
|
||||
def append_objects(
|
||||
file_name, obnames=None, location=(0, 0, 0), link=False, collection="", **kwargs
|
||||
file_name,
|
||||
obnames: Optional[list] = None,
|
||||
location=(0, 0, 0),
|
||||
link: bool = False,
|
||||
parent: Optional[str] = None,
|
||||
collection: str = "",
|
||||
**kwargs,
|
||||
):
|
||||
"""Append object into scene individually. 2 approaches based in definition of name argument.
|
||||
TODO: really split this function into 2 functions: kwargs.get('name')==None and else.
|
||||
@@ -461,7 +583,7 @@ def append_objects(
|
||||
scene = bpy.context.scene
|
||||
sel = utils.selection_get()
|
||||
# Store the original active collection
|
||||
orig_active_collection = bpy.context.view_layer.active_layer_collection
|
||||
orig_active_collection = bpy.context.view_layer.active_layer_collection # type: ignore[union-attr]
|
||||
|
||||
# Activate target collection if specified
|
||||
if collection:
|
||||
@@ -469,10 +591,10 @@ def append_objects(
|
||||
if target_collection:
|
||||
# Find and activate the layer collection
|
||||
layer_collection = find_layer_collection(
|
||||
bpy.context.view_layer.layer_collection, collection
|
||||
bpy.context.view_layer.layer_collection, collection # type: ignore[union-attr]
|
||||
)
|
||||
if layer_collection:
|
||||
bpy.context.view_layer.active_layer_collection = layer_collection
|
||||
bpy.context.view_layer.active_layer_collection = layer_collection # type: ignore[union-attr]
|
||||
|
||||
try:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
@@ -486,6 +608,9 @@ def append_objects(
|
||||
|
||||
path = file_name + "/Collection"
|
||||
collection_name = kwargs.get("name")
|
||||
if collection_name is None:
|
||||
bk_logger.warning("collection_name is None")
|
||||
collection_name = ""
|
||||
bpy.ops.wm.append(filename=collection_name, directory=path)
|
||||
|
||||
# fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D')
|
||||
@@ -496,13 +621,13 @@ def append_objects(
|
||||
appended_collection = None
|
||||
main_object = None
|
||||
# get first at least one parent for sure
|
||||
for ob in bpy.context.scene.objects:
|
||||
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
|
||||
if ob.select_get():
|
||||
if not ob.parent:
|
||||
main_object = ob
|
||||
ob.location = location
|
||||
# do once again to ensure hidden objects are hidden
|
||||
for ob in bpy.context.scene.objects:
|
||||
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
|
||||
if ob.select_get():
|
||||
return_obs.append(ob)
|
||||
# check for object that should be hidden
|
||||
@@ -521,14 +646,14 @@ def append_objects(
|
||||
if kwargs.get("rotation"):
|
||||
main_object.rotation_euler = kwargs["rotation"]
|
||||
|
||||
if kwargs.get("parent") is not None:
|
||||
main_object.parent = bpy.data.objects[kwargs["parent"]]
|
||||
if parent is not None and parent != "":
|
||||
main_object.parent = bpy.data.objects[parent]
|
||||
main_object.matrix_world.translation = location
|
||||
|
||||
# move objects that should be hidden to a sub collection
|
||||
if len(to_hidden_collection) > 0 and appended_collection is not None:
|
||||
hidden_collections = []
|
||||
scene_collection = bpy.context.scene.collection
|
||||
scene_collection = bpy.context.scene.collection # type: ignore[union-attr]
|
||||
for ob in to_hidden_collection:
|
||||
hide_collection = ob.users_collection[0]
|
||||
|
||||
@@ -577,7 +702,7 @@ def append_objects(
|
||||
|
||||
# Restore original active collection
|
||||
if orig_active_collection:
|
||||
bpy.context.view_layer.active_layer_collection = orig_active_collection
|
||||
bpy.context.view_layer.active_layer_collection = orig_active_collection # type: ignore[union-attr]
|
||||
|
||||
utils.selection_set(sel)
|
||||
# let collection also store info that it was created by BlenderKit, for purging reasons
|
||||
@@ -618,7 +743,7 @@ def append_objects(
|
||||
for obj in data_to.objects:
|
||||
if obj is not None:
|
||||
# if obj.name not in scene.objects:
|
||||
scene.collection.objects.link(obj)
|
||||
scene.collection.objects.link(obj) # type: ignore[union-attr]
|
||||
if obj.parent is None:
|
||||
obj.location = location
|
||||
main_object = obj
|
||||
@@ -637,11 +762,11 @@ def append_objects(
|
||||
ob.hide_viewport = True
|
||||
|
||||
if kwargs.get("rotation") is not None:
|
||||
main_object.rotation_euler = kwargs["rotation"]
|
||||
main_object.rotation_euler = kwargs["rotation"] # type: ignore[union-attr]
|
||||
|
||||
if kwargs.get("parent") is not None:
|
||||
main_object.parent = bpy.data.objects[kwargs["parent"]]
|
||||
main_object.matrix_world.translation = location
|
||||
if parent is not None and parent != "":
|
||||
main_object.parent = bpy.data.objects[parent] # type: ignore[union-attr]
|
||||
main_object.matrix_world.translation = location # type: ignore[union-attr]
|
||||
|
||||
try:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
Reference in New Issue
Block a user