Merge branch 'atk-next' into Current

This commit is contained in:
Yusarina
2025-11-19 06:40:58 +00:00
committed by GitHub
29 changed files with 716 additions and 413 deletions
+5 -5
View File
@@ -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]
+1 -1
View File
@@ -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
+64 -4
View File
@@ -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'}
+12 -1
View File
@@ -35,6 +35,11 @@ def update_validation_mode(self: PropertyGroup, context: Context) -> None:
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:
"""Updates logging state and configures logging"""
@@ -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
)
+11 -2
View File
@@ -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",
+11 -2
View File
@@ -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に変換",
+11 -2
View File
@@ -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로 변환",
+3 -1
View File
@@ -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
+3 -2
View File
@@ -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"""
+43 -72
View File
@@ -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
+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"""
+15 -28
View File
@@ -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')
])
+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)
+107 -149
View File
@@ -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,26 +81,37 @@ 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
# 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 = UIStyle.PRIMARY_BUTTON_SCALE
validate_row.operator("avatar_toolkit.validate_armature_manual",
text=t("Validation.validate_now", "Validate Armature Now"),
icon='CHECKMARK')
# Validation mode selector
col.prop(props, "validation_mode", text=t("Settings.validation_mode", "Mode"))
# 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)}"
if cache_key not in _validation_cache:
@@ -107,15 +120,16 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key]
# Check if this is a PMX model
is_pmx_model = False
if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')):
is_pmx_model = True
pmx_detected = is_pmx_model(active_armature)
info_box = col.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)
# If it's a PMX model, display a prominent notice
if is_pmx_model:
pmx_box = info_box.box()
# PMX Model Notice
if pmx_detected:
pmx_box = results_box.box()
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
validation_mode = context.scene.avatar_toolkit.validation_mode
@@ -125,38 +139,35 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
else:
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
# Validation Results
if not is_valid:
# Display non-standard bones and hierarchy issues
# Display found bones
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:
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'):
validation_box.label(text=line)
bones_section.label(text=line)
# Main validation status
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.status.failed"))
row.label(text=t("Validation.status.failed"), icon='ERROR')
# Detailed validation message
validation_box = info_box.box()
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.message.failed.line1"))
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.message.failed.line2"))
row = validation_box.row()
row.alert = True
row.label(text=t("Validation.message.failed.line3"))
# 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()
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)
@@ -164,161 +175,108 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
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 = 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:
# For PMX models, if no non-standard messages but it's a PMX model,
# we should still indicate there might be non-standard bones
if is_pmx_model:
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_basic"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_strict"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_standardize"))
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_non_standard_issues"))
ns_section.label(text=t("Validation.no_non_standard_issues"))
# Hierarchy Issues section
validation_box = info_box.box()
row = validation_box.row()
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:
if hierarchy_messages:
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()
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:
if scale_messages:
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
# Valid armature - show stats
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]
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']:
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')
results_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
# 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')
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 = info_box.box()
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)
+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
]
+15 -23
View File
@@ -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')
+34 -56
View File
@@ -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,
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")
+3 -2
View File
@@ -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"""
+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"""
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)
+3 -2
View File
@@ -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"""
+3 -2
View File
@@ -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"""
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.