diff --git a/core/common.py b/core/common.py index 548c6f4..c4e7300 100644 --- a/core/common.py +++ b/core/common.py @@ -1,5 +1,6 @@ import bpy import numpy as np +from mathutils import Vector from bpy.types import Context, Object, Modifier, EditBone, Operator from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable from ..core.logging_setup import logger @@ -592,3 +593,26 @@ def fix_zero_length_bones(armature: Object) -> None: bpy.ops.object.mode_set(mode='OBJECT') +def calculate_bone_orientation(mesh, vertices): + """Calculate optimal bone orientation based on mesh geometry.""" + + if not vertices: + return Vector((0, 0, 0.1)), 0.0 + + coords = [mesh.data.vertices[v.index].co for v in vertices] + min_co = Vector(map(min, zip(*coords))) + max_co = Vector(map(max, zip(*coords))) + dimensions = max_co - min_co + + roll_angle = 0.0 + + return dimensions, roll_angle + +def add_armature_modifier(mesh: Object, armature: Object): + """Add armature modifier to mesh.""" + for mod in mesh.modifiers: + if mod.type == 'ARMATURE': + mesh.modifiers.remove(mod) + + modifier = mesh.modifiers.new('Armature', 'ARMATURE') + modifier.object = armature \ No newline at end of file diff --git a/core/properties.py b/core/properties.py index 140c6e9..bdf045b 100644 --- a/core/properties.py +++ b/core/properties.py @@ -304,59 +304,69 @@ class AvatarToolkitSceneProperties(PropertyGroup): ) merge_armature_into: StringProperty( - name=t('CustomPanel.merge_into'), - description=t('CustomPanel.merge_into_desc'), + name=t('MergeArmature.into'), + description=t('MergeArmature.into_desc'), default="" ) merge_armature: StringProperty( - name=t('CustomPanel.merge_from'), - description=t('CustomPanel.merge_from_desc'), + name=t('MergeArmature.from'), + description=t('MergeArmature.from_desc'), default="" ) attach_mesh: StringProperty( - name=t('CustomPanel.attach_mesh'), - description=t('CustomPanel.attach_mesh_desc'), + name=t('AttachMesh.select'), + description=t('AttachMesh.select_desc'), default="" ) attach_bone: StringProperty( - name=t('CustomPanel.attach_bone'), - description=t('CustomPanel.attach_bone_desc'), + name=t('AttachBone.select'), + description=t('AttachBone.select_desc'), default="" ) merge_all_bones: BoolProperty( - name=t('CustomPanel.merge_all_bones'), - description=t('CustomPanel.merge_all_bones_desc'), + name=t('MergeArmature.merge_all'), + description=t('MergeArmature.merge_all_desc'), default=True ) apply_transforms: BoolProperty( - name=t('CustomPanel.apply_transforms'), - description=t('CustomPanel.apply_transforms_desc'), + name=t('MergeArmature.apply_transforms'), + description=t('MergeArmature.apply_transforms_desc'), default=True ) join_meshes: BoolProperty( - name=t('CustomPanel.join_meshes'), - description=t('CustomPanel.join_meshes_desc'), + name=t('MergeArmature.join_meshes'), + description=t('MergeArmature.join_meshes_desc'), default=True ) remove_zero_weights: BoolProperty( - name=t('CustomPanel.remove_zero_weights'), - description=t('CustomPanel.remove_zero_weights_desc'), + name=t('MergeArmature.remove_zero_weights'), + description=t('MergeArmature.remove_zero_weights_desc'), default=True ) cleanup_shape_keys: BoolProperty( - name=t('CustomPanel.cleanup_shape_keys'), - description=t('CustomPanel.cleanup_shape_keys_desc'), + name=t('MergeArmature.cleanup_shape_keys'), + description=t('MergeArmature.cleanup_shape_keys_desc'), default=True ) + attach_mesh: StringProperty( + name=t("Tools.attach_mesh_select"), + description=t("Tools.attach_mesh_select_desc") + ) + + attach_bone: StringProperty( + name=t("Tools.attach_bone_select"), + description=t("Tools.attach_bone_select_desc") + ) + def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") diff --git a/functions/custom_tools/armature_merging.py b/functions/custom_tools/armature_merging.py index 2ff4e77..7164652 100644 --- a/functions/custom_tools/armature_merging.py +++ b/functions/custom_tools/armature_merging.py @@ -1,19 +1,15 @@ import bpy import numpy as np from typing import List, Optional, Dict, Set -from mathutils import Vector from bpy.types import Context, Object, Operator from ...core.logging_setup import logger from ...core.translations import t from ...core.common import ( - get_active_armature, get_all_meshes, fix_zero_length_bones, clear_unused_data_blocks, - validate_armature, join_mesh_objects, - fix_uv_coordinates, remove_unused_shapekeys ) @@ -40,7 +36,7 @@ class AvatarToolkit_OT_MergeArmature(Operator): if not base_armature or not merge_armature: logger.error(f"Armature not found: {merge_armature_name}") - self.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name)) + self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name)) return {'CANCELLED'} # Remove Rigid Bodies and Joints @@ -80,21 +76,6 @@ class AvatarToolkit_OT_MergeArmature(Operator): self.report({'ERROR'}, str(e)) return {'CANCELLED'} -def calculate_bone_orientation(mesh, vertices): - """Calculate optimal bone orientation based on mesh geometry.""" - - if not vertices: - return Vector((0, 0, 0.1)), 0.0 - - coords = [mesh.data.vertices[v.index].co for v in vertices] - min_co = Vector(map(min, zip(*coords))) - max_co = Vector(map(max, zip(*coords))) - dimensions = max_co - min_co - - roll_angle = 0.0 - - return dimensions, roll_angle - def delete_rigidbodies_and_joints(armature: Object): """Delete rigid bodies and joints associated with the armature.""" to_delete = [] @@ -398,15 +379,6 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str): vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE') mesh.vertex_groups.remove(vg_from) -def add_armature_modifier(mesh: Object, armature: Object): - """Add armature modifier to mesh.""" - for mod in mesh.modifiers: - if mod.type == 'ARMATURE': - mesh.modifiers.remove(mod) - - modifier = mesh.modifiers.new('Armature', 'ARMATURE') - modifier.object = armature - def remove_unused_vertex_groups(mesh: Object): """Remove vertex groups with no weights.""" for vg in mesh.vertex_groups: diff --git a/functions/custom_tools/mesh_attachment.py b/functions/custom_tools/mesh_attachment.py index e69de29..560521f 100644 --- a/functions/custom_tools/mesh_attachment.py +++ b/functions/custom_tools/mesh_attachment.py @@ -0,0 +1,130 @@ +import bpy +from bpy.types import Operator, Context, Object +from mathutils import Vector +from typing import Set, Optional + +from ...core.logging_setup import logger +from ...core.translations import t +from ...core.common import ( + get_active_armature, + validate_armature, + get_all_meshes, + ProgressTracker, + calculate_bone_orientation, + add_armature_modifier +) + +class AvatarToolkit_OT_AttachMesh(Operator): + """Attach a mesh to an armature bone with automatic weight setup""" + bl_idname = "avatar_toolkit.attach_mesh" + bl_label = t("AttachMesh.label") + bl_description = t("AttachMesh.desc") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + armature = get_active_armature(context) + return armature is not None and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0 + + def execute(self, context: Context) -> Set[str]: + try: + logger.info("Starting mesh attachment process") + + mesh_name = context.scene.avatar_toolkit.attach_mesh + armature = get_active_armature(context) + attach_bone_name = context.scene.avatar_toolkit.attach_bone + mesh = bpy.data.objects.get(mesh_name) + + with ProgressTracker(context, 10, "Attaching Mesh") as progress: + # Validation steps + is_valid, error_msg = validate_mesh_transforms(mesh) + if not is_valid: + raise ValueError(error_msg) + progress.step(t("AttachMesh.validate_transforms")) + + is_valid, error_msg = validate_mesh_name(armature, mesh_name) + if not is_valid: + raise ValueError(error_msg) + progress.step(t("AttachMesh.validate_name")) + + # Parent mesh to armature + mesh.parent = armature + mesh.parent_type = 'OBJECT' + progress.step(t("AttachMesh.parent_mesh")) + + # Setup vertex groups + if mesh.vertex_groups: + for vg in mesh.vertex_groups: + mesh.vertex_groups.remove(vg) + + bpy.ops.object.select_all(action='DESELECT') + mesh.select_set(True) + context.view_layer.objects.active = mesh + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + vg = mesh.vertex_groups.new(name=mesh_name) + bpy.ops.object.vertex_group_assign() + bpy.ops.object.mode_set(mode='OBJECT') + progress.step(t("AttachMesh.setup_weights")) + + # Create and setup bone + bpy.ops.object.select_all(action='DESELECT') + armature.select_set(True) + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + attach_to_bone = armature.data.edit_bones.get(attach_bone_name) + if not attach_to_bone: + raise ValueError(t("AttachMesh.error.bone_not_found", bone=attach_bone_name)) + + mesh_bone = armature.data.edit_bones.new(mesh_name) + mesh_bone.parent = attach_to_bone + progress.step(t("AttachMesh.create_bone")) + + # Calculate bone placement + verts_in_group = [v for v in mesh.data.vertices + for g in v.groups if g.group == vg.index] + dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group) + + # Set bone position and orientation + center = Vector((0, 0, 0)) + for v in verts_in_group: + center += mesh.data.vertices[v.index].co + center /= len(verts_in_group) + + mesh_bone.head = center + mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z))) + mesh_bone.roll = roll_angle + progress.step(t("AttachMesh.position_bone")) + + bpy.ops.object.mode_set(mode='OBJECT') + add_armature_modifier(mesh, armature) + progress.step(t("AttachMesh.add_modifier")) + + logger.info(f"Successfully attached mesh {mesh_name} to bone {attach_bone_name}") + self.report({'INFO'}, t("AttachMesh.success")) + return {'FINISHED'} + + except Exception as e: + logger.error(f"Failed to attach mesh: {str(e)}") + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + +def validate_mesh_transforms(mesh): + """Validate mesh transforms are suitable for attaching.""" + if not mesh: + return False, "Mesh not found" + + # Check for non-uniform scale + scale = mesh.scale + if abs(scale[0] - scale[1]) > 0.001 or abs(scale[1] - scale[2]) > 0.001: + return False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)" + + return True, "" + +def validate_mesh_name(armature, mesh_name): + """Validate mesh name doesn't conflict with existing bones.""" + if mesh_name in armature.data.bones: + return False, f"Bone named '{mesh_name}' already exists in armature" + return True, "" \ No newline at end of file diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 62823e7..dd82000 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -317,48 +317,64 @@ "CustomPanel.label": "Custom Avatar Tools", "CustomPanel.merge_mode": "Merge Mode", - "CustomPanel.merge_mode_desc": "Select mode for merging operations", + "CustomPanel.mesh_selection": "Mesh Selection", + "CustomPanel.select_mesh": "Select Mesh", + "CustomPanel.select_bone": "Select Bone", + "CustomPanel.select_armature": "Select Armature", "CustomPanel.mode.armature": "Armature", "CustomPanel.mode.armature_desc": "Merge armatures together", "CustomPanel.mode.mesh": "Mesh", "CustomPanel.mode.mesh_desc": "Attach meshes to armature", - "CustomPanel.mergeArmatures": "Merge Armatures", - "CustomPanel.warn.twoArmatures": "Need at least two armatures to merge", - "CustomPanel.warn.noArmOrMesh1": "No armature or meshes found", - "CustomPanel.warn.noArmOrMesh2": "Please add required objects first", - "CustomPanel.merge_into": "Merge Into", - "CustomPanel.merge_into_desc": "Target armature to merge into", - "CustomPanel.merge_from": "Merge From", - "CustomPanel.merge_from_desc": "Source armature to merge", - "CustomPanel.toMerge": "To Merge", - "CustomPanel.attachMesh1": "Attach Mesh", - "CustomPanel.attachMesh2": "Select Mesh", - "CustomPanel.attach_mesh": "Mesh to Attach", - "CustomPanel.attach_mesh_desc": "Select mesh to attach", - "CustomPanel.attachToBone": "Attach to Bone", - "CustomPanel.attach_bone": "Target Bone", - "CustomPanel.attach_bone_desc": "Select bone to attach to", - "CustomPanel.merge_same_bones": "Merge Same Bones", - "CustomPanel.merge_same_bones_desc": "Merge bones with matching names", - "CustomPanel.apply_transforms": "Apply Transforms", - "CustomPanel.apply_transforms_desc": "Apply all transformations before merging", - "CustomPanel.join_meshes": "Join Meshes", - "CustomPanel.join_meshes_desc": "Join meshes after merging", - "CustomPanel.remove_zero_weights": "Remove Zero Weights", - "CustomPanel.remove_zero_weights_desc": "Remove vertex groups with no weights", - "CustomPanel.cleanup_shape_keys": "Clean Shape Keys", - "CustomPanel.cleanup_shape_keys_desc": "Remove unused shape keys", - "CustomPanel.merge_all_bones": "Merge Same Bones", - "CustomPanel.merge_all_bones_desc": "Merge bones with matching names", - "CustomPanel.mergeInto": "Merge Into", + + "AttachMesh.label": "Attach Mesh", + "AttachMesh.desc": "Attach a mesh to an armature bone with automatic weight setup", + "AttachMesh.search_desc": "Search for meshes to attach", + "AttachMesh.select": "Select Mesh to Attach", + "AttachMesh.select_desc": "Choose a mesh to attach to the armature", + "AttachMesh.success": "Mesh attached successfully", + "AttachMesh.warn_no_armature": "Select an armature and mesh to attach", + "AttachMesh.validate_transforms": "Validating mesh transforms", + "AttachMesh.validate_name": "Validating mesh name", + "AttachMesh.parent_mesh": "Parenting mesh to armature", + "AttachMesh.setup_weights": "Setting up vertex weights", + "AttachMesh.create_bone": "Creating attachment bone", + "AttachMesh.position_bone": "Positioning bone", + "AttachMesh.add_modifier": "Adding armature modifier", + "AttachMesh.error.bone_not_found": "Attach bone '{bone}' not found", + "AttachMesh.error.mesh_not_found": "Mesh not found", + "AttachMesh.error.non_uniform_scale": "Mesh has non-uniform scale. Please apply scale", + "AttachBone.search_desc": "Search for target bone", + "AttachBone.select": "Select Target Bone", + "AttachBone.select_desc": "Choose the bone to attach the mesh to", + "MergeArmature.label": "Merge Armatures", "MergeArmature.desc": "Merge two armatures together", - "MergeArmature.error.notFound": "Armature '{name}' not found", - "MergeArmature.success": "Armatures merged successfully", - "MergeArmature.error.checkTransforms": "Please check parent transformations", - "MergeArmature.error.pleaseFix": "Please fix parent relationships", + "MergeArmature.options": "Merge Options", + "MergeArmature.warn_two": "Need at least two armatures to merge", + "MergeArmature.into": "Merge Into", + "MergeArmature.into_desc": "Target armature to merge into", + "MergeArmature.into_search_desc": "Search for target armature", + "MergeArmature.from": "Merge From", + "MergeArmature.from_desc": "Source armature to merge from", + "MergeArmature.from_search_desc": "Search for source armature", + "MergeArmature.error.not_found": "Armature '{name}' not found", "MergeArmature.error.transforms_not_aligned": "Transforms must be applied to merge this armature, either do this via the manual method or via apply transform checkmark", - + "MergeArmature.error.check_transforms": "Please check parent transformations", + "MergeArmature.error.fix_parents": "Please fix parent relationships", + "MergeArmature.progress.removing_rigidbodies": "Removing rigid bodies and joints", + "MergeArmature.progress.validating": "Validating armatures", + "MergeArmature.progress.merging": "Merging armatures", + "MergeArmature.success": "Armatures merged successfully", + "MergeArmature.merge_all": "Merge Same Bones", + "MergeArmature.merge_all_desc": "Merge bones with matching names", + "MergeArmature.apply_transforms": "Apply Transforms", + "MergeArmature.apply_transforms_desc": "Apply all transformations before merging", + "MergeArmature.join_meshes": "Join Meshes", + "MergeArmature.join_meshes_desc": "Join meshes after merging", + "MergeArmature.remove_zero_weights": "Remove Zero Weights", + "MergeArmature.remove_zero_weights_desc": "Remove vertex groups with no weights", + "MergeArmature.cleanup_shape_keys": "Clean Shape Keys", + "MergeArmature.cleanup_shape_keys_desc": "Remove unused shape keys", "Settings.label": "Settings", "Settings.language": "Language", diff --git a/ui/custom_avatar_panel.py b/ui/custom_avatar_panel.py index 4332e1b..ed4347c 100644 --- a/ui/custom_avatar_panel.py +++ b/ui/custom_avatar_panel.py @@ -13,13 +13,12 @@ from ..core.common import ( class AvatarToolkit_OT_SearchMergeArmatureInto(Operator): bl_idname = "avatar_toolkit.search_merge_armature_into" bl_label = "" - bl_description = t('CustomPanel.search_merge_into_desc') + bl_description = t('MergeArmature.into_search_desc') bl_property = "search_merge_armature_into_enum" - # Define the enum property within the operator class search_merge_armature_into_enum: bpy.props.EnumProperty( - name=t('CustomPanel.merge_into'), - description=t('CustomPanel.merge_into_desc'), + name=t('MergeArmature.into'), + description=t('MergeArmature.into_desc'), items=get_armature_list ) @@ -34,12 +33,12 @@ class AvatarToolkit_OT_SearchMergeArmatureInto(Operator): class AvatarToolkit_OT_SearchMergeArmature(Operator): bl_idname = "avatar_toolkit.search_merge_armature" bl_label = "" - bl_description = t('CustomPanel.search_merge_desc') + bl_description = t('MergeArmature.from_search_desc') bl_property = "search_merge_armature_enum" search_merge_armature_enum: bpy.props.EnumProperty( - name=t('CustomPanel.merge_from'), - description=t('CustomPanel.merge_from_desc'), + name=t('MergeArmature.from'), + description=t('MergeArmature.from_desc'), items=get_armature_list ) @@ -54,15 +53,17 @@ class AvatarToolkit_OT_SearchMergeArmature(Operator): class AvatarToolkit_OT_SearchAttachMesh(Operator): bl_idname = "avatar_toolkit.search_attach_mesh" bl_label = "" - bl_description = t('CustomPanel.search_mesh_desc') + bl_description = t('AttachMesh.search_desc') bl_property = "search_attach_mesh_enum" search_attach_mesh_enum: bpy.props.EnumProperty( - name=t('CustomPanel.attach_mesh'), - description=t('CustomPanel.attach_mesh_desc'), + name=t('AttachMesh.select'), + description=t('AttachMesh.select_desc'), items=lambda self, context: [ (obj.name, obj.name, "") - for obj in get_all_meshes(context) + for obj in bpy.data.objects + if obj.type == 'MESH' + and not any(mod.type == 'ARMATURE' for mod in obj.modifiers) ] ) @@ -77,12 +78,12 @@ class AvatarToolkit_OT_SearchAttachMesh(Operator): class AvatarToolkit_OT_SearchAttachBone(Operator): bl_idname = "avatar_toolkit.search_attach_bone" bl_label = "" - bl_description = t('CustomPanel.search_bone_desc') + bl_description = t('AttachBone.search_desc') bl_property = "search_attach_bone_enum" search_attach_bone_enum: bpy.props.EnumProperty( - name=t('CustomPanel.attach_bone'), - description=t('CustomPanel.attach_bone_desc'), + name=t('AttachBone.select'), + description=t('AttachBone.select_desc'), items=lambda self, context: [ (bone.name, bone.name, "") for bone in get_active_armature(context).data.bones @@ -109,7 +110,6 @@ class AvatarToolKit_PT_CustomPanel(Panel): bl_options = {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: - """Draw the custom avatar tools panel interface""" layout: UILayout = self.layout toolkit = context.scene.avatar_toolkit @@ -119,113 +119,109 @@ class AvatarToolKit_PT_CustomPanel(Panel): col.label(text=t('CustomPanel.merge_mode'), icon='TOOL_SETTINGS') col.separator(factor=0.5) - # Create a row for the mode buttons with increased scale row: UILayout = col.row(align=True) row.scale_y = 1.5 row.prop(toolkit, "merge_mode", expand=True) - # Armature Merging Tools if toolkit.merge_mode == 'ARMATURE': self.draw_armature_tools(layout, context) - # Mesh Attachment Tools else: self.draw_mesh_tools(layout, context) def draw_armature_tools(self, layout: UILayout, context: Context) -> None: - """Draw the armature merging tools section""" toolkit = context.scene.avatar_toolkit # Merge Settings Box settings_box: UILayout = layout.box() col: UILayout = settings_box.column(align=True) - col.label(text=t('CustomPanel.mergeArmatures'), icon='ARMATURE_DATA') + col.label(text=t('MergeArmature.label'), icon='ARMATURE_DATA') col.separator(factor=0.5) if len(get_armature_list(context)) <= 1: - col.label(text=t('CustomPanel.warn.twoArmatures'), icon='INFO') + col.label(text=t('MergeArmature.warn_two'), icon='INFO') return - # Merge Options + # Options Box with better spacing options_box: UILayout = layout.box() col: UILayout = options_box.column(align=True) - col.label(text=t('Tools.merge_title'), icon='SETTINGS') - col.separator(factor=0.5) - col.prop(toolkit, "merge_all_bones") - col.prop(toolkit, "apply_transforms") - col.prop(toolkit, "join_meshes") - col.prop(toolkit, "remove_zero_weights") - col.prop(toolkit, "cleanup_shape_keys") - - # Armature Selection Box - selection_box: UILayout = layout.box() - col: UILayout = selection_box.column(align=True) - col.label(text=t('QuickAccess.select_armature'), icon='BONE_DATA') + col.label(text=t('MergeArmature.options'), icon='SETTINGS') col.separator(factor=0.5) + # Group related options together + transform_col = col.column(align=True) + transform_col.prop(toolkit, "merge_all_bones") + transform_col.prop(toolkit, "apply_transforms") + + col.separator(factor=0.5) + + cleanup_col = col.column(align=True) + cleanup_col.prop(toolkit, "join_meshes") + cleanup_col.prop(toolkit, "remove_zero_weights") + cleanup_col.prop(toolkit, "cleanup_shape_keys") + + # Selection Box with consistent styling + selection_box: UILayout = layout.box() + col: UILayout = selection_box.column(align=True) + col.label(text=t('CustomPanel.select_armature'), icon='BONE_DATA') + col.separator(factor=0.5) + + # Armature selection with better alignment row: UILayout = col.row(align=True) - row.label(text=t('CustomPanel.mergeInto')) + row.label(text=t('MergeArmature.into'), icon='ARMATURE_DATA') row.operator("avatar_toolkit.search_merge_armature_into", - text=toolkit.merge_armature_into, - icon='ARMATURE_DATA') + text=toolkit.merge_armature_into) row: UILayout = col.row(align=True) - row.label(text=t('CustomPanel.toMerge')) + row.label(text=t('MergeArmature.from'), icon='ARMATURE_DATA') row.operator("avatar_toolkit.search_merge_armature", - text=toolkit.merge_armature, - icon='ARMATURE_DATA') + text=toolkit.merge_armature) - # Merge Button - merge_col: UILayout = layout.column(align=True) - merge_col.scale_y = 1.2 - merge_col.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA') + # Merge button with emphasis + merge_box: UILayout = layout.box() + col = merge_box.column(align=True) + row = col.row(align=True) + row.scale_y = 1.5 + row.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA') def draw_mesh_tools(self, layout: UILayout, context: Context) -> None: - """Draw the mesh attachment tools section""" toolkit = context.scene.avatar_toolkit # Mesh Tools Box tools_box: UILayout = layout.box() col: UILayout = tools_box.column(align=True) - col.label(text=t('CustomPanel.attachMesh1'), icon='MESH_DATA') + col.label(text=t('AttachMesh.label'), icon='MESH_DATA') col.separator(factor=0.5) if not get_active_armature(context) or not get_all_meshes(context): - col.label(text=t('CustomPanel.warn.noArmOrMesh1'), icon='INFO') - col.label(text=t('CustomPanel.warn.noArmOrMesh2')) + col.label(text=t('AttachMesh.warn_no_armature'), icon='INFO') return - # Mesh Options Box - options_box: UILayout = layout.box() - col: UILayout = options_box.column(align=True) - col.label(text=t('Tools.merge_title'), icon='SETTINGS') - col.separator(factor=0.5) - col.prop(toolkit, "join_meshes") - - # Selection Box + # Selection Box with consistent styling selection_box: UILayout = layout.box() col: UILayout = selection_box.column(align=True) - col.label(text=t('Tools.merge_title'), icon='OBJECT_DATA') + col.label(text=t('CustomPanel.mesh_selection'), icon='OBJECT_DATA') col.separator(factor=0.5) + # Selection rows with icons and better alignment row: UILayout = col.row(align=True) - row.label(text=t('CustomPanel.mergeInto')) + row.label(text=t('CustomPanel.select_armature'), icon='ARMATURE_DATA') row.operator("avatar_toolkit.search_merge_armature_into", - text=toolkit.merge_armature_into, - icon='ARMATURE_DATA') + text=toolkit.merge_armature_into) row: UILayout = col.row(align=True) - row.label(text=t('CustomPanel.attachMesh2')) + row.label(text=t('CustomPanel.select_mesh'), icon='MESH_DATA') row.operator("avatar_toolkit.search_attach_mesh", - text=toolkit.attach_mesh, - icon='MESH_DATA') + text=toolkit.attach_mesh) row: UILayout = col.row(align=True) - row.label(text=t('CustomPanel.attachToBone')) + row.label(text=t('CustomPanel.select_bone'), icon='BONE_DATA') row.operator("avatar_toolkit.search_attach_bone", - text=toolkit.attach_bone, - icon='BONE_DATA') + text=toolkit.attach_bone) + + # Attach button with emphasis + attach_box: UILayout = layout.box() + col = attach_box.column(align=True) + row = col.row(align=True) + row.scale_y = 1.5 + row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA') - # Attach Button - attach_col: UILayout = layout.column(align=True) - attach_col.scale_y = 1.2 - attach_col.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA')