61c77cf756
- Added translation service with 3 services. - MyMemory (Free no api key needed but 1000 words a day and Skow) - Deepl (Free with API key, 500000 words a month and fast) - Libre Translate (Paid unless you host your own server, open source) - Added caching for Quick Access and the translate service to speed up the UI. Can be fast depending on the service you use/ PC specs and etc).
325 lines
16 KiB
Python
325 lines
16 KiB
Python
import bpy
|
|
from typing import Set, Dict, List, Optional, Tuple
|
|
from bpy.types import (
|
|
Operator,
|
|
Panel,
|
|
Menu,
|
|
Context,
|
|
UILayout,
|
|
WindowManager,
|
|
Object
|
|
)
|
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
from ..core.translations import t
|
|
from ..core.common import (
|
|
get_active_armature,
|
|
clear_default_objects,
|
|
get_armature_list,
|
|
get_armature_stats
|
|
)
|
|
|
|
# Module-level cache for UI performance (avoids Blender scene property write restrictions)
|
|
_validation_cache = {}
|
|
_stats_cache = {}
|
|
|
|
def clear_armature_caches():
|
|
"""Clear all armature-related caches - called when armature changes"""
|
|
global _validation_cache, _stats_cache
|
|
_validation_cache.clear()
|
|
_stats_cache.clear()
|
|
|
|
from ..functions.pose_mode import (
|
|
AvatarToolkit_OT_StartPoseMode,
|
|
AvatarToolkit_OT_StopPoseMode,
|
|
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
|
AvatarToolkit_OT_ApplyPoseAsRest
|
|
)
|
|
from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose
|
|
from ..core.importers.importer import AvatarToolKit_OT_Import
|
|
from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite
|
|
from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature
|
|
|
|
class AvatarToolKit_OT_ExportFBX(Operator):
|
|
"""Export selected objects as FBX"""
|
|
bl_idname: str = "avatar_toolkit.export_fbx"
|
|
bl_label: str = t("QuickAccess.export_fbx")
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
bpy.ops.export_scene.fbx('INVOKE_DEFAULT')
|
|
return {'FINISHED'}
|
|
|
|
class AvatarToolKit_MT_ExportMenu(Menu):
|
|
"""Export menu containing various export options"""
|
|
bl_idname: str = "AVATAR_TOOLKIT_MT_export_menu"
|
|
bl_label: str = t("QuickAccess.export")
|
|
|
|
def draw(self, context: Context) -> None:
|
|
layout: UILayout = self.layout
|
|
layout.operator(AvatarToolKit_OT_ExportFBX.bl_idname, text=t("QuickAccess.export_fbx"))
|
|
layout.operator(AvatarToolKit_OT_ExportResonite.bl_idname, text=t("QuickAccess.export_resonite"))
|
|
|
|
class AvatarToolKit_OT_ExportMenu(Operator):
|
|
"""Open the export menu"""
|
|
bl_idname: str = "avatar_toolkit.export"
|
|
bl_label: str = t("QuickAccess.export")
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
return get_active_armature(context) is not None
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
bpy.context.window_manager.popup_menu(AvatarToolKit_MT_ExportMenu.draw)
|
|
return {'FINISHED'}
|
|
|
|
class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|
"""Quick access panel for common Avatar Toolkit operations"""
|
|
bl_label: str = t("QuickAccess.label")
|
|
bl_idname: str = "OBJECT_PT_avatar_toolkit_quick_access"
|
|
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 = 0
|
|
|
|
def draw(self, context: Context) -> None:
|
|
"""Draw the panel layout"""
|
|
layout: UILayout = self.layout
|
|
props = context.scene.avatar_toolkit
|
|
|
|
# Armature Selection Box
|
|
armature_box: UILayout = layout.box()
|
|
col: UILayout = armature_box.column(align=True)
|
|
col.label(text=t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
|
|
col.separator(factor=0.5)
|
|
|
|
# Armature Selection
|
|
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
|
|
|
|
# Armature Validation (cached to improve performance)
|
|
active_armature: Optional[Object] = get_active_armature(context)
|
|
if active_armature:
|
|
# Cache validation results to avoid expensive recalculations on every draw
|
|
cache_key = f"validation_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
|
|
|
|
if cache_key not in _validation_cache:
|
|
_validation_cache[cache_key] = validate_armature(active_armature, detailed_messages=True)
|
|
|
|
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key]
|
|
|
|
# Check if this is a PMX model
|
|
is_pmx_model = False
|
|
if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')):
|
|
is_pmx_model = True
|
|
|
|
info_box = col.box()
|
|
|
|
# If it's a PMX model, display a prominent notice
|
|
if is_pmx_model:
|
|
pmx_box = info_box.box()
|
|
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
|
|
|
|
validation_mode = context.scene.avatar_toolkit.validation_mode
|
|
if validation_mode == 'STRICT':
|
|
pmx_box.label(text=t("Armature.validation.pmx_model_strict"))
|
|
pmx_box.label(text=t("Armature.validation.pmx_model_standardize"))
|
|
else:
|
|
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
|
|
|
|
if not is_valid:
|
|
# Display non-standard bones and hierarchy issues
|
|
if messages and len(messages) > 0:
|
|
# Found Bones section
|
|
validation_box = info_box.box()
|
|
row = validation_box.row()
|
|
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
|
|
if props.show_found_bones and len(messages) > 0:
|
|
for line in messages[0].split('\n'):
|
|
validation_box.label(text=line)
|
|
|
|
# Main validation status
|
|
validation_box = info_box.box()
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.label(text=t("Validation.status.failed"))
|
|
|
|
# Detailed validation message
|
|
validation_box = info_box.box()
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.label(text=t("Validation.message.failed.line1"))
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.label(text=t("Validation.message.failed.line2"))
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.label(text=t("Validation.message.failed.line3"))
|
|
|
|
# Non-Standard Bones section
|
|
validation_box = info_box.box()
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
|
|
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
|
|
if props.show_non_standard:
|
|
if non_standard_messages and len(non_standard_messages) > 0:
|
|
for message in non_standard_messages:
|
|
for line in message.split('\n'):
|
|
sub_row = validation_box.row()
|
|
sub_row.alert = True
|
|
sub_row.label(text=line)
|
|
else:
|
|
# For PMX models, if no non-standard messages but it's a PMX model,
|
|
# we should still indicate there might be non-standard bones
|
|
if is_pmx_model:
|
|
sub_row = validation_box.row()
|
|
sub_row.alert = True
|
|
sub_row.label(text=t("Armature.validation.pmx_model_basic"))
|
|
|
|
sub_row = validation_box.row()
|
|
sub_row.alert = True
|
|
sub_row.label(text=t("Armature.validation.pmx_model_strict"))
|
|
|
|
sub_row = validation_box.row()
|
|
sub_row.alert = True
|
|
sub_row.label(text=t("Armature.validation.pmx_model_standardize"))
|
|
|
|
else:
|
|
sub_row = validation_box.row()
|
|
sub_row.label(text=t("Validation.no_non_standard_issues"))
|
|
|
|
# Hierarchy Issues section
|
|
validation_box = info_box.box()
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"),
|
|
icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False)
|
|
if props.show_hierarchy:
|
|
if hierarchy_messages:
|
|
for message in hierarchy_messages:
|
|
sub_row = validation_box.row()
|
|
sub_row.alert = True
|
|
sub_row.label(text=message)
|
|
else:
|
|
sub_row = validation_box.row()
|
|
sub_row.label(text=t("Validation.no_hierarchy_issues"))
|
|
|
|
# Scale Issues section
|
|
validation_box = info_box.box()
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"),
|
|
icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False)
|
|
if props.show_scale_issues:
|
|
if scale_messages:
|
|
for scale_msg in scale_messages:
|
|
sub_row = validation_box.row()
|
|
sub_row.alert = True
|
|
sub_row.label(text=scale_msg)
|
|
else:
|
|
sub_row = validation_box.row()
|
|
sub_row.label(text=t("Validation.no_scale_issues"))
|
|
|
|
pose_box = layout.box()
|
|
col = pose_box.column(align=True)
|
|
col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA')
|
|
col.separator(factor=0.5)
|
|
col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, icon='CHECKMARK')
|
|
|
|
if props.show_tpose_validation:
|
|
validation_box = col.box()
|
|
if props.tpose_validation_result:
|
|
validation_box.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
|
|
else:
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.label(text=t("Validation.tpose.warning"), icon='ERROR')
|
|
|
|
for msg in props.tpose_validation_messages:
|
|
row = validation_box.row()
|
|
row.alert = True
|
|
row.label(text=msg.name)
|
|
else:
|
|
# If no specific issues, show acceptable message
|
|
if messages and len(messages) > 0:
|
|
info_box.label(text=messages[0], icon='INFO')
|
|
if len(messages) > 1:
|
|
info_box.label(text=messages[1])
|
|
if len(messages) > 2:
|
|
info_box.label(text=messages[2])
|
|
else:
|
|
info_box.label(text=t("Validation.no_messages"), icon='INFO')
|
|
elif is_valid and not is_acceptable:
|
|
row = info_box.row()
|
|
split = row.split(factor=0.6)
|
|
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
|
|
|
|
# Cache armature stats to avoid expensive recalculations
|
|
stats_cache_key = f"stats_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
|
|
|
|
if stats_cache_key not in _stats_cache:
|
|
_stats_cache[stats_cache_key] = get_armature_stats(active_armature)
|
|
|
|
stats = _stats_cache[stats_cache_key]
|
|
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
|
|
|
|
if stats['has_pose']:
|
|
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
|
|
elif is_valid and is_acceptable:
|
|
# Show acceptable standard message
|
|
if messages and len(messages) > 0:
|
|
info_box.label(text=messages[0], icon='INFO')
|
|
|
|
# Only try to access additional messages if they exist
|
|
if len(messages) > 1:
|
|
info_box.label(text=messages[1])
|
|
if len(messages) > 2:
|
|
info_box.label(text=messages[2])
|
|
else:
|
|
info_box.label(text=t("Validation.no_messages"), icon='INFO')
|
|
|
|
# Add standardize button
|
|
standardize_box = info_box.box()
|
|
standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname,
|
|
text=t("QuickAccess.standardize_armature"),
|
|
icon='MODIFIER')
|
|
|
|
# Validation Mode Warnings
|
|
validation_mode = context.scene.avatar_toolkit.validation_mode
|
|
if validation_mode == 'BASIC':
|
|
warning_row = info_box.box()
|
|
warning_row.alert = True
|
|
warning_row.label(text=t("QuickAccess.validation_basic_warning"), icon='INFO')
|
|
warning_row.label(text=t("QuickAccess.validation_basic_details"))
|
|
elif validation_mode == 'NONE':
|
|
warning_row = info_box.box()
|
|
warning_row.alert = True
|
|
warning_row.label(text=t("QuickAccess.validation_none_warning"), icon='ERROR')
|
|
warning_row.label(text=t("QuickAccess.validation_none_details"))
|
|
|
|
# Pose Mode Controls
|
|
pose_box: UILayout = layout.box()
|
|
col = pose_box.column(align=True)
|
|
col.label(text=t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
|
|
col.separator(factor=0.5)
|
|
|
|
if context.mode == "POSE":
|
|
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
|
|
col.separator(factor=0.5)
|
|
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, icon='MOD_ARMATURE')
|
|
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, icon='MOD_ARMATURE')
|
|
else:
|
|
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
|
|
|
|
# Import/Export Box
|
|
import_box: UILayout = layout.box()
|
|
col = import_box.column(align=True)
|
|
col.label(text=t("QuickAccess.import_export"), icon='IMPORT')
|
|
col.separator(factor=0.5)
|
|
|
|
# Import/Export Buttons
|
|
button_row: UILayout = col.row(align=True)
|
|
button_row.scale_y = 1.5
|
|
button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT')
|
|
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT')
|
|
|