From 0cb4d6bb3a378c00f7d8713a6e2213af59624884 Mon Sep 17 00:00:00 2001 From: 989onan Date: Thu, 12 Sep 2024 21:56:39 -0400 Subject: [PATCH] Merge armatures button --- core/properties.py | 12 +++-- functions/armature_modifying.py | 74 ++++++++++++++++++++++++++++++- resources/translations/en_US.json | 11 +++++ ui/atlas_materials.py | 2 +- ui/merge_armatures.py | 30 +++++++++++++ ui/settings.py | 2 +- ui/viseme.py | 2 +- 7 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 ui/merge_armatures.py diff --git a/core/properties.py b/core/properties.py index 45a814f..4e0e69a 100644 --- a/core/properties.py +++ b/core/properties.py @@ -21,6 +21,12 @@ def register() -> None: name=t("VisemePanel.selected_mesh.label"), description=t("VisemePanel.selected_mesh.desc") ))) + + register_property((bpy.types.Scene, "merge_armature_source", bpy.props.EnumProperty( + items=get_armatures, + name=t("MergeArmatures.selected_armature.label"), + description=t("MergeArmatures.selected_armature.label") + ))) register_property((bpy.types.Scene, "avatar_toolkit_language_changed", bpy.props.BoolProperty(default=False))) @@ -49,8 +55,8 @@ def register() -> None: register_property((bpy.types.Scene, "selected_armature", bpy.props.EnumProperty( items=get_armatures, - name="Selected Armature", - description="The currently selected armature for Avatar Toolkit operations" + name=t("Quick_Access.selected_armature.label"), + description=t("Quick_Access.selected_armature.desc") ))) #happy with how compressed this get_texture_node_list method is - @989onan @@ -88,7 +94,7 @@ def register() -> None: items=get_texture_node_list))) register_property((Material, "texture_atlas_height", EnumProperty( name=t("TextureAtlas.height"), - description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height_map").lower()), + description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()), default=0, items=get_texture_node_list))) register_property((Material, "texture_atlas_roughness", EnumProperty( diff --git a/functions/armature_modifying.py b/functions/armature_modifying.py index c1ad4bb..b77e67f 100644 --- a/functions/armature_modifying.py +++ b/functions/armature_modifying.py @@ -338,6 +338,78 @@ class AvatarToolkit_OT_MergeBonesToParents(Operator): bone_child.parent = armature_data.edit_bones[bone].parent armature_data.edit_bones.remove(armature_data.edit_bones[bone]) - bpy.ops.object.mode_set(mode=prev_mode) return {'FINISHED'} + + +@register_wrap +class AvatarToolkit_OT_MergeArmatures(Operator): + bl_idname = "avatar_toolkit.merge_armatures" + bl_label = t("MergeArmature.merge_armatures.label") + bl_description = t("MergeArmature.merge_armatures.desc").format(selected_armature_label=t("MergeArmatures.selected_armature.label")) + bl_options = {'REGISTER', 'UNDO'} + + """align_bones: bpy.props.BoolProperty(default=False,name=t("MergeArmature.merge_armatures.align_bones.label"),description=t("MergeArmature.merge_armatures.align_bones.desc"))""" + + @classmethod + def poll(cls, context: Context) -> bool: + return (common.get_selected_armature(context) is not None) and (context.scene.merge_armature_source is not None) + + def make_active(self, obj: bpy.types.Object, context: Context): + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + context.view_layer.objects.active = obj + obj.select_set(True) + + def execute(cls, context: Context) -> set[str]: + source_armature: bpy.types.Object = bpy.data.objects[context.scene.merge_armature_source] + source_armature_data: Armature = source_armature.data + target_armature: bpy.types.Object = common.get_selected_armature(context) + target_armature_data: Armature = target_armature.data + parent_dictionary: dict[str, list[str]] = {} + + cls.make_active(obj=source_armature, context=context) + + + #TODO: This is woefully screwed. This needs to be fixed - @989onan + """if cls.align_bones: + bpy.ops.object.mode_set(mode='POSE') + for bone in source_armature.pose.bones: + if bone.name in target_armature_data.bones: + + #sorry for this one liner - @989onan + bone.matrix = source_armature.convert_space(matrix=target_armature.convert_space(matrix=target_armature_data.bones[bone.name].matrix_local, pose_bone=None,from_space='LOCAL',to_space='WORLD'), pose_bone=None, from_space='WORLD', to_space='LOCAL') + + if not common.apply_pose_as_rest(armature=source_armature,meshes=[i for i in source_armature.children if i.type == 'MESH'], context=context): + cls.report({'ERROR'}, t("Quick_Access.apply_armature_failed")) + return {'FINISHED'}""" + + + + + cls.make_active(obj=source_armature, context=context) + bpy.ops.object.mode_set(mode='EDIT') + source_armature_data: Armature = source_armature.data + for bone_name in [i.name for i in source_armature_data.edit_bones]: + if bone_name in target_armature_data.bones: + parent_dictionary[bone_name] = [i.name for i in source_armature_data.edit_bones[bone_name].children] + source_armature_data.edit_bones.remove(source_armature_data.edit_bones[bone_name]) + bpy.ops.object.mode_set(mode='OBJECT') + + cls.make_active(obj=target_armature, context=context) + source_armature.select_set(True) + + bpy.ops.object.join() + target_armature: bpy.types.Object = common.get_selected_armature(context) + cls.make_active(obj=target_armature, context=context) + bpy.ops.object.mode_set(mode='EDIT') + for bone_name, bone_name_list in parent_dictionary.items(): + if bone_name in target_armature_data.edit_bones: + for bone_child in bone_name_list: + target_armature_data.edit_bones[bone_child].parent = target_armature_data.edit_bones[bone_name] + bpy.ops.object.mode_set(mode='OBJECT') + + + + return {'FINISHED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 7358d95..a567c7d 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -62,6 +62,8 @@ "Optimization.selecting_meshes": "Selecting meshes...", "Optimization.transform_apply_failed": "Transform apply failed", "Optimization.vertex_excluded": "Shapekey has a moved vertex at index \"{index}\", excluding from double merging!", + "Quick_Access.selected_armature.label": "Selected Armature", + "Quick_Access.selected_armature.desc": "The currently \"targeted\" armature for Avatar Toolkit operations", "Quick_Access.export": "Export", "Quick_Access.export_fbx.desc": "Export the model as FBX", "Quick_Access.export_fbx.label": "Export FBX", @@ -162,6 +164,15 @@ "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", + "MergeArmatures.select_armature": "Please select an armature", + "MergeArmatures.title.label": "Merge Armatures:", + "MergeArmatures.label": "Merge Armatures", + "MergeArmatures.selected_armature.label": "Armature to Merge From", + "MergeArmatures.selected_armature.desc": "The armature that should be merged into the targeted armature for Avatar Toolkit.", + "MergeArmature.merge_armatures.label": "Merge Armatures Together", + "MergeArmature.merge_armatures.desc": "Merge {selected_armature_label} to the targeted armature for Avatar Toolkit.", + "MergeArmature.merge_armatures.align_bones.label": "Align Bones", + "MergeArmature.merge_armatures.align_bones.desc": "Align bones from source armature to target armature,\nstretching bones to match before merging.", "VisemePanel.create_visemes": "Create Visemes", "VisemePanel.creating_viseme": "Creating viseme: {viseme_name}", "VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}", diff --git a/ui/atlas_materials.py b/ui/atlas_materials.py index 09a7d1f..e5e1577 100644 --- a/ui/atlas_materials.py +++ b/ui/atlas_materials.py @@ -66,7 +66,7 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 4 + bl_order = 5 def draw(self, context: Context): layout = self.layout diff --git a/ui/merge_armatures.py b/ui/merge_armatures.py new file mode 100644 index 0000000..e2ea43c --- /dev/null +++ b/ui/merge_armatures.py @@ -0,0 +1,30 @@ + +import bpy +from ..core.register import register_wrap +from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from bpy.types import Panel, Context +from ..core.common import get_selected_armature +from ..functions.translations import t +from ..functions.armature_modifying import AvatarToolkit_OT_MergeArmatures + +@register_wrap +class AvatarToolkit_PT_MergeArmaturesPanel(Panel): + bl_label = t("MergeArmatures.label") + bl_idname = "OBJECT_PT_avatar_toolkit_merge_armatures" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = CATEGORY_NAME + bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname + bl_order = 4 + + def draw(self, context: Context): + layout = self.layout + armature = get_selected_armature(context) + + if armature: + layout.label(text=t("MergeArmatures.title.label"), icon='ARMATURE_DATA') + layout.separator(factor=0.5) + layout.prop(context.scene,property="merge_armature_source",icon="ARMATURE_DATA") + layout.operator(operator=AvatarToolkit_OT_MergeArmatures.bl_idname,icon="ARMATURE_DATA") + else: + layout.label(text=t("MergeArmatures.select_armature"), icon='ERROR') \ No newline at end of file diff --git a/ui/settings.py b/ui/settings.py index b88d514..dfe2393 100644 --- a/ui/settings.py +++ b/ui/settings.py @@ -11,7 +11,7 @@ class AvatarToolkitSettingsPanel(bpy.types.Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 6 + bl_order = 7 def draw(self, context): layout = self.layout diff --git a/ui/viseme.py b/ui/viseme.py index 8c6fc3d..ac39b05 100644 --- a/ui/viseme.py +++ b/ui/viseme.py @@ -13,7 +13,7 @@ class AvatarToolkitVisemePanel(bpy.types.Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 5 + bl_order = 6 def draw(self, context: bpy.types.Context) -> None: layout = self.layout