Merge branch 'atk-next' into Current
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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に変換",
|
||||
|
||||
@@ -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로 변환",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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')
|
||||
])
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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"""
|
||||
|
||||
@@ -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.
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user