import bpy from typing import List, Tuple, Optional, Any, Dict, Union, Callable from bpy.types import PropertyGroup, Material, Scene, Object, Context from bpy.props import ( StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty, CollectionProperty, PointerProperty ) from .logging_setup import logger from .translations import t, get_languages_list, update_language from .addon_preferences import get_preference, save_preference from .updater import get_version_list from .common import get_armature_list, get_active_armature, get_all_meshes, SceneMatClass from ..functions.visemes import VisemePreview from ..functions.eye_tracking import set_rotation class ValidationMessageItem(PropertyGroup): """Property group for validation message items""" name: StringProperty(name="Message") class ZeroWeightBoneItem(PropertyGroup): """Property group for zero weight bone list items""" name: StringProperty(name="Bone Name") selected: BoolProperty(name="Selected", default=True) has_children: BoolProperty(name="Has Children", default=False) is_deform: BoolProperty(name="Is Deform Bone", default=False) def update_validation_mode(self: PropertyGroup, context: Context) -> None: """Updates validation mode and saves preference""" logger.info(f"Updating validation mode to: {self.validation_mode}") save_preference("validation_mode", self.validation_mode) def update_logging_state(self: PropertyGroup, context: Context) -> None: """Updates logging state and configures logging""" logger.info(f"Updating logging state to: {self.enable_logging}") save_preference("enable_logging", self.enable_logging) from .logging_setup import configure_logging configure_logging(self.enable_logging) def update_log_level(self: PropertyGroup, context: Context) -> None: """Updates log level and configures logging""" logger.info(f"Updating log level to: {self.log_level}") save_preference("log_level", self.log_level) from .logging_setup import configure_logging configure_logging(self.enable_logging, self.log_level) def update_shape_intensity(self: PropertyGroup, context: Context) -> None: """Updates shape key intensity and refreshes preview""" if self.viseme_preview_mode: VisemePreview.update_preview(context) def highlight_problem_bones(self: PropertyGroup, context: Context) -> None: """Updates problem bone highlighting state and saves preference""" logger.info(f"Updating problem bone highlighting to: {self.highlight_problem_bones}") save_preference("highlight_problem_bones", self.highlight_problem_bones) def get_mesh_objects(self, context): meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH'] if not meshes: return [('NONE', t("Visemes.no_meshes"), '')] return meshes class AvatarToolkitSceneProperties(PropertyGroup): """Property group containing Avatar Toolkit scene-level settings and properties""" show_found_bones: BoolProperty( name="Show Found Bones", default=False ) show_non_standard: BoolProperty( name="Show Non-Standard Bones", default=False ) show_hierarchy: BoolProperty( name="Show Hierarchy Issues", default=False ) material_search_filter: StringProperty( name=t("TextureAtlas.search_materials"), description=t("TextureAtlas.search_materials_desc"), default="" ) def get_texture_node_list(self: Material, context: Context) -> list[tuple]: if self.use_nodes: Object.Enum = [((i.image.name if i.image else i.name+"_image"), (i.image.name if i.image else "node with no image..."), (i.image.name if i.image else i.name), index+1) for index, i in enumerate(self.node_tree.nodes) if i.bl_idname == "ShaderNodeTexImage"] if not len(Object.Enum): Object.Enum = [(t("TextureAtlas.error.label"), t("TextureAtlas.no_images_error.desc"), t("TextureAtlas.error.label"), 0)] else: Object.Enum = [(t("TextureAtlas.error.label"), t("TextureAtlas.no_nodes_error.desc"), t("TextureAtlas.error.label"), 0)] Object.Enum.append((t("TextureAtlas.none.label"), t("TextureAtlas.none.label"), t("TextureAtlas.none.label"), 0)) return Object.Enum Material.texture_atlas_albedo = EnumProperty( name=t("TextureAtlas.albedo"), description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()), default=0, items=get_texture_node_list ) Material.texture_atlas_normal = EnumProperty( name=t("TextureAtlas.normal"), description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()), default=0, items=get_texture_node_list ) Material.texture_atlas_emission = EnumProperty( name=t("TextureAtlas.emission"), description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()), default=0, items=get_texture_node_list ) Material.texture_atlas_ambient_occlusion = EnumProperty( name=t("TextureAtlas.ambient_occlusion"), description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()), default=0, items=get_texture_node_list ) Material.texture_atlas_height = EnumProperty( name=t("TextureAtlas.height"), description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()), default=0, items=get_texture_node_list ) Material.texture_atlas_roughness = EnumProperty( name=t("TextureAtlas.roughness"), description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()), default=0, items=get_texture_node_list ) list_only_mode: BoolProperty( name=t("Tools.list_only_mode"), description=t("Tools.list_only_mode_desc"), default=False ) Material.include_in_atlas = BoolProperty( name=t("TextureAtlas.include_in_atlas"), description=t("TextureAtlas.include_in_atlas_desc"), default=False ) Material.material_expanded = BoolProperty( name=t("TextureAtlas.material_expanded"), description=t("TextureAtlas.material_expanded_desc"), default=False ) texture_atlas_Has_Mat_List_Shown: BoolProperty( name=t("TextureAtlas.list_shown"), description=t("TextureAtlas.list_shown_desc"), default=False ) texture_atlas_material_index: IntProperty( default=-1, get=lambda self: -1, set=lambda self, context: None ) materials: CollectionProperty( type=SceneMatClass ) avatar_toolkit_updater_version_list: EnumProperty( items=get_version_list, name=t("Scene.avatar_toolkit_updater_version_list.name"), description=t("Scene.avatar_toolkit_updater_version_list.description") ) active_armature: EnumProperty( items=get_armature_list, name=t("QuickAccess.select_armature"), description=t("QuickAccess.select_armature"), update=lambda self, context: update_active_armature(self, context) ) language: EnumProperty( name=t("Settings.language"), description=t("Settings.language_desc"), items=get_languages_list, update=update_language ) validation_mode: EnumProperty( name=t("Settings.validation_mode"), description=t("Settings.validation_mode_desc"), items=[ ('STRICT', t("Settings.validation_mode.strict"), t("Settings.validation_mode.strict_desc")), ('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"), update=update_validation_mode ) enable_logging: BoolProperty( name=t("Settings.enable_logging"), description=t("Settings.enable_logging_desc"), default=False, update=update_logging_state ) debug_expand: BoolProperty( name="Debug Settings Expanded", default=False ) remove_doubles_merge_distance: FloatProperty( name=t("Optimization.merge_distance"), description=t("Optimization.merge_distance_desc"), default=0.0001, min=0.00001, max=0.1 ) remove_doubles_advanced: BoolProperty( name=t("Optimization.remove_doubles_advanced"), description=t("Optimization.remove_doubles_advanced_desc"), default=False ) connect_bones_min_distance: FloatProperty( name=t("Tools.connect_bones_min_distance"), description=t("Tools.connect_bones_min_distance_desc"), default=0.001, min=0.0001, max=0.1, precision=4 ) merge_twist_bones: BoolProperty( name=t("MMD.merge_twist_bones"), description=t("MMD.merge_twist_bones_desc"), default=True ) keep_twist_bones: BoolProperty( name=t("MMD.keep_twist_bones"), description=t("MMD.keep_twist_bones_desc"), default=False ) keep_upper_chest: BoolProperty( name=t("MMD.keep_upper_chest"), description=t("MMD.keep_upper_chest_desc"), default=True ) merge_weights_threshold: FloatProperty( name=t("MMD.merge_weights_threshold"), description=t("MMD.merge_weights_threshold_desc"), default=0.01, min=0.0, max=1.0 ) viseme_preview_mode: BoolProperty( name=t("Visemes.preview_mode"), description=t("Visemes.preview_mode_desc"), default=False ) mouth_a: StringProperty( name=t("Visemes.mouth_a"), description=t("Visemes.mouth_a_desc") ) mouth_o: StringProperty( name=t("Visemes.mouth_o"), description=t("Visemes.mouth_o_desc") ) mouth_ch: StringProperty( name=t("Visemes.mouth_ch"), description=t("Visemes.mouth_ch_desc") ) viseme_mesh: EnumProperty( name=t("Visemes.mesh_select"), description=t("Visemes.mesh_select_desc"), items=get_mesh_objects ) shape_intensity: FloatProperty( name=t("Visemes.shape_intensity"), description=t("Visemes.shape_intensity_desc"), default=1.0, min=0.0, max=2.0, precision=3, update=update_shape_intensity ) viseme_preview_selection: EnumProperty( name=t("Visemes.preview_selection"), description=t("Visemes.preview_selection_desc"), items=[ ('vrc.v_aa', 'AA', 'A as in "bat"'), ('vrc.v_ch', 'CH', 'Ch as in "choose"'), ('vrc.v_dd', 'DD', 'D as in "dog"'), ('vrc.v_ih', 'IH', 'I as in "bit"'), ('vrc.v_ff', 'FF', 'F as in "fox"'), ('vrc.v_e', 'E', 'E as in "bet"'), ('vrc.v_kk', 'KK', 'K as in "cat"'), ('vrc.v_nn', 'NN', 'N as in "net"'), ('vrc.v_oh', 'OH', 'O as in "hot"'), ('vrc.v_ou', 'OU', 'O as in "go"'), ('vrc.v_pp', 'PP', 'P as in "pat"'), ('vrc.v_rr', 'RR', 'R as in "red"'), ('vrc.v_sil', 'SIL', 'Silence'), ('vrc.v_ss', 'SS', 'S as in "sit"'), ('vrc.v_th', 'TH', 'Th as in "think"') ], update=lambda s, c: VisemePreview.update_preview(c) ) eye_tracking_type: EnumProperty( name=t("EyeTracking.type"), description=t("EyeTracking.type_desc"), items=[ ('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")), ('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc")) ], default='AV3' ) eye_mode: EnumProperty( name=t("EyeTracking.mode"), items=[ ('CREATION', t("EyeTracking.mode.creation"), ""), ('TESTING', t("EyeTracking.mode.testing"), "") ], default='CREATION' ) eye_rotation_x: FloatProperty( name=t("EyeTracking.rotation.x"), update=set_rotation ) eye_rotation_y: FloatProperty( name=t("EyeTracking.rotation.y"), update=set_rotation ) mesh_name_eye: StringProperty( name=t("EyeTracking.mesh_name"), description=t("EyeTracking.mesh_name_desc") ) head: StringProperty( name=t("EyeTracking.head_bone"), description=t("EyeTracking.head_bone_desc") ) eye_left: StringProperty( name=t("EyeTracking.eye_left"), description=t("EyeTracking.eye_left_desc") ) eye_right: StringProperty( name=t("EyeTracking.eye_right"), description=t("EyeTracking.eye_right_desc") ) disable_eye_movement: BoolProperty( name=t("EyeTracking.disable_movement"), description=t("EyeTracking.disable_movement_desc"), default=False ) disable_eye_blinking: BoolProperty( name=t("EyeTracking.disable_blinking"), description=t("EyeTracking.disable_blinking_desc"), default=False ) eye_distance: FloatProperty( name=t("EyeTracking.distance"), description=t("EyeTracking.distance_desc"), default=0.0, min=-1.0, max=1.0 ) iris_height: FloatProperty( name=t("EyeTracking.iris_height"), description=t("EyeTracking.iris_height_desc"), default=0.0, min=-1.0, max=1.0 ) eye_blink_shape: FloatProperty( name=t("EyeTracking.blink_shape"), description=t("EyeTracking.blink_shape_desc"), default=1.0, min=0.0, max=1.0 ) eye_lowerlid_shape: FloatProperty( name=t("EyeTracking.lowerlid_shape"), description=t("EyeTracking.lowerlid_shape_desc"), default=1.0, min=0.0, max=1.0 ) wink_left: StringProperty( name=t("EyeTracking.wink_left"), description=t("EyeTracking.wink_left_desc") ) wink_right: StringProperty( name=t("EyeTracking.wink_right"), description=t("EyeTracking.wink_right_desc") ) lowerlid_left: StringProperty( name=t("EyeTracking.lowerlid_left"), description=t("EyeTracking.lowerlid_left_desc") ) lowerlid_right: StringProperty( name=t("EyeTracking.lowerlid_right"), description=t("EyeTracking.lowerlid_right_desc") ) merge_mode: EnumProperty( name=t('CustomPanel.merge_mode'), description=t('CustomPanel.merge_mode_desc'), items=[ ('ARMATURE', t('CustomPanel.mode.armature'), t('CustomPanel.mode.armature_desc')), ('MESH', t('CustomPanel.mode.mesh'), t('CustomPanel.mode.mesh_desc')) ], default='ARMATURE' ) merge_armature_into: StringProperty( name=t('MergeArmature.into'), description=t('MergeArmature.into_desc'), default="" ) merge_armature: StringProperty( name=t('MergeArmature.from'), description=t('MergeArmature.from_desc'), default="" ) attach_mesh: StringProperty( name=t('AttachMesh.select'), description=t('AttachMesh.select_desc'), default="" ) attach_bone: StringProperty( name=t('AttachBone.select'), description=t('AttachBone.select_desc'), default="" ) apply_transforms: BoolProperty( name=t('MergeArmature.apply_transforms'), description=t('MergeArmature.apply_transforms_desc'), default=True ) join_meshes: BoolProperty( name=t('MergeArmature.join_meshes'), description=t('MergeArmature.join_meshes_desc'), default=True ) remove_zero_weights: BoolProperty( name=t('MergeArmature.remove_zero_weights'), description=t('MergeArmature.remove_zero_weights_desc'), default=True ) preserve_parent_bones: BoolProperty( name=t("Tools.preserve_parent_bones"), description=t("Tools.preserve_parent_bones_desc"), default=True ) target_bone_type: EnumProperty( name=t("Tools.target_bone_type"), description=t("Tools.target_bone_type_desc"), items=[ ('ALL', t("Tools.target_all_bones"), ""), ('DEFORM', t("Tools.target_deform_bones"), ""), ('NON_DEFORM', t("Tools.target_non_deform_bones"), "") ], default='ALL' ) zero_weight_bones: CollectionProperty( type=ZeroWeightBoneItem, name="Zero Weight Bones", description="List of bones with zero weights" ) zero_weight_bones_index: IntProperty( name="Zero Weight Bone Index", default=0 ) list_only_mode: BoolProperty( name=t("Tools.list_only_mode"), description=t("Tools.list_only_mode_desc"), default=False ) cleanup_shape_keys: BoolProperty( name=t('MergeArmature.cleanup_shape_keys'), description=t('MergeArmature.cleanup_shape_keys_desc'), default=True ) merge_twist_bones: BoolProperty( name=t("Tools.merge_twist_bones"), description=t("Tools.merge_twist_bones_desc"), default=True ) highlight_problem_bones: BoolProperty( name=t("Settings.highlight_problem_bones"), description=t("Settings.highlight_problem_bones_desc"), default=get_preference("highlight_problem_bones", True), update=highlight_problem_bones ) show_scale_issues: BoolProperty( name="Show Scale Issues", default=False ) tpose_validation_result: BoolProperty( name="T-Pose Validation Result", default=True ) tpose_validation_messages: CollectionProperty( type=bpy.types.PropertyGroup, name="T-Pose Validation Messages" ) show_tpose_validation: BoolProperty( name="Show T-Pose Validation Results", default=False ) standardize_fix_names: BoolProperty( name=t("Tools.standardize_fix_names"), description=t("Tools.standardize_fix_names_desc"), default=True ) standardize_fix_hierarchy: BoolProperty( name=t("Tools.standardize_fix_hierarchy"), description=t("Tools.standardize_fix_hierarchy_desc"), default=True ) standardize_fix_scale: BoolProperty( name=t("Tools.standardize_fix_scale"), description=t("Tools.standardize_fix_scale_desc"), default=True ) log_level: EnumProperty( name=t("Settings.log_level"), description=t("Settings.log_level_desc"), items=[ ('DEBUG', t("Settings.log_level.debug"), t("Settings.log_level.debug_desc")), ('INFO', t("Settings.log_level.info"), t("Settings.log_level.info_desc")), ('WARNING', t("Settings.log_level.warning"), t("Settings.log_level.warning_desc")), ('ERROR', t("Settings.log_level.error"), t("Settings.log_level.error_desc")), ], default=get_preference("log_level", "WARNING"), update=update_log_level ) # VRM Conversion Properties vrm_remove_colliders: BoolProperty( name=t("VRM.remove_colliders"), description=t("VRM.remove_colliders_desc"), default=True ) vrm_remove_root: BoolProperty( name=t("VRM.remove_root"), description=t("VRM.remove_root_desc"), default=True ) # Translation System Properties translation_service: EnumProperty( name=t("Translation.service"), description=t("Translation.service_desc"), items=[ ('mymemory', t("Translation.service.mymemory"), t("Translation.service.mymemory_desc")), ('libretranslate', t("Translation.service.libretranslate"), t("Translation.service.libretranslate_desc")), ('deepl', t("Translation.service.deepl"), t("Translation.service.deepl_desc")) ], default=get_preference("translation_service", "mymemory"), update=lambda self, context: update_translation_service(self, context) ) translation_mode: EnumProperty( name=t("Translation.mode"), description=t("Translation.mode_desc"), items=[ ('hybrid', t("Translation.mode.hybrid"), t("Translation.mode.hybrid_desc")), ('dictionary_only', t("Translation.mode.dictionary_only"), t("Translation.mode.dictionary_only_desc")), ('api_only', t("Translation.mode.api_only"), t("Translation.mode.api_only_desc")) ], default=get_preference("translation_mode", "hybrid"), update=lambda self, context: update_translation_mode(self, context) ) translation_expand: BoolProperty( name="Translation Settings Expanded", default=False ) translation_target_language: EnumProperty( name=t("Translation.target_language"), description=t("Translation.target_language_desc"), items=[ ('en', 'English', 'Translate to English'), ('ja', 'Japanese', 'Translate to Japanese'), ('ko', 'Korean', 'Translate to Korean'), ('zh', 'Chinese', 'Translate to Chinese'), ('es', 'Spanish', 'Translate to Spanish'), ('fr', 'French', 'Translate to French'), ('de', 'German', 'Translate to German') ], default='en' ) translation_source_language: EnumProperty( name=t("Translation.source_language"), description=t("Translation.source_language_desc"), items=[ ('auto', 'Auto-detect', 'Automatically detect source language'), ('ja', 'Japanese', 'Source is Japanese'), ('en', 'English', 'Source is English'), ('ko', 'Korean', 'Source is Korean'), ('zh', 'Chinese', 'Source is Chinese') ], default='ja' ) def update_translation_service(self: PropertyGroup, context: Context) -> None: """Update translation service preference""" logger.info(f"Updating translation service to: {self.translation_service}") save_preference("translation_service", self.translation_service) # Clear module-level translation caches when service changes try: from ..ui.translation_panel import _ui_cache _ui_cache['deepl_config'].clear() _ui_cache['libretranslate_config'].clear() _ui_cache['translation_status'].clear() if 'batch_info' in _ui_cache: del _ui_cache['batch_info'] # Clear batch info cache when service changes except ImportError: pass # UI module might not be loaded yet # Set the primary service try: from .translation_manager import get_avatar_translation_manager manager = get_avatar_translation_manager() manager.service_manager.set_primary_service(self.translation_service) except Exception as e: logger.error(f"Failed to update translation service: {e}") def update_translation_mode(self: PropertyGroup, context: Context) -> None: """Update translation mode preference""" logger.info(f"Updating translation mode to: {self.translation_mode}") save_preference("translation_mode", self.translation_mode) # Clear module-level translation status cache when mode changes try: from ..ui.translation_panel import _ui_cache _ui_cache['translation_status'].clear() if 'batch_info' in _ui_cache: del _ui_cache['batch_info'] # Clear batch info cache when mode changes except ImportError: pass # UI module might not be loaded yet try: from .translation_manager import get_avatar_translation_manager, TranslationMode manager = get_avatar_translation_manager() manager.set_translation_mode(TranslationMode(self.translation_mode)) except Exception as e: logger.error(f"Failed to update translation mode: {e}") def update_active_armature(self: PropertyGroup, context: Context) -> None: """Update the active armature when selection changes""" if self.active_armature: logger.info(f"Active armature set to: {self.active_armature}") # Deselect all objects first bpy.ops.object.select_all(action='DESELECT') # Select and make active the chosen armature self.active_armature.select_set(True) context.view_layer.objects.active = self.active_armature logger.info(f"Selected and activated armature: {self.active_armature.name}") # Clear armature caches when armature changes to ensure fresh validation try: from ..ui.quick_access_panel import clear_armature_caches clear_armature_caches() except ImportError: pass # UI module might not be loaded yet else: logger.info("No armature selected") def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") # Only register the property, not the classes (auto_load will handle that) bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties) logger.debug("Properties registered successfully") def unregister() -> None: """Unregister the Avatar Toolkit property group""" logger.info("Unregistering Avatar Toolkit properties") # Remove the property if hasattr(bpy.types.Scene, "avatar_toolkit"): try: del bpy.types.Scene.avatar_toolkit logger.debug("Removed avatar_toolkit property") except Exception as e: logger.warning(f"Failed to remove avatar_toolkit property: {e}") # Not fatal - continue