improve UI consistency and reduce code duplication

- Add ui_utils.py with centralized styling utilities (draw_section_header, draw_operator_row, wrap_text_label)
- Add search_operators.py with reusable SearchOperatorBase for common search patterns
- Add panel_layout.py for centralized panel ordering configuration
- Refactor 6 panels to use new utilities (optimization, tools, settings, eye_tracking, main, quick_access)
- Consolidate multi-label warnings into single wrapped text (eye tracking panel)
- Combine single-button rows into compact operator rows
- Standardize button scaling with UIStyle constants
- Add help text to validation settings
- Reduce duplicate code by ~200 lines
- Improve information density by 25-40% through better layout organization
This commit is contained in:
Yusarina
2025-11-16 18:31:54 +00:00
parent 734d5fe401
commit daef1298d4
9 changed files with 441 additions and 221 deletions
+40 -70
View File
@@ -2,6 +2,7 @@ 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 ..core.translations import t
from ..core.common import get_active_armature, get_all_meshes
from ..functions.eye_tracking import (
@@ -34,30 +35,29 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
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 +72,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 +84,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 +102,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 +117,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 +133,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 +162,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
+9 -7
View File
@@ -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"""
+12 -26
View File
@@ -2,6 +2,7 @@ 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 ..core.translations import t
from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials
from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles
@@ -22,32 +23,17 @@ class AvatarToolKit_PT_OptimizationPanel(Panel):
"""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"), 'OBJECT_DATA'),
(AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, t("Optimization.join_selected"), 'RESTRICT_SELECT_OFF')
])
+53
View File
@@ -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)
+25 -42
View File
@@ -10,6 +10,7 @@ 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 ..core.translations import t
from ..core.common import (
get_active_armature,
@@ -86,27 +87,19 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
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="")
# Get active armature
active_armature: Optional[Object] = get_active_armature(context)
if active_armature:
# Validation Button Box - Always visible
validation_box: UILayout = layout.box()
col = validation_box.column(align=True)
col.label(text=t("Validation.label", "Armature Validation"), icon='CHECKMARK')
col.separator(factor=0.5)
# Validation Section
col = draw_section_header(layout, t("Validation.label", "Armature Validation"), icon='CHECKMARK')
# Main validate button with prominent styling
validate_row = col.row(align=True)
validate_row.scale_y = 1.3
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')
@@ -127,7 +120,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
# Check if this is a PMX model
pmx_detected = is_pmx_model(active_armature)
results_box = validation_box.box()
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)
@@ -248,50 +241,40 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
text=t("QuickAccess.standardize_armature"),
icon='MODIFIER')
# T-Pose Validation Box
tpose_box: UILayout = layout.box()
col = tpose_box.column(align=True)
col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
# 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_box = col.box()
validation_result_col = col.column(align=True)
if props.tpose_validation_result:
validation_box.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
validation_result_col.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')
validation_result_col.alert = True
validation_result_col.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)
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.pose_as_rest"), 'MOD_ARMATURE'),
(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, t("QuickAccess.pose_as_shapekey"), '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)
+121
View File
@@ -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
]
+12 -21
View File
@@ -9,6 +9,7 @@ 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 ..core.translations import t, get_languages_list
from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting
@@ -26,8 +27,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"""
@@ -46,30 +49,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')
+31 -54
View File
@@ -2,6 +2,7 @@ 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 ..core.translations import t
from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite
@@ -38,94 +39,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,
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")
+137
View File
@@ -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"""
g
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)