Files
2025-07-15 18:11:58 -04:00

181 lines
7.6 KiB
Python

import traceback
import bpy
import math
from typing import Set, List
from bpy.types import Operator, Context, Armature, EditBone
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, store_breaking_settings_armature, restore_breaking_settings_armature
from ...core.armature_validation import validate_armature
import traceback
class AvatarToolkit_OT_ConnectBones(Operator):
"""Connect disconnected bones in chain"""
bl_idname = "avatar_toolkit.connect_bones"
bl_label = t("Tools.connect_bones")
bl_description = t("Tools.connect_bones_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
try:
logger.info("Starting bone connection operation")
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
bones_connected = 0
min_distance = context.scene.avatar_toolkit.connect_bones_min_distance
excluded_bones = {'LeftEye', 'RightEye', 'Head', 'Hips'}
for bone in edit_bones:
if len(bone.children) == 1 and bone.name not in excluded_bones:
child = bone.children[0]
distance = math.dist(bone.tail, child.head)
if distance > min_distance:
logger.debug(f"Connecting bone {bone.name} to {child.name}")
bone.tail = child.head
if bone.parent and len(bone.parent.children) == 1:
bone.use_connect = True
bones_connected += 1
bpy.ops.object.mode_set(mode='OBJECT')
restore_breaking_settings_armature(armature, data_breaking)
self.report({'INFO'}, t("Tools.connect_bones_success", count=bones_connected))
return {'FINISHED'}
except Exception:
logger.error(f"Failed to connect bones: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
restore_breaking_settings_armature(armature, data_breaking)
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToActive(Operator):
"""Merge selected bones into active bone and transfer weights"""
bl_idname = "avatar_toolkit.merge_to_active"
bl_label = t("Tools.merge_to_active")
bl_description = t("Tools.merge_to_active_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return (context.mode == 'EDIT_ARMATURE' or context.mode == 'POSE') and context.active_bone
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
try:
bpy.ops.object.mode_set(mode='EDIT')
active_bone = context.active_bone
selected_bones = [b for b in context.selected_editable_bones if b != active_bone]
if not selected_bones:
self.report({'WARNING'}, t("Tools.no_bones_selected"))
return {'CANCELLED'}
logger.info(f"Merging {len(selected_bones)} bones into {active_bone.name}")
# Store weights before merging
meshes = get_all_meshes(context)
weight_data = {}
for bone in selected_bones:
for mesh in meshes:
if bone.name in mesh.vertex_groups:
weights = get_vertex_weights(mesh, bone.name)
weight_data.setdefault(mesh.name, {})[bone.name] = weights
# Transfer weights to active bone
threshold = context.scene.avatar_toolkit.merge_weights_threshold
for mesh_name, bone_weights in weight_data.items():
mesh = bpy.data.objects[mesh_name]
for bone_name, weights in bone_weights.items():
transfer_vertex_weights(mesh, bone_name, active_bone.name, threshold)
# Delete merged bones
for bone in selected_bones:
armature.data.edit_bones.remove(bone)
self.report({'INFO'}, t("Tools.merge_to_active_success", count=len(selected_bones)))
restore_breaking_settings_armature(armature, data_breaking)
return {'FINISHED'}
except Exception:
logger.error(f"Failed to merge bones: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
restore_breaking_settings_armature(armature, data_breaking)
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToParent(Operator):
"""Merge selected bones into their respective parents and transfer weights"""
bl_idname = "avatar_toolkit.merge_to_parent"
bl_label = t("Tools.merge_to_parent")
bl_description = t("Tools.merge_to_parent_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return (context.mode == 'EDIT_ARMATURE' or context.mode == 'POSE')
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
data_breaking = store_breaking_settings_armature(armature)
try:
bpy.ops.object.mode_set(mode='EDIT')
selected_bones = [b for b in context.selected_editable_bones if b.parent]
if not selected_bones:
self.report({'WARNING'}, t("Tools.no_bones_with_parent"))
return {'CANCELLED'}
logger.info(f"Merging {len(selected_bones)} bones to their parents")
# Store weights before merging
meshes = get_all_meshes(context)
merged_count = 0
threshold = context.scene.avatar_toolkit.merge_weights_threshold
for bone in selected_bones:
parent = bone.parent
if not parent:
continue
# Transfer weights to parent
for mesh in meshes:
if bone.name in mesh.vertex_groups:
transfer_vertex_weights(mesh, bone.name, parent.name, threshold)
# Delete merged bone
armature.data.edit_bones.remove(bone)
merged_count += 1
restore_breaking_settings_armature(armature, data_breaking)
self.report({'INFO'}, t("Tools.merge_to_parent_success", count=merged_count))
return {'FINISHED'}
except Exception:
logger.error(f"Failed to merge bones: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
restore_breaking_settings_armature(armature, data_breaking)
return {'CANCELLED'}