Merge pull request #205 from Yusarina/atk-next
improve UI consistency and reduce code duplication
This commit is contained in:
+40
-70
@@ -2,6 +2,7 @@ import bpy
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
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.translations import t
|
||||||
from ..core.common import get_active_armature, get_all_meshes
|
from ..core.common import get_active_armature, get_all_meshes
|
||||||
from ..functions.eye_tracking import (
|
from ..functions.eye_tracking import (
|
||||||
@@ -34,30 +35,29 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# SDK Version Selection Box
|
# SDK Version Selection
|
||||||
sdk_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.sdk_version"), icon='PRESET')
|
||||||
col: UILayout = sdk_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.sdk_version"), icon='PRESET')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.prop(toolkit, "eye_tracking_type", expand=True)
|
row.prop(toolkit, "eye_tracking_type", expand=True)
|
||||||
|
|
||||||
if toolkit.eye_tracking_type == 'SDK2':
|
if toolkit.eye_tracking_type == 'SDK2':
|
||||||
# SDK2 Warning Box
|
# SDK2 Warning
|
||||||
warning_box: UILayout = layout.box()
|
warning_box: UILayout = layout.box()
|
||||||
col: UILayout = warning_box.column(align=True)
|
col: UILayout = warning_box.column(align=True)
|
||||||
col.label(text=t("EyeTracking.sdk2_warning"), icon='INFO')
|
col.alert = True
|
||||||
col.separator(factor=0.5)
|
col.label(text=t("EyeTracking.sdk2_warning"), icon='ERROR')
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail1"))
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail2"))
|
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail3"))
|
|
||||||
col.label(text=t("EyeTracking.sdk2_warning_detail4"))
|
|
||||||
|
|
||||||
# Mode Selection Box
|
warning_text = "\n".join([
|
||||||
mode_box: UILayout = layout.box()
|
t("EyeTracking.sdk2_warning_detail1"),
|
||||||
col: UILayout = mode_box.column(align=True)
|
t("EyeTracking.sdk2_warning_detail2"),
|
||||||
col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS')
|
t("EyeTracking.sdk2_warning_detail3"),
|
||||||
col.separator(factor=0.5)
|
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)
|
col.prop(toolkit, "eye_mode", expand=True)
|
||||||
|
|
||||||
if toolkit.eye_mode == 'CREATION':
|
if toolkit.eye_mode == 'CREATION':
|
||||||
@@ -72,11 +72,9 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
"""Draw the AV3 eye tracking setup interface"""
|
"""Draw the AV3 eye tracking setup interface"""
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Bone Setup Box
|
# Bone Setup
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
||||||
col: UILayout = bone_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if armature:
|
if armature:
|
||||||
@@ -86,21 +84,16 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Create Button
|
|
||||||
row: UILayout = layout.row(align=True)
|
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')
|
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
|
||||||
|
|
||||||
def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
|
def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
|
||||||
"""Draw the eye tracking creation mode interface"""
|
"""Draw the eye tracking creation mode interface"""
|
||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Bone Setup Box
|
# Bone Setup
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
||||||
col: UILayout = bone_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if armature:
|
if armature:
|
||||||
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
|
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
|
||||||
@@ -109,19 +102,12 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Mesh Setup Box
|
# Mesh Setup
|
||||||
mesh_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.mesh_setup"), icon='MESH_DATA')
|
||||||
col: UILayout = mesh_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
|
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
|
||||||
|
|
||||||
# Shape Key Setup Box
|
# Shape Key Setup
|
||||||
shape_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
|
||||||
col: UILayout = shape_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
|
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
|
||||||
if mesh and mesh.data.shape_keys:
|
if mesh and mesh.data.shape_keys:
|
||||||
col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left"))
|
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:
|
else:
|
||||||
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
|
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
|
||||||
|
|
||||||
# Options Box
|
# Options
|
||||||
options_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.options"), icon='SETTINGS')
|
||||||
col: UILayout = options_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.options"), icon='SETTINGS')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop(toolkit, "disable_eye_blinking")
|
col.prop(toolkit, "disable_eye_blinking")
|
||||||
col.prop(toolkit, "disable_eye_movement")
|
col.prop(toolkit, "disable_eye_movement")
|
||||||
if not toolkit.disable_eye_movement:
|
if not toolkit.disable_eye_movement:
|
||||||
col.prop(toolkit, "eye_distance")
|
col.prop(toolkit, "eye_distance")
|
||||||
|
|
||||||
# Create Button
|
|
||||||
row: UILayout = layout.row(align=True)
|
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')
|
row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
|
||||||
|
|
||||||
def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
|
def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
|
||||||
@@ -151,37 +133,25 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
if context.mode != 'POSE':
|
if context.mode != 'POSE':
|
||||||
# Testing Start Box
|
# Testing Start
|
||||||
test_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.testing"), icon='PLAY')
|
||||||
col: UILayout = test_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.testing"), icon='PLAY')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
row: UILayout = col.row(align=True)
|
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')
|
row.operator(StartTestingButton.bl_idname, icon='PLAY')
|
||||||
else:
|
else:
|
||||||
# Eye Rotation Box
|
# Eye Rotation
|
||||||
rotation_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
|
||||||
col: UILayout = rotation_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
|
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
|
||||||
col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y"))
|
col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y"))
|
||||||
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
|
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
|
||||||
|
|
||||||
# Eye Adjustment Box
|
# Eye Adjustment
|
||||||
adjust_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.adjustments"), icon='MODIFIER')
|
||||||
col: UILayout = adjust_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.prop(toolkit, "eye_distance")
|
col.prop(toolkit, "eye_distance")
|
||||||
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
|
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
|
||||||
|
|
||||||
# Blinking Test Box
|
# Blinking Test
|
||||||
blink_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("EyeTracking.blink_testing"), icon='HIDE_OFF')
|
||||||
col: UILayout = blink_box.column(align=True)
|
|
||||||
col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
row: UILayout = col.row(align=True)
|
row: UILayout = col.row(align=True)
|
||||||
row.prop(toolkit, "eye_blink_shape")
|
row.prop(toolkit, "eye_blink_shape")
|
||||||
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
|
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
|
||||||
@@ -192,7 +162,7 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel):
|
|||||||
|
|
||||||
# Stop Testing Button
|
# Stop Testing Button
|
||||||
row: UILayout = layout.row(align=True)
|
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')
|
row.operator(StopTestingButton.bl_idname, icon='PAUSE')
|
||||||
|
|
||||||
# Reset Button
|
# Reset Button
|
||||||
|
|||||||
+9
-7
@@ -1,6 +1,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
from bpy.types import Panel, Context, UILayout
|
from bpy.types import Panel, Context, UILayout
|
||||||
|
from .ui_utils import UIStyle, wrap_text_label
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
|
|
||||||
CATEGORY_NAME: str = "Avatar Toolkit"
|
CATEGORY_NAME: str = "Avatar Toolkit"
|
||||||
@@ -16,13 +17,14 @@ def draw_title(self: Panel) -> None:
|
|||||||
row.scale_y: float = 1.2
|
row.scale_y: float = 1.2
|
||||||
row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA')
|
row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA')
|
||||||
|
|
||||||
# Description as a flowing paragraph
|
# Description
|
||||||
desc_col: UILayout = col.column()
|
col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR)
|
||||||
desc_col.scale_y: float = 0.6
|
description = " ".join([
|
||||||
desc_col.label(text=t("AvatarToolkit.desc1"))
|
t("AvatarToolkit.desc1"),
|
||||||
desc_col.label(text=t("AvatarToolkit.desc2"))
|
t("AvatarToolkit.desc2"),
|
||||||
desc_col.label(text=t("AvatarToolkit.desc3"))
|
t("AvatarToolkit.desc3")
|
||||||
col.separator()
|
])
|
||||||
|
wrap_text_label(col, description, max_length=50)
|
||||||
|
|
||||||
class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
|
class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
|
||||||
"""Main panel for Avatar Toolkit containing general information and settings"""
|
"""Main panel for Avatar Toolkit containing general information and settings"""
|
||||||
|
|||||||
+12
-26
@@ -2,6 +2,7 @@ import bpy
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from bpy.types import Panel, Context, UILayout, Operator
|
from bpy.types import Panel, Context, UILayout, Operator
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
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.translations import t
|
||||||
from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials
|
from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials
|
||||||
from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles
|
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"""
|
"""Draws the optimization panel interface with material, mesh cleanup and join mesh tools"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
# Materials Box
|
# Materials section
|
||||||
materials_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Optimization.materials_title"), icon='MATERIAL')
|
||||||
col: UILayout = materials_box.column(align=True)
|
|
||||||
col.label(text=t("Optimization.materials_title"), icon='MATERIAL')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Material Operations
|
|
||||||
col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL')
|
col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL')
|
||||||
|
|
||||||
# Mesh Cleanup Box
|
# Mesh Cleanup section
|
||||||
cleanup_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Optimization.cleanup_title"), icon='MESH_DATA')
|
||||||
col: UILayout = cleanup_box.column(align=True)
|
col.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA')
|
||||||
col.label(text=t("Optimization.cleanup_title"), icon='MESH_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Remove Doubles Row
|
# Join Meshes section
|
||||||
row: UILayout = col.row(align=True)
|
col = draw_section_header(layout, t("Optimization.join_meshes_title"), icon='OBJECT_DATA')
|
||||||
row.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA')
|
draw_operator_row(col, [
|
||||||
|
(AvatarToolkit_OT_JoinAllMeshes.bl_idname, t("Optimization.join_all"), 'OBJECT_DATA'),
|
||||||
# Join Meshes Box
|
(AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, t("Optimization.join_selected"), 'RESTRICT_SELECT_OFF')
|
||||||
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')
|
|
||||||
|
|||||||
@@ -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
@@ -10,6 +10,7 @@ from bpy.types import (
|
|||||||
Object
|
Object
|
||||||
)
|
)
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
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.translations import t
|
||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
@@ -86,27 +87,19 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
props = context.scene.avatar_toolkit
|
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
|
# Armature Selection
|
||||||
|
col = draw_section_header(layout, t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
|
||||||
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
|
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
|
||||||
|
|
||||||
# Get active armature
|
# Get active armature
|
||||||
active_armature: Optional[Object] = get_active_armature(context)
|
active_armature: Optional[Object] = get_active_armature(context)
|
||||||
if active_armature:
|
if active_armature:
|
||||||
# Validation Button Box - Always visible
|
# Validation Section
|
||||||
validation_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Validation.label", "Armature Validation"), icon='CHECKMARK')
|
||||||
col = validation_box.column(align=True)
|
|
||||||
col.label(text=t("Validation.label", "Armature Validation"), icon='CHECKMARK')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Main validate button with prominent styling
|
# Main validate button with prominent styling
|
||||||
validate_row = col.row(align=True)
|
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",
|
validate_row.operator("avatar_toolkit.validate_armature_manual",
|
||||||
text=t("Validation.validate_now", "Validate Armature Now"),
|
text=t("Validation.validate_now", "Validate Armature Now"),
|
||||||
icon='CHECKMARK')
|
icon='CHECKMARK')
|
||||||
@@ -127,7 +120,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
# Check if this is a PMX model
|
# Check if this is a PMX model
|
||||||
pmx_detected = is_pmx_model(active_armature)
|
pmx_detected = is_pmx_model(active_armature)
|
||||||
|
|
||||||
results_box = validation_box.box()
|
results_box = col.box()
|
||||||
row = results_box.row()
|
row = results_box.row()
|
||||||
row.prop(props, "show_validation_results", text=t("Validation.results", "Validation Results"),
|
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)
|
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"),
|
text=t("QuickAccess.standardize_armature"),
|
||||||
icon='MODIFIER')
|
icon='MODIFIER')
|
||||||
|
|
||||||
# T-Pose Validation Box
|
# T-Pose Validation
|
||||||
tpose_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Validation.tpose.label"), icon='ARMATURE_DATA')
|
||||||
col = tpose_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, text=t("Validation.tpose.validate_now"), icon='CHECKMARK')
|
col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, text=t("Validation.tpose.validate_now"), icon='CHECKMARK')
|
||||||
|
|
||||||
if props.show_tpose_validation:
|
if props.show_tpose_validation:
|
||||||
validation_box = col.box()
|
validation_result_col = col.column(align=True)
|
||||||
if props.tpose_validation_result:
|
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:
|
else:
|
||||||
row = validation_box.row()
|
validation_result_col.alert = True
|
||||||
row.alert = True
|
validation_result_col.label(text=t("Validation.tpose.warning"), icon='ERROR')
|
||||||
row.label(text=t("Validation.tpose.warning"), icon='ERROR')
|
|
||||||
|
|
||||||
for msg in props.tpose_validation_messages:
|
for msg in props.tpose_validation_messages:
|
||||||
row = validation_box.row()
|
validation_result_col.label(text=msg.name)
|
||||||
row.alert = True
|
|
||||||
row.label(text=msg.name)
|
|
||||||
|
|
||||||
# Pose Mode Controls
|
# Pose Mode Controls
|
||||||
pose_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
|
||||||
col = pose_box.column(align=True)
|
|
||||||
col.label(text=t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
if context.mode == "POSE":
|
if context.mode == "POSE":
|
||||||
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
|
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
|
||||||
col.separator(factor=0.5)
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
||||||
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, icon='MOD_ARMATURE')
|
draw_operator_row(col, [
|
||||||
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, icon='MOD_ARMATURE')
|
(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:
|
else:
|
||||||
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
|
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
|
||||||
|
|
||||||
# Import/Export Box
|
# Import/Export Section
|
||||||
import_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("QuickAccess.import_export"), icon='IMPORT')
|
||||||
col = import_box.column(align=True)
|
|
||||||
col.label(text=t("QuickAccess.import_export"), icon='IMPORT')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
|
|
||||||
# Import/Export Buttons
|
# Import/Export Buttons
|
||||||
button_row: UILayout = col.row(align=True)
|
draw_operator_row(col, [
|
||||||
button_row.scale_y = 1.5
|
(AvatarToolKit_OT_Import.bl_idname, t("QuickAccess.import"), 'IMPORT'),
|
||||||
button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT')
|
(AvatarToolKit_OT_ExportMenu.bl_idname, t("QuickAccess.export"), 'EXPORT')
|
||||||
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT')
|
], scale_y=UIStyle.PRIMARY_BUTTON_SCALE)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -9,6 +9,7 @@ from bpy.types import (
|
|||||||
Event
|
Event
|
||||||
)
|
)
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
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.translations import t, get_languages_list
|
||||||
from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting
|
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:
|
def draw(self, context: Context) -> None:
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
layout.label(text=t("Language.changed.success"))
|
col = layout.column(align=True)
|
||||||
layout.label(text=t("Language.changed.restart"))
|
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):
|
class AvatarToolKit_PT_SettingsPanel(Panel):
|
||||||
"""Settings panel for Avatar Toolkit containing language preferences"""
|
"""Settings panel for Avatar Toolkit containing language preferences"""
|
||||||
@@ -46,30 +49,18 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Language Settings
|
# Language Settings
|
||||||
lang_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Settings.language"), icon='WORLD')
|
||||||
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.prop(props, "language", text="")
|
col.prop(props, "language", text="")
|
||||||
|
|
||||||
# Validation Settings
|
# Validation Settings with help text
|
||||||
val_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Settings.validation_mode"), icon='CHECKMARK')
|
||||||
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()
|
|
||||||
col.prop(props, "validation_mode", text="")
|
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 Highlighting Settings
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Settings.bone_highlighting"), icon='BONE_DATA')
|
||||||
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.prop(props, "highlight_problem_bones")
|
col.prop(props, "highlight_problem_bones")
|
||||||
if props.highlight_problem_bones:
|
if props.highlight_problem_bones:
|
||||||
col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR')
|
col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR')
|
||||||
|
|||||||
+32
-55
@@ -2,6 +2,7 @@ import bpy
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from bpy.types import Panel, Context, UILayout, Operator, UIList
|
from bpy.types import Panel, Context, UILayout, Operator, UIList
|
||||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
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.translations import t
|
||||||
|
|
||||||
from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite
|
from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite
|
||||||
@@ -38,94 +39,70 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
|
|||||||
toolkit = context.scene.avatar_toolkit
|
toolkit = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# General Tools
|
# General Tools
|
||||||
tools_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.general_title"), icon='TOOL_SETTINGS')
|
||||||
col: UILayout = tools_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.general_title"), icon='TOOL_SETTINGS')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT')
|
col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT')
|
||||||
|
|
||||||
# Separation Tools
|
# Separation Tools
|
||||||
sep_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.separate_title"), icon='MOD_EXPLODE')
|
||||||
col = sep_box.column(align=True)
|
draw_operator_row(col, [
|
||||||
col.label(text=t("Tools.separate_title"), icon='MOD_EXPLODE')
|
(AvatarToolKit_OT_SeparateByMaterials.bl_idname, t("Tools.separate_materials"), 'MATERIAL'),
|
||||||
col.separator(factor=0.5)
|
(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, t("Tools.separate_loose"), 'MESH_DATA')
|
||||||
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')
|
|
||||||
|
|
||||||
# Bone Tools
|
# Bone Tools
|
||||||
bone_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.bone_title"), icon='BONE_DATA')
|
||||||
col = bone_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.bone_title"), icon='BONE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), 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 Tools
|
||||||
mesh_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.mesh_title"), icon='MESH_DATA')
|
||||||
col = mesh_box.column(align=True)
|
col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname, text=t("Tools.find_shortest_seam_path"), icon="MESH_DATA")
|
||||||
col.label(text=t("Tools.mesh_title"), icon='MESH_DATA')
|
col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname, text=t("Tools.apply_modifier_on_shapekey_obj"), icon="SHAPEKEY_DATA")
|
||||||
col.separator(factor=0.5)
|
col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname, text=t("Tools.explode_mesh"), icon="MOD_EXPLODE")
|
||||||
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
|
# Standardization Tools
|
||||||
standardize_box: UILayout = bone_box.box()
|
col = draw_section_header(layout, t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
|
||||||
col = standardize_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK')
|
col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK')
|
||||||
|
|
||||||
# Weight Tools
|
# Weight Tools
|
||||||
weight_box: UILayout = bone_box.box()
|
col = draw_section_header(layout, t("Tools.weight_title"), icon='GROUP_BONE')
|
||||||
col = weight_box.column(align=True)
|
|
||||||
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
||||||
col.prop(toolkit, "preserve_parent_bones")
|
col.prop(toolkit, "preserve_parent_bones")
|
||||||
col.prop(toolkit, "target_bone_type")
|
col.prop(toolkit, "target_bone_type")
|
||||||
col.prop(toolkit, "list_only_mode")
|
col.prop(toolkit, "list_only_mode")
|
||||||
|
|
||||||
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
|
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
|
||||||
box = weight_box.box()
|
sub_col = col.box()
|
||||||
row = box.row()
|
row = sub_col.row()
|
||||||
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
|
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
|
||||||
toolkit, "zero_weight_bones",
|
toolkit, "zero_weight_bones",
|
||||||
toolkit, "zero_weight_bones_index")
|
toolkit, "zero_weight_bones_index")
|
||||||
|
|
||||||
col = box.column(align=True)
|
sub_col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname,
|
||||||
col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname,
|
text=t("Tools.remove_selected_bones"))
|
||||||
text=t("Tools.remove_selected_bones"))
|
|
||||||
|
|
||||||
row = col.row(align=True)
|
# Combine weight
|
||||||
row.operator(AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.clean_weights"), icon='GROUP_BONE')
|
draw_operator_row(col, [
|
||||||
row.operator(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
|
(AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, t("Tools.clean_weights"), 'GROUP_BONE'),
|
||||||
row = col.row(align=True)
|
(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, t("Tools.clean_constraints"), 'CONSTRAINT_BONE')
|
||||||
row.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE')
|
])
|
||||||
|
col.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE')
|
||||||
|
|
||||||
# Merge Tools
|
# Merge Tools
|
||||||
merge_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.merge_title"), icon='AUTOMERGE_ON')
|
||||||
col = merge_box.column(align=True)
|
draw_operator_row(col, [
|
||||||
col.label(text=t("Tools.merge_title"), icon='AUTOMERGE_ON')
|
(AvatarToolkit_OT_MergeToActive.bl_idname, t("Tools.merge_to_active"), 'BONE_DATA'),
|
||||||
col.separator(factor=0.5)
|
(AvatarToolkit_OT_MergeToParent.bl_idname, t("Tools.merge_to_parent"), 'BONE_DATA')
|
||||||
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.operator(AvatarToolkit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones"), icon='BONE_DATA')
|
col.operator(AvatarToolkit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones"), icon='BONE_DATA')
|
||||||
|
|
||||||
# Additional Tools
|
# Additional Tools
|
||||||
extra_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.additional_title"), icon='TOOL_SETTINGS')
|
||||||
col = extra_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.additional_title"), icon='TOOL_SETTINGS')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
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')
|
col.operator(AvatarToolkit_OT_CleanShapekeys.bl_idname, text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
||||||
|
|
||||||
# Rigify Tools
|
# Rigify Tools
|
||||||
rigify_box: UILayout = layout.box()
|
col = draw_section_header(layout, t("Tools.rigify_title"), icon='ARMATURE_DATA')
|
||||||
col = rigify_box.column(align=True)
|
|
||||||
col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA')
|
|
||||||
col.separator(factor=0.5)
|
|
||||||
col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA')
|
col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA')
|
||||||
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
|
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
|
||||||
|
|
||||||
|
|||||||
+137
@@ -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)
|
||||||
Reference in New Issue
Block a user