From 239e212cf4ea7a9a59007e4ccb5bff4a3ad023dc Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sun, 26 Jan 2025 15:22:20 +0000 Subject: [PATCH] Remove Zero Weights Improvements - Added Options to preserve Parent Bones. - Added List mode only where the user can select the bones there want to remove. - Added Options to only target Deform bones only and non deform bones only. This is complete, the UI needs a little cleanup but I do this in a UI cleanup nearer Alpha 2. --- core/properties.py | 41 ++++++++++++++++ functions/tools/bone_tools.py | 78 +++++++++++++++++++++++++------ resources/translations/en_US.json | 13 ++++++ resources/translations/ja_JP.json | 13 ++++++ resources/translations/ko_KR.json | 13 ++++++ ui/tools_panel.py | 32 ++++++++++++- 6 files changed, 175 insertions(+), 15 deletions(-) diff --git a/core/properties.py b/core/properties.py index 2ab83e6..21e0142 100644 --- a/core/properties.py +++ b/core/properties.py @@ -18,6 +18,13 @@ from .common import get_armature_list, get_active_armature, get_all_meshes from ..functions.visemes import VisemePreview from ..functions.eye_tracking import set_rotation +class ZeroWeightBoneItem(PropertyGroup): + """Property group for zero weight bone list items""" + name: StringProperty(name="Bone Name") + selected: BoolProperty(name="Selected", default=True) + has_children: BoolProperty(name="Has Children", default=False) + is_deform: BoolProperty(name="Is Deform Bone", default=False) + def update_validation_mode(self: PropertyGroup, context: Context) -> None: """Updates validation mode and saves preference""" logger.info(f"Updating validation mode to: {self.validation_mode}") @@ -361,6 +368,40 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=True ) + preserve_parent_bones: BoolProperty( + name=t("Tools.preserve_parent_bones"), + description=t("Tools.preserve_parent_bones_desc"), + default=True + ) + + target_bone_type: EnumProperty( + name=t("Tools.target_bone_type"), + description=t("Tools.target_bone_type_desc"), + items=[ + ('ALL', t("Tools.target_all_bones"), ""), + ('DEFORM', t("Tools.target_deform_bones"), ""), + ('NON_DEFORM', t("Tools.target_non_deform_bones"), "") + ], + default='ALL' + ) + + zero_weight_bones: CollectionProperty( + type=ZeroWeightBoneItem, + name="Zero Weight Bones", + description="List of bones with zero weights" + ) + + zero_weight_bones_index: IntProperty( + name="Zero Weight Bone Index", + default=0 + ) + + list_only_mode: BoolProperty( + name=t("Tools.list_only_mode"), + description=t("Tools.list_only_mode_desc"), + default=False + ) + cleanup_shape_keys: BoolProperty( name=t('MergeArmature.cleanup_shape_keys'), description=t('MergeArmature.cleanup_shape_keys_desc'), diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index e90ceaf..1d09883 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -134,17 +134,11 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator): def execute(self, context: Context) -> set[str]: """Execute the constraint removal operation""" - - # Make sure we are in Object mode first or it will error bpy.ops.object.mode_set(mode='OBJECT') - armature = get_active_armature(context) - - # Select armature and make it active before changing mode bpy.ops.object.select_all(action='DESELECT') armature.select_set(True) context.view_layer.objects.active = armature - bpy.ops.object.mode_set(mode='POSE') constraints_removed = 0 @@ -157,7 +151,6 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator): self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed)) return {'FINISHED'} - class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): """Operator to remove bones with no vertex weights""" bl_idname = "avatar_toolkit.clean_weights" @@ -167,10 +160,37 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): def should_preserve_bone(self, bone_name: str, context: Context) -> bool: """Check if bone should be preserved based on settings""" - if context.scene.avatar_toolkit.merge_twist_bones: - return "twist" in bone_name.lower() + toolkit = context.scene.avatar_toolkit + bone = context.active_object.data.bones.get(bone_name) + + if not bone: + return False + + if toolkit.preserve_parent_bones and bone.children: + return True + + if toolkit.target_bone_type == 'DEFORM' and not bone.use_deform: + return True + + if toolkit.target_bone_type == 'NON_DEFORM' and bone.use_deform: + return True + return False + def populate_bone_list(self, context: Context, zero_weight_bones: List[str]) -> None: + """Populate the zero weight bones list""" + toolkit = context.scene.avatar_toolkit + toolkit.zero_weight_bones.clear() + + armature = get_active_armature(context) + for bone_name in zero_weight_bones: + bone = armature.data.bones.get(bone_name) + if bone: + item = toolkit.zero_weight_bones.add() + item.name = bone_name + item.has_children = len(bone.children) > 0 + item.is_deform = bone.use_deform + def execute(self, context: Context) -> set[str]: """Execute the zero weight bone removal operation""" armature = get_active_armature(context) @@ -192,6 +212,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): # Get weighted bones weighted_bones: List[str] = [] meshes = get_all_meshes(context) + zero_weight_bones: List[str] = [] for mesh in meshes: mesh_data: Mesh = mesh.data @@ -209,6 +230,10 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): if (bone.name not in weighted_bones and not self.should_preserve_bone(bone.name, context)): + if context.scene.avatar_toolkit.list_only_mode: + zero_weight_bones.append(bone.name) + continue + # Store children data children = bone.children children_data = {child.name: initial_transforms[child.name] for child in children} @@ -227,11 +252,38 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): for child_name, data in children_data.items(): if child_name in armature_data.edit_bones: child = armature_data.edit_bones[child_name] - child.head = data['head'] - child.tail = data['tail'] - child.roll = data['roll'] - child.matrix = data['matrix'] + restore_bone_transforms(child, data) bpy.ops.object.mode_set(mode='OBJECT') + + if context.scene.avatar_toolkit.list_only_mode: + self.populate_bone_list(context, zero_weight_bones) + return {'FINISHED'} + self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count)) + return {'FINISHED'} + +class AvatarToolKit_OT_RemoveSelectedBones(Operator): + """Operator to remove selected bones from the zero weight bones list""" + bl_idname = "avatar_toolkit.remove_selected_bones" + bl_label = t("Tools.remove_selected_bones") + bl_description = t("Tools.remove_selected_bones_desc") + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context: Context) -> set[str]: + armature = get_active_armature(context) + toolkit = context.scene.avatar_toolkit + + selected_bones = [item.name for item in toolkit.zero_weight_bones + if item.selected] + + bpy.ops.object.mode_set(mode='EDIT') + for bone_name in selected_bones: + if bone_name in armature.data.edit_bones: + armature.data.edit_bones.remove(armature.data.edit_bones[bone_name]) + + bpy.ops.object.mode_set(mode='OBJECT') + toolkit.zero_weight_bones.clear() + + self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones))) return {'FINISHED'} \ No newline at end of file diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index dc60653..96491be 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -149,6 +149,19 @@ "Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight", "Tools.clean_weights": "Remove Zero Weight Bones", "Tools.clean_weights_desc": "Remove bones with no vertex weights", + "Tools.preserve_parent_bones": "Preserve Parent Bones", + "Tools.preserve_parent_bones_desc": "Keep bones that have children even if they have no weights", + "Tools.target_bone_type": "Target Bone Type", + "Tools.target_bone_type_desc": "Filter which types of bones to process", + "Tools.target_all_bones": "All Bones", + "Tools.target_deform_bones": "Deform Bones Only", + "Tools.target_non_deform_bones": "Non-Deform Bones Only", + "Tools.list_only_mode": "List Mode Only", + "Tools.list_only_mode_desc": "List zero weight bones instead of removing them", + "Tools.zero_weight_bones_found": "Zero weight bones found: {bones}", + "Tools.remove_selected_bones": "Remove Selected Bones", + "Tools.remove_selected_bones_desc": "Remove selected zero weight bones from armature", + "Tools.bones_removed": "Removed {count} bones", "Tools.clean_constraints": "Delete Bone Constraints", "Tools.clean_constraints_desc": "Remove all bone constraints from armature", "Tools.clean_constraints_success": "Removed {count} bone constraints", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 2a7f445..fddcb2a 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -149,6 +149,19 @@ "Tools.merge_twist_bones_desc": "チェックすると、重みが0でもツイストボーンを保持します", "Tools.clean_weights": "重みなしボーンを削除", "Tools.clean_weights_desc": "頂点の重みがないボーンを削除", + "Tools.preserve_parent_bones": "親ボーンを保持", + "Tools.preserve_parent_bones_desc": "ウェイトがなくても子ボーンを持つボーンを保持", + "Tools.target_bone_type": "対象ボーンタイプ", + "Tools.target_bone_type_desc": "処理するボーンタイプを選択", + "Tools.target_all_bones": "全てのボーン", + "Tools.target_deform_bones": "変形ボーンのみ", + "Tools.target_non_deform_bones": "非変形ボーンのみ", + "Tools.list_only_mode": "リストモードのみ", + "Tools.list_only_mode_desc": "ゼロウェイトボーンを削除せずにリスト表示", + "Tools.zero_weight_bones_found": "ゼロウェイトボーンが見つかりました: {bones}", + "Tools.remove_selected_bones": "選択したボーンを削除", + "Tools.remove_selected_bones_desc": "選択したゼロウェイトボーンをアーマチュアから削除", + "Tools.bones_removed": "{count}個のボーンを削除しました", "Tools.clean_constraints": "ボーンのコンストレイントを削除", "Tools.clean_constraints_desc": "アーマチュアからすべてのボーンコンストレイントを削除", "Tools.clean_constraints_success": "{count}個のボーンコンストレイントを削除しました", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index 28ce1ed..3c455b0 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -149,6 +149,19 @@ "Tools.merge_twist_bones_desc": "체크하면 가중치가 0이어도 트위스트 본 유지", "Tools.clean_weights": "0 가중치 본 제거", "Tools.clean_weights_desc": "버텍스 가중치가 없는 본 제거", + "Tools.preserve_parent_bones": "부모 본 보존", + "Tools.preserve_parent_bones_desc": "가중치가 없어도 자식 본이 있는 본 유지", + "Tools.target_bone_type": "대상 본 유형", + "Tools.target_bone_type_desc": "처리할 본 유형 필터링", + "Tools.target_all_bones": "모든 본", + "Tools.target_deform_bones": "변형 본만", + "Tools.target_non_deform_bones": "비변형 본만", + "Tools.list_only_mode": "목록 모드만", + "Tools.list_only_mode_desc": "제로 가중치 본을 제거하지 않고 목록으로 표시", + "Tools.zero_weight_bones_found": "제로 가중치 본 발견: {bones}", + "Tools.remove_selected_bones": "선택한 본 제거", + "Tools.remove_selected_bones_desc": "선택한 제로 가중치 본을 아마추어에서 제거", + "Tools.bones_removed": "{count}개의 본이 제거되었습니다", "Tools.clean_constraints": "본 제약 조건 삭제", "Tools.clean_constraints_desc": "아마추어에서 모든 본 제약 조건 제거", "Tools.clean_constraints_success": "{count}개의 본 제약 조건 제거됨", diff --git a/ui/tools_panel.py b/ui/tools_panel.py index ce0614a..c010f17 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -1,9 +1,21 @@ import bpy from typing import Set -from bpy.types import Panel, Context, UILayout, Operator +from bpy.types import Panel, Context, UILayout, Operator, UIList from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.translations import t +class AVATAR_TOOLKIT_UL_ZeroWeightBones(UIList): + """UI List for displaying zero weight bones with selection options""" + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row(align=True) + row.prop(item, "selected", text="") + row.label(text=item.name) + if item.has_children: + row.label(text="", icon='OUTLINER_OB_ARMATURE') + if item.is_deform: + row.label(text="", icon='MOD_ARMATURE') + class AvatarToolKit_PT_ToolsPanel(Panel): """Panel containing various tools for avatar customization and optimization""" bl_label: str = t("Tools.label") @@ -18,6 +30,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel): def draw(self, context: Context) -> None: """Draw the tools panel interface""" layout: UILayout = self.layout + toolkit = context.scene.avatar_toolkit # General Tools tools_box: UILayout = layout.box() @@ -45,7 +58,22 @@ class AvatarToolKit_PT_ToolsPanel(Panel): # Weight Tools weight_box: UILayout = bone_box.box() col = weight_box.column(align=True) - col.prop(context.scene.avatar_toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones")) + col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones")) + col.prop(toolkit, "preserve_parent_bones") + col.prop(toolkit, "target_bone_type") + col.prop(toolkit, "list_only_mode") + + if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0: + box = weight_box.box() + row = box.row() + row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "", + toolkit, "zero_weight_bones", + toolkit, "zero_weight_bones_index") + + col = box.column(align=True) + col.operator("avatar_toolkit.remove_selected_bones", + text=t("Tools.remove_selected_bones")) + row = col.row(align=True) row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE') row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')