Attach Meshes

This commit is contained in:
Yusarina
2024-12-16 12:29:35 +00:00
parent 847bf68f9d
commit c081b89233
6 changed files with 300 additions and 152 deletions
+24
View File
@@ -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
+28 -18
View File
@@ -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")
+1 -29
View File
@@ -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:
+130
View File
@@ -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, ""
+51 -35
View File
@@ -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",
+66 -70
View File
@@ -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')