From 843147db695ea171061a240ccb5d8f9f90749ec3 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 29 Nov 2025 22:44:26 +0000 Subject: [PATCH] Fix garbled Japanese/Unicode text in armature and mesh dropdowns - Add proper caching to EnumProperty callbacks to prevent encoding corruption - Use ASCII-safe identifiers (ARM_/MESH_ + pointer) with Unicode display names - Add get_mesh_from_identifier() helper for safe mesh retrieval - Update visemes panel to use new mesh identifier system - Ensure stable string objects prevent Blender RNA encoding issues --- CHANGELOG_NON_ASCII_FIX.md | 79 ++++++++++++++++++++++++++++++++++++++ core/common.py | 70 +++++++++++++++++++++++++++++---- core/properties.py | 38 ++++++++++++++++-- functions/visemes.py | 12 ++++-- ui/visemes_panel.py | 5 ++- 5 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 CHANGELOG_NON_ASCII_FIX.md diff --git a/CHANGELOG_NON_ASCII_FIX.md b/CHANGELOG_NON_ASCII_FIX.md new file mode 100644 index 0000000..45da7e7 --- /dev/null +++ b/CHANGELOG_NON_ASCII_FIX.md @@ -0,0 +1,79 @@ +# Fix for Garbled Japanese/Non-ASCII Text in Dropdowns + +## Problem +Japanese, Korean, Chinese, and other non-ASCII characters were displaying as garbled/corrupted text in dropdown menus for: +- Armature selection in Quick Access panel +- Mesh selection in Visemes panel + +This is a known issue with Blender's EnumProperty system when using dynamic callbacks that return Unicode strings. + +## Root Cause +Blender's EnumProperty RNA system can have encoding issues when: +1. The enum items function is called multiple times with changing data +2. Unicode strings in display names aren't properly cached +3. The internal C API receives the same Python string object in different states + +## Solution +Implemented proper caching with invalidation for EnumProperty items: + +### Changes Made + +1. **core/common.py** - Enhanced `get_armature_list()` function + - Added cache key based on (name, pointer) tuples + - Cache is invalidated only when actual objects change + - Prevents Blender from re-encoding strings on every access + - Added `clear_enum_caches()` helper function + +2. **core/properties.py** - Enhanced `get_mesh_objects()` function + - Added same caching mechanism as armature list + - Cache key based on mesh objects (name, pointer) + - Stable cache prevents encoding corruption + +3. **core/common.py** - `get_mesh_from_identifier()` helper + - Converts safe identifier back to mesh object + - Handles both new format (`MESH_{pointer}`) and legacy format + - Returns None if mesh not found + +4. **ui/visemes_panel.py** - Updated mesh retrieval + - Uses `get_mesh_from_identifier()` instead of direct lookup + +5. **functions/visemes.py** - Updated all mesh access points + - All operators now use the helper function consistently + +## Technical Details + +### ASCII-Safe Identifiers +- Dropdown identifier: `ARM_{memory_pointer}` or `MESH_{memory_pointer}` (ASCII-safe, unique) +- Dropdown display: Original object name (preserves Unicode characters) +- Backwards compatibility: Falls back to direct name lookup + +### Caching Strategy +The cache uses function attributes to store: +- `_cache_key`: Tuple of (name, pointer) for all relevant objects +- `_cached_items`: The actual list of enum items + +Cache is invalidated when: +- Objects are added/removed +- Objects are renamed +- Object pointers change (object recreated) + +This ensures Blender's RNA system receives the exact same Python string objects on subsequent calls, preventing encoding corruption. + +## Testing + +To verify the fix works: +1. Create armature/mesh objects with Japanese/Korean/Chinese names (e.g., "アバター", "아바타", "化身") +2. Open Quick Access panel - armature dropdown should display correctly +3. Open Visemes panel - mesh dropdown should display correctly +4. Select items - operations should work with the selected objects +5. Rename objects - dropdowns should update and still display correctly + +## Related Files +- `core/properties.py` - Property definitions and mesh enumeration +- `core/common.py` - Common utility functions and armature enumeration +- `ui/visemes_panel.py` - Visemes UI panel +- `ui/quick_access_panel.py` - Quick Access UI panel +- `functions/visemes.py` - Viseme operators + +## Note on prop_search +The `prop_search` widget used for shape key/bone selection inherently handles non-ASCII characters correctly since it searches Blender's internal data structures directly, not custom enum properties. diff --git a/core/common.py b/core/common.py index 8299b30..f63163e 100644 --- a/core/common.py +++ b/core/common.py @@ -142,6 +142,41 @@ def set_active_armature(context: Context, armature: Object) -> None: else: context.scene.avatar_toolkit.active_armature = 'NONE' +def get_mesh_from_identifier(mesh_id: str) -> Optional[Object]: + """Get mesh object from safe identifier + + Args: + mesh_id: Safe identifier in format "MESH_{pointer}" or direct object name + + Returns: + Mesh object or None if not found + """ + if not mesh_id or mesh_id == 'NONE': + return None + + # Handle new-style identifiers (MESH_{pointer}) + if mesh_id.startswith('MESH_'): + try: + pointer_str = mesh_id[5:] # Remove "MESH_" prefix + target_pointer = int(pointer_str) + + # Search for object with matching pointer + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.as_pointer() == target_pointer: + return obj + except (ValueError, AttributeError): + pass + + # Fallback for old-style identifiers (direct name) + return bpy.data.objects.get(mesh_id) + +def clear_enum_caches() -> None: + """Clear all enum property caches to force refresh of dropdown lists""" + if hasattr(get_armature_list, '_cache_key'): + delattr(get_armature_list, '_cache_key') + if hasattr(get_armature_list, '_cached_items'): + delattr(get_armature_list, '_cached_items') + def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]: """Get list of all armature objects in the scene @@ -149,21 +184,40 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N - identifier: ASCII-safe unique ID (uses object's memory address) - display_name: The actual object name (can contain Japanese characters) - description: Empty string + + Uses caching to prevent encoding issues with Blender's EnumProperty system """ if context is None: context = bpy.context - # Use object's as_pointer() value as a safe ASCII identifier + # Create a cache key based on armature objects in scene + armature_objects = [obj for obj in context.scene.objects if obj.type == 'ARMATURE'] + cache_key = tuple((obj.name, obj.as_pointer()) for obj in armature_objects) + + # Check if we have a cached result + if hasattr(get_armature_list, '_cache_key') and get_armature_list._cache_key == cache_key: + if hasattr(get_armature_list, '_cached_items'): + return get_armature_list._cached_items + + # Build the list armatures = [] - for obj in context.scene.objects: - if obj.type == 'ARMATURE': - # Create a safe ASCII identifier using the object pointer - safe_id = f"ARM_{obj.as_pointer()}" - armatures.append((safe_id, obj.name, "")) + for obj in armature_objects: + # Create a safe ASCII identifier using the object pointer + safe_id = f"ARM_{obj.as_pointer()}" + # Use the name directly - Blender should handle Unicode in display names + display_name = obj.name + armatures.append((safe_id, display_name, "")) if not armatures: - return [('NONE', t("Armature.validation.no_armature"), '')] - return armatures + result = [('NONE', t("Armature.validation.no_armature"), '')] + else: + result = armatures + + # Cache the result + get_armature_list._cache_key = cache_key + get_armature_list._cached_items = result + + return result def auto_select_single_armature(context: Context) -> None: """Automatically select armature if only one exists in scene""" diff --git a/core/properties.py b/core/properties.py index 7c14f56..24c93fb 100644 --- a/core/properties.py +++ b/core/properties.py @@ -67,10 +67,42 @@ def highlight_problem_bones(self: PropertyGroup, context: Context) -> None: 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'] + """Get list of all mesh objects with ASCII-safe identifiers + + Returns tuples of (identifier, display_name, description) where: + - identifier: ASCII-safe unique ID (uses object's memory address) + - display_name: The actual object name (can contain Japanese/non-ASCII characters) + - description: Empty string + + Uses caching to prevent encoding issues with Blender's EnumProperty system + """ + # Create a cache key based on mesh objects + mesh_objects = [obj for obj in bpy.data.objects if obj.type == 'MESH'] + cache_key = tuple((obj.name, obj.as_pointer()) for obj in mesh_objects) + + # Check if we have a cached result + if hasattr(get_mesh_objects, '_cache_key') and get_mesh_objects._cache_key == cache_key: + if hasattr(get_mesh_objects, '_cached_items'): + return get_mesh_objects._cached_items + + # Build the list + meshes = [] + for obj in mesh_objects: + safe_id = f"MESH_{obj.as_pointer()}" + # Use the name directly - Blender should handle Unicode in display names + display_name = obj.name + meshes.append((safe_id, display_name, "")) + if not meshes: - return [('NONE', t("Visemes.no_meshes"), '')] - return meshes + result = [('NONE', t("Visemes.no_meshes"), '')] + else: + result = meshes + + # Cache the result + get_mesh_objects._cache_key = cache_key + get_mesh_objects._cached_items = result + + return result def auto_populate_merge_armatures(context: Context) -> None: """Auto-populate merge armature fields when there are 2+ armatures""" diff --git a/functions/visemes.py b/functions/visemes.py index 008bd3c..da22101 100644 --- a/functions/visemes.py +++ b/functions/visemes.py @@ -137,15 +137,17 @@ class AvatarToolkit_OT_PreviewVisemes(Operator): return False # Get mesh from UI selection + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh_obj = bpy.data.objects.get(props.viseme_mesh) + mesh_obj = get_mesh_from_identifier(props.viseme_mesh) # Validate mesh return mesh_obj and mesh_obj.type == 'MESH' def execute(self, context: Context) -> Set[str]: + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh = bpy.data.objects.get(props.viseme_mesh) + mesh = get_mesh_from_identifier(props.viseme_mesh) if props.viseme_preview_mode: VisemePreview.end_preview(mesh) @@ -191,15 +193,17 @@ class AvatarToolkit_OT_CreateVisemes(Operator): return False # Get mesh from UI selection + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh_obj = bpy.data.objects.get(props.viseme_mesh) + mesh_obj = get_mesh_from_identifier(props.viseme_mesh) # Validate mesh return mesh_obj and mesh_obj.type == 'MESH' def execute(self, context: Context) -> Set[str]: + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object + mesh = get_mesh_from_identifier(props.viseme_mesh) if not mesh or not mesh.data.shape_keys: self.report({'ERROR'}, t("Visemes.error.no_shapekeys")) diff --git a/ui/visemes_panel.py b/ui/visemes_panel.py index b5563dd..544cde8 100644 --- a/ui/visemes_panel.py +++ b/ui/visemes_panel.py @@ -34,8 +34,9 @@ class AvatarToolKit_PT_VisemesPanel(Panel): else: col.label(text=t("Visemes.no_armature"), icon='ERROR') - # Get selected mesh - mesh_obj = bpy.data.objects.get(props.viseme_mesh) + # Get selected mesh using safe identifier + from ..core.common import get_mesh_from_identifier + mesh_obj = get_mesh_from_identifier(props.viseme_mesh) if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys: layout.label(text=t("Visemes.no_shapekeys")) return