Files
Avatar-Toolkit/core/properties.py
T

943 lines
32 KiB
Python

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)
# 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"""
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
def auto_populate_merge_armatures(context: Context) -> None:
"""Auto-populate merge armature fields when there are 2+ armatures"""
armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE']
if len(armatures) >= 2:
toolkit = context.scene.avatar_toolkit
if not toolkit.merge_armature_into and not toolkit.merge_armature:
toolkit.merge_armature_into = armatures[0].name
toolkit.merge_armature = armatures[1].name
logger.debug(f"Auto-populated merge armatures: {armatures[0].name} <- {armatures[1].name}")
elif toolkit.merge_armature_into and not toolkit.merge_armature:
for armature in armatures:
if armature.name != toolkit.merge_armature_into:
toolkit.merge_armature = armature.name
logger.debug(f"Auto-populated merge_armature: {armature.name}")
break
elif not toolkit.merge_armature_into and toolkit.merge_armature:
for armature in armatures:
if armature.name != toolkit.merge_armature:
toolkit.merge_armature_into = armature.name
logger.debug(f"Auto-populated merge_armature_into: {armature.name}")
break
def update_merge_armature_into(self: PropertyGroup, context: Context) -> None:
"""Update function for merge_armature_into property"""
auto_populate_merge_armatures(context)
def update_merge_armature(self: PropertyGroup, context: Context) -> None:
"""Update function for merge_armature property"""
auto_populate_merge_armatures(context)
@bpy.app.handlers.persistent
def depsgraph_update_handler(scene: Scene, depsgraph) -> None:
"""Handler to auto-populate merge armatures when objects change"""
# Check for any armature-related updates
armature_updated = False
for update in depsgraph.updates:
if hasattr(update, 'id') and update.id and hasattr(update.id, 'type'):
if update.id.type == 'ARMATURE':
armature_updated = True
break
if armature_updated:
# Use a timer to defer the update to avoid context issues
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
def auto_populate_safe() -> None:
"""Safe auto-populate function that can be called from timer"""
try:
if bpy.context and hasattr(bpy.context, 'scene') and hasattr(bpy.context.scene, 'avatar_toolkit'):
auto_populate_merge_armatures(bpy.context)
except (AttributeError, ReferenceError):
pass
return None # Don't repeat the timer
@bpy.app.handlers.persistent
def undo_post_handler(scene: Scene) -> None:
"""Handler for undo operations that might add/remove armatures"""
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
@bpy.app.handlers.persistent
def redo_post_handler(scene: Scene) -> None:
"""Handler for redo operations that might add/remove armatures"""
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
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
)
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"),
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", "NONE"),
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="",
update=update_merge_armature_into
)
merge_armature: StringProperty(
name=t('MergeArmature.from'),
description=t('MergeArmature.from_desc'),
default="",
update=update_merge_armature
)
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
)
# MMD Conversion Properties
mmd_make_parent: BoolProperty(
name=t("MMD.make_armature_parent"),
description="Remove parent Empty object and make armature the main parent",
default=True
)
mmd_rename_armature: BoolProperty(
name=t("MMD.rename_to_armature"),
description="Rename the armature object to 'Armature'",
default=True
)
mmd_translate_names: BoolProperty(
name=t("MMD.translate_names"),
description="Translate Japanese names to English using MMD dictionary and translation services",
default=True
)
mmd_translate_bones: BoolProperty(
name=t("MMD.translate_bones"),
description="Translate bone names",
default=True
)
mmd_translate_materials: BoolProperty(
name=t("MMD.translate_materials"),
description="Translate material names",
default=True
)
mmd_translate_shapekeys: BoolProperty(
name=t("MMD.translate_shapekeys"),
description="Translate shape key names",
default=True
)
mmd_translate_objects: BoolProperty(
name=t("MMD.translate_objects"),
description="Translate object names",
default=True
)
mmd_restructure_bones: BoolProperty(
name=t("MMD.restructure_bones"),
description="Restructure bone hierarchy to Unity humanoid format (Hips, Spine, Chest, etc.)",
default=True
)
mmd_remove_twist_bones: BoolProperty(
name=t("MMD.remove_twist_bones"),
description="Remove twist bones",
default=True
)
mmd_remove_zero_weight_bones: BoolProperty(
name=t("MMD.remove_zero_weight_bones"),
description="Remove bones with zero or near-zero vertex weights",
default=False
)
# 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 and self.active_armature != 'NONE':
# Get the actual armature object from the identifier
armature = get_active_armature(context)
if armature:
logger.info(f"Active armature set to: {armature.name}")
# Deselect all objects first
bpy.ops.object.select_all(action='DESELECT')
# Select and make active the chosen armature
armature.select_set(True)
context.view_layer.objects.active = armature
logger.info(f"Selected and activated armature: {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.warning("Failed to get armature object from identifier")
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)
# Register handlers for auto-populating merge armatures
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_handler)
bpy.app.handlers.undo_post.append(undo_post_handler)
bpy.app.handlers.redo_post.append(redo_post_handler)
# Initial auto-populate
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=1.0)
logger.debug("Properties registered successfully")
def unregister() -> None:
"""Unregister the Avatar Toolkit property group"""
logger.info("Unregistering Avatar Toolkit properties")
# Remove handlers
if depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_handler)
if undo_post_handler in bpy.app.handlers.undo_post:
bpy.app.handlers.undo_post.remove(undo_post_handler)
if redo_post_handler in bpy.app.handlers.redo_post:
bpy.app.handlers.redo_post.remove(redo_post_handler)
# 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