07adaa590b
also merge all bones isn't needed. we should do that by default This also now uses dictionary matching to find bone types like hips, spine, and chest that should be merged. Deletes bone shared and merges armatures, and parents bones back, causing a seamless merge.
233 lines
9.1 KiB
Python
233 lines
9.1 KiB
Python
import bpy
|
|
from typing import Set, List, Tuple, Any
|
|
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
from ..core.translations import t
|
|
from ..core.common import (
|
|
get_active_armature,
|
|
get_all_meshes,
|
|
get_armature_list
|
|
)
|
|
from ..core.armature_validation import validate_armature
|
|
|
|
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
|
"""Search operator for selecting target armature to merge into"""
|
|
bl_idname: str = "avatar_toolkit.search_merge_armature_into"
|
|
bl_label: str = ""
|
|
bl_description: str = t('MergeArmature.into_search_desc')
|
|
bl_property: str = "search_merge_armature_into_enum"
|
|
|
|
search_merge_armature_into_enum: bpy.props.EnumProperty(
|
|
name=t('MergeArmature.into'),
|
|
description=t('MergeArmature.into_desc'),
|
|
items=get_armature_list
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
context.scene.avatar_toolkit.merge_armature_into = self.search_merge_armature_into_enum
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
|
context.window_manager.invoke_search_popup(self)
|
|
return {'FINISHED'}
|
|
|
|
class AvatarToolkit_OT_SearchMergeArmature(Operator):
|
|
"""Search operator for selecting source armature to merge from"""
|
|
bl_idname: str = "avatar_toolkit.search_merge_armature"
|
|
bl_label: str = ""
|
|
bl_description: str = t('MergeArmature.from_search_desc')
|
|
bl_property: str = "search_merge_armature_enum"
|
|
|
|
search_merge_armature_enum: bpy.props.EnumProperty(
|
|
name=t('MergeArmature.from'),
|
|
description=t('MergeArmature.from_desc'),
|
|
items=get_armature_list
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
context.scene.avatar_toolkit.merge_armature = self.search_merge_armature_enum
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
|
context.window_manager.invoke_search_popup(self)
|
|
return {'FINISHED'}
|
|
|
|
class AvatarToolkit_OT_SearchAttachMesh(Operator):
|
|
"""Search operator for selecting mesh to attach to armature"""
|
|
bl_idname: str = "avatar_toolkit.search_attach_mesh"
|
|
bl_label: str = ""
|
|
bl_description: str = t('AttachMesh.search_desc')
|
|
bl_property: str = "search_attach_mesh_enum"
|
|
|
|
search_attach_mesh_enum: bpy.props.EnumProperty(
|
|
name=t('AttachMesh.select'),
|
|
description=t('AttachMesh.select_desc'),
|
|
items=lambda self, context: [
|
|
(obj.name, obj.name, "")
|
|
for obj in bpy.data.objects
|
|
if obj.type == 'MESH'
|
|
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
|
|
]
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
context.scene.avatar_toolkit.attach_mesh = self.search_attach_mesh_enum
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
|
context.window_manager.invoke_search_popup(self)
|
|
return {'FINISHED'}
|
|
|
|
class AvatarToolkit_OT_SearchAttachBone(Operator):
|
|
"""Search operator for selecting bone to attach mesh to"""
|
|
bl_idname: str = "avatar_toolkit.search_attach_bone"
|
|
bl_label: str = ""
|
|
bl_description: str = t('AttachBone.search_desc')
|
|
bl_property: str = "search_attach_bone_enum"
|
|
|
|
search_attach_bone_enum: bpy.props.EnumProperty(
|
|
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
|
|
] if get_active_armature(context) else []
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
context.scene.avatar_toolkit.attach_bone = self.search_attach_bone_enum
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
|
context.window_manager.invoke_search_popup(self)
|
|
return {'FINISHED'}
|
|
|
|
class AvatarToolKit_PT_CustomPanel(Panel):
|
|
"""Panel containing tools for custom avatar creation and merging"""
|
|
bl_label: str = t('CustomPanel.label')
|
|
bl_idname: str = "VIEW3D_PT_avatar_toolkit_custom"
|
|
bl_space_type: str = 'VIEW_3D'
|
|
bl_region_type: str = 'UI'
|
|
bl_category: str = CATEGORY_NAME
|
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
bl_order: int = 4
|
|
bl_options: Set[str] = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context: Context) -> None:
|
|
"""Draw the custom avatar panel UI"""
|
|
layout: UILayout = self.layout
|
|
toolkit = context.scene.avatar_toolkit
|
|
|
|
# Mode Selection Box
|
|
mode_box: UILayout = layout.box()
|
|
col: UILayout = mode_box.column(align=True)
|
|
col.label(text=t('CustomPanel.merge_mode'), icon='TOOL_SETTINGS')
|
|
col.separator(factor=0.5)
|
|
|
|
row: UILayout = col.row(align=True)
|
|
row.scale_y = 1.5
|
|
row.prop(toolkit, "merge_mode", expand=True)
|
|
|
|
if toolkit.merge_mode == 'ARMATURE':
|
|
self.draw_armature_tools(layout, context)
|
|
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('MergeArmature.label'), icon='ARMATURE_DATA')
|
|
col.separator(factor=0.5)
|
|
|
|
if len(get_armature_list(context)) <= 1:
|
|
col.label(text=t('MergeArmature.warn_two'), icon='INFO')
|
|
return
|
|
|
|
# Options Box with better spacing
|
|
options_box: UILayout = layout.box()
|
|
col: UILayout = options_box.column(align=True)
|
|
col.label(text=t('MergeArmature.options'), icon='SETTINGS')
|
|
col.separator(factor=0.5)
|
|
|
|
# Group related options together
|
|
transform_col: UILayout = col.column(align=True)
|
|
transform_col.prop(toolkit, "apply_transforms")
|
|
|
|
col.separator(factor=0.5)
|
|
|
|
cleanup_col: UILayout = 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('MergeArmature.into'), icon='ARMATURE_DATA')
|
|
row.operator("avatar_toolkit.search_merge_armature_into",
|
|
text=toolkit.merge_armature_into)
|
|
|
|
row: UILayout = col.row(align=True)
|
|
row.label(text=t('MergeArmature.from'), icon='ARMATURE_DATA')
|
|
row.operator("avatar_toolkit.search_merge_armature",
|
|
text=toolkit.merge_armature)
|
|
|
|
# Merge button with emphasis
|
|
merge_box: UILayout = layout.box()
|
|
col: UILayout = merge_box.column(align=True)
|
|
row: UILayout = 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('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('AttachMesh.warn_no_armature'), icon='INFO')
|
|
return
|
|
|
|
# Selection Box with consistent styling
|
|
selection_box: UILayout = layout.box()
|
|
col: UILayout = selection_box.column(align=True)
|
|
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.select_armature'), icon='ARMATURE_DATA')
|
|
row.operator("avatar_toolkit.search_merge_armature_into",
|
|
text=toolkit.merge_armature_into)
|
|
|
|
row: UILayout = col.row(align=True)
|
|
row.label(text=t('CustomPanel.select_mesh'), icon='MESH_DATA')
|
|
row.operator("avatar_toolkit.search_attach_mesh",
|
|
text=toolkit.attach_mesh)
|
|
|
|
row: UILayout = col.row(align=True)
|
|
row.label(text=t('CustomPanel.select_bone'), icon='BONE_DATA')
|
|
row.operator("avatar_toolkit.search_attach_bone",
|
|
text=toolkit.attach_bone)
|
|
|
|
# Attach button with emphasis
|
|
attach_box: UILayout = layout.box()
|
|
col: UILayout = attach_box.column(align=True)
|
|
row: UILayout = col.row(align=True)
|
|
row.scale_y = 1.5
|
|
row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA')
|