From 3e187bd18acb204c1fccb0bdda4110663620ead6 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 5 Dec 2024 15:09:40 +0000 Subject: [PATCH] Start of MMD Tools - The idea is to have several buttons which kinda mimic what Cats used to do. - These are very basic, don't work very well, will improve before Alpha 1. --- core/dictionaries.py | 76 +++++ core/properties.py | 26 ++ core/updater.py | 2 +- functions/mmd_tools.py | 498 ++++++++++++++++++++++++++++++ resources/translations/en_US.json | 29 ++ ui/mmd_panel.py | 52 ++++ ui/settings_panel.py | 2 +- 7 files changed, 683 insertions(+), 2 deletions(-) create mode 100644 functions/mmd_tools.py create mode 100644 ui/mmd_panel.py diff --git a/core/dictionaries.py b/core/dictionaries.py index 26f0a0d..3d5b8f2 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -166,3 +166,79 @@ resonite_translations = { 'thumb_2_r': "thumb2.R", 'thumb_3_r': "thumb3.R" } + +mmd_bone_renames = { + # Core body + "センター": "Center", + "グルーブ": "Groove", + "腰": "Waist", + "上半身": "Upper Body", + "上半身2": "Upper Body 2", + "下半身": "Lower Body", + + # Head + "首": "Neck", + "頭": "Head", + "両目": "Eyes", + "左目": "Eye_L", + "右目": "Eye_R", + + # Arms + "左肩": "Shoulder_L", + "左腕": "Arm_L", + "左ひじ": "Elbow_L", + "左手首": "Wrist_L", + "右肩": "Shoulder_R", + "右腕": "Arm_R", + "右ひじ": "Elbow_R", + "右手首": "Wrist_R", + + # Fingers + "左親指1": "Thumb1_L", + "左親指2": "Thumb2_L", + "左人指1": "Index1_L", + "左人指2": "Index2_L", + "左人指3": "Index3_L", + "左中指1": "Middle1_L", + "左中指2": "Middle2_L", + "左中指3": "Middle3_L", + "左薬指1": "Ring1_L", + "左薬指2": "Ring2_L", + "左薬指3": "Ring3_L", + "左小指1": "Pinky1_L", + "左小指2": "Pinky2_L", + "左小指3": "Pinky3_L", + + "右親指1": "Thumb1_R", + "右親指2": "Thumb2_R", + "右人指1": "Index1_R", + "右人指2": "Index2_R", + "右人指3": "Index3_R", + "右中指1": "Middle1_R", + "右中指2": "Middle2_R", + "右中指3": "Middle3_R", + "右薬指1": "Ring1_R", + "右薬指2": "Ring2_R", + "右薬指3": "Ring3_R", + "右小指1": "Pinky1_R", + "右小指2": "Pinky2_R", + "右小指3": "Pinky3_R", + + # Legs + "左足": "Leg_L", + "左ひざ": "Knee_L", + "左足首": "Ankle_L", + "右足": "Leg_R", + "右ひざ": "Knee_R", + "右足首": "Ankle_R", + + # Toes + "左つま先": "Toe_L", + "右つま先": "Toe_R", + + # IK bones + "左足IK": "Leg_IK_L", + "右足IK": "Leg_IK_R", + "左つま先IK": "Toe_IK_L", + "右つま先IK": "Toe_IK_R" +} diff --git a/core/properties.py b/core/properties.py index 66e2acf..9dbb70d 100644 --- a/core/properties.py +++ b/core/properties.py @@ -116,6 +116,32 @@ class AvatarToolkitSceneProperties(PropertyGroup): max=1.0 ) + mmd_keep_upper_chest: BoolProperty( + name=t("MMDTools.keep_upper_chest"), + description=t("MMDTools.keep_upper_chest_desc"), + default=True + ) + + mmd_remove_unused_bones: BoolProperty( + name=t("MMDTools.remove_unused"), + description=t("MMDTools.remove_unused_desc"), + default=True + ) + + mmd_merge_distance: FloatProperty( + name=t("MMDTools.merge_distance"), + description=t("MMDTools.merge_distance_desc"), + default=0.001, + min=0.0001, + max=0.1 + ) + + mmd_cleanup_shapekeys: BoolProperty( + name=t("MMDTools.cleanup_shapekeys"), + description=t("MMDTools.cleanup_shapekeys_desc"), + default=True + ) + def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") diff --git a/core/updater.py b/core/updater.py index 96e55c8..d490504 100644 --- a/core/updater.py +++ b/core/updater.py @@ -76,7 +76,7 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 3 + bl_order = 4 def draw(self, context: bpy.types.Context) -> None: layout = self.layout diff --git a/functions/mmd_tools.py b/functions/mmd_tools.py new file mode 100644 index 0000000..93645fa --- /dev/null +++ b/functions/mmd_tools.py @@ -0,0 +1,498 @@ +import bpy +import numpy as np +from typing import Set, Dict, List, Optional, Tuple +from bpy.types import Operator, Context, Object, EditBone, Mesh +from ..core.logging_setup import logger +from ..core.translations import t +from ..core.common import ( + get_active_armature, + validate_armature, + get_all_meshes, + ProgressTracker, + transfer_vertex_weights, + remove_unused_shapekeys +) +from ..core.dictionaries import bone_names, mmd_bone_renames + +class AvatarToolkit_OT_FixBoneNames(Operator): + """Standardize and fix bone names""" + bl_idname = "avatar_toolkit.fix_bone_names" + bl_label = t("MMDTools.fix_bone_names") + bl_description = t("MMDTools.fix_bone_names_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) + + with ProgressTracker(context, 3, "Fixing Bone Names") as progress: + bpy.ops.object.mode_set(mode='EDIT') + + # First pass - standardize names + for bone in armature.data.edit_bones: + bone.name = self.standardize_bone_name(bone.name) + progress.step("Standardized names") + + # Second pass - apply MMD mappings + for bone in armature.data.edit_bones: + if bone.name in mmd_bone_renames: + bone.name = mmd_bone_renames[bone.name] + progress.step("Applied MMD mappings") + + # Third pass - fix common names + for bone in armature.data.edit_bones: + self.fix_common_names(bone) + progress.step("Fixed common names") + + self.report({'INFO'}, t("MMDTools.bones_renamed")) + return {'FINISHED'} + + def standardize_bone_name(self, name: str) -> str: + """Standardize bone naming convention""" + prefixes = ['def-', 'def_', 'sk_', 'b_', 'bone_', 'mmd_'] + name_lower = name.lower() + + # Remove common prefixes + for prefix in prefixes: + if name_lower.startswith(prefix): + name = name[len(prefix):] + break + + # Fix side indicators + name = name.replace('_l', '_L').replace('_r', '_R') + name = name.replace('.l', '_L').replace('.r', '_R') + name = name.replace('左', '_L').replace('右', '_R') + + return name + + def fix_common_names(self, bone: EditBone) -> None: + """Fix common bone names to standard names""" + for standard_name, variations in bone_names.items(): + if bone.name.lower() in variations: + bone.name = standard_name + break + +class AvatarToolkit_OT_FixBoneHierarchy(Operator): + """Fix bone parenting and hierarchy""" + bl_idname = "avatar_toolkit.fix_bone_hierarchy" + bl_label = t("MMDTools.fix_hierarchy") + bl_description = t("MMDTools.fix_hierarchy_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) + + with ProgressTracker(context, 3, "Fixing Bone Hierarchy") as progress: + bpy.ops.object.mode_set(mode='EDIT') + + # Fix spine chain + self.fix_spine_chain(armature) + progress.step("Fixed spine chain") + + # Fix limb chains + self.fix_limb_chains(armature) + progress.step("Fixed limb chains") + + # Fix bone orientations + self.fix_bone_orientations(armature) + progress.step("Fixed bone orientations") + + self.report({'INFO'}, t("MMDTools.hierarchy_fixed")) + return {'FINISHED'} + + def fix_spine_chain(self, armature: Object) -> None: + """Fix the spine bone chain hierarchy""" + edit_bones = armature.data.edit_bones + spine_chain = ['Hips', 'Spine', 'Chest', 'Neck', 'Head'] + previous = None + + for bone_name in spine_chain: + if bone_name in edit_bones: + bone = edit_bones[bone_name] + if previous: + bone.parent = edit_bones[previous] + previous = bone_name + + def fix_limb_chains(self, armature: Object) -> None: + """Fix arm and leg bone chains""" + edit_bones = armature.data.edit_bones + limb_chains = { + 'Left': { + 'arm': ['Left shoulder', 'Left arm', 'Left elbow', 'Left wrist'], + 'leg': ['Left leg', 'Left knee', 'Left ankle', 'Left toe'] + }, + 'Right': { + 'arm': ['Right shoulder', 'Right arm', 'Right elbow', 'Right wrist'], + 'leg': ['Right leg', 'Right knee', 'Right ankle', 'Right toe'] + } + } + + for side in limb_chains: + for chain in limb_chains[side].values(): + previous = None + for bone_name in chain: + if bone_name in edit_bones: + bone = edit_bones[bone_name] + if previous: + bone.parent = edit_bones[previous] + previous = bone_name + + def fix_bone_orientations(self, armature: Object) -> None: + """Fix bone roll and axis orientations""" + edit_bones = armature.data.edit_bones + + # Fix spine chain orientations + spine_bones = ['Hips', 'Spine', 'Chest'] + for name in spine_bones: + if name in edit_bones: + bone = edit_bones[name] + bone.roll = 0 + bone.tail.y = bone.head.y + + # Fix arm orientations + arm_bones = ['Left arm', 'Right arm', 'Left elbow', 'Right elbow'] + for name in arm_bones: + if name in edit_bones: + bone = edit_bones[name] + bone.roll = 0 if 'Left' in name else np.pi + +class AvatarToolkit_OT_FixBoneWeights(Operator): + """Fix and clean up bone weights""" + bl_idname = "avatar_toolkit.fix_bone_weights" + bl_label = t("MMDTools.fix_weights") + bl_description = t("MMDTools.fix_weights_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) + meshes = get_all_meshes(context) + + if not meshes: + self.report({'WARNING'}, t("MMDTools.no_meshes")) + return {'CANCELLED'} + + with ProgressTracker(context, len(meshes), "Fixing Bone Weights") as progress: + for mesh in meshes: + # Clean weights + self.clean_weights(mesh, context.scene.avatar_toolkit.clean_weights_threshold) + + # Handle twist bones + if context.scene.avatar_toolkit.merge_twist_bones: + self.process_twist_bones(mesh) + + # Remove empty groups + self.remove_empty_groups(mesh) + + # Normalize weights + self.normalize_weights(mesh) + + progress.step(f"Processed {mesh.name}") + + self.report({'INFO'}, t("MMDTools.weights_fixed")) + return {'FINISHED'} + + def clean_weights(self, mesh: Object, threshold: float) -> None: + """Remove weights below threshold""" + for vertex_group in mesh.vertex_groups: + for vertex in mesh.data.vertices: + try: + weight = vertex_group.weight(vertex.index) + if weight < threshold: + vertex_group.remove([vertex.index]) + except RuntimeError: + continue + + def process_twist_bones(self, mesh: Object) -> None: + """Process and merge twist bone weights""" + twist_groups = [g for g in mesh.vertex_groups if 'twist' in g.name.lower()] + for group in twist_groups: + base_name = group.name.lower().replace('twist', '').strip('_') + for target in mesh.vertex_groups: + if target.name.lower() == base_name: + transfer_vertex_weights(mesh, group.name, target.name) + break + + def remove_empty_groups(self, mesh: Object) -> None: + """Remove vertex groups with no weights""" + empty_groups = [] + for group in mesh.vertex_groups: + has_weights = False + for vert in mesh.data.vertices: + for g in vert.groups: + if g.group == group.index and g.weight > 0: + has_weights = True + break + if has_weights: + break + if not has_weights: + empty_groups.append(group) + + for group in empty_groups: + mesh.vertex_groups.remove(group) + + def normalize_weights(self, mesh: Object) -> None: + """Normalize vertex weights""" + for vertex in mesh.data.vertices: + total_weight = sum(group.weight for group in vertex.groups) + if total_weight > 0: + for group in vertex.groups: + group.weight /= total_weight + +class AvatarToolkit_OT_FixMMDFeatures(Operator): + """Fix MMD-specific features and settings""" + bl_idname = "avatar_toolkit.fix_mmd_features" + bl_label = t("MMDTools.fix_mmd_features") + bl_description = t("MMDTools.fix_mmd_features_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) + meshes = get_all_meshes(context) + + with ProgressTracker(context, 4, "Fixing MMD Features") as progress: + # Process shape keys + for mesh in meshes: + self.process_shape_keys(mesh) + progress.step("Processed shape keys") + + # Fix MMD shading + self.fix_mmd_shading(meshes) + progress.step("Fixed MMD shading") + + # Handle physics cleanup + self.cleanup_physics(armature) + progress.step("Cleaned up physics") + + # Remove unused data + self.cleanup_unused_data(context) + progress.step("Cleaned up unused data") + + return {'FINISHED'} + + def process_shape_keys(self, mesh: Object) -> None: + """Process and clean up shape keys""" + if not mesh.data.shape_keys: + return + + # Clean unused shape keys + remove_unused_shapekeys(mesh) + + # Sort and rename shape keys + shape_keys = mesh.data.shape_keys.key_blocks + for key in shape_keys: + # Handle Japanese prefixes + if key.name.startswith('防'): + key.name = key.name[1:] + # Handle common MMD prefixes + if key.name.startswith('表情'): + key.name = key.name[2:] + + def fix_mmd_shading(self, meshes: List[Object]) -> None: + """Fix MMD material shading settings""" + for mesh in meshes: + for material in mesh.data.materials: + if material: + material.use_backface_culling = True + material.blend_method = 'HASHED' + if material.node_tree: + for node in material.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + node.inputs['Alpha'].default_value = 1.0 + + def cleanup_physics(self, armature: Object) -> None: + """Clean up MMD physics objects""" + physics_objects = [obj for obj in bpy.data.objects + if obj.parent == armature and + (obj.rigid_body or obj.rigid_body_constraint)] + + for obj in physics_objects: + bpy.data.objects.remove(obj, do_unlink=True) + + def cleanup_unused_data(self, context: Context) -> None: + """Clean up unused MMD data""" + # Remove unused actions + for action in bpy.data.actions: + if not action.users: + bpy.data.actions.remove(action) + + # Remove empty vertex groups + for mesh in get_all_meshes(context): + self.remove_empty_groups(mesh) + + def remove_empty_groups(self, mesh: Object) -> None: + """Remove empty vertex groups""" + empty_groups = [] + for group in mesh.vertex_groups: + has_weights = False + for vert in mesh.data.vertices: + for g in vert.groups: + if g.group == group.index and g.weight > 0: + has_weights = True + break + if has_weights: + break + if not has_weights: + empty_groups.append(group) + + for group in empty_groups: + mesh.vertex_groups.remove(group) + +class AvatarToolkit_OT_AdvancedBoneOps(Operator): + """Advanced bone operations and fixes""" + bl_idname = "avatar_toolkit.advanced_bone_ops" + bl_label = t("MMDTools.advanced_bone_ops") + bl_description = t("MMDTools.advanced_bone_ops_desc") + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context: Context) -> Set[str]: + armature = get_active_armature(context) + + with ProgressTracker(context, 4, "Advanced Bone Operations") as progress: + # Fix zero length bones + self.fix_zero_length_bones(armature) + progress.step("Fixed zero length bones") + + # Connect bones with children + self.connect_bone_chains(armature) + progress.step("Connected bone chains") + + # Handle bone roll values + self.fix_bone_rolls(armature) + progress.step("Fixed bone rolls") + + # Fix bone orientations + self.fix_bone_orientations(armature) + progress.step("Fixed bone orientations") + + return {'FINISHED'} + + def fix_zero_length_bones(self, armature: Object) -> None: + """Fix bones with zero length by extending them""" + min_length = 0.001 + for bone in armature.data.edit_bones: + length = (bone.tail - bone.head).length + if length < min_length: + if bone.parent: + bone.tail = bone.head + bone.parent.vector * 0.1 + else: + bone.tail.z = bone.head.z + 0.1 + + def connect_bone_chains(self, armature: Object) -> None: + """Connect bones that should form chains""" + min_distance = bpy.context.scene.avatar_toolkit.connect_bones_min_distance + + for bone in armature.data.edit_bones: + if len(bone.children) == 1: + child = bone.children[0] + distance = (bone.tail - child.head).length + if distance < min_distance: + child.use_connect = True + child.head = bone.tail + + def fix_bone_rolls(self, armature: Object) -> None: + """Fix bone roll values for proper orientation""" + for bone in armature.data.edit_bones: + if 'spine' in bone.name.lower() or 'chest' in bone.name.lower(): + bone.roll = 0 + elif 'shoulder' in bone.name.lower(): + bone.roll = 0 if 'left' in bone.name.lower() else np.pi + +class AvatarToolkit_OT_CleanupOperations(Operator): + """Cleanup unused data and objects""" + bl_idname = "avatar_toolkit.cleanup_operations" + bl_label = t("MMDTools.cleanup_operations") + bl_description = t("MMDTools.cleanup_operations_desc") + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context: Context) -> Set[str]: + armature = get_active_armature(context) + + with ProgressTracker(context, 4, "Cleanup Operations") as progress: + # Remove rigidbodies and joints + self.remove_physics_objects(armature) + progress.step("Removed physics objects") + + # Clear unused animation data + self.clear_unused_animations(armature) + progress.step("Cleared unused animations") + + # Remove empty objects + self.remove_empty_objects() + progress.step("Removed empty objects") + + # Clean up collections + self.cleanup_collections(armature) + progress.step("Cleaned up collections") + + return {'FINISHED'} + + def remove_physics_objects(self, armature: Object) -> None: + """Remove all physics objects and constraints""" + physics_objects = [obj for obj in bpy.data.objects + if obj.parent == armature and + (obj.rigid_body or obj.rigid_body_constraint)] + + for obj in physics_objects: + bpy.data.objects.remove(obj, do_unlink=True) + + def clear_unused_animations(self, armature: Object) -> None: + """Remove unused animation data""" + if armature.animation_data: + if armature.animation_data.action and armature.animation_data.action.users == 0: + bpy.data.actions.remove(armature.animation_data.action) + + # Clear unused NLA tracks + if armature.animation_data.nla_tracks: + for track in armature.animation_data.nla_tracks: + if not track.strips: + armature.animation_data.nla_tracks.remove(track) + + def remove_empty_objects(self) -> None: + """Remove empty objects from the scene""" + empty_objects = [obj for obj in bpy.data.objects + if obj.type == 'EMPTY' and not obj.children] + + for obj in empty_objects: + bpy.data.objects.remove(obj, do_unlink=True) + + def cleanup_collections(self, armature: Object) -> None: + """Clean up and organize collections""" + # Remove empty collections + for collection in bpy.data.collections: + if not collection.objects and not collection.children: + bpy.data.collections.remove(collection) + + # Ensure armature is in main collection + if armature.users_collection[0] != bpy.context.scene.collection: + bpy.context.scene.collection.objects.link(armature) \ No newline at end of file diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 46594a3..d42172e 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -188,6 +188,35 @@ "Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used", "Tools.shapekeys_removed": "Removed {count} unused shape keys", + "MMDTools.label": "MMD Tools", + "MMDTools.basic_tools": "Basic MMD Tools", + "MMDTools.advanced_tools": "Advanced Tools", + "MMDTools.settings": "MMD Settings", + "MMDTools.cleanup": "Cleanup Tools", + "MMDTools.fix_bone_names": "Fix Bone Names", + "MMDTools.fix_bone_names_desc": "Standardize and fix bone names", + "MMDTools.fix_hierarchy": "Fix Bone Hierarchy", + "MMDTools.fix_hierarchy_desc": "Fix bone parenting and hierarchy", + "MMDTools.fix_weights": "Fix Bone Weights", + "MMDTools.fix_weights_desc": "Clean up and normalize bone weights", + "MMDTools.fix_mmd_features": "Fix MMD Features", + "MMDTools.fix_mmd_features_desc": "Fix MMD-specific features and settings", + "MMDTools.advanced_bone_ops": "Advanced Bone Operations", + "MMDTools.advanced_bone_ops_desc": "Perform advanced bone fixes and cleanup", + "MMDTools.keep_upper_chest": "Keep Upper Chest", + "MMDTools.keep_upper_chest_desc": "Keep the upper chest bone during cleanup", + "MMDTools.remove_unused": "Remove Unused Bones", + "MMDTools.remove_unused_desc": "Remove bones with no weights or influence", + "MMDTools.merge_distance": "Merge Distance", + "MMDTools.merge_distance_desc": "Distance threshold for merging vertices", + "MMDTools.cleanup_shapekeys": "Clean Shape Keys", + "MMDTools.cleanup_shapekeys_desc": "Remove unused and duplicate shape keys", + "MMDTools.bones_renamed": "Bone names standardized successfully", + "MMDTools.hierarchy_fixed": "Bone hierarchy fixed successfully", + "MMDTools.weights_fixed": "Bone weights cleaned and normalized", + "MMDTools.no_meshes": "No meshes found to process", + "MMDTools.not_mmd_model": "Selected armature is not an MMD model", + "Settings.label": "Settings", "Settings.language": "Language", "Settings.language_desc": "Select interface language", diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py new file mode 100644 index 0000000..39f5821 --- /dev/null +++ b/ui/mmd_panel.py @@ -0,0 +1,52 @@ +import bpy +from typing import Set +from bpy.types import Panel, Context, UILayout, Operator +from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from ..core.translations import t + +class AvatarToolKit_PT_MMDPanel(Panel): + """Panel containing MMD-specific tools and operations""" + bl_label = t("MMDTools.label") + bl_idname = "OBJECT_PT_avatar_toolkit_mmd" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = CATEGORY_NAME + bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname + bl_order = 3 + + def draw(self, context: Context) -> None: + """Draw the MMD tools panel interface""" + layout = self.layout + + # Basic MMD Tools Box + basic_box = layout.box() + col = basic_box.column(align=True) + col.label(text=t("MMDTools.basic_tools"), icon='ARMATURE_DATA') + col.separator(factor=0.5) + col.operator("avatar_toolkit.fix_bone_names", icon='SORTALPHA') + col.operator("avatar_toolkit.fix_bone_hierarchy", icon='BONE_DATA') + col.operator("avatar_toolkit.fix_bone_weights", icon='GROUP_BONE') + + # Advanced MMD Tools Box + advanced_box = layout.box() + col = advanced_box.column(align=True) + col.label(text=t("MMDTools.advanced_tools"), icon='MODIFIER') + col.separator(factor=0.5) + col.operator("avatar_toolkit.fix_mmd_features", icon='SHAPEKEY_DATA') + col.operator("avatar_toolkit.advanced_bone_ops", icon='CONSTRAINT_BONE') + + # Settings Box + settings_box = layout.box() + col = settings_box.column(align=True) + col.label(text=t("MMDTools.settings"), icon='PREFERENCES') + col.separator(factor=0.5) + col.prop(context.scene.avatar_toolkit, "mmd_keep_upper_chest") + col.prop(context.scene.avatar_toolkit, "mmd_remove_unused_bones") + col.prop(context.scene.avatar_toolkit, "mmd_cleanup_shapekeys") + + # Cleanup Box + cleanup_box = layout.box() + col = cleanup_box.column(align=True) + col.label(text=t("MMDTools.cleanup"), icon='TRASH') + col.separator(factor=0.5) + col.operator("avatar_toolkit.cleanup_operations", icon='BRUSH_DATA') diff --git a/ui/settings_panel.py b/ui/settings_panel.py index df86987..67b782f 100644 --- a/ui/settings_panel.py +++ b/ui/settings_panel.py @@ -36,7 +36,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 4 + bl_order: int = 5 def draw(self, context: Context) -> None: """Draw the settings panel layout with language selection"""