2025-12-01
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import time
|
||||
from .transfer_functions import (
|
||||
attributes,
|
||||
constraints,
|
||||
custom_props,
|
||||
modifiers,
|
||||
parent,
|
||||
shape_keys,
|
||||
vertex_groups,
|
||||
materials,
|
||||
)
|
||||
from typing import List
|
||||
from ... import constants, logging
|
||||
from .transfer_util import (
|
||||
transfer_data_add_entry,
|
||||
find_ownership_data,
|
||||
link_objs_to_collection,
|
||||
isolate_collection,
|
||||
)
|
||||
|
||||
|
||||
# TODO use logging module here
|
||||
def copy_transfer_data_ownership(
|
||||
td_type_key: str, target_obj: bpy.types.Object, transfer_data_dict: dict
|
||||
) -> None:
|
||||
"""Copy Transferable Data item to object if non entry exists
|
||||
|
||||
Args:
|
||||
transfer_data_item: Item of bpy.types.CollectionProperty from source object
|
||||
target_obj (bpy.types.Object): Object to add Transferable Data item to
|
||||
"""
|
||||
transfer_data = target_obj.transfer_data_ownership
|
||||
ownership_data = find_ownership_data(
|
||||
transfer_data,
|
||||
transfer_data_dict["name"],
|
||||
td_type_key,
|
||||
)
|
||||
if not ownership_data:
|
||||
transfer_data_add_entry(
|
||||
transfer_data,
|
||||
transfer_data_dict["name"],
|
||||
td_type_key,
|
||||
transfer_data_dict["owner"],
|
||||
transfer_data_dict["surrender"],
|
||||
)
|
||||
|
||||
|
||||
def transfer_data_clean(obj):
|
||||
vertex_groups.vertex_groups_clean(obj)
|
||||
modifiers.modifiers_clean(obj)
|
||||
constraints.constraints_clean(obj)
|
||||
custom_props.custom_prop_clean(obj)
|
||||
shape_keys.shape_keys_clean(obj)
|
||||
attributes.attribute_clean(obj)
|
||||
parent.parent_clean(obj)
|
||||
|
||||
|
||||
def transfer_data_is_missing(transfer_data_item) -> bool:
|
||||
"""Check if Transferable Data item is missing
|
||||
|
||||
Args:
|
||||
transfer_data_item: Item of class ASSET_TRANSFER_DATA
|
||||
|
||||
Returns:
|
||||
bool: bool if item is missing
|
||||
"""
|
||||
return bool(
|
||||
vertex_groups.vertex_group_is_missing(transfer_data_item)
|
||||
or modifiers.modifier_is_missing(transfer_data_item)
|
||||
or constraints.constraint_is_missing(transfer_data_item)
|
||||
or custom_props.custom_prop_is_missing(transfer_data_item)
|
||||
or shape_keys.shape_key_is_missing(transfer_data_item)
|
||||
or attributes.attribute_is_missing(transfer_data_item)
|
||||
)
|
||||
|
||||
|
||||
def init_transfer_data(
|
||||
scene: bpy.types.Scene,
|
||||
obj: bpy.types.Object,
|
||||
):
|
||||
"""Collect Transferable Data Items on a given object
|
||||
|
||||
Args:
|
||||
obj (bpy.types.Object): Target object for Transferable Data
|
||||
task_layer_name (str): Name of task layer
|
||||
temp_transfer_data: Item of class ASSET_TRANSFER_DATA_TEMP
|
||||
"""
|
||||
if obj.library:
|
||||
# Don't create ownership data for object data if the object is linked.
|
||||
return
|
||||
|
||||
constraints.init_constraints(scene, obj)
|
||||
custom_props.init_custom_prop(scene, obj)
|
||||
parent.init_parent(scene, obj)
|
||||
modifiers.init_modifiers(scene, obj)
|
||||
|
||||
if not obj.data or obj.data.library:
|
||||
# Don't create ownership data for mesh data if the mesh is linked, or Empties.
|
||||
return
|
||||
|
||||
vertex_groups.init_vertex_groups(scene, obj)
|
||||
materials.init_materials(scene, obj)
|
||||
shape_keys.init_shape_keys(scene, obj)
|
||||
attributes.init_attributes(scene, obj)
|
||||
|
||||
|
||||
def apply_transfer_data_items(
|
||||
context,
|
||||
source_obj: bpy.types.Object,
|
||||
target_obj: bpy.types.Object,
|
||||
td_type_key: str,
|
||||
transfer_data_dicts: List[dict],
|
||||
):
|
||||
logger = logging.get_logger()
|
||||
# Get source/target from first item in list, because all items in list are same object/type
|
||||
if target_obj is None:
|
||||
logger.warning(f"Failed to Transfer {td_type_key.title()} from {source_obj.name}")
|
||||
return
|
||||
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
copy_transfer_data_ownership(td_type_key, target_obj, transfer_data_dict)
|
||||
|
||||
# if TD Source is Target, restore the ownership data but don't transfer anything
|
||||
if source_obj == target_obj:
|
||||
return
|
||||
|
||||
if td_type_key == constants.VERTEX_GROUP_KEY:
|
||||
# Transfer All Vertex Groups in one go
|
||||
logger.debug(f"Transferring All Vertex Groups from {source_obj.name} to {target_obj.name}.")
|
||||
vertex_groups.transfer_vertex_groups(
|
||||
vertex_group_names=[item["name"] for item in transfer_data_dicts],
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
)
|
||||
if td_type_key == constants.MODIFIER_KEY:
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
logger.debug(
|
||||
f"Transferring Modifier {transfer_data_dict['name']} from {source_obj.name} to {target_obj.name}."
|
||||
)
|
||||
modifiers.transfer_modifier(
|
||||
context,
|
||||
modifier_name=transfer_data_dict["name"],
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
)
|
||||
if td_type_key == constants.CONSTRAINT_KEY:
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
logger.debug(
|
||||
f"Transferring Constraint {transfer_data_dict['name']} from {source_obj.name} to {target_obj.name}."
|
||||
)
|
||||
constraints.transfer_constraint(
|
||||
constraint_name=transfer_data_dict["name"],
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
)
|
||||
if td_type_key == constants.CUSTOM_PROP_KEY:
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
logger.debug(
|
||||
f"Transferring Custom Property {transfer_data_dict['name']} from {source_obj.name} to {target_obj.name}."
|
||||
)
|
||||
custom_props.transfer_custom_prop(
|
||||
prop_name=transfer_data_dict["name"],
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
)
|
||||
if td_type_key == constants.MATERIAL_SLOT_KEY:
|
||||
logger.debug(f"Transferring Materials from {source_obj.name} to {target_obj.name}.")
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
materials.transfer_materials(
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
)
|
||||
if td_type_key == constants.SHAPE_KEY_KEY:
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
logger.debug(
|
||||
f"Transferring Shape Key {transfer_data_dict['name']} from {source_obj.name} to {target_obj.name}."
|
||||
)
|
||||
shape_keys.transfer_shape_key(
|
||||
context=context,
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
shape_key_name=transfer_data_dict["name"],
|
||||
)
|
||||
if td_type_key == constants.ATTRIBUTE_KEY:
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
logger.debug(
|
||||
f"Transferring Attribute {transfer_data_dict['name']} from {source_obj.name} to {target_obj.name}."
|
||||
)
|
||||
attributes.transfer_attribute(
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
attribute_name=transfer_data_dict["name"],
|
||||
)
|
||||
if td_type_key == constants.PARENT_KEY:
|
||||
for transfer_data_dict in transfer_data_dicts:
|
||||
logger.debug(
|
||||
f"Transferring Parent Relationship from {source_obj.name} to {target_obj.name}."
|
||||
)
|
||||
parent.transfer_parent(
|
||||
target_obj=target_obj,
|
||||
source_obj=source_obj,
|
||||
)
|
||||
|
||||
|
||||
def apply_transfer_data(context: bpy.types.Context, transfer_data_map) -> None:
|
||||
"""Apply all Transferable Data from Transferable Data map onto objects.
|
||||
Copies any Transferable Data owned by local layer onto objects owned by external layers.
|
||||
Applies Transferable Data from external layers onto objects owned by local layers
|
||||
|
||||
Transfer_data_map is generated by class 'AssetTransferMapping'
|
||||
|
||||
Args:
|
||||
context (bpy.types.Context): context of .blend file
|
||||
transfer_data_map: Map generated by class AssetTransferMapping
|
||||
"""
|
||||
# Create/isolate tmp collection to reduce depsgraph update time
|
||||
profiler = logging.get_profiler()
|
||||
td_col = bpy.data.collections.new("ISO_COL_TEMP")
|
||||
with isolate_collection(context, td_col):
|
||||
# Loop over objects in Transfer data map
|
||||
for source_obj in transfer_data_map:
|
||||
target_obj = transfer_data_map[source_obj]["target_obj"]
|
||||
td_types = transfer_data_map[source_obj]["td_types"]
|
||||
with link_objs_to_collection(set([target_obj, source_obj]), td_col):
|
||||
for td_type_key, td_dicts in td_types.items():
|
||||
start_time = time.time()
|
||||
apply_transfer_data_items(
|
||||
context, source_obj, target_obj, td_type_key, td_dicts
|
||||
)
|
||||
profiler.add(time.time() - start_time, td_type_key)
|
||||
bpy.data.collections.remove(td_col)
|
||||
@@ -0,0 +1,261 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import bmesh
|
||||
import numpy as np
|
||||
from .transfer_function_util.proximity_core import (
|
||||
tris_per_face,
|
||||
closest_face_to_point,
|
||||
closest_tri_on_face,
|
||||
is_obdata_identical,
|
||||
transfer_corner_data,
|
||||
)
|
||||
from ..transfer_util import find_ownership_data
|
||||
from ...naming import merge_get_basename
|
||||
from ...task_layer import get_transfer_data_owner
|
||||
from .... import constants, logging
|
||||
|
||||
|
||||
def attributes_get_editable(attributes):
|
||||
return [
|
||||
attribute
|
||||
for attribute in attributes
|
||||
if not (
|
||||
attribute.is_internal
|
||||
or attribute.is_required
|
||||
# Material Index is part of material transfer and should be skipped
|
||||
or attribute.name == 'material_index'
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def attribute_clean(obj):
|
||||
logger = logging.get_logger()
|
||||
if obj.type != "MESH":
|
||||
return
|
||||
attributes = attributes_get_editable(obj.data.attributes)
|
||||
attributes_to_remove = []
|
||||
for attribute in attributes:
|
||||
ownership_data = find_ownership_data(
|
||||
obj.transfer_data_ownership,
|
||||
merge_get_basename(attribute.name),
|
||||
constants.ATTRIBUTE_KEY,
|
||||
)
|
||||
if not ownership_data:
|
||||
attributes_to_remove.append(attribute.name)
|
||||
|
||||
for attribute_name_to_remove in reversed(attributes_to_remove):
|
||||
attribute_to_remove = obj.data.attributes.get(attribute_name_to_remove)
|
||||
logger.debug(f"Cleaning attribute {attribute.name}")
|
||||
obj.data.attributes.remove(attribute_to_remove)
|
||||
|
||||
|
||||
def attribute_is_missing(transfer_data_item):
|
||||
obj = transfer_data_item.id_data
|
||||
if obj.type != "MESH":
|
||||
return
|
||||
attributes = attributes_get_editable(obj.data.attributes)
|
||||
attribute_names = [attribute.name for attribute in attributes]
|
||||
if (
|
||||
transfer_data_item.type == constants.ATTRIBUTE_KEY
|
||||
and not transfer_data_item["name"] in attribute_names
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
def init_attributes(scene, obj):
|
||||
asset_pipe = scene.asset_pipeline
|
||||
if obj.type != "MESH":
|
||||
return
|
||||
transfer_data = obj.transfer_data_ownership
|
||||
td_type_key = constants.ATTRIBUTE_KEY
|
||||
for atttribute in attributes_get_editable(obj.data.attributes):
|
||||
# Only add new ownership transfer_data_item if vertex group doesn't have an owner
|
||||
ownership_data = find_ownership_data(transfer_data, atttribute.name, td_type_key)
|
||||
if not ownership_data:
|
||||
task_layer_owner, auto_surrender = get_transfer_data_owner(
|
||||
asset_pipe, td_type_key, atttribute.name
|
||||
)
|
||||
asset_pipe.add_temp_transfer_data(
|
||||
name=atttribute.name,
|
||||
owner=task_layer_owner,
|
||||
type=td_type_key,
|
||||
obj_name=obj.name,
|
||||
surrender=auto_surrender,
|
||||
)
|
||||
|
||||
|
||||
def transfer_attribute(
|
||||
attribute_name: str,
|
||||
target_obj: bpy.types.Object,
|
||||
source_obj: bpy.types.Object,
|
||||
):
|
||||
source_attributes = source_obj.data.attributes
|
||||
target_attributes = target_obj.data.attributes
|
||||
source_attribute = source_attributes.get(attribute_name)
|
||||
target_attribute = target_attributes.get(attribute_name)
|
||||
|
||||
logger = logging.get_logger()
|
||||
if not source_attribute:
|
||||
logger.debug(f"Failed to find attribute to transfer: {attribute_name}")
|
||||
return
|
||||
|
||||
if target_attribute:
|
||||
target_attributes.remove(target_attribute)
|
||||
|
||||
target_attribute = target_attributes.new(
|
||||
name=attribute_name,
|
||||
type=source_attribute.data_type,
|
||||
domain=source_attribute.domain,
|
||||
)
|
||||
if not target_attribute:
|
||||
logger.debug(f"Failed to create attribute: {target_obj.name} -> {attribute_name}")
|
||||
return
|
||||
|
||||
if not is_obdata_identical(source_obj, target_obj):
|
||||
proximity_transfer_single_attribute(
|
||||
source_obj, target_obj, source_attribute, target_attribute
|
||||
)
|
||||
return
|
||||
|
||||
for source_data_item in source_attribute.data.items():
|
||||
index = source_data_item[0]
|
||||
source_data = source_data_item[1]
|
||||
keys = set(source_data.bl_rna.properties.keys()) - set(
|
||||
bpy.types.Attribute.bl_rna.properties.keys()
|
||||
)
|
||||
for key in list(keys):
|
||||
target_data = target_attribute.data[index]
|
||||
setattr(target_data, key, getattr(source_data, key))
|
||||
|
||||
|
||||
def proximity_transfer_single_attribute(
|
||||
source_obj: bpy.types.Object,
|
||||
target_obj: bpy.types.Object,
|
||||
source_attribute: bpy.types.Attribute,
|
||||
target_attribute: bpy.types.Attribute,
|
||||
):
|
||||
# src_dat = source_obj.data
|
||||
# tgt_dat = target_obj.data
|
||||
# if type(src_dat) is not type(tgt_dat) or not (src_dat or tgt_dat):
|
||||
# return False
|
||||
# if type(tgt_dat) is not bpy.types.Mesh: # TODO: support more types
|
||||
# return False
|
||||
|
||||
# If target attribute already exists, remove it.
|
||||
# tgt_attr = tgt_dat.attributes.get(source_attribute.name)
|
||||
# if tgt_attr is not None:
|
||||
# try:
|
||||
# tgt_dat.attributes.remove(tgt_attr)
|
||||
# except RuntimeError:
|
||||
# # Built-ins like "position" cannot be removed, and should be skipped.
|
||||
# return
|
||||
|
||||
# Create target attribute.
|
||||
# target_attribute = tgt_dat.attributes.new(
|
||||
# source_attribute.name, source_attribute.data_type, source_attribute.domain
|
||||
# )
|
||||
logger = logging.get_logger()
|
||||
|
||||
data_sfx = {
|
||||
'INT8': 'value',
|
||||
'INT': 'value',
|
||||
'FLOAT': 'value',
|
||||
'FLOAT2': 'vector',
|
||||
'BOOLEAN': 'value',
|
||||
'STRING': 'value',
|
||||
'BYTE_COLOR': 'color',
|
||||
'FLOAT_COLOR': 'color',
|
||||
'FLOAT_VECTOR': 'vector',
|
||||
}
|
||||
|
||||
data_sfx = data_sfx[source_attribute.data_type]
|
||||
|
||||
# if topo_match:
|
||||
# # TODO: optimize using foreach_get/set rather than loop
|
||||
# for i in range(len(source_attribute.data)):
|
||||
# setattr(tgt_attr.data[i], data_sfx, getattr(source_attribute.data[i], data_sfx))
|
||||
# return
|
||||
|
||||
# proximity fallback
|
||||
if source_attribute.data_type == 'STRING':
|
||||
# TODO: add NEAREST transfer fallback for attributes without interpolation
|
||||
logger.warning(
|
||||
f'Proximity based transfer for generic attributes of type STRING not supported yet. Skipping attribute {source_attribute.name} on {target_obj}.'
|
||||
)
|
||||
return
|
||||
|
||||
domain = source_attribute.domain
|
||||
if (
|
||||
domain == 'POINT'
|
||||
): # TODO: deduplicate interpolated point domain proximity transfer
|
||||
bm_source = bmesh.new()
|
||||
bm_source.from_mesh(source_obj.data)
|
||||
bm_source.faces.ensure_lookup_table()
|
||||
|
||||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||
|
||||
tris_dict = tris_per_face(bm_source)
|
||||
|
||||
for i, vert in enumerate(target_obj.data.vertices):
|
||||
p = vert.co
|
||||
face = closest_face_to_point(bm_source, p, bvh_tree)
|
||||
|
||||
(tri, point) = closest_tri_on_face(tris_dict, face, p)
|
||||
if not tri:
|
||||
continue
|
||||
weights = mathutils.interpolate.poly_3d_calc(
|
||||
[tri[i].vert.co for i in range(3)], point
|
||||
)
|
||||
|
||||
if data_sfx in ['color']:
|
||||
vals_weighted = [
|
||||
weights[i]
|
||||
* (
|
||||
np.array(
|
||||
getattr(source_attribute.data[tri[i].vert.index], data_sfx)
|
||||
)
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
else:
|
||||
vals_weighted = [
|
||||
weights[i]
|
||||
* (getattr(source_attribute.data[tri[i].vert.index], data_sfx))
|
||||
for i in range(3)
|
||||
]
|
||||
setattr(target_attribute.data[i], data_sfx, sum(np.array(vals_weighted)))
|
||||
return
|
||||
elif domain == 'EDGE':
|
||||
# TODO support proximity fallback for generic edge attributes
|
||||
logger.warning(
|
||||
f'Proximity based transfer of generic edge attributes not supported yet. Skipping attribute {source_attribute.name} on {target_obj}.'
|
||||
)
|
||||
return
|
||||
elif domain == 'FACE':
|
||||
bm_source = bmesh.new()
|
||||
bm_source.from_mesh(source_obj.data)
|
||||
bm_source.faces.ensure_lookup_table()
|
||||
|
||||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||
for i, face in enumerate(target_obj.data.polygons):
|
||||
p_target = face.center
|
||||
closest_face = closest_face_to_point(bm_source, p_target, bvh_tree)
|
||||
setattr(
|
||||
target_attribute.data[i],
|
||||
data_sfx,
|
||||
getattr(source_attribute.data[closest_face.index], data_sfx),
|
||||
)
|
||||
return
|
||||
elif domain == 'CORNER':
|
||||
transfer_corner_data(
|
||||
source_obj,
|
||||
target_obj,
|
||||
source_attribute.data,
|
||||
target_attribute.data,
|
||||
data_suffix=data_sfx,
|
||||
)
|
||||
return
|
||||
@@ -0,0 +1,88 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from ..transfer_util import (
|
||||
transfer_data_clean,
|
||||
transfer_data_item_is_missing,
|
||||
find_ownership_data,
|
||||
)
|
||||
from ...naming import task_layer_prefix_name_get
|
||||
from .transfer_function_util.drivers import transfer_drivers, cleanup_drivers
|
||||
from ...task_layer import get_transfer_data_owner
|
||||
from .... import constants, logging
|
||||
|
||||
|
||||
def constraints_clean(obj):
|
||||
cleaned_names = transfer_data_clean(
|
||||
obj=obj, data_list=obj.constraints, td_type_key=constants.CONSTRAINT_KEY
|
||||
)
|
||||
|
||||
# Remove Drivers that match the cleaned item's name
|
||||
for name in cleaned_names:
|
||||
cleanup_drivers(obj, 'constraints', name)
|
||||
|
||||
def constraint_is_missing(transfer_data_item):
|
||||
return transfer_data_item_is_missing(
|
||||
transfer_data_item=transfer_data_item,
|
||||
td_type_key=constants.CONSTRAINT_KEY,
|
||||
data_list=transfer_data_item.id_data.constraints,
|
||||
)
|
||||
|
||||
|
||||
def init_constraints(scene, obj):
|
||||
td_type_key = constants.CONSTRAINT_KEY
|
||||
transfer_data = obj.transfer_data_ownership
|
||||
asset_pipe = scene.asset_pipeline
|
||||
task_layer_owner, auto_surrender = get_transfer_data_owner(
|
||||
asset_pipe,
|
||||
td_type_key,
|
||||
)
|
||||
for const in obj.constraints:
|
||||
# Only add new ownership transfer_data_item if vertex group doesn't have an owner
|
||||
ownership_data = find_ownership_data(transfer_data, const.name, td_type_key)
|
||||
if not ownership_data:
|
||||
ownership_data = asset_pipe.add_temp_transfer_data(
|
||||
name=const.name,
|
||||
owner=task_layer_owner,
|
||||
type=td_type_key,
|
||||
obj_name=obj.name,
|
||||
surrender=auto_surrender,
|
||||
)
|
||||
|
||||
const.name = task_layer_prefix_name_get(const.name, ownership_data.owner)
|
||||
|
||||
|
||||
def transfer_constraint(constraint_name, target_obj, source_obj):
|
||||
logger = logging.get_logger()
|
||||
context = bpy.context
|
||||
# Remove old and sync existing constraints.
|
||||
old_con = target_obj.constraints.get(constraint_name)
|
||||
if old_con:
|
||||
target_obj.constraints.remove(old_con)
|
||||
|
||||
src_idx = source_obj.constraints.find(constraint_name)
|
||||
if src_idx == -1:
|
||||
# This happens if a modifier's transfer data is still around, but the modifier
|
||||
# itself was removed.
|
||||
logger.debug(f"Constraint Transfer cancelled, '{constraint_name}' not found on '{source_obj.name}'")
|
||||
return
|
||||
|
||||
src_con = source_obj.constraints[src_idx]
|
||||
new_con = target_obj.constraints.new(src_con.type)
|
||||
new_con.name = src_con.name
|
||||
|
||||
props = [p.identifier for p in src_con.bl_rna.properties if not p.is_readonly]
|
||||
for prop in props:
|
||||
value = getattr(src_con, prop)
|
||||
setattr(new_con, prop, value)
|
||||
|
||||
# Armature constraints have some nested properties we need to copy...
|
||||
if src_con.type == "ARMATURE":
|
||||
for target_item in src_con.targets:
|
||||
new_target = new_con.targets.new()
|
||||
new_target.target = target_item.target
|
||||
new_target.subtarget = target_item.subtarget
|
||||
|
||||
transfer_drivers(source_obj, target_obj, 'constraints', constraint_name)
|
||||
@@ -0,0 +1,61 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from ..transfer_util import find_ownership_data
|
||||
from ...task_layer import get_transfer_data_owner
|
||||
|
||||
from .... import constants
|
||||
from .transfer_function_util.properties import (
|
||||
get_all_runtime_prop_names,
|
||||
remove_property,
|
||||
copy_runtime_property,
|
||||
)
|
||||
|
||||
def transfer_custom_prop(prop_name, target_obj, source_obj):
|
||||
copy_runtime_property(source_obj, target_obj, prop_name)
|
||||
|
||||
|
||||
def custom_prop_clean(obj):
|
||||
cleaned_item_names = set()
|
||||
for key in get_valid_runtime_prop_names(obj):
|
||||
ownership_data = find_ownership_data(
|
||||
obj.transfer_data_ownership,
|
||||
key,
|
||||
constants.CUSTOM_PROP_KEY,
|
||||
)
|
||||
if not ownership_data:
|
||||
cleaned_item_names.add(key)
|
||||
remove_property(obj, key)
|
||||
|
||||
return cleaned_item_names
|
||||
|
||||
|
||||
def custom_prop_is_missing(transfer_data_item):
|
||||
obj = transfer_data_item.id_data
|
||||
return transfer_data_item.type == constants.CUSTOM_PROP_KEY and not transfer_data_item["name"] in get_valid_runtime_prop_names(obj)
|
||||
|
||||
|
||||
def init_custom_prop(scene, obj):
|
||||
asset_pipe = scene.asset_pipeline
|
||||
transfer_data = obj.transfer_data_ownership
|
||||
td_type_key = constants.CUSTOM_PROP_KEY
|
||||
|
||||
for prop_name in get_valid_runtime_prop_names(obj):
|
||||
ownership_data = find_ownership_data(transfer_data, prop_name, td_type_key)
|
||||
if not ownership_data:
|
||||
task_layer_owner, auto_surrender = get_transfer_data_owner(
|
||||
asset_pipe, td_type_key, prop_name
|
||||
)
|
||||
asset_pipe.add_temp_transfer_data(
|
||||
name=prop_name,
|
||||
owner=task_layer_owner,
|
||||
type=td_type_key,
|
||||
obj_name=obj.name,
|
||||
surrender=auto_surrender,
|
||||
)
|
||||
|
||||
|
||||
def get_valid_runtime_prop_names(id):
|
||||
all_props = get_all_runtime_prop_names(id)
|
||||
return [p for p in all_props if p not in constants.ADDON_OWN_PROPERTIES]
|
||||
@@ -0,0 +1,107 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from .attributes import transfer_attribute
|
||||
from ..transfer_util import find_ownership_data
|
||||
from ...task_layer import get_transfer_data_owner
|
||||
from .... import constants
|
||||
from .transfer_function_util.proximity_core import (
|
||||
is_obdata_identical,
|
||||
)
|
||||
|
||||
|
||||
def materials_clean(obj):
|
||||
# Material slots cannot use generic transfer_data_clean() function
|
||||
|
||||
ownership_data = find_ownership_data(
|
||||
obj.transfer_data_ownership,
|
||||
constants.MATERIAL_TRANSFER_DATA_ITEM_NAME,
|
||||
constants.MATERIAL_SLOT_KEY,
|
||||
)
|
||||
|
||||
# Clear Materials if No Transferable Data is Found
|
||||
if ownership_data:
|
||||
return
|
||||
|
||||
if obj.data and hasattr(obj.data, 'materials'):
|
||||
obj.data.materials.clear()
|
||||
|
||||
|
||||
def materials_is_missing(transfer_data_item):
|
||||
if (
|
||||
transfer_data_item.type == constants.MATERIAL_SLOT_KEY
|
||||
and len(transfer_data_item.id_data.material_slots) == 0
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
def init_materials(scene, obj):
|
||||
asset_pipe = scene.asset_pipeline
|
||||
td_type_key = constants.MATERIAL_SLOT_KEY
|
||||
name = constants.MATERIAL_TRANSFER_DATA_ITEM_NAME
|
||||
transfer_data = obj.transfer_data_ownership
|
||||
|
||||
if not (obj.data and hasattr(obj.data, 'materials')):
|
||||
return
|
||||
|
||||
ownership_data = find_ownership_data(transfer_data, name, td_type_key)
|
||||
# Only add new ownership transfer_data_item if material doesn't have an owner
|
||||
if not ownership_data:
|
||||
task_layer_owner, auto_surrender = get_transfer_data_owner(
|
||||
asset_pipe,
|
||||
td_type_key,
|
||||
)
|
||||
asset_pipe.add_temp_transfer_data(
|
||||
name=name,
|
||||
owner=task_layer_owner,
|
||||
type=td_type_key,
|
||||
obj_name=obj.name,
|
||||
surrender=auto_surrender,
|
||||
)
|
||||
|
||||
|
||||
def transfer_materials(target_obj: bpy.types.Object, source_obj):
|
||||
# Delete all material slots of target object.
|
||||
target_obj.data.materials.clear()
|
||||
|
||||
# Transfer material slots
|
||||
for idx in range(len(source_obj.material_slots)):
|
||||
target_obj.data.materials.append(source_obj.material_slots[idx].material)
|
||||
target_obj.material_slots[idx].link = source_obj.material_slots[idx].link
|
||||
|
||||
# Transfer active material slot index
|
||||
target_obj.active_material_index = source_obj.active_material_index
|
||||
|
||||
# Transfer material slot assignments for curve
|
||||
if target_obj.type == "CURVE":
|
||||
for spl_to, spl_from in zip(target_obj.data.splines, source_obj.data.splines):
|
||||
spl_to.material_index = spl_from.material_index
|
||||
|
||||
if source_obj.type == "MESH":
|
||||
if source_obj.data.attributes.get(constants.MATERIAL_ATTRIBUTE_NAME):
|
||||
transfer_attribute(constants.MATERIAL_ATTRIBUTE_NAME, target_obj, source_obj)
|
||||
|
||||
transfer_uv_seams(source_obj, target_obj)
|
||||
|
||||
|
||||
def transfer_uv_seams(source_obj, target_obj):
|
||||
if is_obdata_identical(source_obj, target_obj):
|
||||
for edge_from, edge_to in zip(source_obj.data.edges, target_obj.data.edges):
|
||||
edge_to.use_seam = edge_from.use_seam
|
||||
elif len(source_obj.data.edges) > 0 and len(target_obj.data.edges) > 0:
|
||||
# Create proxy object as transfer source to avoid transferring from evaluated mesh
|
||||
temp_source_obj = bpy.data.objects.new('TEMP', source_obj.data)
|
||||
with bpy.context.temp_override(
|
||||
object=temp_source_obj,
|
||||
active_object=temp_source_obj,
|
||||
selected_editable_objects=[temp_source_obj, target_obj],
|
||||
):
|
||||
bpy.ops.object.data_transfer(
|
||||
data_type="SEAM",
|
||||
edge_mapping="NEAREST",
|
||||
mix_mode="REPLACE",
|
||||
use_object_transform=False,
|
||||
)
|
||||
bpy.data.objects.remove(temp_source_obj)
|
||||
@@ -0,0 +1,206 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from .transfer_function_util.drivers import transfer_drivers, cleanup_drivers
|
||||
from .transfer_function_util.visibility import override_obj_visibility
|
||||
from ..transfer_util import (
|
||||
transfer_data_clean,
|
||||
transfer_data_item_is_missing,
|
||||
find_ownership_data,
|
||||
activate_shapekey,
|
||||
disable_modifiers,
|
||||
)
|
||||
from ...naming import task_layer_prefix_name_get, task_layer_prefix_basename_get
|
||||
from ...task_layer import get_transfer_data_owner
|
||||
from .... import constants, logging
|
||||
|
||||
BIND_OPS = {
|
||||
'SURFACE_DEFORM': bpy.ops.object.surfacedeform_bind,
|
||||
'MESH_DEFORM': bpy.ops.object.meshdeform_bind,
|
||||
'CORRECTIVE_SMOOTH': bpy.ops.object.correctivesmooth_bind,
|
||||
}
|
||||
|
||||
|
||||
def modifiers_clean(obj):
|
||||
cleaned_names = transfer_data_clean(
|
||||
obj=obj, data_list=obj.modifiers, td_type_key=constants.MODIFIER_KEY
|
||||
)
|
||||
|
||||
# Remove Drivers that match the cleaned item's name
|
||||
for name in cleaned_names:
|
||||
cleanup_drivers(obj, 'modifiers', name)
|
||||
|
||||
|
||||
def modifier_is_missing(transfer_data_item):
|
||||
return transfer_data_item_is_missing(
|
||||
transfer_data_item=transfer_data_item,
|
||||
td_type_key=constants.MODIFIER_KEY,
|
||||
data_list=transfer_data_item.id_data.modifiers,
|
||||
)
|
||||
|
||||
|
||||
def init_modifiers(scene, obj):
|
||||
asset_pipe = scene.asset_pipeline
|
||||
td_type_key = constants.MODIFIER_KEY
|
||||
transfer_data = obj.transfer_data_ownership
|
||||
task_layer_owner, auto_surrender = get_transfer_data_owner(
|
||||
asset_pipe,
|
||||
td_type_key,
|
||||
)
|
||||
|
||||
for mod in obj.modifiers:
|
||||
# Only add new ownership transfer_data_item if vertex group doesn't have an owner
|
||||
ownership_data = find_ownership_data(transfer_data, mod.name, td_type_key)
|
||||
if not ownership_data:
|
||||
ownership_data = asset_pipe.add_temp_transfer_data(
|
||||
name=mod.name,
|
||||
owner=task_layer_owner,
|
||||
type=td_type_key,
|
||||
obj_name=obj.name,
|
||||
surrender=auto_surrender,
|
||||
)
|
||||
|
||||
mod.name = task_layer_prefix_name_get(mod.name, ownership_data.owner)
|
||||
|
||||
|
||||
def transfer_modifier(context, modifier_name, target_obj, source_obj):
|
||||
"""Transfer a single modifier from source_obj to target_obj.
|
||||
For example, when pulling into rigging and transferring a rigging modifier,
|
||||
then source_obj will be the local object, and target_obj will be the external object.
|
||||
"""
|
||||
logger = logging.get_logger()
|
||||
|
||||
source_mod = source_obj.modifiers.get(modifier_name)
|
||||
target_mod = target_obj.modifiers.get(modifier_name)
|
||||
|
||||
if not source_mod:
|
||||
# This happens if a modifier's transfer data is still around, but the modifier
|
||||
# itself was removed.
|
||||
logger.debug(
|
||||
f"Modifer Transfer cancelled, '{modifier_name}' not found on '{source_obj.name}'"
|
||||
)
|
||||
if target_mod:
|
||||
target_obj.modifiers.remove(target_mod)
|
||||
return
|
||||
|
||||
if not target_mod:
|
||||
target_mod = target_obj.modifiers.new(source_mod.name, source_mod.type)
|
||||
|
||||
place_modifier_in_stack(source_obj, target_obj, modifier_name)
|
||||
transfer_modifier_props(context, source_mod, target_mod)
|
||||
transfer_drivers(source_obj, target_obj, 'modifiers', modifier_name)
|
||||
if is_modifier_bound(source_mod):
|
||||
bind_modifier(context, target_obj, modifier_name)
|
||||
|
||||
def place_modifier_in_stack(source_obj, target_obj, modifier_name):
|
||||
"""Modifiers will try to be placed below the modifier they were below on the source object.
|
||||
This is not very foolproof, since re-ordering multiple modifiers or renaming plus re-ordering,
|
||||
or removing plus re-ordering, all in one step, could make it hard to determine the ideal order.
|
||||
In such cases, user may need to fix the order and sync a 2nd time.
|
||||
"""
|
||||
|
||||
logger = logging.get_logger()
|
||||
idx_tgt = target_obj.modifiers.find(modifier_name)
|
||||
idx_src = source_obj.modifiers.find(modifier_name)
|
||||
|
||||
idx_new = 0
|
||||
name_anchor = ""
|
||||
# Order modifier based on previous modifier in source obj.
|
||||
if idx_src > 0:
|
||||
mod_anchor = source_obj.modifiers[idx_src - 1]
|
||||
name_anchor = task_layer_prefix_basename_get(mod_anchor.name)
|
||||
|
||||
for idx, mod_of_tgt in enumerate(target_obj.modifiers):
|
||||
if name_anchor == task_layer_prefix_basename_get(mod_of_tgt.name):
|
||||
idx_new = min(len(target_obj.modifiers)-1, idx+1)
|
||||
break
|
||||
|
||||
if idx_tgt != idx_new:
|
||||
target_obj.modifiers.move(idx_tgt, idx_new)
|
||||
msg = f" Moved {modifier_name} to index {idx_new}"
|
||||
if name_anchor:
|
||||
msg += f"(after {name_anchor})"
|
||||
logger.debug(msg)
|
||||
|
||||
|
||||
def transfer_modifier_props(context, source_mod, target_mod):
|
||||
props = [p.identifier for p in source_mod.bl_rna.properties if not p.is_readonly]
|
||||
for prop in props:
|
||||
value = getattr(source_mod, prop)
|
||||
setattr(target_mod, prop, value)
|
||||
|
||||
if source_mod.type == 'NODES':
|
||||
# NOTE: This matches inputs by their internal name, not their display name.
|
||||
# That means you can rename sockets, but removing and adding new ones might cause trouble.
|
||||
|
||||
# Transfer geo node attributes
|
||||
for key, value in source_mod.items():
|
||||
typ = type(getattr(target_mod, f'["{key}"]'))
|
||||
if typ in (int, float, bool, str):
|
||||
value = typ(value)
|
||||
target_mod[key] = value
|
||||
|
||||
# Transfer geo node bake settings
|
||||
target_mod.bake_directory = source_mod.bake_directory
|
||||
for index, target_bake in enumerate(target_mod.bakes):
|
||||
source_bake = source_mod.bakes[index]
|
||||
props = [p.identifier for p in source_bake.bl_rna.properties if not p.is_readonly]
|
||||
for prop in props:
|
||||
value = getattr(source_bake, prop)
|
||||
setattr(target_bake, prop, value)
|
||||
|
||||
# refresh node modifier UI
|
||||
if target_mod.node_group:
|
||||
target_mod.node_group.interface_update(context)
|
||||
|
||||
|
||||
def bind_modifier(context, obj, modifier_name):
|
||||
"""Binding data cannot be transferred. Instead, modifiers that require binding will have the bind operator executed.
|
||||
|
||||
Sometimes binding is meant to be done in a bind pose other than the default. For this, shape keys can be added
|
||||
to the deforming and/or the deformed mesh, named "BIND-<name_of_modifier_with_prefix>". Such shape keys will be enabled
|
||||
during binding. Other deforming modifiers will be disabled during binding.
|
||||
"""
|
||||
|
||||
# NOTE: This could be optimized by not re-binding unnecessarily, but Blender doesn't allow checking
|
||||
# if the binding is broken or not. https://projects.blender.org/blender/blender/issues/140550
|
||||
# Another way to get around this is to let the rigging task layer own the object base.
|
||||
|
||||
logger = logging.get_logger()
|
||||
modifier = obj.modifiers.get(modifier_name)
|
||||
assert modifier
|
||||
bind_op = BIND_OPS.get(modifier.type)
|
||||
if (
|
||||
not bind_op or
|
||||
(hasattr(modifier, 'target') and not modifier.target) or
|
||||
not modifier.show_viewport or
|
||||
(modifier.type=='CORRECTIVE_SMOOTH' and modifier.rest_source=='ORCO')
|
||||
):
|
||||
return
|
||||
|
||||
objs = [obj]
|
||||
if hasattr(modifier, 'target') and modifier.target:
|
||||
objs.append(modifier.target)
|
||||
with activate_shapekey(objs, "BIND-"+modifier_name):
|
||||
modifiers_to_disable = ['LATTICE', 'ARMATURE', 'SHRINKWRAP', 'SMOOTH']
|
||||
if modifier.type != 'CORRECTIVE_SMOOTH':
|
||||
modifiers_to_disable.append('CORRECTIVE_SMOOTH')
|
||||
with disable_modifiers(objs, modifiers_to_disable):
|
||||
for i in range(2):
|
||||
context.view_layer.update()
|
||||
with override_obj_visibility(obj=obj, scene=context.scene):
|
||||
with context.temp_override(object=obj, active_object=obj):
|
||||
bind_op(modifier=modifier.name)
|
||||
word = "Bound" if is_modifier_bound(modifier) else "Un-bound"
|
||||
logger.debug(f"{word} {modifier_name} on {obj.name}")
|
||||
if is_modifier_bound(modifier):
|
||||
return
|
||||
|
||||
|
||||
def is_modifier_bound(modifier) -> bool | None:
|
||||
if modifier.type == 'CORRECTIVE_SMOOTH':
|
||||
return modifier.is_bind
|
||||
elif hasattr(modifier, 'is_bound'):
|
||||
return modifier.is_bound
|
||||
@@ -0,0 +1,69 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from ..transfer_util import find_ownership_data
|
||||
from ...task_layer import get_transfer_data_owner
|
||||
from ...naming import merge_get_basename
|
||||
from .... import constants, logging
|
||||
|
||||
|
||||
def parent_clean(obj):
|
||||
logger = logging.get_logger()
|
||||
ownership_data = find_ownership_data(
|
||||
obj.transfer_data_ownership,
|
||||
merge_get_basename(constants.PARENT_TRANSFER_DATA_ITEM_NAME),
|
||||
constants.PARENT_KEY,
|
||||
)
|
||||
|
||||
if ownership_data:
|
||||
return
|
||||
|
||||
obj.parent = None
|
||||
logger.debug("Cleaning Parent Relationship")
|
||||
|
||||
|
||||
def parent_is_missing(transfer_data_item):
|
||||
if (
|
||||
transfer_data_item.type == constants.PARENT_KEY
|
||||
and transfer_data_item.id_data.parent == None
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
def init_parent(scene, obj):
|
||||
asset_pipe = scene.asset_pipeline
|
||||
td_type_key = constants.PARENT_KEY
|
||||
name = constants.PARENT_TRANSFER_DATA_ITEM_NAME
|
||||
transfer_data = obj.transfer_data_ownership
|
||||
|
||||
if obj.parent not in list(asset_pipe.asset_collection.all_objects) and obj.parent is not None:
|
||||
raise Exception(f"Object parent {obj.parent.name} cannot be outside of asset collection")
|
||||
ownership_data = find_ownership_data(transfer_data, name, td_type_key)
|
||||
# Only add new ownership transfer_data_item if vertex group doesn't have an owner
|
||||
if not ownership_data:
|
||||
task_layer_owner, auto_surrender = get_transfer_data_owner(
|
||||
asset_pipe,
|
||||
td_type_key,
|
||||
)
|
||||
asset_pipe.add_temp_transfer_data(
|
||||
name=name,
|
||||
owner=task_layer_owner,
|
||||
type=td_type_key,
|
||||
obj_name=obj.name,
|
||||
surrender=auto_surrender,
|
||||
)
|
||||
|
||||
|
||||
def transfer_parent(target_obj, source_obj):
|
||||
target_obj.parent = source_obj.parent
|
||||
target_obj.parent_type = source_obj.parent_type
|
||||
target_obj.parent_bone = source_obj.parent_bone
|
||||
|
||||
target_obj.location = source_obj.location
|
||||
if source_obj.rotation_mode == 'QUATERNION':
|
||||
target_obj.rotation_quaternion = source_obj.rotation_quaternion
|
||||
else:
|
||||
target_obj.rotation_euler = source_obj.rotation_euler
|
||||
target_obj.scale = source_obj.scale
|
||||
target_obj.matrix_parent_inverse = source_obj.matrix_parent_inverse.copy()
|
||||
@@ -0,0 +1,164 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import bmesh
|
||||
import numpy as np
|
||||
from .transfer_function_util.proximity_core import (
|
||||
tris_per_face,
|
||||
closest_face_to_point,
|
||||
closest_tri_on_face,
|
||||
)
|
||||
from .transfer_function_util.drivers import transfer_drivers, cleanup_drivers
|
||||
from ..transfer_util import (
|
||||
transfer_data_item_is_missing,
|
||||
transfer_data_item_init,
|
||||
find_ownership_data,
|
||||
)
|
||||
from ...naming import merge_get_basename
|
||||
from .... import constants, logging
|
||||
|
||||
|
||||
def shape_key_set_active(obj, shape_key_name):
|
||||
for index, shape_key in enumerate(obj.data.shape_keys.key_blocks):
|
||||
if shape_key.name == shape_key_name:
|
||||
obj.active_shape_key_index = index
|
||||
|
||||
|
||||
def shape_keys_clean(obj):
|
||||
if obj.type != "MESH" or obj.data.shape_keys is None:
|
||||
return
|
||||
|
||||
cleaned_item_names = set()
|
||||
|
||||
for shape_key in obj.data.shape_keys.key_blocks:
|
||||
ownership_data = find_ownership_data(
|
||||
obj.transfer_data_ownership,
|
||||
merge_get_basename(shape_key.name),
|
||||
constants.SHAPE_KEY_KEY,
|
||||
)
|
||||
if not ownership_data:
|
||||
cleaned_item_names.add(shape_key.name)
|
||||
obj.shape_key_remove(shape_key)
|
||||
|
||||
if not obj.data.shape_keys:
|
||||
# It's possible there are no shape keys anymore.
|
||||
return
|
||||
|
||||
for name in cleaned_item_names:
|
||||
cleanup_drivers(obj.data.shape_keys, 'key_blocks', name)
|
||||
|
||||
|
||||
def shape_key_is_missing(transfer_data_item):
|
||||
if not transfer_data_item.type == constants.SHAPE_KEY_KEY:
|
||||
return
|
||||
obj = transfer_data_item.id_data
|
||||
if obj.type != 'MESH':
|
||||
return
|
||||
if not obj.data.shape_keys:
|
||||
return True
|
||||
return transfer_data_item_is_missing(
|
||||
transfer_data_item=transfer_data_item,
|
||||
td_type_key=constants.SHAPE_KEY_KEY,
|
||||
data_list=obj.data.shape_keys.key_blocks,
|
||||
)
|
||||
|
||||
|
||||
def init_shape_keys(scene, obj):
|
||||
if obj.type != "MESH" or obj.data.shape_keys is None:
|
||||
return
|
||||
|
||||
# Check that the order is legal.
|
||||
# Key Blocks must be ordered after the key they are Relative To.
|
||||
for i, kb in enumerate(obj.data.shape_keys.key_blocks):
|
||||
if kb.relative_key:
|
||||
base_shape_idx = obj.data.shape_keys.key_blocks.find(kb.relative_key.name)
|
||||
if base_shape_idx > i:
|
||||
raise Exception(
|
||||
f'Shape Key "{kb.name}" must be ordered after its base shape "{kb.relative_key.name}" on object "{obj.name}".'
|
||||
)
|
||||
|
||||
transfer_data_item_init(
|
||||
scene=scene,
|
||||
obj=obj,
|
||||
data_list=obj.data.shape_keys.key_blocks,
|
||||
td_type_key=constants.SHAPE_KEY_KEY,
|
||||
)
|
||||
|
||||
|
||||
def transfer_shape_key(
|
||||
context: bpy.types.Context,
|
||||
shape_key_name: str,
|
||||
target_obj: bpy.types.Object,
|
||||
source_obj: bpy.types.Object,
|
||||
):
|
||||
logger = logging.get_logger()
|
||||
if not source_obj.data.shape_keys:
|
||||
return
|
||||
sk_source = source_obj.data.shape_keys.key_blocks.get(shape_key_name)
|
||||
assert sk_source
|
||||
|
||||
sk_target = None
|
||||
if not target_obj.data.shape_keys:
|
||||
sk_target = target_obj.shape_key_add()
|
||||
if not sk_target:
|
||||
sk_target = target_obj.data.shape_keys.key_blocks.get(shape_key_name)
|
||||
if not sk_target:
|
||||
sk_target = target_obj.shape_key_add()
|
||||
|
||||
sk_target.name = sk_source.name
|
||||
sk_target.value = 0
|
||||
sk_target.vertex_group = sk_source.vertex_group
|
||||
if sk_source.relative_key != sk_source:
|
||||
relative_key = None
|
||||
if target_obj.data.shape_keys:
|
||||
relative_key = target_obj.data.shape_keys.key_blocks.get(sk_source.relative_key.name)
|
||||
if relative_key:
|
||||
sk_target.relative_key = relative_key
|
||||
else:
|
||||
# If the base shape of one of our shapes was removed by another task layer,
|
||||
# the result will probably be pretty bad, but it's not a catastrophic failure.
|
||||
# Proceed with a warning.
|
||||
logger.warning(
|
||||
f'Base shape "{sk_source.relative_key.name}" of Key "{sk_source.name}" was removed from "{target_obj.name}"'
|
||||
)
|
||||
|
||||
sk_target.slider_min = sk_source.slider_min
|
||||
sk_target.slider_max = sk_source.slider_max
|
||||
sk_target.value = sk_source.value
|
||||
sk_target.mute = sk_source.mute
|
||||
|
||||
bm_source = bmesh.new()
|
||||
bm_source.from_mesh(source_obj.data)
|
||||
bm_source.faces.ensure_lookup_table()
|
||||
|
||||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||
tris_dict = tris_per_face(bm_source)
|
||||
for i, vert in enumerate(target_obj.data.vertices):
|
||||
p = vert.co
|
||||
face = closest_face_to_point(bm_source, p, bvh_tree)
|
||||
|
||||
(tri, point) = closest_tri_on_face(tris_dict, face, p)
|
||||
if not tri:
|
||||
continue
|
||||
weights = mathutils.interpolate.poly_3d_calc([tri[i].vert.co for i in range(3)], point)
|
||||
|
||||
vals_weighted = [
|
||||
weights[i]
|
||||
* (
|
||||
sk_source.data[tri[i].vert.index].co
|
||||
- source_obj.data.vertices[tri[i].vert.index].co
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
val = mathutils.Vector(sum(np.array(vals_weighted)))
|
||||
sk_target.data[i].co = vert.co + val
|
||||
|
||||
if source_obj.data.shape_keys is None:
|
||||
return
|
||||
|
||||
transfer_drivers(
|
||||
source_obj.data.shape_keys, target_obj.data.shape_keys, 'key_blocks', shape_key_name
|
||||
)
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
def transfer_active_color_attribute_index(target_obj, active_color_name):
|
||||
# active_color_name = source_obj.data.color_attributes.active_color_name
|
||||
if active_color_name is None or active_color_name == "":
|
||||
return
|
||||
for color_attribute in target_obj.data.color_attributes:
|
||||
if color_attribute.name == active_color_name:
|
||||
target_obj.data.color_attributes.active_color = color_attribute
|
||||
|
||||
|
||||
def transfer_active_uv_layer_index(target_obj, active_uv_name):
|
||||
# active_uv = source_obj.data.uv_layers.active
|
||||
if active_uv_name is None or active_uv_name == "":
|
||||
return
|
||||
for uv_layer in target_obj.data.uv_layers:
|
||||
if uv_layer.name == active_uv_name:
|
||||
target_obj.data.uv_layers.active = uv_layer
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
def copy_driver(
|
||||
from_fcurve: bpy.types.FCurve, target: bpy.types.ID, data_path=None, index=None
|
||||
) -> bpy.types.FCurve:
|
||||
"""Copy an existing FCurve containing a driver to a new ID, by creating a copy
|
||||
of the existing driver on the target ID.
|
||||
|
||||
Args:
|
||||
from_fcurve (bpy.types.FCurve): FCurve containing a driver
|
||||
target (bpy.types.ID): ID that can have drivers added to it
|
||||
data_path (_type_, optional): Data Path of existing driver. Defaults to None.
|
||||
index (_type_, optional): array index of the property drive. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bpy.types.FCurve: Fcurve containing copy of driver on target ID
|
||||
"""
|
||||
|
||||
if not target.animation_data:
|
||||
target.animation_data_create()
|
||||
|
||||
new_fc = target.animation_data.drivers.from_existing(src_driver = from_fcurve)
|
||||
|
||||
if data_path:
|
||||
new_fc.data_path = data_path
|
||||
if index:
|
||||
new_fc.array_index = index
|
||||
|
||||
return new_fc
|
||||
|
||||
|
||||
def find_drivers(id: bpy.types.ID, target_type: str, target_name: str) -> list[bpy.types.FCurve]:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
drivers (list[bpy.types.FCurve]): List or Collection Property containing Fcurves with drivers
|
||||
target_type (str): Name of data type found in driver data path, e.g. "modifiers"
|
||||
target_name (str): Name of data found in driver path, e.g. modifier's name
|
||||
|
||||
Returns:
|
||||
list[bpy.types.FCurve]: List of FCurves containing drivers that match type & name
|
||||
"""
|
||||
|
||||
if not id.animation_data:
|
||||
return []
|
||||
|
||||
found_drivers = []
|
||||
if id.animation_data is None or id.animation_data.drivers is None:
|
||||
return found_drivers
|
||||
drivers = id.animation_data.drivers
|
||||
for driver in drivers:
|
||||
if f'{target_type}["{target_name}"]' in driver.data_path:
|
||||
found_drivers.append(driver)
|
||||
return found_drivers
|
||||
|
||||
|
||||
def transfer_drivers(
|
||||
source_id: bpy.types.ID, target_id: bpy.types.ID, target_type: str, target_name: str
|
||||
) -> None:
|
||||
"""Transfers Drivers from one ID to another, will copy and new drivres from source to from
|
||||
source to target, and will remove any drivers on the target that are not in the source.
|
||||
|
||||
Args:
|
||||
source_id (bpy.types.ID): Source ID, containing drivers to copy
|
||||
target_id (bpy.types.ID): Target ID, which will recieve the drivers from source
|
||||
target_type (str): Name of driver target's type, like `modifier` or `constraint`
|
||||
target_name (str): Name of driver target, e.g. name of a modifier or contraint
|
||||
"""
|
||||
source_fcurves = find_drivers(source_id, target_type, target_name)
|
||||
target_fcurves = find_drivers(target_id, target_type, target_name)
|
||||
|
||||
# Clear old drivers
|
||||
for old_fcurve in list(set(target_fcurves) - set(source_fcurves)):
|
||||
target_id.animation_data.drivers.remove(old_fcurve)
|
||||
|
||||
# Transfer new drivers
|
||||
for fcurve in source_fcurves:
|
||||
copy_driver(from_fcurve=fcurve, target=target_id)
|
||||
|
||||
|
||||
def cleanup_drivers(id: bpy.types.ID, target_type: str, target_name: str) -> None:
|
||||
"""Remove all drivers for transfer data that has been removed.
|
||||
|
||||
Args:
|
||||
object (bpy.types.ID): ID, which has drivers to remove
|
||||
target_type (str): Name of driver target's type, like `modifier` or `constraint`
|
||||
target_name (str): Name of driver target, e.g. name of a modifier or contraint
|
||||
"""
|
||||
target_fcurves = find_drivers(id, target_type, target_name)
|
||||
for fcurve in target_fcurves:
|
||||
id.animation_data.drivers.remove(fcurve)
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup, bpy_prop_collection, Object
|
||||
from rna_prop_ui import IDPropertyGroup
|
||||
from bpy.utils import flip_name
|
||||
|
||||
# Functions to manage runtime properties, which include custom properties and add-on properties.
|
||||
# These functions aim to abstract away that distinction, and also abstract away whether something is a single value,
|
||||
# a PropertyGroup, or a CollectionProperty.
|
||||
# Currently the minimum Blender version for this code is 5.0, but it could probably be made backwards-compatible.
|
||||
|
||||
def copy_all_runtime_properties(src_id, tgt_id, x_mirror=False):
|
||||
"""Copy add-on and custom properties from source to target.
|
||||
Both should be the same type.
|
||||
Should support anything that supports custom properties or property registration.
|
||||
"""
|
||||
for prop_name in get_all_runtime_prop_names(src_id):
|
||||
copy_runtime_property(src_id, tgt_id, prop_name, x_mirror)
|
||||
|
||||
def get_all_runtime_prop_names(owner):
|
||||
custom_props = list(owner.keys())
|
||||
addon_props = get_addon_prop_names(owner)
|
||||
props = custom_props + addon_props
|
||||
return props
|
||||
|
||||
def get_addon_prop_names(owner):
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
return list(owner.bl_system_properties_get().keys())
|
||||
else:
|
||||
return [prop_name for prop_name in owner.keys() if is_addon_prop(owner, prop_name)]
|
||||
|
||||
def copy_runtime_property(src_id, tgt_id, prop_name, x_mirror=False):
|
||||
"""Copy add-on properties or custom properties."""
|
||||
if is_addon_prop(src_id, prop_name):
|
||||
if is_registered_addon_prop(src_id, prop_name):
|
||||
src_prop = getattr(src_id, prop_name)
|
||||
tgt_prop = getattr(tgt_id, prop_name)
|
||||
if isinstance(src_prop, bpy_prop_collection):
|
||||
copy_coll_prop(src_prop, tgt_prop, x_mirror)
|
||||
elif isinstance(src_prop, PropertyGroup):
|
||||
copy_property_group(src_prop, tgt_prop, x_mirror)
|
||||
else:
|
||||
copy_single_addon_prop(src_id, tgt_id, prop_name, x_mirror)
|
||||
else:
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# HACK: If we need to copy add-on properties, but the add-on is not present,
|
||||
# we have to write to the system properties, which is API abuse that could
|
||||
# lose support any moment, but there is no other way to do this atm.
|
||||
try:
|
||||
tgt_id.bl_system_properties_get()[prop_name] = src_id.bl_system_properties_get()[prop_name]
|
||||
except TypeError:
|
||||
# Happens for at least a mysterious "booleans" custom property which seems to be an empty PropGroup. Where is it coming from!?
|
||||
pass
|
||||
else:
|
||||
tgt_id[prop_name] = src_id[prop_name]
|
||||
else:
|
||||
copy_custom_property(src_id, tgt_id, prop_name)
|
||||
|
||||
def copy_property_group(src_pg: PropertyGroup, tgt_pg: PropertyGroup, x_mirror=False):
|
||||
"""
|
||||
Copy the values from one PropertyGroup into another of the same type.
|
||||
Optionally, X-mirror names (e.g., ".L" <-> ".R") in strings and Object references.
|
||||
"""
|
||||
assert isinstance(tgt_pg, PropertyGroup) and isinstance(src_pg, PropertyGroup)
|
||||
assert tgt_pg.__class__ == src_pg.__class__
|
||||
|
||||
for prop_name in src_pg.bl_rna.properties.keys():
|
||||
if prop_name in ('rna_type', 'bl_rna'):
|
||||
continue
|
||||
if not src_pg.is_property_set(prop_name):
|
||||
tgt_pg.property_unset(prop_name)
|
||||
continue
|
||||
value = getattr(src_pg, prop_name)
|
||||
if isinstance(value, bpy_prop_collection):
|
||||
tgt_collprop = getattr(tgt_pg, prop_name)
|
||||
copy_coll_prop(value, tgt_collprop)
|
||||
elif isinstance(value, PropertyGroup):
|
||||
copy_property_group(value, getattr(tgt_pg, prop_name), x_mirror)
|
||||
else:
|
||||
copy_single_addon_prop(src_pg, tgt_pg, prop_name, x_mirror)
|
||||
for prop_name in src_pg.keys():
|
||||
if is_custom_prop(src_pg, prop_name):
|
||||
# PropertyGroups also support custom properties.
|
||||
copy_custom_property(src_pg, tgt_pg, prop_name, x_mirror)
|
||||
|
||||
def copy_coll_prop(src_cp, tgt_cp, x_mirror=False):
|
||||
tgt_cp.clear()
|
||||
for src_pg in src_cp:
|
||||
assert isinstance(src_pg, PropertyGroup)
|
||||
tgt_pg = tgt_cp.add()
|
||||
copy_property_group(src_pg, tgt_pg, x_mirror)
|
||||
|
||||
def copy_custom_property(src_owner, tgt_owner, prop_name, x_mirror=False):
|
||||
"""Copy a custom property (one that was created via the UI or via Python dictionary syntax)."""
|
||||
prop = src_owner.id_properties_ui(prop_name)
|
||||
assert prop, f'Property "{prop_name}" not found in {src_owner}.'
|
||||
value = src_owner[prop_name]
|
||||
if x_mirror:
|
||||
value = x_mirror_value(value)
|
||||
|
||||
tgt_owner[prop_name] = value
|
||||
new_prop = tgt_owner.id_properties_ui(prop_name)
|
||||
new_prop.update_from(prop)
|
||||
|
||||
def copy_single_addon_prop(src, tgt, prop_name, x_mirror=False) -> True:
|
||||
if src.is_property_readonly(prop_name):
|
||||
# This "early" exit has to come after CollectionProperty & PropertyGroup
|
||||
# checks, since they are technically read-only.
|
||||
return False
|
||||
|
||||
value = getattr(src, prop_name)
|
||||
if x_mirror:
|
||||
value = x_mirror_value(value)
|
||||
|
||||
setattr(tgt, prop_name, value)
|
||||
return True
|
||||
|
||||
def x_mirror_value(value):
|
||||
if isinstance(value, str):
|
||||
return flip_name(value)
|
||||
elif isinstance(value, Object):
|
||||
get_opposite_obj(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def get_opposite_obj(obj: Object) -> Object:
|
||||
"""Return the X-mirrored version of a Blender object by name (and library if linked)."""
|
||||
flipped_name = flip_name(obj.name)
|
||||
lib = obj.library
|
||||
return (
|
||||
bpy.data.objects.get((lib, flipped_name)) if lib else
|
||||
bpy.data.objects.get(flipped_name)
|
||||
) or obj
|
||||
|
||||
def is_addon_prop(owner, prop_name):
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
return prop_name in owner.bl_system_properties_get().keys()
|
||||
else:
|
||||
# NOTE: I don't think it's possible to detect pre-5.0 non-PropertyGroup/CollectionProperty non-registered add-on properties.
|
||||
# They just behave completely as custom properties.
|
||||
return prop_name in owner and (isinstance(owner[prop_name], IDPropertyGroup) or isinstance(owner[prop_name], list))
|
||||
|
||||
def is_registered_addon_prop(owner, prop_name):
|
||||
return is_addon_prop(owner, prop_name) and prop_name in owner.bl_rna.properties
|
||||
|
||||
def is_custom_prop(owner, prop_name):
|
||||
return prop_name in owner.keys() and not is_addon_prop(owner, prop_name)
|
||||
|
||||
def remove_property(obj, prop_name):
|
||||
if is_custom_prop(obj, prop_name):
|
||||
del obj[prop_name]
|
||||
if is_registered_addon_prop(obj, prop_name):
|
||||
obj.property_unset(prop_name)
|
||||
elif is_addon_prop(obj, prop_name):
|
||||
disabled_addon_props = obj.bl_system_properties_get()
|
||||
del disabled_addon_props[prop_name]
|
||||
else:
|
||||
raise KeyError(f"{prop_name} not found in {obj.name}")
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import bmesh
|
||||
import numpy as np
|
||||
|
||||
|
||||
def closest_face_to_point(bm_source, p_target, bvh_tree=None):
|
||||
if not bvh_tree:
|
||||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||
(loc, norm, index, distance) = bvh_tree.find_nearest(p_target)
|
||||
return bm_source.faces[index]
|
||||
|
||||
|
||||
def tris_per_face(bm_source):
|
||||
tris_source = bm_source.calc_loop_triangles()
|
||||
tris_dict = dict()
|
||||
for face in bm_source.faces:
|
||||
tris_face = []
|
||||
for i in range(len(tris_source))[::-1]:
|
||||
if tris_source[i][0] in face.loops:
|
||||
tris_face.append(tris_source.pop(i))
|
||||
tris_dict[face] = tris_face
|
||||
return tris_dict
|
||||
|
||||
|
||||
def closest_tri_on_face(tris_dict, face, p):
|
||||
points = []
|
||||
dist = []
|
||||
tris = []
|
||||
for tri in tris_dict[face]:
|
||||
point = mathutils.geometry.closest_point_on_tri(
|
||||
p, *[tri[i].vert.co for i in range(3)]
|
||||
)
|
||||
tris.append(tri)
|
||||
points.append(point)
|
||||
dist.append((point - p).length)
|
||||
min_idx = np.argmin(np.array(dist))
|
||||
point = points[min_idx]
|
||||
tri = tris[min_idx]
|
||||
return (tri, point)
|
||||
|
||||
|
||||
def closest_edge_on_face_to_line(face, p1, p2, skip_edges=None):
|
||||
"""Returns edge of a face which is closest to line."""
|
||||
for edge in face.edges:
|
||||
if skip_edges:
|
||||
if edge in skip_edges:
|
||||
continue
|
||||
res = mathutils.geometry.intersect_line_line(
|
||||
p1, p2, *[edge.verts[i].co for i in range(2)]
|
||||
)
|
||||
if not res:
|
||||
continue
|
||||
(p_traversal, p_edge) = res
|
||||
frac_1 = (edge.verts[1].co - edge.verts[0].co).dot(
|
||||
p_edge - edge.verts[0].co
|
||||
) / (edge.verts[1].co - edge.verts[0].co).length ** 2.0
|
||||
frac_2 = (p2 - p1).dot(p_traversal - p1) / (p2 - p1).length ** 2.0
|
||||
if (frac_1 >= 0 and frac_1 <= 1) and (frac_2 >= 0 and frac_2 <= 1):
|
||||
return edge
|
||||
return None
|
||||
|
||||
|
||||
def edge_data_split(edge, data_layer, data_suffix: str):
|
||||
for vert in edge.verts:
|
||||
vals = []
|
||||
for loop in vert.link_loops:
|
||||
loops_edge_vert = set([loop for f in edge.link_faces for loop in f.loops])
|
||||
if loop not in loops_edge_vert:
|
||||
continue
|
||||
dat = data_layer[loop.index]
|
||||
element = list(getattr(dat, data_suffix))
|
||||
if not vals:
|
||||
vals.append(element)
|
||||
elif not vals[0] == element:
|
||||
vals.append(element)
|
||||
if len(vals) > 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def interpolate_data_from_face(
|
||||
bm_source, tris_dict, face, p, data_layer_source, data_suffix=''
|
||||
):
|
||||
"""Returns interpolated value of a data layer within a face closest to a point."""
|
||||
|
||||
(tri, point) = closest_tri_on_face(tris_dict, face, p)
|
||||
if not tri:
|
||||
return None
|
||||
weights = mathutils.interpolate.poly_3d_calc(
|
||||
[tri[i].vert.co for i in range(3)], point
|
||||
)
|
||||
|
||||
if not data_suffix:
|
||||
cols_weighted = [
|
||||
weights[i] * np.array(data_layer_source[tri[i].index]) for i in range(3)
|
||||
]
|
||||
col = sum(np.array(cols_weighted))
|
||||
else:
|
||||
cols_weighted = [
|
||||
weights[i] * np.array(getattr(data_layer_source[tri[i].index], data_suffix))
|
||||
for i in range(3)
|
||||
]
|
||||
col = sum(np.array(cols_weighted))
|
||||
return col
|
||||
|
||||
|
||||
def transfer_corner_data(
|
||||
obj_source, obj_target, data_layer_source, data_layer_target, data_suffix=''
|
||||
):
|
||||
"""
|
||||
Transfers interpolated face corner data from data layer of a source object to data layer of a
|
||||
target object, while approximately preserving data seams (e.g. necessary for UV Maps).
|
||||
The transfer is face interpolated per target corner within the source face that is closest
|
||||
to the target corner point and does not have any data seams on the way back to the
|
||||
source face that is closest to the target face's center.
|
||||
"""
|
||||
|
||||
bm_source = bmesh.new()
|
||||
bm_source.from_mesh(obj_source.data)
|
||||
bm_source.faces.ensure_lookup_table()
|
||||
bm_target = bmesh.new()
|
||||
bm_target.from_mesh(obj_target.data)
|
||||
bm_target.faces.ensure_lookup_table()
|
||||
|
||||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||
|
||||
tris_dict = tris_per_face(bm_source)
|
||||
|
||||
for face_target in bm_target.faces:
|
||||
face_target_center = face_target.calc_center_median()
|
||||
|
||||
face_source = closest_face_to_point(bm_source, face_target_center, bvh_tree)
|
||||
|
||||
for corner_target in face_target.loops:
|
||||
# find nearest face on target compared to face that loop belongs to
|
||||
p = corner_target.vert.co
|
||||
|
||||
face_source_closest = closest_face_to_point(bm_source, p, bvh_tree)
|
||||
enclosed = face_source_closest is face_source
|
||||
face_source_int = face_source
|
||||
if not enclosed:
|
||||
# traverse faces between point and face center
|
||||
traversed_faces = set()
|
||||
traversed_edges = set()
|
||||
while face_source_int is not face_source_closest:
|
||||
traversed_faces.add(face_source_int)
|
||||
edge = closest_edge_on_face_to_line(
|
||||
face_source_int,
|
||||
face_target_center,
|
||||
p,
|
||||
skip_edges=traversed_edges,
|
||||
)
|
||||
if edge == None:
|
||||
break
|
||||
if len(edge.link_faces) != 2:
|
||||
break
|
||||
traversed_edges.add(edge)
|
||||
|
||||
split = edge_data_split(edge, data_layer_source, data_suffix)
|
||||
if split:
|
||||
break
|
||||
|
||||
# set new source face to other face belonging to edge
|
||||
face_source_int = (
|
||||
edge.link_faces[1]
|
||||
if edge.link_faces[1] is not face_source_int
|
||||
else edge.link_faces[0]
|
||||
)
|
||||
|
||||
# avoid looping behaviour
|
||||
if face_source_int in traversed_faces:
|
||||
face_source_int = face_source
|
||||
break
|
||||
|
||||
# interpolate data from selected face
|
||||
col = interpolate_data_from_face(
|
||||
bm_source, tris_dict, face_source_int, p, data_layer_source, data_suffix
|
||||
)
|
||||
if col is None:
|
||||
continue
|
||||
if not data_suffix:
|
||||
data_layer_target.data[corner_target.index] = col
|
||||
else:
|
||||
setattr(data_layer_target[corner_target.index], data_suffix, list(col))
|
||||
return
|
||||
|
||||
|
||||
def is_mesh_identical(mesh_a, mesh_b) -> bool:
|
||||
if len(mesh_a.vertices) != len(mesh_b.vertices):
|
||||
return False
|
||||
if len(mesh_a.edges) != len(mesh_b.edges):
|
||||
return False
|
||||
if len(mesh_a.polygons) != len(mesh_b.polygons):
|
||||
return False
|
||||
for e1, e2 in zip(mesh_a.edges, mesh_b.edges):
|
||||
for v1, v2 in zip(e1.vertices, e2.vertices):
|
||||
if v1 != v2:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_curve_identical(curve_a: bpy.types.Curve, curve_b: bpy.types.Curve) -> bool:
|
||||
if len(curve_a.splines) != len(curve_b.splines):
|
||||
return False
|
||||
for spline1, spline2 in zip(curve_a.splines, curve_b.splines):
|
||||
if len(spline1.points) != len(spline2.points):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_obdata_identical(
|
||||
a: bpy.types.Object or bpy.types.Mesh, b: bpy.types.Object or bpy.types.Mesh
|
||||
) -> bool:
|
||||
"""Checks if two objects have matching topology (efficiency over exactness)"""
|
||||
if type(a) == bpy.types.Object:
|
||||
a = a.data
|
||||
if type(b) == bpy.types.Object:
|
||||
b = b.data
|
||||
|
||||
if type(a) != type(b):
|
||||
return False
|
||||
|
||||
if type(a) == bpy.types.Mesh:
|
||||
return is_mesh_identical(a, b)
|
||||
elif type(a) == bpy.types.Curve:
|
||||
return is_curve_identical(a, b)
|
||||
else:
|
||||
# TODO: Support geometry types other than mesh or curve.
|
||||
return
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import contextlib
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def get_visibility_driver(obj) -> Optional[bpy.types.FCurve]:
|
||||
obj = bpy.data.objects.get(obj.name)
|
||||
assert obj, "Object was renamed while its visibility was being ensured?"
|
||||
if hasattr(obj, "animation_data") and obj.animation_data:
|
||||
return obj.animation_data.drivers.find("hide_viewport")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def override_obj_visibility(obj: bpy.types.Object, scene: bpy.types.Scene):
|
||||
"""Temporarily Change the visibility of an Object so an bpy.ops or other
|
||||
function that requires the object to be visible can be called.
|
||||
|
||||
Args:
|
||||
obj (bpy.types.Object): Object to un-hide
|
||||
scene (bpy.types.Scene): Scene Object is in
|
||||
"""
|
||||
hide = obj.hide_get() # eye icon
|
||||
hide_viewport = obj.hide_viewport # hide viewport
|
||||
select = obj.hide_select # selectable
|
||||
|
||||
driver = get_visibility_driver(obj)
|
||||
if driver:
|
||||
driver_mute = driver.mute
|
||||
|
||||
try:
|
||||
obj.hide_set(False)
|
||||
obj.hide_viewport = False
|
||||
obj.hide_select = False
|
||||
if driver:
|
||||
driver.mute = True
|
||||
|
||||
assigned_to_scene_root = False
|
||||
if obj.name not in scene.collection.objects:
|
||||
assigned_to_scene_root = True
|
||||
scene.collection.objects.link(obj)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
obj.hide_set(hide)
|
||||
obj.hide_viewport = hide_viewport
|
||||
obj.hide_select = select
|
||||
if driver:
|
||||
driver.mute = driver_mute
|
||||
|
||||
if assigned_to_scene_root and obj.name in scene.collection.objects:
|
||||
scene.collection.objects.unlink(obj)
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector, kdtree
|
||||
from typing import Dict, Tuple, List
|
||||
from ..transfer_util import (
|
||||
transfer_data_clean,
|
||||
transfer_data_item_is_missing,
|
||||
transfer_data_item_init,
|
||||
)
|
||||
from .transfer_function_util.proximity_core import (
|
||||
is_obdata_identical,
|
||||
)
|
||||
from .... import constants, logging
|
||||
|
||||
|
||||
def vertex_groups_clean(obj):
|
||||
transfer_data_clean(
|
||||
obj=obj, data_list=obj.vertex_groups, td_type_key=constants.VERTEX_GROUP_KEY
|
||||
)
|
||||
|
||||
|
||||
def vertex_group_is_missing(transfer_data_item):
|
||||
return transfer_data_item_is_missing(
|
||||
transfer_data_item=transfer_data_item,
|
||||
td_type_key=constants.VERTEX_GROUP_KEY,
|
||||
data_list=transfer_data_item.id_data.vertex_groups,
|
||||
)
|
||||
|
||||
|
||||
def init_vertex_groups(scene, obj):
|
||||
transfer_data_item_init(
|
||||
scene=scene,
|
||||
obj=obj,
|
||||
data_list=obj.vertex_groups,
|
||||
td_type_key=constants.VERTEX_GROUP_KEY,
|
||||
)
|
||||
|
||||
|
||||
def transfer_vertex_groups(
|
||||
vertex_group_names: List[str],
|
||||
target_obj: bpy.types.Object,
|
||||
source_obj: bpy.types.Object,
|
||||
):
|
||||
logger = logging.get_logger()
|
||||
for vertex_group_name in vertex_group_names:
|
||||
if not source_obj.vertex_groups.get(vertex_group_name):
|
||||
logger.error(f"Vertex Group {vertex_group_name} not found in {source_obj.name}")
|
||||
return
|
||||
|
||||
# If topology matches transfer directly, otherwise use vertex proximity
|
||||
if is_obdata_identical(source_obj, target_obj):
|
||||
for vertex_group_name in vertex_group_names:
|
||||
transfer_single_vgroup_by_topology(source_obj, target_obj, vertex_group_name)
|
||||
else:
|
||||
precalc_and_transfer_multiple_groups(source_obj, target_obj, vertex_group_names, expand=2)
|
||||
|
||||
|
||||
def transfer_single_vgroup_by_topology(source_obj, target_obj, vgroup_name):
|
||||
"""Function to quickly transfer single vertex group between mesh objects in case of matching topology."""
|
||||
|
||||
remove_vgroups([target_obj], [vgroup_name])
|
||||
|
||||
vgroup_src = source_obj.vertex_groups.get(vgroup_name)
|
||||
vgroup_tgt = target_obj.vertex_groups.new(name=vgroup_name)
|
||||
|
||||
for v in source_obj.data.vertices:
|
||||
if vgroup_src.index in [g.group for g in v.groups]:
|
||||
vgroup_tgt.add([v.index], vgroup_src.weight(v.index), 'REPLACE')
|
||||
|
||||
|
||||
def remove_vgroups(objs, vgroup_names):
|
||||
for obj in objs:
|
||||
for vgroup_name in vgroup_names:
|
||||
target_vgroup = obj.vertex_groups.get(vgroup_name)
|
||||
if target_vgroup:
|
||||
obj.vertex_groups.remove(target_vgroup)
|
||||
|
||||
|
||||
def precalc_and_transfer_multiple_groups(source_obj, target_obj, vgroup_names, expand=2):
|
||||
"""Convenience function to transfer multiple groups."""
|
||||
|
||||
remove_vgroups([target_obj], vgroup_names)
|
||||
|
||||
kd_tree = build_kdtree(source_obj.data)
|
||||
vert_influence_map = build_vert_influence_map(source_obj, target_obj, kd_tree, expand)
|
||||
transfer_multiple_vertex_groups(
|
||||
source_obj,
|
||||
target_obj,
|
||||
vert_influence_map,
|
||||
src_vgroups=[source_obj.vertex_groups[name] for name in vgroup_names],
|
||||
)
|
||||
|
||||
|
||||
def precalc_and_transfer_single_group(source_obj, target_obj, vgroup_name, expand=2):
|
||||
"""Convenience function to transfer a single group. For transferring multiple groups,
|
||||
precalc_and_transfer_multiple_groups should be used as it is more efficient."""
|
||||
|
||||
remove_vgroups([target_obj], [vgroup_name])
|
||||
|
||||
kd_tree = build_kdtree(source_obj.data)
|
||||
vert_influence_map = build_vert_influence_map(source_obj, target_obj, kd_tree, expand)
|
||||
|
||||
transfer_multiple_vertex_groups(
|
||||
source_obj,
|
||||
target_obj,
|
||||
vert_influence_map,
|
||||
[source_obj.vertex_groups[vgroup_name]],
|
||||
)
|
||||
|
||||
|
||||
def build_kdtree(mesh):
|
||||
kd = kdtree.KDTree(len(mesh.vertices))
|
||||
for i, v in enumerate(mesh.vertices):
|
||||
kd.insert(v.co, i)
|
||||
kd.balance()
|
||||
return kd
|
||||
|
||||
|
||||
def build_vert_influence_map(obj_from, obj_to, kd_tree, expand=2):
|
||||
verts_of_edge = {i: (e.vertices[0], e.vertices[1]) for i, e in enumerate(obj_from.data.edges)}
|
||||
|
||||
edges_of_vert: Dict[int, List[int]] = {}
|
||||
for edge_idx, edge in enumerate(obj_from.data.edges):
|
||||
for vert_idx in edge.vertices:
|
||||
if vert_idx not in edges_of_vert:
|
||||
edges_of_vert[vert_idx] = []
|
||||
edges_of_vert[vert_idx].append(edge_idx)
|
||||
|
||||
# A mapping from target vertex index to a list of source vertex indicies and
|
||||
# their influence.
|
||||
# This can be pre-calculated once per object pair, to minimize re-calculations
|
||||
# of subsequent transferring of individual vertex groups.
|
||||
vert_influence_map: List[int, List[Tuple[int, float]]] = {}
|
||||
for i, dest_vert in enumerate(obj_to.data.vertices):
|
||||
vert_influence_map[i] = get_source_vert_influences(
|
||||
dest_vert, obj_from, kd_tree, expand, edges_of_vert, verts_of_edge
|
||||
)
|
||||
|
||||
return vert_influence_map
|
||||
|
||||
|
||||
def get_source_vert_influences(
|
||||
target_vert, obj_from, kd_tree, expand=2, edges_of_vert={}, verts_of_edge={}
|
||||
) -> List[Tuple[int, float]]:
|
||||
_coord, idx, dist = get_nearest_vert(target_vert.co, kd_tree)
|
||||
source_vert_indices = [idx]
|
||||
|
||||
if dist == 0:
|
||||
# If the vertex position is a perfect match, just use that one vertex with max influence.
|
||||
return [(idx, 1)]
|
||||
|
||||
for i in range(0, expand):
|
||||
new_indices = []
|
||||
for vert_idx in source_vert_indices:
|
||||
for edge in edges_of_vert[vert_idx]:
|
||||
vert_other = other_vert_of_edge(edge, vert_idx, verts_of_edge)
|
||||
if vert_other not in source_vert_indices:
|
||||
new_indices.append(vert_other)
|
||||
source_vert_indices.extend(new_indices)
|
||||
|
||||
distances: List[Tuple[int, float]] = []
|
||||
distance_total = 0
|
||||
for src_vert_idx in source_vert_indices:
|
||||
distance = (target_vert.co - obj_from.data.vertices[src_vert_idx].co).length
|
||||
distance_total += distance
|
||||
distances.append((src_vert_idx, distance))
|
||||
|
||||
# Calculate influences such that the total of all influences adds up to 1.0,
|
||||
# and the influence is inversely correlated with the distance.
|
||||
parts = [1 / (dist / distance_total) for idx, dist in distances]
|
||||
parts_sum = sum(parts)
|
||||
|
||||
influences = [
|
||||
(idx, 1 if dist == 0 else part / parts_sum) for part, dist in zip(parts, distances)
|
||||
]
|
||||
|
||||
return influences
|
||||
|
||||
|
||||
def get_nearest_vert(coords: Vector, kd_tree: kdtree.KDTree) -> Tuple[Vector, int, float]:
|
||||
"""Return coordinate, index, and distance of nearest vert to coords in kd_tree."""
|
||||
return kd_tree.find(coords)
|
||||
|
||||
|
||||
def other_vert_of_edge(edge: int, vert: int, verts_of_edge: Dict[int, Tuple[int, int]]) -> int:
|
||||
verts = verts_of_edge[edge]
|
||||
assert vert in verts, f"Vert {vert} not part of edge {edge}."
|
||||
return verts[0] if vert == verts[1] else verts[1]
|
||||
|
||||
|
||||
def transfer_multiple_vertex_groups(obj_from, obj_to, vert_influence_map, src_vgroups):
|
||||
"""Transfer src_vgroups in obj_from to obj_to using a pre-calculated vert_influence_map."""
|
||||
|
||||
for src_vg in src_vgroups:
|
||||
target_vg = obj_to.vertex_groups.get(src_vg.name)
|
||||
if target_vg == None:
|
||||
target_vg = obj_to.vertex_groups.new(name=src_vg.name)
|
||||
|
||||
for i, dest_vert in enumerate(obj_to.data.vertices):
|
||||
source_verts = vert_influence_map[i]
|
||||
|
||||
# Vertex Group Name : Weight
|
||||
vgroup_weights = {}
|
||||
|
||||
for src_vert_idx, influence in source_verts:
|
||||
for group in obj_from.data.vertices[src_vert_idx].groups:
|
||||
group_idx = group.group
|
||||
vg = obj_from.vertex_groups[group_idx]
|
||||
if vg not in src_vgroups:
|
||||
continue
|
||||
if vg.name not in vgroup_weights:
|
||||
vgroup_weights[vg.name] = 0
|
||||
vgroup_weights[vg.name] += vg.weight(src_vert_idx) * influence
|
||||
|
||||
# Assign final weights of this vertex in the vertex groups.
|
||||
for vg_name in vgroup_weights.keys():
|
||||
target_vg = obj_to.vertex_groups.get(vg_name)
|
||||
target_vg.add([dest_vert.index], vgroup_weights[vg_name], 'REPLACE')
|
||||
@@ -0,0 +1,91 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from ... import constants
|
||||
from ..task_layer import draw_task_layer_selection
|
||||
|
||||
|
||||
def draw_transfer_data_type(
|
||||
context: bpy.types.Context,
|
||||
layout: bpy.types.UILayout,
|
||||
transfer_data: bpy.types.CollectionProperty,
|
||||
) -> None:
|
||||
"""Draw UI Element for items of a Transferable Data type"""
|
||||
asset_pipe = bpy.context.scene.asset_pipeline
|
||||
if transfer_data == []:
|
||||
return
|
||||
name, icon = constants.TRANSFER_DATA_TYPES[transfer_data[0].type]
|
||||
|
||||
box = layout.box()
|
||||
header, panel = box.panel(transfer_data[0].obj_name + name, default_closed=True)
|
||||
header.label(text=name, icon=icon)
|
||||
if not panel:
|
||||
return
|
||||
|
||||
box = panel.box()
|
||||
for transfer_data_item in transfer_data:
|
||||
main_row = box.row()
|
||||
main_row.label(text=f"{transfer_data_item.name}: ")
|
||||
|
||||
if transfer_data_item.surrender:
|
||||
# Disable entire row if the item is surrendered
|
||||
main_row.operator(
|
||||
"assetpipe.update_surrendered_transfer_data"
|
||||
).transfer_data_item_name = transfer_data_item.name
|
||||
|
||||
draw_task_layer_selection(
|
||||
context,
|
||||
layout=main_row.row(),
|
||||
data=transfer_data_item,
|
||||
)
|
||||
surrender_icon = "ORPHAN_DATA" if transfer_data_item.surrender else "HEART"
|
||||
surrender_row = main_row.row()
|
||||
surrender_row.enabled = transfer_data_item.owner in asset_pipe.local_task_layers
|
||||
surrender_row.prop(
|
||||
transfer_data_item, "surrender", text="", icon=surrender_icon
|
||||
)
|
||||
|
||||
|
||||
def draw_transfer_data(
|
||||
context: bpy.types.Context,
|
||||
transfer_data: bpy.types.CollectionProperty,
|
||||
layout: bpy.types.UILayout,
|
||||
) -> None:
|
||||
"""Draw UI List of Transferable Data"""
|
||||
vertex_groups = []
|
||||
material_slots = []
|
||||
modifiers = []
|
||||
constraints = []
|
||||
custom_props = []
|
||||
shape_keys = []
|
||||
attributes = []
|
||||
parent = []
|
||||
|
||||
for transfer_data_item in transfer_data:
|
||||
if transfer_data_item.type == constants.VERTEX_GROUP_KEY:
|
||||
vertex_groups.append(transfer_data_item)
|
||||
if transfer_data_item.type == constants.MATERIAL_SLOT_KEY:
|
||||
material_slots.append(transfer_data_item)
|
||||
if transfer_data_item.type == constants.MODIFIER_KEY:
|
||||
modifiers.append(transfer_data_item)
|
||||
if transfer_data_item.type == constants.CONSTRAINT_KEY:
|
||||
constraints.append(transfer_data_item)
|
||||
if transfer_data_item.type == constants.CUSTOM_PROP_KEY:
|
||||
custom_props.append(transfer_data_item)
|
||||
if transfer_data_item.type == constants.SHAPE_KEY_KEY:
|
||||
shape_keys.append(transfer_data_item)
|
||||
if transfer_data_item.type == constants.ATTRIBUTE_KEY:
|
||||
attributes.append(transfer_data_item)
|
||||
if transfer_data_item.type == constants.PARENT_KEY:
|
||||
parent.append(transfer_data_item)
|
||||
|
||||
draw_transfer_data_type(context, layout, vertex_groups)
|
||||
draw_transfer_data_type(context, layout, modifiers)
|
||||
draw_transfer_data_type(context, layout, material_slots)
|
||||
draw_transfer_data_type(context, layout, constraints)
|
||||
draw_transfer_data_type(context, layout, custom_props)
|
||||
draw_transfer_data_type(context, layout, shape_keys)
|
||||
draw_transfer_data_type(context, layout, attributes)
|
||||
draw_transfer_data_type(context, layout, parent)
|
||||
@@ -0,0 +1,211 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from ..naming import merge_get_basename, task_layer_prefix_basename_get
|
||||
from ..task_layer import get_transfer_data_owner
|
||||
import contextlib
|
||||
from ...props import AssetTransferData
|
||||
|
||||
def find_ownership_data(
|
||||
transfer_data: bpy.types.CollectionProperty,
|
||||
key: str,
|
||||
td_type_key: str,
|
||||
) -> AssetTransferData | None:
|
||||
"""Return matching AssetTransferData if it exists."""
|
||||
existing_items = [
|
||||
transfer_data_item
|
||||
for transfer_data_item in transfer_data
|
||||
if transfer_data_item.type == td_type_key and key == transfer_data_item.name
|
||||
]
|
||||
if existing_items:
|
||||
return existing_items[0]
|
||||
|
||||
|
||||
def transfer_data_add_entry(
|
||||
transfer_data: bpy.types.CollectionProperty,
|
||||
name: str,
|
||||
td_type_key: str,
|
||||
task_layer_name: str,
|
||||
surrender: bool,
|
||||
):
|
||||
"""Add entry to Transferable Data ownership
|
||||
|
||||
Args:
|
||||
ownership (bpy.types.CollectionProperty): Transferable Data of an object
|
||||
name (str): Name of new Transferable Data item
|
||||
td_type_key (str): Type of Transferable Data
|
||||
task_layer_name (str): Name of current task layer
|
||||
surrender (bool): Whether this data's ownership should be surrendered to begin with
|
||||
"""
|
||||
transfer_data_item = transfer_data.add()
|
||||
transfer_data_item.name = name
|
||||
transfer_data_item.owner = task_layer_name
|
||||
transfer_data_item.type = td_type_key
|
||||
transfer_data_item.surrender = surrender
|
||||
return transfer_data_item
|
||||
|
||||
|
||||
def transfer_data_clean(
|
||||
obj: bpy.types.Object, data_list: bpy.types.CollectionProperty, td_type_key: str
|
||||
):
|
||||
"""Removes data if a transfer_data_item doesn't exist but the data does exist
|
||||
Args:
|
||||
obj (bpy.types.Object): Object containing Transferable Data
|
||||
data_list (bpy.types.CollectionProperty): Collection Property containing a type of possible Transferable Data e.g. obj.modifiers
|
||||
td_type_key (str): Key for the Transferable Data type
|
||||
"""
|
||||
cleaned_item_names = set()
|
||||
|
||||
for item in data_list:
|
||||
ownership_data = find_ownership_data(
|
||||
obj.transfer_data_ownership,
|
||||
merge_get_basename(item.name),
|
||||
td_type_key,
|
||||
)
|
||||
if not ownership_data:
|
||||
cleaned_item_names.add(item.name)
|
||||
data_list.remove(item)
|
||||
|
||||
return cleaned_item_names
|
||||
|
||||
|
||||
def transfer_data_item_is_missing(
|
||||
transfer_data_item, data_list: bpy.types.CollectionProperty, td_type_key: str
|
||||
) -> bool:
|
||||
"""Returns true if a transfer_data_item exists the data doesn't exist
|
||||
|
||||
Args:
|
||||
transfer_data_item (_type_): Item of Transferable Data
|
||||
data_list (bpy.types.CollectionProperty): Collection Property containing a type of possible Transferable Data e.g. obj.modifiers
|
||||
td_type_key (str): Key for the Transferable Data type
|
||||
Returns:
|
||||
bool: Returns True if transfer_data_item is missing
|
||||
"""
|
||||
if transfer_data_item.type == td_type_key and not data_list.get(
|
||||
transfer_data_item["name"]
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
"""Intilize Transferable Data to a temporary collection property, used
|
||||
to draw a display of new Transferable Data to the user before merge process.
|
||||
"""
|
||||
|
||||
|
||||
def transfer_data_item_init(
|
||||
scene: bpy.types.Scene,
|
||||
obj: bpy.types.Object,
|
||||
data_list: bpy.types.CollectionProperty,
|
||||
td_type_key: str,
|
||||
):
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
scene (bpy.types.Scene): Scene that contains a the file's asset
|
||||
obj (bpy.types.Object): Object containing possible Transferable Data
|
||||
data_list (bpy.types.CollectionProperty): Collection Property containing a type of possible Transferable Data e.g. obj.modifiers
|
||||
td_type_key (str): Key for the Transferable Data type
|
||||
"""
|
||||
asset_pipe = scene.asset_pipeline
|
||||
transfer_data = obj.transfer_data_ownership
|
||||
|
||||
for item in data_list:
|
||||
# Only add new ownership transfer_data_item if vertex group doesn't have an owner
|
||||
ownership_data = find_ownership_data(transfer_data, item.name, td_type_key)
|
||||
if not ownership_data:
|
||||
task_layer_owner, auto_surrender = get_transfer_data_owner(
|
||||
asset_pipe,
|
||||
td_type_key,
|
||||
)
|
||||
asset_pipe.add_temp_transfer_data(
|
||||
name=item.name,
|
||||
owner=task_layer_owner,
|
||||
type=td_type_key,
|
||||
obj_name=obj.name,
|
||||
surrender=auto_surrender,
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def isolate_collection(context, iso_col: bpy.types.Collection):
|
||||
col_exclude = {}
|
||||
view_layer_col = context.view_layer.layer_collection
|
||||
view_layer_col.collection.children.link(iso_col)
|
||||
for col in view_layer_col.children:
|
||||
col_exclude[col.name] = col.exclude
|
||||
|
||||
try:
|
||||
# Exclude all collections that are not iso collection
|
||||
for col in view_layer_col.children:
|
||||
col.exclude = col.name != iso_col.name
|
||||
yield
|
||||
|
||||
finally:
|
||||
for col in view_layer_col.children:
|
||||
col.exclude = col_exclude[col.name]
|
||||
view_layer_col.collection.children.unlink(iso_col)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def link_objs_to_collection(objs: set, col: bpy.types.Collection):
|
||||
try:
|
||||
for obj in objs:
|
||||
col.objects.link(obj)
|
||||
yield
|
||||
|
||||
finally:
|
||||
for obj in objs:
|
||||
col.objects.unlink(obj)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def activate_shapekey(objs: set, sk_name: str):
|
||||
old_values = {}
|
||||
try:
|
||||
for obj in objs:
|
||||
if not obj.data.shape_keys:
|
||||
continue
|
||||
sk = obj.data.shape_keys.key_blocks.get(sk_name)
|
||||
if not sk:
|
||||
continue
|
||||
old_values[obj] = sk.value
|
||||
sk.value = 1
|
||||
yield
|
||||
|
||||
finally:
|
||||
for obj, val in old_values.items():
|
||||
obj.data.shape_keys.key_blocks[sk_name].value = val
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disable_modifiers(objs: set, mod_types: set[str]):
|
||||
mods_to_enable = {obj: [] for obj in objs}
|
||||
try:
|
||||
for obj in objs:
|
||||
for mod in obj.modifiers:
|
||||
if mod.type in mod_types and mod.show_viewport:
|
||||
mods_to_enable[obj].append(mod.name)
|
||||
mod.show_viewport = False
|
||||
yield
|
||||
|
||||
finally:
|
||||
for obj, mod_names in mods_to_enable.items():
|
||||
for mod_name in mod_names:
|
||||
obj.modifiers[mod_name].show_viewport = True
|
||||
|
||||
@contextlib.contextmanager
|
||||
def simplify(scene):
|
||||
"""Disable subdivision surface modifiers globally using the scene's Simplify setting.
|
||||
Important for binding modifiers, but also probably doesn't hurt for general performance.
|
||||
"""
|
||||
orig_simplify = scene.render.use_simplify
|
||||
levels = scene.render.simplify_subdivision
|
||||
|
||||
scene.render.use_simplify = True
|
||||
scene.render.simplify_subdivision = 0
|
||||
|
||||
yield
|
||||
|
||||
scene.render.use_simplify = orig_simplify
|
||||
scene.render.simplify_subdivision = levels
|
||||
Reference in New Issue
Block a user