diff --git a/blender_manifest.toml b/blender_manifest.toml index 9578ecd..5caf9c5 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" id = "avatar_toolkit" -version = "0.5.0" +version = "0.6.0" name = "Avatar Toolkit" tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." maintainer = "Team NekoNeo" @@ -16,10 +16,10 @@ license = [ ] wheels = [ - "./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl", - "./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl", - "./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl" + "./wheels/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", + "./wheels/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", + "./wheels/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "./wheels/lz4-4.4.5-cp311-cp311-win_amd64.whl" ] [permissions] diff --git a/core/addon_preferences.py b/core/addon_preferences.py index 31f580d..2234447 100644 --- a/core/addon_preferences.py +++ b/core/addon_preferences.py @@ -63,6 +63,6 @@ def get_addon_preferences(context): # Initialize preferences if the file doesn't exist if not os.path.exists(PREFERENCES_FILE): save_preference("language", 0) # Set default language to 0 (auto) - save_preference("validation_mode", "STRICT") # Set default validation mode + save_preference("validation_mode", "NONE") # Set default validation mode to NONE (off by default) save_preference("enable_logging", False) # Set default logging mode save_preference("highlight_problem_bones", True) # Set default bone highlighting diff --git a/core/armature_validation.py b/core/armature_validation.py index 570c098..a7a82a7 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -15,6 +15,26 @@ from ..core.dictionaries import ( ) from ..core.logging_setup import logger +def is_pmx_model(armature: Object) -> bool: + """ + Check if the armature is a PMX/MMD model. + PMX models have an mmd_type attribute set to 'ROOT' on the root object. + """ + if not armature: + return False + + # Check if armature itself has mmd_type set to ROOT + if hasattr(armature, 'mmd_type') and armature.mmd_type == 'ROOT': + return True + + # Check if parent has mmd_type set to ROOT (parent container model) + if hasattr(armature, 'parent') and armature.parent: + parent = armature.parent + if hasattr(parent, 'mmd_type') and parent.mmd_type == 'ROOT': + return True + + return False + def validate_armature(armature: Object, detailed_messages: bool = False, override_mode: Optional[str] = None) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]: """ Validates armature and returns validation results @@ -27,9 +47,8 @@ def validate_armature(armature: Object, detailed_messages: bool = False, overrid scale_messages: List[str] = [] # Check if this is a PMX model - is_pmx_model = False - if armature and hasattr(armature, 'mmd_type') or (hasattr(armature, 'parent') and armature.parent and hasattr(armature.parent, 'mmd_type')): - is_pmx_model = True + pmx_model = is_pmx_model(armature) + if pmx_model: logger.debug("Detected PMX model, using specialized validation") if validation_mode == 'NONE': @@ -157,7 +176,7 @@ def validate_armature(armature: Object, detailed_messages: bool = False, overrid non_standard_messages.append(t("Armature.validation.standardize_note.line3")) # Special handling for PMX models - if is_pmx_model: + if pmx_model: logger.info("PMX model detected, applying specialized validation") # For PMX models, we'll be more lenient with validation # and provide specific guidance for these models @@ -783,3 +802,44 @@ class AvatarToolkit_OT_ClearBoneHighlighting(Operator): logger.info("Bone highlighting cleared") self.report({'INFO'}, t("Validation.highlighting_cleared")) return {'FINISHED'} + +class AvatarToolkit_OT_ValidateArmatureManual(Operator): + """Manually validate armature and show results""" + bl_idname = "avatar_toolkit.validate_armature_manual" + bl_label = t("Validation.validate_now", "Validate Armature Now") + bl_description = t("Validation.validate_now_desc", "Run armature validation and display detailed results") + + @classmethod + def poll(cls, context): + return get_active_armature(context) is not None + + def execute(self, context): + armature = get_active_armature(context) + if not armature: + logger.warning("No active armature found for validation") + self.report({'ERROR'}, t("Validation.no_armature")) + return {'CANCELLED'} + + logger.info(f"Running manual validation for armature: {armature.name}") + + # Clear the validation cache to force a refresh + from ..ui.quick_access_panel import clear_armature_caches + clear_armature_caches() + + # Toggle the show_validation_results flag to display results + props = context.scene.avatar_toolkit + props.show_validation_results = True + + # Run validation + is_valid, messages, is_acceptable = validate_armature(armature, detailed_messages=False) + + if is_valid: + if is_acceptable: + self.report({'INFO'}, t("Armature.validation.acceptable_standard.success")) + else: + self.report({'INFO'}, t("QuickAccess.valid_armature")) + else: + self.report({'WARNING'}, t("Validation.status.failed")) + + logger.info("Manual validation complete") + return {'FINISHED'} diff --git a/core/properties.py b/core/properties.py index 4e6ea22..4db9744 100644 --- a/core/properties.py +++ b/core/properties.py @@ -34,6 +34,11 @@ def update_validation_mode(self: PropertyGroup, context: Context) -> None: """Updates validation mode and saves preference""" logger.info(f"Updating validation mode to: {self.validation_mode}") save_preference("validation_mode", self.validation_mode) + + # Hide validation results if mode is set to NONE + if self.validation_mode == 'NONE': + self.show_validation_results = False + logger.debug("Validation mode set to NONE, hiding validation results") def update_logging_state(self: PropertyGroup, context: Context) -> None: @@ -153,6 +158,12 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=False ) + show_validation_results: BoolProperty( + name="Show Validation Results", + default=False, + description="Show the validation results section" + ) + material_search_filter: StringProperty( name=t("TextureAtlas.search_materials"), description=t("TextureAtlas.search_materials_desc"), @@ -283,7 +294,7 @@ class AvatarToolkitSceneProperties(PropertyGroup): ('BASIC', t("Settings.validation_mode.basic"), t("Settings.validation_mode.basic_desc")), ('NONE', t("Settings.validation_mode.none"), t("Settings.validation_mode.none_desc")) ], - default=get_preference("validation_mode", "STRICT"), + default=get_preference("validation_mode", "NONE"), update=update_validation_mode ) diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 1fdef90..bcb8ec5 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.5.0)", + "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.6.0)", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc3": "please report it on our Github.", @@ -117,6 +117,15 @@ "Validation.clear_bone_highlighting": "Clear Bone Highlighting", "Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default", "Validation.highlighting_cleared": "Bone highlighting cleared successfully", + "Validation.label": "Armature Validation", + "Validation.validate_now": "Validate Armature Now", + "Validation.validate_now_desc": "Run armature validation and display detailed results", + "Validation.results": "Validation Results", + "Validation.tpose.validate_now": "Validate T-Pose Now", + + "Armature.validation.acceptable_standard.success": "Armature meets acceptable standards", + "Armature.validation.acceptable_standard.note": "This is a valid armature format that is compatible with most avatar systems", + "Armature.validation.acceptable_standard.option": "You can standardize the armature if desired", "Mesh.validation.no_data": "No mesh data", "Mesh.validation.no_vertex_groups": "No vertex groups found", @@ -191,6 +200,7 @@ "Tools.digitigrade_error": "Failed to create digitigrade legs: {error}", "Tools.digitigrade_success": "Successfully created digitigrade leg setup", "Tools.processing_leg": "Processing leg bone: {bone}", + "Tools.weight_title": "Weight Tools", "Tools.merge_twist_bones": "Keep Twist Bones", "Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight", "Tools.clean_weights": "Remove Zero Weight Bones", @@ -587,7 +597,6 @@ "VRM.validation.hierarchy_issues": "Conversion completed but hierarchy validation found issues:", "VRM.validation.armature_passed": "Armature passes standard validation", "VRM.validation.failed": "Conversion completed but validation failed: {error}", - "VRM.remove_colliders": "Remove Colliders", "VRM.remove_colliders_desc": "Remove VRM collider bones during conversion", "VRM.remove_root": "Remove Root Bone", "VRM.remove_root_desc": "Remove unnecessary VRM root bone and make Hips the root bone", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 9761092..6f85670 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "アバターツールキット (アルファ 0.5.0)", + "AvatarToolkit.label": "アバターツールキット (アルファ 0.6.0)", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc3": "GitHubで報告してください。", @@ -117,6 +117,15 @@ "Validation.clear_bone_highlighting": "ボーンの強調表示をクリア", "Validation.clear_bone_highlighting_desc": "ボーンの強調表示を削除し、ボーンの色をデフォルトにリセット", "Validation.highlighting_cleared": "ボーンの強調表示が正常にクリアされました", + "Validation.label": "アーマチュア検証", + "Validation.validate_now": "アーマチュアを検証する", + "Validation.validate_now_desc": "アーマチュア検証を実行し、詳細な結果を表示", + "Validation.results": "検証結果", + "Validation.tpose.validate_now": "T-ポーズを検証する", + + "Armature.validation.acceptable_standard.success": "アーマチュアが許容可能な標準を満たしています", + "Armature.validation.acceptable_standard.note": "これは、ほとんどのアバターシステムと互換性のある有効なアーマチュア形式です", + "Armature.validation.acceptable_standard.option": "必要に応じてアーマチュアを標準化できます", "Mesh.validation.no_data": "メッシュデータがありません", "Mesh.validation.no_vertex_groups": "頂点グループが見つかりません", @@ -194,6 +203,7 @@ "Tools.digitigrade_error": "デジティグレード脚の作成に失敗: {error}", "Tools.digitigrade_success": "デジティグレード脚の設定が正常に作成されました", "Tools.processing_leg": "脚のボーンを処理中: {bone}", + "Tools.weight_title": "ウェイトツール", "Tools.merge_twist_bones": "ツイストボーンを保持", "Tools.merge_twist_bones_desc": "チェックすると、ウェイトがゼロでもツイストボーンが保持されます", "Tools.clean_weights": "ゼロウェイトボーンを削除", @@ -549,7 +559,6 @@ "VRM.armature_name": "アーマチュア: {name}", "VRM.armature_detected": "VRMアーマチュアが検出されました", "VRM.no_vrm_bones_detected": "VRMボーンが検出されませんでした", - "VRM.remove_colliders": "コライダーを削除", "VRM.remove_root_bone": "ルートボーンを削除", "VRM.convert_to_unity_format": "Unity形式に変換", "VRM.convert_to_unity.label": "VRMをUnityに変換", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index 2e000c7..4d00a88 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "아바타 툴킷 (알파 0.5.0)", + "AvatarToolkit.label": "아바타 툴킷 (알파 0.6.0)", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc3": "Github에 보고해 주세요.", @@ -117,6 +117,15 @@ "Validation.clear_bone_highlighting": "본 강조 표시 지우기", "Validation.clear_bone_highlighting_desc": "본 강조 표시를 제거하고 본 색상을 기본값으로 재설정", "Validation.highlighting_cleared": "본 강조 표시 지우기 성공", + "Validation.label": "아마추어 검증", + "Validation.validate_now": "지금 아마추어 검증", + "Validation.validate_now_desc": "아마추어 검증을 실행하고 자세한 결과 표시", + "Validation.results": "검증 결과", + "Validation.tpose.validate_now": "지금 T-포즈 검증", + + "Armature.validation.acceptable_standard.success": "아마추어가 허용 가능한 표준을 충족합니다", + "Armature.validation.acceptable_standard.note": "이것은 대부분의 아바타 시스템과 호환되는 유효한 아마추어 형식입니다", + "Armature.validation.acceptable_standard.option": "필요한 경우 아마추어를 표준화할 수 있습니다", "Mesh.validation.no_data": "메시 데이터 없음", "Mesh.validation.no_vertex_groups": "버텍스 그룹을 찾을 수 없음", @@ -194,6 +203,7 @@ "Tools.digitigrade_error": "디지티그레이드 다리 생성 실패: {error}", "Tools.digitigrade_success": "디지티그레이드 다리 설정 생성 성공", "Tools.processing_leg": "다리 본 처리 중: {bone}", + "Tools.weight_title": "가중치 도구", "Tools.merge_twist_bones": "트위스트 본 유지", "Tools.merge_twist_bones_desc": "체크하면 가중치가 0이더라도 트위스트 본이 유지됩니다", "Tools.clean_weights": "가중치 0인 본 제거", @@ -549,7 +559,6 @@ "VRM.armature_name": "아마추어: {name}", "VRM.armature_detected": "VRM 아마추어 감지됨", "VRM.no_vrm_bones_detected": "VRM 본이 감지되지 않음", - "VRM.remove_colliders": "콜라이더 제거", "VRM.remove_root_bone": "루트 본 제거", "VRM.convert_to_unity_format": "Unity 형식으로 변환", "VRM.convert_to_unity.label": "VRM을 Unity로 변환", diff --git a/ui/atlas_materials_panel.py b/ui/atlas_materials_panel.py index 2d1a31e..cba1782 100644 --- a/ui/atlas_materials_panel.py +++ b/ui/atlas_materials_panel.py @@ -2,6 +2,7 @@ from bpy.types import UIList, Panel, UILayout, Object, Context, Material, Operat import bpy from math import sqrt from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .panel_layout import get_panel_order, should_open_by_default from ..core.common import SceneMatClass, MaterialListBool, get_active_armature from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials from ..core.translations import t @@ -214,7 +215,8 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 7 + bl_order = get_panel_order('texture_atlas') + bl_options = set() if not should_open_by_default('TEXTURE_ATLAS') else {'DEFAULT_CLOSED'} def draw(self, context: Context): layout = self.layout diff --git a/ui/custom_avatar_panel.py b/ui/custom_avatar_panel.py index 451c88d..b80ca0a 100644 --- a/ui/custom_avatar_panel.py +++ b/ui/custom_avatar_panel.py @@ -2,6 +2,7 @@ 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 .panel_layout import get_panel_order, should_open_by_default from ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature from ..core.translations import t @@ -112,8 +113,8 @@ class AvatarToolKit_PT_CustomPanel(Panel): 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'} + bl_order: int = get_panel_order('custom_avatar') + bl_options: Set[str] = set() if not should_open_by_default('CUSTOM_AVATAR') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draw the custom avatar panel UI""" diff --git a/ui/eye_tracking_panel.py b/ui/eye_tracking_panel.py index cf7b6c4..c7f542b 100644 --- a/ui/eye_tracking_panel.py +++ b/ui/eye_tracking_panel.py @@ -2,6 +2,8 @@ import bpy from typing import Set from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .ui_utils import UIStyle, draw_section_header, wrap_text_label +from .panel_layout import get_panel_order, should_open_by_default from ..core.translations import t from ..core.common import get_active_armature, get_all_meshes from ..functions.eye_tracking import ( @@ -26,38 +28,37 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 6 - bl_options: Set[str] = {'DEFAULT_CLOSED'} + bl_order: int = get_panel_order('eye_tracking') + bl_options: Set[str] = set() if not should_open_by_default('EYE_TRACKING') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draw the eye tracking panel interface""" layout: UILayout = self.layout toolkit = context.scene.avatar_toolkit - # SDK Version Selection Box - sdk_box: UILayout = layout.box() - col: UILayout = sdk_box.column(align=True) - col.label(text=t("EyeTracking.sdk_version"), icon='PRESET') - col.separator(factor=0.5) + # SDK Version Selection + col = draw_section_header(layout, t("EyeTracking.sdk_version"), icon='PRESET') row: UILayout = col.row(align=True) row.prop(toolkit, "eye_tracking_type", expand=True) if toolkit.eye_tracking_type == 'SDK2': - # SDK2 Warning Box + # SDK2 Warning warning_box: UILayout = layout.box() col: UILayout = warning_box.column(align=True) - col.label(text=t("EyeTracking.sdk2_warning"), icon='INFO') - col.separator(factor=0.5) - col.label(text=t("EyeTracking.sdk2_warning_detail1")) - col.label(text=t("EyeTracking.sdk2_warning_detail2")) - col.label(text=t("EyeTracking.sdk2_warning_detail3")) - col.label(text=t("EyeTracking.sdk2_warning_detail4")) + col.alert = True + col.label(text=t("EyeTracking.sdk2_warning"), icon='ERROR') + col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR) - # Mode Selection Box - mode_box: UILayout = layout.box() - col: UILayout = mode_box.column(align=True) - col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS') - col.separator(factor=0.5) + warning_text = "\n".join([ + t("EyeTracking.sdk2_warning_detail1"), + t("EyeTracking.sdk2_warning_detail2"), + t("EyeTracking.sdk2_warning_detail3"), + t("EyeTracking.sdk2_warning_detail4") + ]) + wrap_text_label(col, warning_text, max_length=45) + + # Mode Selection + col = draw_section_header(layout, t("EyeTracking.setup"), icon='TOOL_SETTINGS') col.prop(toolkit, "eye_mode", expand=True) if toolkit.eye_mode == 'CREATION': @@ -72,11 +73,9 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): """Draw the AV3 eye tracking setup interface""" toolkit = context.scene.avatar_toolkit - # Bone Setup Box - bone_box: UILayout = layout.box() - col: UILayout = bone_box.column(align=True) - col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA') - col.separator(factor=0.5) + # Bone Setup + col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA') + armature = get_active_armature(context) if armature: @@ -86,21 +85,16 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): else: col.label(text=t("EyeTracking.no_armature"), icon='ERROR') - # Create Button row: UILayout = layout.row(align=True) - row.scale_y = 1.5 + row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY') def draw_creation_mode(self, context: Context, layout: UILayout) -> None: """Draw the eye tracking creation mode interface""" toolkit = context.scene.avatar_toolkit - # Bone Setup Box - bone_box: UILayout = layout.box() - col: UILayout = bone_box.column(align=True) - col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA') - col.separator(factor=0.5) - + # Bone Setup + col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA') armature = get_active_armature(context) if armature: col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone")) @@ -109,19 +103,12 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): else: col.label(text=t("EyeTracking.no_armature"), icon='ERROR') - # Mesh Setup Box - mesh_box: UILayout = layout.box() - col: UILayout = mesh_box.column(align=True) - col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA') - col.separator(factor=0.5) + # Mesh Setup + col = draw_section_header(layout, t("EyeTracking.mesh_setup"), icon='MESH_DATA') col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="") - # Shape Key Setup Box - shape_box: UILayout = layout.box() - col: UILayout = shape_box.column(align=True) - col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA') - col.separator(factor=0.5) - + # Shape Key Setup + col = draw_section_header(layout, t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA') mesh = bpy.data.objects.get(toolkit.mesh_name_eye) if mesh and mesh.data.shape_keys: col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left")) @@ -131,19 +118,15 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): else: col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR') - # Options Box - options_box: UILayout = layout.box() - col: UILayout = options_box.column(align=True) - col.label(text=t("EyeTracking.options"), icon='SETTINGS') - col.separator(factor=0.5) + # Options + col = draw_section_header(layout, t("EyeTracking.options"), icon='SETTINGS') col.prop(toolkit, "disable_eye_blinking") col.prop(toolkit, "disable_eye_movement") if not toolkit.disable_eye_movement: col.prop(toolkit, "eye_distance") - # Create Button row: UILayout = layout.row(align=True) - row.scale_y = 1.5 + row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY') def draw_testing_mode(self, context: Context, layout: UILayout) -> None: @@ -151,37 +134,25 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): toolkit = context.scene.avatar_toolkit if context.mode != 'POSE': - # Testing Start Box - test_box: UILayout = layout.box() - col: UILayout = test_box.column(align=True) - col.label(text=t("EyeTracking.testing"), icon='PLAY') - col.separator(factor=0.5) + # Testing Start + col = draw_section_header(layout, t("EyeTracking.testing"), icon='PLAY') row: UILayout = col.row(align=True) - row.scale_y = 1.5 + row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE row.operator(StartTestingButton.bl_idname, icon='PLAY') else: - # Eye Rotation Box - rotation_box: UILayout = layout.box() - col: UILayout = rotation_box.column(align=True) - col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE') - col.separator(factor=0.5) + # Eye Rotation + col = draw_section_header(layout, t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE') col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x")) col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y")) col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK') - # Eye Adjustment Box - adjust_box: UILayout = layout.box() - col: UILayout = adjust_box.column(align=True) - col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER') - col.separator(factor=0.5) + # Eye Adjustment + col = draw_section_header(layout, t("EyeTracking.adjustments"), icon='MODIFIER') col.prop(toolkit, "eye_distance") col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO') - # Blinking Test Box - blink_box: UILayout = layout.box() - col: UILayout = blink_box.column(align=True) - col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF') - col.separator(factor=0.5) + # Blinking Test + col = draw_section_header(layout, t("EyeTracking.blink_testing"), icon='HIDE_OFF') row: UILayout = col.row(align=True) row.prop(toolkit, "eye_blink_shape") row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF') @@ -192,7 +163,7 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): # Stop Testing Button row: UILayout = layout.row(align=True) - row.scale_y = 1.5 + row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE row.operator(StopTestingButton.bl_idname, icon='PAUSE') # Reset Button diff --git a/ui/main_panel.py b/ui/main_panel.py index 16bf637..b07b11d 100644 --- a/ui/main_panel.py +++ b/ui/main_panel.py @@ -1,6 +1,7 @@ import bpy from typing import Optional, Set from bpy.types import Panel, Context, UILayout +from .ui_utils import UIStyle, wrap_text_label from ..core.translations import t CATEGORY_NAME: str = "Avatar Toolkit" @@ -16,13 +17,14 @@ def draw_title(self: Panel) -> None: row.scale_y: float = 1.2 row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA') - # Description as a flowing paragraph - desc_col: UILayout = col.column() - desc_col.scale_y: float = 0.6 - desc_col.label(text=t("AvatarToolkit.desc1")) - desc_col.label(text=t("AvatarToolkit.desc2")) - desc_col.label(text=t("AvatarToolkit.desc3")) - col.separator() + # Description + col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR) + description = " ".join([ + t("AvatarToolkit.desc1"), + t("AvatarToolkit.desc2"), + t("AvatarToolkit.desc3") + ]) + wrap_text_label(col, description, max_length=50) class AvatarToolKit_PT_AvatarToolkitPanel(Panel): """Main panel for Avatar Toolkit containing general information and settings""" diff --git a/ui/optimization_panel.py b/ui/optimization_panel.py index cfa1559..efaa5d0 100644 --- a/ui/optimization_panel.py +++ b/ui/optimization_panel.py @@ -2,6 +2,8 @@ import bpy from typing import Set from bpy.types import Panel, Context, UILayout, Operator from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .ui_utils import UIStyle, draw_section_header, draw_operator_row +from .panel_layout import get_panel_order, should_open_by_default from ..core.translations import t from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles @@ -15,39 +17,24 @@ class AvatarToolKit_PT_OptimizationPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 1 - bl_options = {'DEFAULT_CLOSED'} + bl_order: int = get_panel_order('optimization') + bl_options = set() if not should_open_by_default('OPTIMIZATION') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draws the optimization panel interface with material, mesh cleanup and join mesh tools""" layout: UILayout = self.layout - # Materials Box - materials_box: UILayout = layout.box() - col: UILayout = materials_box.column(align=True) - col.label(text=t("Optimization.materials_title"), icon='MATERIAL') - col.separator(factor=0.5) - - # Material Operations + # Materials section + col = draw_section_header(layout, t("Optimization.materials_title"), icon='MATERIAL') col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL') - # Mesh Cleanup Box - cleanup_box: UILayout = layout.box() - col: UILayout = cleanup_box.column(align=True) - col.label(text=t("Optimization.cleanup_title"), icon='MESH_DATA') - col.separator(factor=0.5) + # Mesh Cleanup section + col = draw_section_header(layout, t("Optimization.cleanup_title"), icon='MESH_DATA') + col.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA') - # Remove Doubles Row - row: UILayout = col.row(align=True) - row.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA') - - # Join Meshes Box - join_box: UILayout = layout.box() - col: UILayout = join_box.column(align=True) - col.label(text=t("Optimization.join_meshes_title"), icon='OBJECT_DATA') - col.separator(factor=0.5) - - # Join Meshes Row - row: UILayout = col.row(align=True) - row.operator(AvatarToolkit_OT_JoinAllMeshes.bl_idname, icon='OBJECT_DATA') - row.operator(AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, icon='RESTRICT_SELECT_OFF') + # Join Meshes section + col = draw_section_header(layout, t("Optimization.join_meshes_title"), icon='OBJECT_DATA') + draw_operator_row(col, [ + (AvatarToolkit_OT_JoinAllMeshes.bl_idname, t("Optimization.join_all_meshes"), 'OBJECT_DATA'), + (AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, t("Optimization.join_selected_meshes"), 'RESTRICT_SELECT_OFF') + ]) diff --git a/ui/panel_layout.py b/ui/panel_layout.py new file mode 100644 index 0000000..90ccc32 --- /dev/null +++ b/ui/panel_layout.py @@ -0,0 +1,53 @@ +"""Panel ordering and organization guide for Avatar Toolkit UI +This module defines the standard panel order and grouping for the Avatar Toolkit. +""" + +# Main Panel +MAIN_PANEL_ORDER = -1 # Always first (parent panel) +QUICK_ACCESS_ORDER = 0 +OPTIMIZATION_ORDER = 1 +TOOLS_ORDER = 2 +CUSTOM_TOOLS_ORDER = 3 +CUSTOM_AVATAR_ORDER = 4 +TRANSLATION_ORDER = 5 +VISEMES_ORDER = 6 +EYE_TRACKING_ORDER = 7 +TEXTURE_ATLAS_ORDER = 8 +VRM_UNITY_ORDER = 9 +SETTINGS_ORDER = 10 + +# Panel open/closed by default +PANELS_OPEN_BY_DEFAULT = { + 'QUICK_ACCESS': False, + 'OPTIMIZATION': True, + 'TOOLS': True, + 'CUSTOM_TOOLS': True, + 'CUSTOM_AVATAR': True, + 'VISEMES': True, + 'EYE_TRACKING': True, + 'TEXTURE_ATLAS': True, + 'VRM_UNITY': True, + 'SETTINGS': True, + 'TRANSLATION': True, +} + +def get_panel_order(panel_name: str) -> int: + """Get the recommended bl_order value for a panel""" + order_map = { + 'quick_access': QUICK_ACCESS_ORDER, + 'optimization': OPTIMIZATION_ORDER, + 'tools': TOOLS_ORDER, + 'custom_tools': CUSTOM_TOOLS_ORDER, + 'custom_avatar': CUSTOM_AVATAR_ORDER, + 'translation': TRANSLATION_ORDER, + 'visemes': VISEMES_ORDER, + 'eye_tracking': EYE_TRACKING_ORDER, + 'texture_atlas': TEXTURE_ATLAS_ORDER, + 'vrm_unity': VRM_UNITY_ORDER, + 'settings': SETTINGS_ORDER, + } + return order_map.get(panel_name.lower(), 99) + +def should_open_by_default(panel_name: str) -> bool: + """Check if a panel should be open by default""" + return PANELS_OPEN_BY_DEFAULT.get(panel_name.upper(), True) diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index 6e43e69..9ae2e3e 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -10,6 +10,8 @@ from bpy.types import ( Object ) from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .ui_utils import UIStyle, draw_section_header, draw_operator_row +from .panel_layout import get_panel_order, should_open_by_default from ..core.translations import t from ..core.common import ( get_active_armature, @@ -34,7 +36,7 @@ from ..functions.pose_mode import ( AvatarToolkit_OT_ApplyPoseAsShapekey, AvatarToolkit_OT_ApplyPoseAsRest ) -from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose +from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose, is_pmx_model 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 @@ -79,246 +81,202 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 0 + bl_order: int = get_panel_order('quick_access') + bl_options = {'DEFAULT_CLOSED'} if should_open_by_default('QUICK_ACCESS') else set() 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 = draw_section_header(layout, t("QuickAccess.select_armature"), icon='ARMATURE_DATA') col.prop(context.scene.avatar_toolkit, "active_armature", text="") - # Armature Validation (cached to improve performance) + # Get active armature 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)}" + # Validation Section + col = draw_section_header(layout, t("Validation.label", "Armature Validation"), icon='CHECKMARK') - if cache_key not in _validation_cache: - _validation_cache[cache_key] = validate_armature(active_armature, detailed_messages=True) + # Main validate button with prominent styling + validate_row = col.row(align=True) + validate_row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE + validate_row.operator("avatar_toolkit.validate_armature_manual", + text=t("Validation.validate_now", "Validate Armature Now"), + icon='CHECKMARK') - is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key] + # Validation mode selector + col.prop(props, "validation_mode", text=t("Settings.validation_mode", "Mode")) - # 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') + # Show validation results if flag is set + if props.show_validation_results: + # Cache validation results + cache_key = f"validation_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}" - 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) + 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 + pmx_detected = is_pmx_model(active_armature) + + results_box = col.box() + row = results_box.row() + row.prop(props, "show_validation_results", text=t("Validation.results", "Validation Results"), + icon='TRIA_DOWN' if props.show_validation_results else 'TRIA_RIGHT', emboss=False) + + # PMX Model Notice + if pmx_detected: + pmx_box = results_box.box() + pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO') - # Main validation status - validation_box = info_box.box() - row = validation_box.row() - row.alert = True - row.label(text=t("Validation.status.failed")) + 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")) + + # Validation Results + if not is_valid: + # Display found bones + if messages and len(messages) > 0: + bones_section = results_box.box() + row = bones_section.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: + for line in messages[0].split('\n'): + bones_section.label(text=line) - # Detailed validation message - validation_box = info_box.box() - row = validation_box.row() + # Status message + status_box = results_box.box() + row = status_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")) + row.label(text=t("Validation.status.failed"), icon='ERROR') + + # Error explanation + error_box = results_box.box() + error_box.alert = True + error_box.label(text=t("Validation.message.failed.line1")) + error_box.label(text=t("Validation.message.failed.line2")) + error_box.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")) - + if non_standard_messages or pmx_detected: + ns_section = results_box.box() + row = ns_section.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 = ns_section.row() + sub_row.alert = True + sub_row.label(text=line) + elif pmx_detected: + ns_section.alert = True + ns_section.label(text=t("Armature.validation.pmx_model_basic")) + ns_section.label(text=t("Armature.validation.pmx_model_strict")) + ns_section.label(text=t("Armature.validation.pmx_model_standardize")) else: - sub_row = validation_box.row() - sub_row.label(text=t("Validation.no_non_standard_issues")) - + ns_section.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: + if hierarchy_messages: + hier_section = results_box.box() + row = hier_section.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: for message in hierarchy_messages: - sub_row = validation_box.row() + sub_row = hier_section.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: + if scale_messages: + scale_section = results_box.box() + row = scale_section.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: for scale_msg in scale_messages: - sub_row = validation_box.row() + sub_row = scale_section.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') + elif is_valid and not is_acceptable: + # Valid armature - show stats + stats_cache_key = f"stats_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}" - # 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') + if stats_cache_key not in _stats_cache: + _stats_cache[stats_cache_key] = get_armature_stats(active_armature) + + stats = _stats_cache[stats_cache_key] + + status_box = results_box.box() + row = status_box.row() + row.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') + split = row.split(factor=0.4) + split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) + + if stats['has_pose']: + results_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') - # Add standardize button - standardize_box = info_box.box() - standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, - text=t("QuickAccess.standardize_armature"), - icon='MODIFIER') + elif is_valid and is_acceptable: + # Acceptable standard + status_box = results_box.box() + status_box.label(text=t("Armature.validation.acceptable_standard.success"), icon='INFO') + status_box.label(text=t("Armature.validation.acceptable_standard.note")) + status_box.label(text=t("Armature.validation.acceptable_standard.option")) + + # Add standardize button + standardize_box = results_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")) + # T-Pose Validation + col = draw_section_header(layout, t("Validation.tpose.label"), icon='ARMATURE_DATA') + col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, text=t("Validation.tpose.validate_now"), icon='CHECKMARK') + + if props.show_tpose_validation: + validation_result_col = col.column(align=True) + if props.tpose_validation_result: + validation_result_col.label(text=t("Validation.tpose.valid"), icon='CHECKMARK') + else: + validation_result_col.alert = True + validation_result_col.label(text=t("Validation.tpose.warning"), icon='ERROR') + + for msg in props.tpose_validation_messages: + validation_result_col.label(text=msg.name) # 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) + col = draw_section_header(layout, t("QuickAccess.pose_controls"), icon='ARMATURE_DATA') 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') + col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR) + draw_operator_row(col, [ + (AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, t("QuickAccess.apply_pose_as_rest.label"), 'MOD_ARMATURE'), + (AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, t("QuickAccess.apply_pose_as_shapekey.label"), '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 Section + col = draw_section_header(layout, t("QuickAccess.import_export"), icon='IMPORT') # 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') + draw_operator_row(col, [ + (AvatarToolKit_OT_Import.bl_idname, t("QuickAccess.import"), 'IMPORT'), + (AvatarToolKit_OT_ExportMenu.bl_idname, t("QuickAccess.export"), 'EXPORT') + ], scale_y=UIStyle.PRIMARY_BUTTON_SCALE) diff --git a/ui/search_operators.py b/ui/search_operators.py new file mode 100644 index 0000000..da9eb35 --- /dev/null +++ b/ui/search_operators.py @@ -0,0 +1,121 @@ +"""Base classes for reusable search operators""" + +from typing import Set, Callable, Optional +from bpy.types import Operator, Context, Event, WindowManager + + +class SearchOperatorBase(Operator): + """ + Reusable base class for search/selection operators. + + This is an abstract base class - do not use directly. + Subclass and implement your specific search operator instead. + + Subclasses should: + 1. Define bl_idname, bl_label, bl_description + 2. Define search_property_name (name of EnumProperty) + 3. Define target_property_name (name of property to set on scene) + 4. Define get_items_func (function to get enum items) + 5. Optionally override get_enum_property() to customize the enum + + This was created because search in ATK was all over the place and inconsistent, this way we have a standard way to do it. + """ + + # Mark this as abstract by setting a non-Blender-compatible idname + bl_idname = "wm.search_operator_base" # Will be overridden in subclasses + bl_label = "Search and Select" + bl_options = {'REGISTER', 'INTERNAL'} + + # These should be overridden in subclasses + search_property_name: str = "search_enum" + target_property_name: str = "target_property" + + @staticmethod + def get_items_func(scene, context) -> list: + """Override this to provide enum items. Return list of (id, name, description) tuples""" + return [] + + def get_enum_property(self) -> None: + """ + Create the enum property dynamically. Override if you need custom behavior. + This is called during class creation. + """ + import bpy + setattr( + type(self), + self.search_property_name, + bpy.props.EnumProperty( + name="Search", + description="Select item", + items=self.get_items_func + ) + ) + + def execute(self, context: Context) -> Set[str]: + """Set the target property from the search selection""" + search_value = getattr(self, self.search_property_name, None) + if search_value: + setattr(context.scene.avatar_toolkit, self.target_property_name, search_value) + return {'FINISHED'} + + def invoke(self, context: Context, event: Event) -> Set[str]: + """Open search popup""" + wm: WindowManager = context.window_manager + wm.invoke_search_popup(self) + return {'FINISHED'} + + +class ArmatureSearchOperator(SearchOperatorBase): + """Specialized search operator for selecting armatures""" + + bl_label = "Search Armatures" + search_property_name: str = "search_armature_enum" + + @staticmethod + def get_items_func(scene, context) -> list: + """Get list of all armature objects in scene""" + import bpy + return [ + (obj.name, obj.name, "") + for obj in bpy.data.objects + if obj.type == 'ARMATURE' + ] + + +class MeshSearchOperator(SearchOperatorBase): + """Specialized search operator for selecting meshes""" + + bl_label = "Search Meshes" + search_property_name: str = "search_mesh_enum" + + @staticmethod + def get_items_func(scene, context) -> list: + """Get list of all mesh objects without armature modifiers""" + import bpy + return [ + (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) + ] + + +class BoneSearchOperator(SearchOperatorBase): + """Specialized search operator for selecting bones from active armature""" + + bl_label = "Search Bones" + search_property_name: str = "search_bone_enum" + + @staticmethod + def get_items_func(scene, context) -> list: + """Get list of all bones from active armature""" + from ..core.common import get_active_armature + + armature = get_active_armature(context) + if not armature: + return [] + + return [ + (bone.name, bone.name, "") + for bone in armature.data.bones + ] diff --git a/ui/settings_panel.py b/ui/settings_panel.py index bdaa783..a39a04b 100644 --- a/ui/settings_panel.py +++ b/ui/settings_panel.py @@ -9,6 +9,8 @@ from bpy.types import ( Event ) from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .ui_utils import UIStyle, draw_section_header, wrap_text_label +from .panel_layout import get_panel_order, should_open_by_default from ..core.translations import t, get_languages_list from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting @@ -26,8 +28,10 @@ class AvatarToolkit_OT_TranslationRestartPopup(Operator): def draw(self, context: Context) -> None: layout: UILayout = self.layout - layout.label(text=t("Language.changed.success")) - layout.label(text=t("Language.changed.restart")) + col = layout.column(align=True) + col.label(text=t("Language.changed.success")) + col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR) + wrap_text_label(col, t("Language.changed.restart"), max_length=50) class AvatarToolKit_PT_SettingsPanel(Panel): """Settings panel for Avatar Toolkit containing language preferences""" @@ -37,8 +41,8 @@ class AvatarToolKit_PT_SettingsPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 8 - bl_options = {'DEFAULT_CLOSED'} + bl_order: int = get_panel_order('settings') + bl_options = set() if not should_open_by_default('SETTINGS') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draw the settings panel layout with language selection""" @@ -46,30 +50,18 @@ class AvatarToolKit_PT_SettingsPanel(Panel): props = context.scene.avatar_toolkit # Language Settings - lang_box: UILayout = layout.box() - col: UILayout = lang_box.column(align=True) - row: UILayout = col.row() - row.scale_y = 1.2 - row.label(text=t("Settings.language"), icon='WORLD') - col.separator() + col = draw_section_header(layout, t("Settings.language"), icon='WORLD') col.prop(props, "language", text="") - # Validation Settings - val_box: UILayout = layout.box() - col = val_box.column(align=True) - row = col.row() - row.scale_y = 1.2 - row.label(text=t("Settings.validation_mode"), icon='CHECKMARK') - col.separator() + # Validation Settings with help text + col = draw_section_header(layout, t("Settings.validation_mode"), icon='CHECKMARK') col.prop(props, "validation_mode", text="") + # Help text for validation mode + col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR) + wrap_text_label(col, "Select how strictly to validate armature bone structure and naming conventions.", max_length=40) # Bone Highlighting Settings - bone_box: UILayout = layout.box() - col = bone_box.column(align=True) - row = col.row() - row.scale_y = 1.2 - row.label(text=t("Settings.bone_highlighting"), icon='BONE_DATA') - col.separator() + col = draw_section_header(layout, t("Settings.bone_highlighting"), icon='BONE_DATA') col.prop(props, "highlight_problem_bones") if props.highlight_problem_bones: col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR') diff --git a/ui/tools_panel.py b/ui/tools_panel.py index fd4f25c..e94e1ef 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -2,6 +2,8 @@ import bpy from typing import Set from bpy.types import Panel, Context, UILayout, Operator, UIList from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .ui_utils import UIStyle, draw_section_header, draw_operator_row +from .panel_layout import get_panel_order, should_open_by_default from ..core.translations import t from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite @@ -29,8 +31,8 @@ class AvatarToolKit_PT_ToolsPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 2 - bl_options = {'DEFAULT_CLOSED'} + bl_order: int = get_panel_order('tools') + bl_options = set() if not should_open_by_default('TOOLS') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draw the tools panel interface""" @@ -38,94 +40,70 @@ class AvatarToolKit_PT_ToolsPanel(Panel): toolkit = context.scene.avatar_toolkit # General Tools - tools_box: UILayout = layout.box() - col: UILayout = tools_box.column(align=True) - col.label(text=t("Tools.general_title"), icon='TOOL_SETTINGS') - col.separator(factor=0.5) + col = draw_section_header(layout, t("Tools.general_title"), icon='TOOL_SETTINGS') col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT') # Separation Tools - sep_box: UILayout = layout.box() - col = sep_box.column(align=True) - col.label(text=t("Tools.separate_title"), icon='MOD_EXPLODE') - col.separator(factor=0.5) - row: UILayout = col.row(align=True) - row.operator(AvatarToolKit_OT_SeparateByMaterials.bl_idname, text=t("Tools.separate_materials"), icon='MATERIAL') - row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_loose"), icon='MESH_DATA') + col = draw_section_header(layout, t("Tools.separate_title"), icon='MOD_EXPLODE') + draw_operator_row(col, [ + (AvatarToolKit_OT_SeparateByMaterials.bl_idname, t("Tools.separate_materials"), 'MATERIAL'), + (AvatarToolKit_OT_SeparateByLooseParts.bl_idname, t("Tools.separate_loose"), 'MESH_DATA') + ]) # Bone Tools - bone_box: UILayout = layout.box() - col = bone_box.column(align=True) - col.label(text=t("Tools.bone_title"), icon='BONE_DATA') - col.separator(factor=0.5) + col = draw_section_header(layout, t("Tools.bone_title"), icon='BONE_DATA') col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), icon='BONE_DATA') - col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname,text=t("Tools.flip_pose_frames"),icon="ACTION") + col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname, text=t("Tools.flip_pose_frames"), icon="ACTION") # Mesh Tools - mesh_box: UILayout = layout.box() - col = mesh_box.column(align=True) - col.label(text=t("Tools.mesh_title"), icon='MESH_DATA') - col.separator(factor=0.5) - col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname,text=t("Tools.find_shortest_seam_path"),icon="MESH_DATA") - col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname,text=t("Tools.apply_modifier_on_shapekey_obj"),icon="SHAPEKEY_DATA") - col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname,text=t("Tools.explode_mesh"),icon="MOD_EXPLODE") - + col = draw_section_header(layout, t("Tools.mesh_title"), icon='MESH_DATA') + col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname, text=t("Tools.find_shortest_seam_path"), icon="MESH_DATA") + col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname, text=t("Tools.apply_modifier_on_shapekey_obj"), icon="SHAPEKEY_DATA") + col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname, text=t("Tools.explode_mesh"), icon="MOD_EXPLODE") # Standardization Tools - standardize_box: UILayout = bone_box.box() - col = standardize_box.column(align=True) - col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE') - col.separator(factor=0.5) + col = draw_section_header(layout, t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE') col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK') # Weight Tools - weight_box: UILayout = bone_box.box() - col = weight_box.column(align=True) + col = draw_section_header(layout, t("Tools.weight_title"), icon='GROUP_BONE') col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones")) col.prop(toolkit, "preserve_parent_bones") col.prop(toolkit, "target_bone_type") col.prop(toolkit, "list_only_mode") if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0: - box = weight_box.box() - row = box.row() + sub_col = col.box() + row = sub_col.row() row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "", toolkit, "zero_weight_bones", toolkit, "zero_weight_bones_index") - col = box.column(align=True) - col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname, - text=t("Tools.remove_selected_bones")) + sub_col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname, + text=t("Tools.remove_selected_bones")) - row = col.row(align=True) - row.operator(AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.clean_weights"), icon='GROUP_BONE') - row.operator(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE') - row = col.row(align=True) - row.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE') + # Combine weight + draw_operator_row(col, [ + (AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, t("Tools.clean_weights"), 'GROUP_BONE'), + (AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, t("Tools.clean_constraints"), 'CONSTRAINT_BONE') + ]) + col.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE') # Merge Tools - merge_box: UILayout = layout.box() - col = merge_box.column(align=True) - col.label(text=t("Tools.merge_title"), icon='AUTOMERGE_ON') - col.separator(factor=0.5) - row = col.row(align=True) - row.operator(AvatarToolkit_OT_MergeToActive.bl_idname, text=t("Tools.merge_to_active"), icon='BONE_DATA') - row.operator(AvatarToolkit_OT_MergeToParent.bl_idname, text=t("Tools.merge_to_parent"), icon='BONE_DATA') + col = draw_section_header(layout, t("Tools.merge_title"), icon='AUTOMERGE_ON') + draw_operator_row(col, [ + (AvatarToolkit_OT_MergeToActive.bl_idname, t("Tools.merge_to_active"), 'BONE_DATA'), + (AvatarToolkit_OT_MergeToParent.bl_idname, t("Tools.merge_to_parent"), 'BONE_DATA') + ]) col.operator(AvatarToolkit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones"), icon='BONE_DATA') # Additional Tools - extra_box: UILayout = layout.box() - col = extra_box.column(align=True) - col.label(text=t("Tools.additional_title"), icon='TOOL_SETTINGS') - col.separator(factor=0.5) + col = draw_section_header(layout, t("Tools.additional_title"), icon='TOOL_SETTINGS') col.operator(AvatarToolkit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms"), icon='OBJECT_DATA') col.operator(AvatarToolkit_OT_CleanShapekeys.bl_idname, text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA') # Rigify Tools - rigify_box: UILayout = layout.box() - col = rigify_box.column(align=True) - col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA') - col.separator(factor=0.5) + col = draw_section_header(layout, t("Tools.rigify_title"), icon='ARMATURE_DATA') col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA') col.prop(context.scene.avatar_toolkit, "merge_twist_bones") diff --git a/ui/translation_panel.py b/ui/translation_panel.py index 8ea5cb9..fc8f990 100644 --- a/ui/translation_panel.py +++ b/ui/translation_panel.py @@ -12,6 +12,7 @@ from bpy.types import ( Object ) from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .panel_layout import get_panel_order, should_open_by_default from ..core.translations import t from ..core.logging_setup import logger from ..core.common import get_active_armature, ProgressTracker @@ -465,8 +466,8 @@ class AvatarToolKit_PT_TranslationPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 9 - bl_options = {'DEFAULT_CLOSED'} + bl_order: int = get_panel_order('translation') + bl_options = set() if not should_open_by_default('TRANSLATION') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draw the translation panel layout""" diff --git a/ui/ui_utils.py b/ui/ui_utils.py new file mode 100644 index 0000000..cb388b0 --- /dev/null +++ b/ui/ui_utils.py @@ -0,0 +1,137 @@ +"""UI utilities and styling helpers for consistent Avatar Toolkit panel design""" + +from typing import Callable, Optional +from bpy.types import UILayout, Context, Operator + + +class UIStyle: + """Centralized UI styling constants for consistent appearance""" + + SECTION_SEPARATOR_FACTOR: float = 0.5 + SUBSECTION_SEPARATOR_FACTOR: float = 0.3 + PRIMARY_BUTTON_SCALE: float = 1.5 + STANDARD_BUTTON_SCALE: float = 1.0 + COMPACT_BUTTON_SCALE: float = 0.9 + DEFAULT_PADDING: float = 1.0 + COMPACT_PADDING: float = 0.5 + + CATEGORY_ICONS = { + 'optimization': 'MOD_SMOOTH', + 'tools': 'TOOL_SETTINGS', + 'custom': 'TOOL_OPTIONS', + 'eye_tracking': 'OBJECT_CAMERA', + 'settings': 'PREFERENCES', + 'import_export': 'EXPORT', + 'pose': 'POSE_HLT', + 'materials': 'MATERIAL', + 'mesh': 'MESH_DATA', + 'bones': 'BONE_DATA', + 'vfx': 'MOD_DISPLACE' + } + + +def draw_section_header(layout: UILayout, title: str, icon: str = 'NONE', separator: bool = True) -> UILayout: + """Draw a consistent section header with optional icon and separator""" + header_box = layout.box() + col = header_box.column(align=True) + row = col.row() + row.scale_y = 1.2 + row.label(text=title, icon=icon) + + if separator: + col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR) + + return col + + +def draw_subsection(layout: UILayout, title: str, icon: str = 'NONE') -> UILayout: + """Draw a subsection with reduced visual weight (no box)""" + col = layout.column(align=True) + row = col.row() + row.label(text=title, icon=icon) + col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR) + return col + + +def draw_info_text(layout: UILayout, text: str, icon: str = 'INFO') -> None: + """Draw informational text that can wrap (replaces multiple labels)""" + col = layout.column() + col.alert = False + # Split long text for wrapping + row = col.row() + row.label(text=text, icon=icon) + + +def draw_warning_text(layout: UILayout, text: str) -> None: + """Draw warning-styled text""" + col = layout.column() + col.alert = True + row = col.row() + row.label(text=text, icon='ERROR') + + +def draw_primary_button(layout: UILayout, operator_idname: str, text: str = "", + icon: str = 'NONE', **kwargs) -> None: + """Draw a primary action button with standard scaling""" + row = layout.row(align=True) + row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE + row.operator(operator_idname, text=text, icon=icon, **kwargs) + + +def draw_operator_row(layout: UILayout, operators: list[tuple[str, str, str]], + scale_y: float = 1.0, equal_width: bool = True) -> None: + """Draw multiple operators in a single row with consistent sizing""" + if not operators: + return + + row = layout.row(align=equal_width) + row.scale_y = scale_y + + for op_id, text, icon in operators: + row.operator(op_id, text=text, icon=icon) + + +def draw_collapsible_section(layout: UILayout, title: str, icon: str, + draw_func: Callable[[UILayout], None], + context: Context, storage_attr: str) -> None: + """Draw a collapsible section (using context scene properties for state)""" + col = layout.column(align=True) + row = col.row() + + scene = context.scene + attr_name = f"_ui_expand_{storage_attr}" + is_expanded = getattr(scene, attr_name, False) + icon_name = 'DISCLOSURE_TRI_DOWN' if is_expanded else 'DISCLOSURE_TRI_RIGHT' + row.prop(scene, attr_name, text="", icon=icon_name, emboss=False) + row.label(text=title, icon=icon) + + if is_expanded: + col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR) + draw_func(col) + + +def apply_operator_disable_feedback(operator: Operator, layout: UILayout, + is_disabled: bool, reason: str = "") -> UILayout: + """Prepare layout for disabled operator with visual feedback""" + if is_disabled: + layout.enabled = False + return layout + + +def wrap_text_label(layout: UILayout, text: str, max_length: int = 50) -> None: + """Draw a label that wraps long text across multiple lines""" + words = text.split() + current_line = "" + + col = layout.column() + + for word in words: + test_line = (current_line + " " + word).strip() + if len(test_line) > max_length and current_line: + col.label(text=current_line) + current_line = word + else: + current_line = test_line + + if current_line: + col.label(text=current_line) diff --git a/ui/visemes_panel.py b/ui/visemes_panel.py index 11d2a30..b5563dd 100644 --- a/ui/visemes_panel.py +++ b/ui/visemes_panel.py @@ -2,6 +2,7 @@ import bpy from bpy.types import Panel, Context, UILayout, Object, ShapeKey from ..core.translations import t from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .panel_layout import get_panel_order, should_open_by_default from ..core.common import get_active_armature from ..functions.visemes import AvatarToolkit_OT_PreviewVisemes, AvatarToolkit_OT_CreateVisemes @@ -13,8 +14,8 @@ class AvatarToolKit_PT_VisemesPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 5 - bl_options: set[str] = {'DEFAULT_CLOSED'} + bl_order: int = get_panel_order('visemes') + bl_options: set[str] = set() if not should_open_by_default('VISEMES') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draw the visemes panel interface with shape key selection and preview controls""" diff --git a/ui/vrm_unity_panel.py b/ui/vrm_unity_panel.py index f1565ab..0263ccc 100644 --- a/ui/vrm_unity_panel.py +++ b/ui/vrm_unity_panel.py @@ -1,6 +1,7 @@ import bpy from bpy.types import Panel, Context, UILayout from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .panel_layout import get_panel_order, should_open_by_default from ..core.translations import t from ..core.common import get_active_armature from ..core.vrm_unity_converter import detect_vrm_armature @@ -15,8 +16,8 @@ class AvatarToolKit_PT_VRMUnityPanel(Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 3 - bl_options = {'DEFAULT_CLOSED'} + bl_order = get_panel_order('vrm_unity') + bl_options = set() if not should_open_by_default('VRM_UNITY') else {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: """Draw the VRM to Unity conversion panel interface""" diff --git a/wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl b/wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl deleted file mode 100644 index 992aee7..0000000 Binary files a/wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl and /dev/null differ diff --git a/wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl b/wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl deleted file mode 100644 index 373420e..0000000 Binary files a/wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl and /dev/null differ diff --git a/wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index cdc8e27..0000000 Binary files a/wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl b/wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl deleted file mode 100644 index a2d06a8..0000000 Binary files a/wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl and /dev/null differ diff --git a/wheels/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl b/wheels/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl new file mode 100644 index 0000000..a3113ab Binary files /dev/null and b/wheels/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl differ diff --git a/wheels/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl b/wheels/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl new file mode 100644 index 0000000..10cbbcd Binary files /dev/null and b/wheels/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl differ diff --git a/wheels/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl b/wheels/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl new file mode 100644 index 0000000..044cdd8 Binary files /dev/null and b/wheels/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl differ diff --git a/wheels/lz4-4.4.5-cp311-cp311-win_amd64.whl b/wheels/lz4-4.4.5-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..24bc823 Binary files /dev/null and b/wheels/lz4-4.4.5-cp311-cp311-win_amd64.whl differ