diff --git a/.gitignore b/.gitignore index 5aea8c5..a0efdb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .vscode/settings.json +core/preferences.json diff --git a/core/properties.py b/core/properties.py index fa1b0b5..38a42a9 100644 --- a/core/properties.py +++ b/core/properties.py @@ -455,12 +455,6 @@ class AvatarToolkitSceneProperties(PropertyGroup): default="" ) - merge_all_bones: BoolProperty( - name=t('MergeArmature.merge_all'), - description=t('MergeArmature.merge_all_desc'), - default=True - ) - apply_transforms: BoolProperty( name=t('MergeArmature.apply_transforms'), description=t('MergeArmature.apply_transforms_desc'), @@ -529,7 +523,10 @@ def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") try: + bpy.utils.register_class(ZeroWeightBoneItem) bpy.utils.register_class(AvatarToolkitSceneProperties) + + except ValueError: # Class already registered, we can continue pass @@ -544,7 +541,9 @@ def unregister() -> None: except: pass try: + bpy.utils.unregister_class(ZeroWeightBoneItem) bpy.utils.unregister_class(AvatarToolkitSceneProperties) + except RuntimeError: pass logger.debug("Properties unregistered successfully") diff --git a/functions/custom_tools/armature_merging.py b/functions/custom_tools/armature_merging.py index 4713629..adb8d88 100644 --- a/functions/custom_tools/armature_merging.py +++ b/functions/custom_tools/armature_merging.py @@ -2,7 +2,7 @@ import bpy import numpy as np from typing import List, Optional, Dict, Set, Tuple, Any from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey - +from ...core.dictionaries import bone_names from ...core.logging_setup import logger from ...core.translations import t from ...core.common import ( @@ -10,7 +10,8 @@ from ...core.common import ( fix_zero_length_bones, clear_unused_data_blocks, join_mesh_objects, - remove_unused_shapekeys + remove_unused_shapekeys, + simplify_bonename ) class AvatarToolkit_OT_MergeArmature(bpy.types.Operator): @@ -52,7 +53,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator): wm.progress_update(80) # Get settings from scene properties - merge_all_bones: bool = context.scene.avatar_toolkit.merge_all_bones join_meshes: bool = context.scene.avatar_toolkit.join_meshes # Merge armatures @@ -60,7 +60,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator): base_armature_name, merge_armature_name, mesh_only=False, - merge_all_bones=merge_all_bones, join_meshes=join_meshes, operator=self ) @@ -100,16 +99,12 @@ def validate_parents_and_transforms(merge_armature: Object, base_armature: Objec base_parent: Optional[Object] = base_armature.parent if merge_parent or base_parent: - if context.scene.merge_all_bones: - for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]: - if parent: - if not is_transform_clean(parent): - logger.error("Parent transforms are not clean") - return False - bpy.data.objects.remove(parent, do_unlink=True) - else: - logger.error("Parent relationships need fixing") - return False + for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]: + if parent: + if not is_transform_clean(parent): + logger.error("Parent transforms are not clean") + return False + bpy.data.objects.remove(parent, do_unlink=True) return True def is_transform_clean(obj: Object) -> bool: @@ -135,7 +130,6 @@ def merge_armatures( base_armature_name: str, merge_armature_name: str, mesh_only: bool, - merge_all_bones: bool = False, join_meshes: bool = False, operator: Optional[Operator] = None ) -> None: @@ -174,25 +168,50 @@ def merge_armatures( # Store original parent relationships original_parents: Dict[str, Optional[str]] = {} - for bone in merge_armature.data.bones: + merge_armature_data: bpy.types.Armature = merge_armature.data + for bone in merge_armature_data.bones: original_parents[bone.name] = bone.parent.name if bone.parent else None + #create reverse lookup + reverse_bone_lookup = {} + for preferred_name, name_list in bone_names.items(): + for name in name_list: + reverse_bone_lookup[name] = preferred_name + # Get base bone names base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones} + base_armature_standards: Dict[str,Optional[str]] = {} + for bone in base_bone_names: + if simplify_bonename(bone) in reverse_bone_lookup: + base_armature_standards[reverse_bone_lookup[simplify_bonename(bone)]] = bone + # Switch to edit mode on merge armature and rename bones bpy.context.view_layer.objects.active = merge_armature bpy.ops.object.mode_set(mode='EDIT') - # Handle bone renaming based on merge_all_bones setting - for bone in merge_armature.data.edit_bones: - if not merge_all_bones: - # Only rename bones that don't exist in base armature - if bone.name not in base_bone_names: - bone.name += '.merge' + # Handle bone renaming/removing to target armature. + bone_names_source: list[str] = [bone.name for bone in merge_armature_data.edit_bones] + for bone in bone_names_source: + bone_name = bone + if bone_name not in base_bone_names: #not auto mergable to original + + if simplify_bonename(bone_name) in reverse_bone_lookup: #if is a standard bone through standard translation. + if reverse_bone_lookup[simplify_bonename(bone_name)] in base_armature_standards: #if this bone equals for example, "hips", does a bone that should be "hips" exist on our target armature? + #if so, rename this bone to that one + merge_armature_data.edit_bones[bone_name].name = base_armature_standards[reverse_bone_lookup[simplify_bonename(bone_name)]] + bone_name = merge_armature_data.edit_bones[bone_name].name + #adjust original parents list to point to the new name. + for child_bone in merge_armature_data.edit_bones[bone_name]: + original_parents[child_bone.name] = bone_name + #then remove so it doesn't clash when merged. + merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name]) + continue + + #if it really doesn't have a counter part, just don't bother. else: - # Rename all bones from merge armature - bone.name += '.merge' + merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name]) + # Return to object mode bpy.ops.object.mode_set(mode='OBJECT') @@ -204,14 +223,15 @@ def merge_armatures( bpy.context.view_layer.objects.active = base_armature bpy.ops.object.join() + base_armature_data: bpy.types.Armature = base_armature.data + # Restore parent relationships bpy.ops.object.mode_set(mode='EDIT') - for bone in base_armature.data.edit_bones: - base_name: str = bone.name.replace('.merge', '') - if base_name in original_parents: - parent_name: Optional[str] = original_parents[base_name] + for bone in base_armature_data.edit_bones: + if bone.name in original_parents: + parent_name: Optional[str] = original_parents[bone.name] if parent_name: - parent_bone: Optional[EditBone] = base_armature.data.edit_bones.get(parent_name) + parent_bone: Optional[EditBone] = base_armature_data.edit_bones.get(parent_name) if parent_bone: bone.parent = parent_bone @@ -250,11 +270,6 @@ def merge_armatures( # Remove any remaining .merge bones bpy.context.view_layer.objects.active = base_armature - bpy.ops.object.mode_set(mode='EDIT') - edit_bones: List[EditBone] = base_armature.data.edit_bones - bones_to_remove: List[EditBone] = [bone for bone in edit_bones if bone.name.endswith('.merge')] - for bone in bones_to_remove: - edit_bones.remove(bone) bpy.ops.object.mode_set(mode='OBJECT') # Final cleanup @@ -298,8 +313,7 @@ def adjust_merge_armature_transforms( def detect_bones_to_merge( base_edit_bones: bpy.types.ArmatureEditBones, merge_edit_bones: bpy.types.ArmatureEditBones, - tolerance: float, - merge_all_bones: bool + tolerance: float ) -> List[str]: """Detect corresponding bones between base and merge armatures using smart detection and position tolerance""" bones_to_merge: List[str] = [] @@ -314,7 +328,7 @@ def detect_bones_to_merge( merge_bone_position: np.ndarray = np.array(merge_bone.head) found_match: bool = False - if merge_all_bones and merge_bone.name in base_bones_positions: + if merge_bone.name in base_bones_positions: # If merging same bones by name bones_to_merge.append(merge_bone.name) found_match = True diff --git a/ui/custom_avatar_panel.py b/ui/custom_avatar_panel.py index be7f6a9..4e3ff62 100644 --- a/ui/custom_avatar_panel.py +++ b/ui/custom_avatar_panel.py @@ -155,7 +155,6 @@ class AvatarToolKit_PT_CustomPanel(Panel): # Group related options together transform_col: UILayout = col.column(align=True) - transform_col.prop(toolkit, "merge_all_bones") transform_col.prop(toolkit, "apply_transforms") col.separator(factor=0.5)