diff --git a/functions/additional_tools.py b/functions/additional_tools.py index fe1a7c1..9745945 100644 --- a/functions/additional_tools.py +++ b/functions/additional_tools.py @@ -1,4 +1,5 @@ import bpy +import math from bpy.types import Context, Operator from ..core.register import register_wrap from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes @@ -35,3 +36,56 @@ class AvatarToolKit_OT_ApplyTransforms(Operator): self.report({'INFO'}, t("Tools.apply_transforms.success")) return {'FINISHED'} + +@register_wrap +class AvatarToolKit_OT_ConnectBones(Operator): + bl_idname = "avatar_toolkit.connect_bones" + bl_label = t("Tools.connect_bones.label") + bl_description = t("Tools.connect_bones.desc") + bl_options = {'REGISTER', 'UNDO'} + + min_distance: bpy.props.FloatProperty( + name=t("Tools.connect_bones.min_distance.label"), + description=t("Tools.connect_bones.min_distance.desc"), + default=0.005, + min=0.001, + max=0.1 + ) + + @classmethod + def poll(cls, context: Context) -> bool: + return get_selected_armature(context) is not None + + def execute(self, context: Context) -> set[str]: + armature = get_selected_armature(context) + if not is_valid_armature(armature): + self.report({'ERROR'}, t("Tools.connect_bones.invalid_armature")) + return {'CANCELLED'} + + bpy.ops.object.mode_set(mode='EDIT') + + edit_bones = armature.data.edit_bones + bones_connected = 0 + + for bone in edit_bones: + if len(bone.children) == 1 and bone.name not in ['LeftEye', 'RightEye', 'Head', 'Hips']: + child = bone.children[0] + distance = math.dist(bone.head, child.head) + + if distance > self.min_distance: + 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') + + self.report({'INFO'}, t("Tools.connect_bones.success").format(bones_connected=bones_connected)) + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.prop(self, "min_distance") diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 4101169..e4c242e 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -165,6 +165,12 @@ "Tools.merge_bones_to_parents.label": "Merge Bones to Individual Parents", "Tools.remove_zero_weight_bones.threshold.label": "Weight Threshold", "Tools.remove_zero_weight_bones.threshold.desc": "If a bone is not weighted to any part of any mesh under the armature with a threshold greater than this, it is removed", + "Tools.connect_bones.label": "Connect Bones", + "Tools.connect_bones.desc": "Connect bones with their respective children", + "Tools.connect_bones.invalid_armature": "Invalid armature selected", + "Tools.connect_bones.min_distance.label": "Minimum Distance", + "Tools.connect_bones.min_distance.desc": "Minimum distance between bones to connect them", + "Tools.connect_bones.success": "Connected {bones_connected} bones successfully", "MergeArmatures.select_armature": "Please select an armature", "MergeArmatures.title.label": "Merge Armatures:", "MergeArmatures.label": "Merge Armatures", diff --git a/ui/tools.py b/ui/tools.py index 05f3fa9..21cd476 100644 --- a/ui/tools.py +++ b/ui/tools.py @@ -8,7 +8,7 @@ from ..functions.translations import t from ..core.common import get_selected_armature from ..functions.mesh_tools import AvatarToolkit_OT_RemoveUnusedShapekeys from ..functions.seperate_by import AvatarToolKit_OT_SeparateByMaterials, AvatarToolKit_OT_SeparateByLooseParts -from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms +from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms, AvatarToolKit_OT_ConnectBones from ..functions.armature_modifying import AvatarToolkit_OT_RemoveZeroWeightBones, AvatarToolkit_OT_MergeBonesToActive, AvatarToolkit_OT_MergeBonesToParents @register_wrap @@ -47,5 +47,7 @@ class AvatarToolkit_PT_ToolsPanel(bpy.types.Panel): row = layout.row(align=True) row.operator(AvatarToolkit_OT_MergeBonesToActive.bl_idname, text=t("Tools.merge_bones_to_active.label"), icon='BONE_DATA') row.operator(AvatarToolkit_OT_MergeBonesToParents.bl_idname, text=t("Tools.merge_bones_to_parents.label"), icon='BONE_DATA') + row = layout.row(align=True) + row.operator(AvatarToolKit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones.label"), icon='BONE_DATA') else: layout.label(text=t("Tools.select_armature"), icon='ERROR')