2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
+211 -86
View File
@@ -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")