Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37b92ded6d | |||
| fb470f19da | |||
| 843147db69 | |||
| fe2b0d50cb | |||
| c4dca2455d |
@@ -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.
|
||||||
+62
-8
@@ -142,6 +142,41 @@ def set_active_armature(context: Context, armature: Object) -> None:
|
|||||||
else:
|
else:
|
||||||
context.scene.avatar_toolkit.active_armature = 'NONE'
|
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]]:
|
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
|
"""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)
|
- identifier: ASCII-safe unique ID (uses object's memory address)
|
||||||
- display_name: The actual object name (can contain Japanese characters)
|
- display_name: The actual object name (can contain Japanese characters)
|
||||||
- description: Empty string
|
- description: Empty string
|
||||||
|
|
||||||
|
Uses caching to prevent encoding issues with Blender's EnumProperty system
|
||||||
"""
|
"""
|
||||||
if context is None:
|
if context is None:
|
||||||
context = bpy.context
|
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 = []
|
armatures = []
|
||||||
for obj in context.scene.objects:
|
for obj in armature_objects:
|
||||||
if obj.type == 'ARMATURE':
|
# Create a safe ASCII identifier using the object pointer
|
||||||
# Create a safe ASCII identifier using the object pointer
|
safe_id = f"ARM_{obj.as_pointer()}"
|
||||||
safe_id = f"ARM_{obj.as_pointer()}"
|
# Use the name directly - Blender should handle Unicode in display names
|
||||||
armatures.append((safe_id, obj.name, ""))
|
display_name = obj.name
|
||||||
|
armatures.append((safe_id, display_name, ""))
|
||||||
|
|
||||||
if not armatures:
|
if not armatures:
|
||||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
result = [('NONE', t("Armature.validation.no_armature"), '')]
|
||||||
return armatures
|
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:
|
def auto_select_single_armature(context: Context) -> None:
|
||||||
"""Automatically select armature if only one exists in scene"""
|
"""Automatically select armature if only one exists in scene"""
|
||||||
|
|||||||
+35
-3
@@ -67,10 +67,42 @@ def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
|
|||||||
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
||||||
|
|
||||||
def get_mesh_objects(self, context):
|
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:
|
if not meshes:
|
||||||
return [('NONE', t("Visemes.no_meshes"), '')]
|
result = [('NONE', t("Visemes.no_meshes"), '')]
|
||||||
return 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:
|
def auto_populate_merge_armatures(context: Context) -> None:
|
||||||
"""Auto-populate merge armature fields when there are 2+ armatures"""
|
"""Auto-populate merge armature fields when there are 2+ armatures"""
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
|||||||
# Define which version series this installation can update to
|
# Define which version series this installation can update to
|
||||||
# For example: ["0.1"] means only look for 0.1.x updates
|
# For example: ["0.1"] means only look for 0.1.x updates
|
||||||
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x
|
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x
|
||||||
ALLOWED_VERSION_SERIES = ["0.6"]
|
ALLOWED_VERSION_SERIES = ["0.5"]
|
||||||
|
|
||||||
is_checking_for_update: bool = False
|
is_checking_for_update: bool = False
|
||||||
update_needed: bool = False
|
update_needed: bool = False
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class AvatarToolkit_OT_StopPoseMode(Operator):
|
|||||||
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
|
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
||||||
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
|
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
|
||||||
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
|
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
|
||||||
@@ -136,7 +136,7 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
|||||||
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
|
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
||||||
bl_label = t("QuickAccess.apply_pose_as_rest.label")
|
bl_label = t("QuickAccess.apply_pose_as_rest.label")
|
||||||
bl_description = t("QuickAccess.apply_pose_as_rest.desc")
|
bl_description = t("QuickAccess.apply_pose_as_rest.desc")
|
||||||
|
|||||||
@@ -137,15 +137,17 @@ class AvatarToolkit_OT_PreviewVisemes(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get mesh from UI selection
|
# Get mesh from UI selection
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
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
|
# Validate mesh
|
||||||
return mesh_obj and mesh_obj.type == 'MESH'
|
return mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
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:
|
if props.viseme_preview_mode:
|
||||||
VisemePreview.end_preview(mesh)
|
VisemePreview.end_preview(mesh)
|
||||||
@@ -191,15 +193,17 @@ class AvatarToolkit_OT_CreateVisemes(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get mesh from UI selection
|
# Get mesh from UI selection
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
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
|
# Validate mesh
|
||||||
return mesh_obj and mesh_obj.type == 'MESH'
|
return mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
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:
|
if not mesh or not mesh.data.shape_keys:
|
||||||
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
||||||
|
|||||||
+3
-2
@@ -34,8 +34,9 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Get selected mesh
|
# Get selected mesh using safe identifier
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
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:
|
if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys:
|
||||||
layout.label(text=t("Visemes.no_shapekeys"))
|
layout.label(text=t("Visemes.no_shapekeys"))
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user