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 bpy
import numpy as np import numpy as np
from mathutils import Vector
from bpy.types import Context, Object, Modifier, EditBone, Operator from bpy.types import Context, Object, Modifier, EditBone, Operator
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
from ..core.logging_setup import logger 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') 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( merge_armature_into: StringProperty(
name=t('CustomPanel.merge_into'), name=t('MergeArmature.into'),
description=t('CustomPanel.merge_into_desc'), description=t('MergeArmature.into_desc'),
default="" default=""
) )
merge_armature: StringProperty( merge_armature: StringProperty(
name=t('CustomPanel.merge_from'), name=t('MergeArmature.from'),
description=t('CustomPanel.merge_from_desc'), description=t('MergeArmature.from_desc'),
default="" default=""
) )
attach_mesh: StringProperty( attach_mesh: StringProperty(
name=t('CustomPanel.attach_mesh'), name=t('AttachMesh.select'),
description=t('CustomPanel.attach_mesh_desc'), description=t('AttachMesh.select_desc'),
default="" default=""
) )
attach_bone: StringProperty( attach_bone: StringProperty(
name=t('CustomPanel.attach_bone'), name=t('AttachBone.select'),
description=t('CustomPanel.attach_bone_desc'), description=t('AttachBone.select_desc'),
default="" default=""
) )
merge_all_bones: BoolProperty( merge_all_bones: BoolProperty(
name=t('CustomPanel.merge_all_bones'), name=t('MergeArmature.merge_all'),
description=t('CustomPanel.merge_all_bones_desc'), description=t('MergeArmature.merge_all_desc'),
default=True default=True
) )
apply_transforms: BoolProperty( apply_transforms: BoolProperty(
name=t('CustomPanel.apply_transforms'), name=t('MergeArmature.apply_transforms'),
description=t('CustomPanel.apply_transforms_desc'), description=t('MergeArmature.apply_transforms_desc'),
default=True default=True
) )
join_meshes: BoolProperty( join_meshes: BoolProperty(
name=t('CustomPanel.join_meshes'), name=t('MergeArmature.join_meshes'),
description=t('CustomPanel.join_meshes_desc'), description=t('MergeArmature.join_meshes_desc'),
default=True default=True
) )
remove_zero_weights: BoolProperty( remove_zero_weights: BoolProperty(
name=t('CustomPanel.remove_zero_weights'), name=t('MergeArmature.remove_zero_weights'),
description=t('CustomPanel.remove_zero_weights_desc'), description=t('MergeArmature.remove_zero_weights_desc'),
default=True default=True
) )
cleanup_shape_keys: BoolProperty( cleanup_shape_keys: BoolProperty(
name=t('CustomPanel.cleanup_shape_keys'), name=t('MergeArmature.cleanup_shape_keys'),
description=t('CustomPanel.cleanup_shape_keys_desc'), description=t('MergeArmature.cleanup_shape_keys_desc'),
default=True 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: def register() -> None:
"""Register the Avatar Toolkit property group""" """Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties") logger.info("Registering Avatar Toolkit properties")
+1 -29
View File
@@ -1,19 +1,15 @@
import bpy import bpy
import numpy as np import numpy as np
from typing import List, Optional, Dict, Set from typing import List, Optional, Dict, Set
from mathutils import Vector
from bpy.types import Context, Object, Operator from bpy.types import Context, Object, Operator
from ...core.logging_setup import logger from ...core.logging_setup import logger
from ...core.translations import t from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature,
get_all_meshes, get_all_meshes,
fix_zero_length_bones, fix_zero_length_bones,
clear_unused_data_blocks, clear_unused_data_blocks,
validate_armature,
join_mesh_objects, join_mesh_objects,
fix_uv_coordinates,
remove_unused_shapekeys remove_unused_shapekeys
) )
@@ -40,7 +36,7 @@ class AvatarToolkit_OT_MergeArmature(Operator):
if not base_armature or not merge_armature: if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}") 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'} return {'CANCELLED'}
# Remove Rigid Bodies and Joints # Remove Rigid Bodies and Joints
@@ -80,21 +76,6 @@ class AvatarToolkit_OT_MergeArmature(Operator):
self.report({'ERROR'}, str(e)) self.report({'ERROR'}, str(e))
return {'CANCELLED'} 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): def delete_rigidbodies_and_joints(armature: Object):
"""Delete rigid bodies and joints associated with the armature.""" """Delete rigid bodies and joints associated with the armature."""
to_delete = [] 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') vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
mesh.vertex_groups.remove(vg_from) 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): def remove_unused_vertex_groups(mesh: Object):
"""Remove vertex groups with no weights.""" """Remove vertex groups with no weights."""
for vg in mesh.vertex_groups: 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.label": "Custom Avatar Tools",
"CustomPanel.merge_mode": "Merge Mode", "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": "Armature",
"CustomPanel.mode.armature_desc": "Merge armatures together", "CustomPanel.mode.armature_desc": "Merge armatures together",
"CustomPanel.mode.mesh": "Mesh", "CustomPanel.mode.mesh": "Mesh",
"CustomPanel.mode.mesh_desc": "Attach meshes to armature", "CustomPanel.mode.mesh_desc": "Attach meshes to armature",
"CustomPanel.mergeArmatures": "Merge Armatures",
"CustomPanel.warn.twoArmatures": "Need at least two armatures to merge", "AttachMesh.label": "Attach Mesh",
"CustomPanel.warn.noArmOrMesh1": "No armature or meshes found", "AttachMesh.desc": "Attach a mesh to an armature bone with automatic weight setup",
"CustomPanel.warn.noArmOrMesh2": "Please add required objects first", "AttachMesh.search_desc": "Search for meshes to attach",
"CustomPanel.merge_into": "Merge Into", "AttachMesh.select": "Select Mesh to Attach",
"CustomPanel.merge_into_desc": "Target armature to merge into", "AttachMesh.select_desc": "Choose a mesh to attach to the armature",
"CustomPanel.merge_from": "Merge From", "AttachMesh.success": "Mesh attached successfully",
"CustomPanel.merge_from_desc": "Source armature to merge", "AttachMesh.warn_no_armature": "Select an armature and mesh to attach",
"CustomPanel.toMerge": "To Merge", "AttachMesh.validate_transforms": "Validating mesh transforms",
"CustomPanel.attachMesh1": "Attach Mesh", "AttachMesh.validate_name": "Validating mesh name",
"CustomPanel.attachMesh2": "Select Mesh", "AttachMesh.parent_mesh": "Parenting mesh to armature",
"CustomPanel.attach_mesh": "Mesh to Attach", "AttachMesh.setup_weights": "Setting up vertex weights",
"CustomPanel.attach_mesh_desc": "Select mesh to attach", "AttachMesh.create_bone": "Creating attachment bone",
"CustomPanel.attachToBone": "Attach to Bone", "AttachMesh.position_bone": "Positioning bone",
"CustomPanel.attach_bone": "Target Bone", "AttachMesh.add_modifier": "Adding armature modifier",
"CustomPanel.attach_bone_desc": "Select bone to attach to", "AttachMesh.error.bone_not_found": "Attach bone '{bone}' not found",
"CustomPanel.merge_same_bones": "Merge Same Bones", "AttachMesh.error.mesh_not_found": "Mesh not found",
"CustomPanel.merge_same_bones_desc": "Merge bones with matching names", "AttachMesh.error.non_uniform_scale": "Mesh has non-uniform scale. Please apply scale",
"CustomPanel.apply_transforms": "Apply Transforms", "AttachBone.search_desc": "Search for target bone",
"CustomPanel.apply_transforms_desc": "Apply all transformations before merging", "AttachBone.select": "Select Target Bone",
"CustomPanel.join_meshes": "Join Meshes", "AttachBone.select_desc": "Choose the bone to attach the mesh to",
"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",
"MergeArmature.label": "Merge Armatures", "MergeArmature.label": "Merge Armatures",
"MergeArmature.desc": "Merge two armatures together", "MergeArmature.desc": "Merge two armatures together",
"MergeArmature.error.notFound": "Armature '{name}' not found", "MergeArmature.options": "Merge Options",
"MergeArmature.success": "Armatures merged successfully", "MergeArmature.warn_two": "Need at least two armatures to merge",
"MergeArmature.error.checkTransforms": "Please check parent transformations", "MergeArmature.into": "Merge Into",
"MergeArmature.error.pleaseFix": "Please fix parent relationships", "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.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.label": "Settings",
"Settings.language": "Language", "Settings.language": "Language",
+62 -66
View File
@@ -13,13 +13,12 @@ from ..core.common import (
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator): class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
bl_idname = "avatar_toolkit.search_merge_armature_into" bl_idname = "avatar_toolkit.search_merge_armature_into"
bl_label = "" bl_label = ""
bl_description = t('CustomPanel.search_merge_into_desc') bl_description = t('MergeArmature.into_search_desc')
bl_property = "search_merge_armature_into_enum" bl_property = "search_merge_armature_into_enum"
# Define the enum property within the operator class
search_merge_armature_into_enum: bpy.props.EnumProperty( search_merge_armature_into_enum: bpy.props.EnumProperty(
name=t('CustomPanel.merge_into'), name=t('MergeArmature.into'),
description=t('CustomPanel.merge_into_desc'), description=t('MergeArmature.into_desc'),
items=get_armature_list items=get_armature_list
) )
@@ -34,12 +33,12 @@ class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
class AvatarToolkit_OT_SearchMergeArmature(Operator): class AvatarToolkit_OT_SearchMergeArmature(Operator):
bl_idname = "avatar_toolkit.search_merge_armature" bl_idname = "avatar_toolkit.search_merge_armature"
bl_label = "" bl_label = ""
bl_description = t('CustomPanel.search_merge_desc') bl_description = t('MergeArmature.from_search_desc')
bl_property = "search_merge_armature_enum" bl_property = "search_merge_armature_enum"
search_merge_armature_enum: bpy.props.EnumProperty( search_merge_armature_enum: bpy.props.EnumProperty(
name=t('CustomPanel.merge_from'), name=t('MergeArmature.from'),
description=t('CustomPanel.merge_from_desc'), description=t('MergeArmature.from_desc'),
items=get_armature_list items=get_armature_list
) )
@@ -54,15 +53,17 @@ class AvatarToolkit_OT_SearchMergeArmature(Operator):
class AvatarToolkit_OT_SearchAttachMesh(Operator): class AvatarToolkit_OT_SearchAttachMesh(Operator):
bl_idname = "avatar_toolkit.search_attach_mesh" bl_idname = "avatar_toolkit.search_attach_mesh"
bl_label = "" bl_label = ""
bl_description = t('CustomPanel.search_mesh_desc') bl_description = t('AttachMesh.search_desc')
bl_property = "search_attach_mesh_enum" bl_property = "search_attach_mesh_enum"
search_attach_mesh_enum: bpy.props.EnumProperty( search_attach_mesh_enum: bpy.props.EnumProperty(
name=t('CustomPanel.attach_mesh'), name=t('AttachMesh.select'),
description=t('CustomPanel.attach_mesh_desc'), description=t('AttachMesh.select_desc'),
items=lambda self, context: [ items=lambda self, context: [
(obj.name, obj.name, "") (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): class AvatarToolkit_OT_SearchAttachBone(Operator):
bl_idname = "avatar_toolkit.search_attach_bone" bl_idname = "avatar_toolkit.search_attach_bone"
bl_label = "" bl_label = ""
bl_description = t('CustomPanel.search_bone_desc') bl_description = t('AttachBone.search_desc')
bl_property = "search_attach_bone_enum" bl_property = "search_attach_bone_enum"
search_attach_bone_enum: bpy.props.EnumProperty( search_attach_bone_enum: bpy.props.EnumProperty(
name=t('CustomPanel.attach_bone'), name=t('AttachBone.select'),
description=t('CustomPanel.attach_bone_desc'), description=t('AttachBone.select_desc'),
items=lambda self, context: [ items=lambda self, context: [
(bone.name, bone.name, "") (bone.name, bone.name, "")
for bone in get_active_armature(context).data.bones for bone in get_active_armature(context).data.bones
@@ -109,7 +110,6 @@ class AvatarToolKit_PT_CustomPanel(Panel):
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the custom avatar tools panel interface"""
layout: UILayout = self.layout layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit 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.label(text=t('CustomPanel.merge_mode'), icon='TOOL_SETTINGS')
col.separator(factor=0.5) col.separator(factor=0.5)
# Create a row for the mode buttons with increased scale
row: UILayout = col.row(align=True) row: UILayout = col.row(align=True)
row.scale_y = 1.5 row.scale_y = 1.5
row.prop(toolkit, "merge_mode", expand=True) row.prop(toolkit, "merge_mode", expand=True)
# Armature Merging Tools
if toolkit.merge_mode == 'ARMATURE': if toolkit.merge_mode == 'ARMATURE':
self.draw_armature_tools(layout, context) self.draw_armature_tools(layout, context)
# Mesh Attachment Tools
else: else:
self.draw_mesh_tools(layout, context) self.draw_mesh_tools(layout, context)
def draw_armature_tools(self, layout: UILayout, context: Context) -> None: def draw_armature_tools(self, layout: UILayout, context: Context) -> None:
"""Draw the armature merging tools section"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Merge Settings Box # Merge Settings Box
settings_box: UILayout = layout.box() settings_box: UILayout = layout.box()
col: UILayout = settings_box.column(align=True) 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) col.separator(factor=0.5)
if len(get_armature_list(context)) <= 1: 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 return
# Merge Options # Options Box with better spacing
options_box: UILayout = layout.box() options_box: UILayout = layout.box()
col: UILayout = options_box.column(align=True) col: UILayout = options_box.column(align=True)
col.label(text=t('Tools.merge_title'), icon='SETTINGS') col.label(text=t('MergeArmature.options'), icon='SETTINGS')
col.separator(factor=0.5) 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 # 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() selection_box: UILayout = layout.box()
col: UILayout = selection_box.column(align=True) col: UILayout = selection_box.column(align=True)
col.label(text=t('QuickAccess.select_armature'), icon='BONE_DATA') col.label(text=t('CustomPanel.select_armature'), icon='BONE_DATA')
col.separator(factor=0.5) col.separator(factor=0.5)
# Armature selection with better alignment
row: UILayout = col.row(align=True) 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", row.operator("avatar_toolkit.search_merge_armature_into",
text=toolkit.merge_armature_into, text=toolkit.merge_armature_into)
icon='ARMATURE_DATA')
row: UILayout = col.row(align=True) 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", row.operator("avatar_toolkit.search_merge_armature",
text=toolkit.merge_armature, text=toolkit.merge_armature)
icon='ARMATURE_DATA')
# Merge Button # Merge button with emphasis
merge_col: UILayout = layout.column(align=True) merge_box: UILayout = layout.box()
merge_col.scale_y = 1.2 col = merge_box.column(align=True)
merge_col.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA') 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: def draw_mesh_tools(self, layout: UILayout, context: Context) -> None:
"""Draw the mesh attachment tools section"""
toolkit = context.scene.avatar_toolkit toolkit = context.scene.avatar_toolkit
# Mesh Tools Box # Mesh Tools Box
tools_box: UILayout = layout.box() tools_box: UILayout = layout.box()
col: UILayout = tools_box.column(align=True) 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) col.separator(factor=0.5)
if not get_active_armature(context) or not get_all_meshes(context): 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('AttachMesh.warn_no_armature'), icon='INFO')
col.label(text=t('CustomPanel.warn.noArmOrMesh2'))
return return
# Mesh Options Box # Selection Box with consistent styling
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: UILayout = layout.box() selection_box: UILayout = layout.box()
col: UILayout = selection_box.column(align=True) 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) col.separator(factor=0.5)
# Selection rows with icons and better alignment
row: UILayout = col.row(align=True) 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", row.operator("avatar_toolkit.search_merge_armature_into",
text=toolkit.merge_armature_into, text=toolkit.merge_armature_into)
icon='ARMATURE_DATA')
row: UILayout = col.row(align=True) 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", row.operator("avatar_toolkit.search_attach_mesh",
text=toolkit.attach_mesh, text=toolkit.attach_mesh)
icon='MESH_DATA')
row: UILayout = col.row(align=True) 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", row.operator("avatar_toolkit.search_attach_bone",
text=toolkit.attach_bone, text=toolkit.attach_bone)
icon='BONE_DATA')
# 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')