Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7f6632ea | |||
| 24b489f7a2 | |||
| 1e734a518e | |||
| 4b538cb8b2 | |||
| d85231b62b | |||
| b13ca15ece | |||
| 84bacca923 | |||
| 53d2ac10b7 | |||
| 95cb726485 | |||
| cb5b891d0d | |||
| aedd83e078 | |||
| 5719a55ae5 | |||
| 659f3eb91e | |||
| ff19a895dc | |||
| e6e5a98e58 | |||
| 3fe00da569 | |||
| 108f9d3bc8 | |||
| 1847628dc8 | |||
| 25a43afdbc | |||
| baaf4049f6 | |||
| 299800e5c2 | |||
| f6197ccbbf |
@@ -3,7 +3,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "avatar_toolkit"
|
id = "avatar_toolkit"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
name = "Avatar Toolkit"
|
name = "Avatar Toolkit"
|
||||||
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
||||||
maintainer = "Team NekoNeo"
|
maintainer = "Team NekoNeo"
|
||||||
|
|||||||
+118
-9
@@ -92,23 +92,132 @@ class ProgressTracker:
|
|||||||
|
|
||||||
def get_active_armature(context: Context) -> Optional[Object]:
|
def get_active_armature(context: Context) -> Optional[Object]:
|
||||||
"""Get the currently selected armature from Avatar Toolkit properties"""
|
"""Get the currently selected armature from Avatar Toolkit properties"""
|
||||||
armature_name = str(context.scene.avatar_toolkit.active_armature)
|
try:
|
||||||
if armature_name and armature_name != 'NONE':
|
# Get the safe identifier from the enum property
|
||||||
return bpy.data.objects.get(armature_name)
|
armature_id = context.scene.avatar_toolkit.active_armature
|
||||||
|
|
||||||
|
if not armature_id or armature_id == 'NONE':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# The identifier format is "ARM_{pointer_value}"
|
||||||
|
if armature_id.startswith('ARM_'):
|
||||||
|
try:
|
||||||
|
pointer_str = armature_id[4:]
|
||||||
|
pointer_value = int(pointer_str)
|
||||||
|
|
||||||
|
# Find the armature with this pointer value
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'ARMATURE' and obj.as_pointer() == pointer_value:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
logger.warning(f"Armature with pointer {pointer_value} not found")
|
||||||
|
except (ValueError, AttributeError) as e:
|
||||||
|
logger.error(f"Failed to parse armature identifier: {e}")
|
||||||
|
|
||||||
|
# Fallback for old-style identifiers (direct name)
|
||||||
|
# This handles backward compatibility
|
||||||
|
return bpy.data.objects.get(armature_id)
|
||||||
|
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError) as e:
|
||||||
|
# Handle encoding issues as a last resort
|
||||||
|
logger.warning(f"Encoding issue with active_armature property: {e}")
|
||||||
|
|
||||||
|
# Final fallback: return active object if it's an armature, or first armature found
|
||||||
|
if context.view_layer.objects.active and context.view_layer.objects.active.type == 'ARMATURE':
|
||||||
|
return context.view_layer.objects.active
|
||||||
|
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'ARMATURE':
|
||||||
|
logger.info(f"Falling back to first armature found: {obj.name}")
|
||||||
|
return obj
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_active_armature(context: Context, armature: Object) -> None:
|
def set_active_armature(context: Context, armature: Object) -> None:
|
||||||
"""Set the active armature for Avatar Toolkit operations"""
|
"""Set the active armature for Avatar Toolkit operations using safe identifier"""
|
||||||
context.scene.avatar_toolkit.active_armature = armature
|
if armature and armature.type == 'ARMATURE':
|
||||||
|
# Use the same safe identifier format as get_armature_list
|
||||||
|
safe_id = f"ARM_{armature.as_pointer()}"
|
||||||
|
context.scene.avatar_toolkit.active_armature = safe_id
|
||||||
|
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]]:
|
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
|
||||||
|
|
||||||
|
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 characters)
|
||||||
|
- 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
|
||||||
armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE']
|
|
||||||
|
# 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 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:
|
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"""
|
||||||
|
|||||||
@@ -174,6 +174,130 @@ physics_names: Dict[str, List[str]] = {
|
|||||||
"breast_tip": ["胸先", "むねさき", "ブレストティップ", "breasttip"],
|
"breast_tip": ["胸先", "むねさき", "ブレストティップ", "breasttip"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MMD bone name patterns (for detection)
|
||||||
|
mmd_bone_patterns: List[str] = [
|
||||||
|
# Japanese bone names
|
||||||
|
'全ての親', 'センター', '上半身', '下半身', '首', '頭',
|
||||||
|
'右腕', '左腕', '右ひじ', '左ひじ', '右手首', '左手首',
|
||||||
|
'右足', '左足', '右ひざ', '左ひざ', '右足首', '左足首',
|
||||||
|
'両目', '左目', '右目', '右肩', '左肩',
|
||||||
|
# English bone names (common in MMD exports)
|
||||||
|
'center', 'groove', 'waist', 'upperbody', 'upperbody2', 'lowerbody',
|
||||||
|
'neck', 'head',
|
||||||
|
'shoulder_r', 'shoulder_l', 'arm_r', 'arm_l',
|
||||||
|
'elbow_r', 'elbow_l', 'wrist_r', 'wrist_l',
|
||||||
|
'leg_r', 'leg_l', 'knee_r', 'knee_l',
|
||||||
|
'ankle_r', 'ankle_l', 'toe_r', 'toe_l',
|
||||||
|
# Mixed/Romanized patterns
|
||||||
|
'센터', 'グルーブ', 'ウエスト',
|
||||||
|
# Common MMD suffixes
|
||||||
|
'_r', '_l', '.r', '.l'
|
||||||
|
]
|
||||||
|
|
||||||
|
# MMD to Unity bone mapping
|
||||||
|
# Maps MMD bone names (after English translation) to Unity humanoid bone names
|
||||||
|
mmd_to_unity_bone_map: Dict[str, Optional[str]] = {
|
||||||
|
# Root and core
|
||||||
|
"ParentNode": None, # Remove this
|
||||||
|
"Center": "Hips",
|
||||||
|
"センター": "Hips",
|
||||||
|
"Groove": None, # Remove this
|
||||||
|
"グルーブ": None,
|
||||||
|
"Waist": None, # Will be merged into Hips
|
||||||
|
|
||||||
|
# Spine chain
|
||||||
|
"LowerBody": "Hips",
|
||||||
|
"下半身": "Hips",
|
||||||
|
"UpperBody": "Spine",
|
||||||
|
"上半身": "Spine",
|
||||||
|
"UpperBody2": "Chest",
|
||||||
|
"上半身2": "Chest",
|
||||||
|
"Neck": "Neck",
|
||||||
|
"首": "Neck",
|
||||||
|
"Head": "Head",
|
||||||
|
"頭": "Head",
|
||||||
|
|
||||||
|
# Right leg
|
||||||
|
"RightLeg": "Right leg",
|
||||||
|
"右足": "Right leg",
|
||||||
|
"RightLegD": None, # Remove D variant
|
||||||
|
"RightKnee": "Right knee",
|
||||||
|
"右ひざ": "Right knee",
|
||||||
|
"RightAnkle": "Right ankle",
|
||||||
|
"右足首": "Right ankle",
|
||||||
|
"RightToe": "Right toe",
|
||||||
|
"右つま先": "Right toe",
|
||||||
|
|
||||||
|
# Left leg
|
||||||
|
"LeftLeg": "Left leg",
|
||||||
|
"左足": "Left leg",
|
||||||
|
"LeftLegD": None, # Remove D variant
|
||||||
|
"LeftKnee": "Left knee",
|
||||||
|
"左ひざ": "Left knee",
|
||||||
|
"LeftAnkle": "Left ankle",
|
||||||
|
"左足首": "Left ankle",
|
||||||
|
"LeftToe": "Left toe",
|
||||||
|
"左つま先": "Left toe",
|
||||||
|
|
||||||
|
# Right arm
|
||||||
|
"RightShoulder": "Right shoulder",
|
||||||
|
"右肩": "Right shoulder",
|
||||||
|
"RightArm": "Right arm",
|
||||||
|
"右腕": "Right arm",
|
||||||
|
"RightElbow": "Right elbow",
|
||||||
|
"右ひじ": "Right elbow",
|
||||||
|
"RightWrist": "Right wrist",
|
||||||
|
"右手首": "Right wrist",
|
||||||
|
|
||||||
|
# Left arm
|
||||||
|
"LeftShoulder": "Left shoulder",
|
||||||
|
"左肩": "Left shoulder",
|
||||||
|
"LeftArm": "Left arm",
|
||||||
|
"左腕": "Left arm",
|
||||||
|
"LeftElbow": "Left elbow",
|
||||||
|
"左ひじ": "Left elbow",
|
||||||
|
"LeftWrist": "Left wrist",
|
||||||
|
"左手首": "Left wrist",
|
||||||
|
|
||||||
|
# Cancel/Helper bones (remove these)
|
||||||
|
"WaistCancelRight": None,
|
||||||
|
"WaistCancelLeft": None,
|
||||||
|
"LegIKParentRight": None,
|
||||||
|
"LegIKParentLeft": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unity humanoid bone hierarchy
|
||||||
|
# Defines parent-child relationships for Unity standard
|
||||||
|
unity_bone_hierarchy: Dict[str, Optional[str]] = {
|
||||||
|
"Hips": None, # Root bone
|
||||||
|
"Spine": "Hips",
|
||||||
|
"Chest": "Spine",
|
||||||
|
"Neck": "Chest",
|
||||||
|
"Head": "Neck",
|
||||||
|
|
||||||
|
# Arms
|
||||||
|
"Left shoulder": "Chest",
|
||||||
|
"Left arm": "Left shoulder",
|
||||||
|
"Left elbow": "Left arm",
|
||||||
|
"Left wrist": "Left elbow",
|
||||||
|
|
||||||
|
"Right shoulder": "Chest",
|
||||||
|
"Right arm": "Right shoulder",
|
||||||
|
"Right elbow": "Right arm",
|
||||||
|
"Right wrist": "Right elbow",
|
||||||
|
|
||||||
|
# Legs
|
||||||
|
"Left leg": "Hips",
|
||||||
|
"Left knee": "Left leg",
|
||||||
|
"Left ankle": "Left knee",
|
||||||
|
"Left toe": "Left ankle",
|
||||||
|
|
||||||
|
"Right leg": "Hips",
|
||||||
|
"Right knee": "Right leg",
|
||||||
|
"Right ankle": "Right knee",
|
||||||
|
"Right toe": "Right ankle",
|
||||||
|
}
|
||||||
|
|
||||||
# Create reverse lookup dictionaries
|
# Create reverse lookup dictionaries
|
||||||
reverse_shapekey_lookup: Dict[str, str] = {}
|
reverse_shapekey_lookup: Dict[str, str] = {}
|
||||||
reverse_material_lookup: Dict[str, str] = {}
|
reverse_material_lookup: Dict[str, str] = {}
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ def configure_logging(enabled: bool = False, level: str = "WARNING") -> None:
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
def error_with_traceback(msg, *args, **kwargs):
|
def error_with_traceback(msg, *args, **kwargs):
|
||||||
if isinstance(kwargs.get('exception', None), Exception):
|
# If exc_info is True, include traceback in the message
|
||||||
|
if kwargs.get('exc_info', False):
|
||||||
full_msg = f"{msg}\n{traceback.format_exc()}"
|
full_msg = f"{msg}\n{traceback.format_exc()}"
|
||||||
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
|
_original_error(full_msg, *args, **{k: v for k, v in kwargs.items() if k != 'exc_info'})
|
||||||
else:
|
else:
|
||||||
_original_error(msg, *args, **kwargs)
|
_original_error(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
+116
-17
@@ -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"""
|
||||||
@@ -703,6 +735,67 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=True
|
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 System Properties
|
||||||
translation_service: EnumProperty(
|
translation_service: EnumProperty(
|
||||||
name=t("Translation.service"),
|
name=t("Translation.service"),
|
||||||
@@ -812,21 +905,27 @@ def update_translation_mode(self: PropertyGroup, context: Context) -> None:
|
|||||||
|
|
||||||
def update_active_armature(self: PropertyGroup, context: Context) -> None:
|
def update_active_armature(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Update the active armature when selection changes"""
|
"""Update the active armature when selection changes"""
|
||||||
if self.active_armature:
|
if self.active_armature and self.active_armature != 'NONE':
|
||||||
logger.info(f"Active armature set to: {self.active_armature}")
|
# Get the actual armature object from the identifier
|
||||||
# Deselect all objects first
|
armature = get_active_armature(context)
|
||||||
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
|
if armature:
|
||||||
try:
|
logger.info(f"Active armature set to: {armature.name}")
|
||||||
from ..ui.quick_access_panel import clear_armature_caches
|
# Deselect all objects first
|
||||||
clear_armature_caches()
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
except ImportError:
|
# Select and make active the chosen armature
|
||||||
pass # UI module might not be loaded yet
|
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:
|
else:
|
||||||
logger.info("No armature selected")
|
logger.info("No armature selected")
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,14 @@ class TranslationCache:
|
|||||||
"""Load cache from file"""
|
"""Load cache from file"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(self._cache_file):
|
if os.path.exists(self._cache_file):
|
||||||
with open(self._cache_file, 'r', encoding='utf-8') as f:
|
# Try UTF-8 first, fallback to other encodings
|
||||||
self._cache = json.load(f)
|
try:
|
||||||
|
with open(self._cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
self._cache = json.load(f)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Try with UTF-8 error handling
|
||||||
|
with open(self._cache_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
self._cache = json.load(f)
|
||||||
logger.debug(f"Loaded translation cache with {len(self._cache)} entries")
|
logger.debug(f"Loaded translation cache with {len(self._cache)} entries")
|
||||||
else:
|
else:
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
@@ -147,6 +153,15 @@ class AvatarToolkitTranslationManager:
|
|||||||
def translate_single(self, name: str, category: str = "auto",
|
def translate_single(self, name: str, category: str = "auto",
|
||||||
source_lang: str = "ja", target_lang: str = "en") -> TranslationResult:
|
source_lang: str = "ja", target_lang: str = "en") -> TranslationResult:
|
||||||
"""Translate a single name with comprehensive fallback logic"""
|
"""Translate a single name with comprehensive fallback logic"""
|
||||||
|
# Import safe_decode_text from translation_service
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
# Ensure name is properly encoded
|
||||||
|
try:
|
||||||
|
name = safe_decode_text(name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode name: {e}")
|
||||||
|
|
||||||
if not name or not name.strip():
|
if not name or not name.strip():
|
||||||
return TranslationResult(name, name, "skipped")
|
return TranslationResult(name, name, "skipped")
|
||||||
|
|
||||||
@@ -300,6 +315,8 @@ class AvatarToolkitTranslationManager:
|
|||||||
def _process_category_batch_optimized(self, category_jobs: List[TranslationJob],
|
def _process_category_batch_optimized(self, category_jobs: List[TranslationJob],
|
||||||
completed: int, total_jobs: int, start_time: float) -> Optional[List[TranslationResult]]:
|
completed: int, total_jobs: int, start_time: float) -> Optional[List[TranslationResult]]:
|
||||||
"""Process a batch of jobs from the same category using optimized API batch translation"""
|
"""Process a batch of jobs from the same category using optimized API batch translation"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if not category_jobs:
|
if not category_jobs:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -315,6 +332,14 @@ class AvatarToolkitTranslationManager:
|
|||||||
results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category)
|
results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Ensure name is properly encoded
|
||||||
|
try:
|
||||||
|
original_name = safe_decode_text(job.name.strip())
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode job name: {e}")
|
||||||
|
original_name = job.name.strip()
|
||||||
|
continue
|
||||||
|
|
||||||
original_name = job.name.strip()
|
original_name = job.name.strip()
|
||||||
|
|
||||||
# Check cache first
|
# Check cache first
|
||||||
@@ -426,13 +451,21 @@ class AvatarToolkitTranslationManager:
|
|||||||
|
|
||||||
def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]:
|
def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all bone names in an armature"""
|
"""Translate all bone names in an armature"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if not armature or armature.type != 'ARMATURE':
|
if not armature or armature.type != 'ARMATURE':
|
||||||
return []
|
return []
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
for bone in armature.data.bones:
|
for bone in armature.data.bones:
|
||||||
|
try:
|
||||||
|
bone_name = safe_decode_text(bone.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode bone name, using as-is: {e}")
|
||||||
|
bone_name = bone.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=bone.name,
|
name=bone_name,
|
||||||
category="bones",
|
category="bones",
|
||||||
object_ref=bone,
|
object_ref=bone,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
@@ -442,13 +475,21 @@ class AvatarToolkitTranslationManager:
|
|||||||
|
|
||||||
def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]:
|
def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all shape key names in a mesh object"""
|
"""Translate all shape key names in a mesh object"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if not mesh_obj or mesh_obj.type != 'MESH' or not mesh_obj.data.shape_keys:
|
if not mesh_obj or mesh_obj.type != 'MESH' or not mesh_obj.data.shape_keys:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
for shape_key in mesh_obj.data.shape_keys.key_blocks:
|
for shape_key in mesh_obj.data.shape_keys.key_blocks:
|
||||||
|
try:
|
||||||
|
sk_name = safe_decode_text(shape_key.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode shape key name, using as-is: {e}")
|
||||||
|
sk_name = shape_key.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=shape_key.name,
|
name=sk_name,
|
||||||
category="shapekeys",
|
category="shapekeys",
|
||||||
object_ref=shape_key,
|
object_ref=shape_key,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
@@ -458,6 +499,8 @@ class AvatarToolkitTranslationManager:
|
|||||||
|
|
||||||
def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]:
|
def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all material names in the scene"""
|
"""Translate all material names in the scene"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
processed_materials: Set[str] = set()
|
processed_materials: Set[str] = set()
|
||||||
|
|
||||||
@@ -465,8 +508,14 @@ class AvatarToolkitTranslationManager:
|
|||||||
if obj.type == 'MESH' and obj.data.materials:
|
if obj.type == 'MESH' and obj.data.materials:
|
||||||
for material in obj.data.materials:
|
for material in obj.data.materials:
|
||||||
if material and material.name not in processed_materials:
|
if material and material.name not in processed_materials:
|
||||||
|
try:
|
||||||
|
mat_name = safe_decode_text(material.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode material name, using as-is: {e}")
|
||||||
|
mat_name = material.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=material.name,
|
name=mat_name,
|
||||||
category="materials",
|
category="materials",
|
||||||
object_ref=material,
|
object_ref=material,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
@@ -478,14 +527,22 @@ class AvatarToolkitTranslationManager:
|
|||||||
def translate_scene_objects(self, object_types: Optional[Set[str]] = None,
|
def translate_scene_objects(self, object_types: Optional[Set[str]] = None,
|
||||||
apply_results: bool = True) -> List[TranslationResult]:
|
apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all object names in the scene"""
|
"""Translate all object names in the scene"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if object_types is None:
|
if object_types is None:
|
||||||
object_types = {'MESH', 'ARMATURE', 'EMPTY'}
|
object_types = {'MESH', 'ARMATURE', 'EMPTY'}
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
for obj in bpy.data.objects:
|
for obj in bpy.data.objects:
|
||||||
if obj.type in object_types:
|
if obj.type in object_types:
|
||||||
|
try:
|
||||||
|
obj_name = safe_decode_text(obj.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode object name, using as-is: {e}")
|
||||||
|
obj_name = obj.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=obj.name,
|
name=obj_name,
|
||||||
category="objects",
|
category="objects",
|
||||||
object_ref=obj,
|
object_ref=obj,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
|
|||||||
@@ -14,6 +14,43 @@ from .logging_setup import logger
|
|||||||
from .addon_preferences import save_preference, get_preference
|
from .addon_preferences import save_preference, get_preference
|
||||||
|
|
||||||
|
|
||||||
|
def safe_decode_text(text: str) -> str:
|
||||||
|
"""Safely decode text that might be in various encodings (UTF-8, Shift-JIS, etc.)"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# If it's already a proper string, return it
|
||||||
|
if isinstance(text, str):
|
||||||
|
try:
|
||||||
|
# Test if it's valid UTF-8
|
||||||
|
text.encode('utf-8')
|
||||||
|
return text
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try common encodings for Japanese text
|
||||||
|
encodings = ['utf-8', 'shift-jis', 'cp932', 'euc-jp', 'iso-2022-jp']
|
||||||
|
|
||||||
|
for encoding in encodings:
|
||||||
|
try:
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
return text.decode(encoding)
|
||||||
|
else:
|
||||||
|
# Try to re-encode and decode
|
||||||
|
return text.encode('latin-1', errors='ignore').decode(encoding, errors='ignore')
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback: replace problematic characters
|
||||||
|
try:
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
return text.decode('utf-8', errors='replace')
|
||||||
|
else:
|
||||||
|
return str(text).encode('utf-8', errors='replace').decode('utf-8')
|
||||||
|
except:
|
||||||
|
return str(text)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TranslationRequest:
|
class TranslationRequest:
|
||||||
"""Represents a translation request"""
|
"""Represents a translation request"""
|
||||||
@@ -116,6 +153,8 @@ class DeepLService(TranslationService):
|
|||||||
|
|
||||||
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
"""Translate text using DeepL API"""
|
"""Translate text using DeepL API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
logger.info(f"DeepL: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
logger.info(f"DeepL: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
@@ -220,6 +259,8 @@ class DeepLService(TranslationService):
|
|||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
logger.info(f"DeepL: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
logger.info(f"DeepL: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
results = [None] * len(texts)
|
results = [None] * len(texts)
|
||||||
@@ -341,6 +382,8 @@ class MyMemoryService(TranslationService):
|
|||||||
|
|
||||||
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
"""Translate text using MyMemory free API"""
|
"""Translate text using MyMemory free API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
logger.info(f"MyMemory: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
logger.info(f"MyMemory: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
@@ -430,6 +473,8 @@ class MyMemoryService(TranslationService):
|
|||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
logger.info(f"MyMemory: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
logger.info(f"MyMemory: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
results = [None] * len(texts)
|
results = [None] * len(texts)
|
||||||
@@ -545,6 +590,8 @@ class LibreTranslateService(TranslationService):
|
|||||||
|
|
||||||
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
"""Translate text using LibreTranslate API"""
|
"""Translate text using LibreTranslate API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
logger.info(f"LibreTranslate: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
logger.info(f"LibreTranslate: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
@@ -658,6 +705,8 @@ class LibreTranslateService(TranslationService):
|
|||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
logger.info(f"LibreTranslate: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
logger.info(f"LibreTranslate: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
# Check cache and separate cached vs uncached texts
|
# Check cache and separate cached vs uncached texts
|
||||||
@@ -814,6 +863,8 @@ class TranslationServiceManager:
|
|||||||
|
|
||||||
def translate_with_fallback(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> Tuple[str, str]:
|
def translate_with_fallback(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> Tuple[str, str]:
|
||||||
"""Translate text with automatic fallback to other services"""
|
"""Translate text with automatic fallback to other services"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
return text, "none"
|
return text, "none"
|
||||||
|
|
||||||
|
|||||||
+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.5"]
|
ALLOWED_VERSION_SERIES = ["0.6"]
|
||||||
|
|
||||||
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"))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.5.1)",
|
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.6.0)",
|
||||||
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
||||||
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
||||||
"AvatarToolkit.desc3": "please report it on our Github.",
|
"AvatarToolkit.desc3": "please report it on our Github.",
|
||||||
@@ -601,6 +601,79 @@
|
|||||||
"VRM.remove_root": "Remove Root Bone",
|
"VRM.remove_root": "Remove Root Bone",
|
||||||
"VRM.remove_root_desc": "Remove unnecessary VRM root bone and make Hips the root bone",
|
"VRM.remove_root_desc": "Remove unnecessary VRM root bone and make Hips the root bone",
|
||||||
|
|
||||||
|
"MMD.panel.label": "MMD Converter",
|
||||||
|
"MMD.converter.title": "MMD Armature Converter",
|
||||||
|
"MMD.no_armature_selected": "No armature selected",
|
||||||
|
"MMD.select_armature_to_convert": "Select an armature to convert",
|
||||||
|
"MMD.armature_name": "Armature: {name}",
|
||||||
|
"MMD.armature_detected": "MMD armature detected",
|
||||||
|
"MMD.no_mmd_bones_detected": "No MMD bones detected",
|
||||||
|
"MMD.not_mmd_armature": "Selected armature does not appear to be MMD format",
|
||||||
|
"MMD.make_armature_parent": "Make Armature Main Parent",
|
||||||
|
"MMD.rename_to_armature": "Rename to 'Armature'",
|
||||||
|
"MMD.translate_names": "Translate Names to English",
|
||||||
|
"MMD.translate_bones": "Bones",
|
||||||
|
"MMD.translate_materials": "Materials",
|
||||||
|
"MMD.translate_shapekeys": "Shape Keys",
|
||||||
|
"MMD.translate_objects": "Objects",
|
||||||
|
"MMD.restructure_bones": "Restructure to Unity Format",
|
||||||
|
"MMD.bone_cleanup": "Bone Cleanup Options:",
|
||||||
|
"MMD.remove_ik_bones": "Remove IK Bones",
|
||||||
|
"MMD.remove_twist_bones": "Remove Twist Bones",
|
||||||
|
"MMD.remove_zero_weight_bones": "Remove Zero Weight Bones",
|
||||||
|
"MMD.translation_options": "Translation Options:",
|
||||||
|
"MMD.convert_armature_button": "Convert MMD Armature",
|
||||||
|
"MMD.convert_armature.label": "Convert MMD Armature",
|
||||||
|
"MMD.convert_armature.desc": "Convert MMD armature to standard Blender format",
|
||||||
|
"MMD.conversion_info.title": "Conversion Info:",
|
||||||
|
"MMD.conversion_info.removes_parent": "• Removes parent Empty object",
|
||||||
|
"MMD.conversion_info.renames_armature": "• Renames armature to 'Armature'",
|
||||||
|
"MMD.conversion_info.restructures_bones": "• Converts to Unity bone structure (Hips/Spine/Chest)",
|
||||||
|
"MMD.conversion_info.removes_ik_bones": "• Removes IK (Inverse Kinematics) bones",
|
||||||
|
"MMD.conversion_info.removes_twist_bones": "• Removes twist bones",
|
||||||
|
"MMD.conversion_info.removes_zero_weight_bones": "• Removes bones with zero vertex weights",
|
||||||
|
"MMD.conversion_info.maintains_hierarchy": "• Maintains object hierarchy",
|
||||||
|
"MMD.conversion_info.translates_names": "• Translates Japanese names to English",
|
||||||
|
"MMD.detection_failed.title": "MMD Detection Failed:",
|
||||||
|
"MMD.detection_failed.not_mmd_format": "• Selected armature is not MMD format",
|
||||||
|
"MMD.detection_failed.need_mmd_bones": "• Need at least 5 MMD bones detected",
|
||||||
|
"MMD.detection_failed.check_bone_names": "• Check armature bone names",
|
||||||
|
"MMD.error.invalid_armature": "Invalid armature object",
|
||||||
|
"MMD.error.not_mmd_armature": "Armature does not appear to be MMD format",
|
||||||
|
"MMD.error.rename_failed": "Failed to rename armature: {error}",
|
||||||
|
"MMD.armature_already_root": "Armature already has no parent",
|
||||||
|
"MMD.armature_already_named": "Armature is already named 'Armature'",
|
||||||
|
"MMD.parent_removed_and_reparented": "Removed parent '{parent_name}' and reparented {count} objects to armature",
|
||||||
|
"MMD.parent_unlinked_and_reparented": "Unlinked from parent '{parent_name}' and reparented {count} objects",
|
||||||
|
"MMD.parent_unlinked": "Unlinked armature from parent '{parent_name}'",
|
||||||
|
"MMD.armature_renamed": "Renamed armature from '{old_name}' to '{new_name}'",
|
||||||
|
"MMD.armature_renamed_with_suffix": "Renamed armature from '{old_name}' to '{new_name}' (name collision)",
|
||||||
|
"MMD.conversion_complete": "MMD armature conversion completed successfully",
|
||||||
|
"MMD.translation_starting": "Starting name translation...",
|
||||||
|
"MMD.bones_translated": "Translated {count} bones",
|
||||||
|
"MMD.bones_failed": "Failed to translate {count} bones",
|
||||||
|
"MMD.materials_translated": "Translated {count} materials",
|
||||||
|
"MMD.materials_failed": "Failed to translate {count} materials",
|
||||||
|
"MMD.shapekeys_translated": "Translated {count} shape keys",
|
||||||
|
"MMD.shapekeys_failed": "Failed to translate {count} shape keys",
|
||||||
|
"MMD.objects_translated": "Translated {count} objects",
|
||||||
|
"MMD.objects_failed": "Failed to translate {count} objects",
|
||||||
|
"MMD.translation_complete": "Translation complete: {total} items translated",
|
||||||
|
"MMD.restructure_starting": "Restructuring bones to Unity format...",
|
||||||
|
"MMD.bones_restructured": "Restructured {count} bones to Unity format",
|
||||||
|
"MMD.bones_removed": "Removed {count} unnecessary bones",
|
||||||
|
"MMD.bones_reparented": "Reparented {count} bones",
|
||||||
|
"MMD.restructure_failed": "Bone restructuring failed: {error}",
|
||||||
|
"MMD.ik_bones_removed": "Removed {count} IK bones",
|
||||||
|
"MMD.no_ik_bones_found": "No IK bones found to remove",
|
||||||
|
"MMD.ik_removal_failed": "IK bone removal failed: {error}",
|
||||||
|
"MMD.twist_bones_removed": "Removed {count} twist bones",
|
||||||
|
"MMD.no_twist_bones_found": "No twist bones found to remove",
|
||||||
|
"MMD.twist_removal_failed": "Twist bone removal failed: {error}",
|
||||||
|
"MMD.zero_weight_bones_removed": "Removed {count} zero weight bones",
|
||||||
|
"MMD.no_zero_weight_bones_found": "No zero weight bones found to remove",
|
||||||
|
"MMD.zero_weight_removal_failed": "Zero weight bone removal failed: {error}",
|
||||||
|
|
||||||
"Translation.label": "Translation",
|
"Translation.label": "Translation",
|
||||||
"Translation.service": "Translation Service",
|
"Translation.service": "Translation Service",
|
||||||
"Translation.service_desc": "Choose the translation service to use",
|
"Translation.service_desc": "Choose the translation service to use",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.5.1)",
|
"AvatarToolkit.label": "アバターツールキット (アルファ 0.6.0)",
|
||||||
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
||||||
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
||||||
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.5.1)",
|
"AvatarToolkit.label": "아바타 툴킷 (알파 0.6.0)",
|
||||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
||||||
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
||||||
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
||||||
|
|||||||
+4
-1
@@ -14,7 +14,8 @@ VISEMES_ORDER = 6
|
|||||||
EYE_TRACKING_ORDER = 7
|
EYE_TRACKING_ORDER = 7
|
||||||
TEXTURE_ATLAS_ORDER = 8
|
TEXTURE_ATLAS_ORDER = 8
|
||||||
VRM_UNITY_ORDER = 9
|
VRM_UNITY_ORDER = 9
|
||||||
SETTINGS_ORDER = 10
|
MMD_ORDER = 10
|
||||||
|
SETTINGS_ORDER = 11
|
||||||
|
|
||||||
# Panel open/closed by default
|
# Panel open/closed by default
|
||||||
PANELS_OPEN_BY_DEFAULT = {
|
PANELS_OPEN_BY_DEFAULT = {
|
||||||
@@ -27,6 +28,7 @@ PANELS_OPEN_BY_DEFAULT = {
|
|||||||
'EYE_TRACKING': True,
|
'EYE_TRACKING': True,
|
||||||
'TEXTURE_ATLAS': True,
|
'TEXTURE_ATLAS': True,
|
||||||
'VRM_UNITY': True,
|
'VRM_UNITY': True,
|
||||||
|
'MMD': True,
|
||||||
'SETTINGS': True,
|
'SETTINGS': True,
|
||||||
'TRANSLATION': True,
|
'TRANSLATION': True,
|
||||||
}
|
}
|
||||||
@@ -44,6 +46,7 @@ def get_panel_order(panel_name: str) -> int:
|
|||||||
'eye_tracking': EYE_TRACKING_ORDER,
|
'eye_tracking': EYE_TRACKING_ORDER,
|
||||||
'texture_atlas': TEXTURE_ATLAS_ORDER,
|
'texture_atlas': TEXTURE_ATLAS_ORDER,
|
||||||
'vrm_unity': VRM_UNITY_ORDER,
|
'vrm_unity': VRM_UNITY_ORDER,
|
||||||
|
'mmd': MMD_ORDER,
|
||||||
'settings': SETTINGS_ORDER,
|
'settings': SETTINGS_ORDER,
|
||||||
}
|
}
|
||||||
return order_map.get(panel_name.lower(), 99)
|
return order_map.get(panel_name.lower(), 99)
|
||||||
|
|||||||
+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