Avatar Toolkit 0.3.2
- Version bumo - Fixed standardised avatar only work in strict validation mode. - Fixed Armature merging is using the armature selection in quick access, not the one you selected in Armature Merging for the base. - Fixed error where if you were not in object mode merge would fail, it now switches to object mode before merge starting. _ Merge Armature now attempts to auto populate the merge from and to boxes. - Fixed bug in general mesh tools spamming the console (It was trying to check nothing).
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "avatar_toolkit"
|
id = "avatar_toolkit"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
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"
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ from ..core.dictionaries import (
|
|||||||
)
|
)
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
|
|
||||||
def validate_armature(armature: Object, detailed_messages: bool = False) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
|
def validate_armature(armature: Object, detailed_messages: bool = False, override_mode: Optional[str] = None) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
|
||||||
"""
|
"""
|
||||||
Validates armature and returns validation results
|
Validates armature and returns validation results
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
|
logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
|
||||||
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
|
validation_mode = override_mode if override_mode else bpy.context.scene.avatar_toolkit.validation_mode
|
||||||
messages: List[str] = []
|
messages: List[str] = []
|
||||||
hierarchy_messages: List[str] = []
|
hierarchy_messages: List[str] = []
|
||||||
non_standard_messages: List[str] = []
|
non_standard_messages: List[str] = []
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ def get_all_meshes(context: Context) -> List[Object]:
|
|||||||
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_meshes_for_armature(armature: Object) -> List[Object]:
|
||||||
|
"""Get all mesh objects parented to a specific armature"""
|
||||||
|
if armature and armature.type == 'ARMATURE':
|
||||||
|
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||||
|
return []
|
||||||
|
|
||||||
def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
|
def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
|
||||||
"""Validate mesh object for pose operations"""
|
"""Validate mesh object for pose operations"""
|
||||||
if not mesh_obj.data:
|
if not mesh_obj.data:
|
||||||
|
|||||||
+89
-2
@@ -67,6 +67,74 @@ def get_mesh_objects(self, context):
|
|||||||
return [('NONE', t("Visemes.no_meshes"), '')]
|
return [('NONE', t("Visemes.no_meshes"), '')]
|
||||||
return 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):
|
class AvatarToolkitSceneProperties(PropertyGroup):
|
||||||
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||||
|
|
||||||
@@ -465,13 +533,15 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
merge_armature_into: StringProperty(
|
merge_armature_into: StringProperty(
|
||||||
name=t('MergeArmature.into'),
|
name=t('MergeArmature.into'),
|
||||||
description=t('MergeArmature.into_desc'),
|
description=t('MergeArmature.into_desc'),
|
||||||
default=""
|
default="",
|
||||||
|
update=update_merge_armature_into
|
||||||
)
|
)
|
||||||
|
|
||||||
merge_armature: StringProperty(
|
merge_armature: StringProperty(
|
||||||
name=t('MergeArmature.from'),
|
name=t('MergeArmature.from'),
|
||||||
description=t('MergeArmature.from_desc'),
|
description=t('MergeArmature.from_desc'),
|
||||||
default=""
|
default="",
|
||||||
|
update=update_merge_armature
|
||||||
)
|
)
|
||||||
|
|
||||||
attach_mesh: StringProperty(
|
attach_mesh: StringProperty(
|
||||||
@@ -614,6 +684,15 @@ def register() -> None:
|
|||||||
|
|
||||||
# Only register the property, not the classes (auto_load will handle that)
|
# Only register the property, not the classes (auto_load will handle that)
|
||||||
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
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")
|
logger.debug("Properties registered successfully")
|
||||||
|
|
||||||
|
|
||||||
@@ -621,6 +700,14 @@ def unregister() -> None:
|
|||||||
"""Unregister the Avatar Toolkit property group"""
|
"""Unregister the Avatar Toolkit property group"""
|
||||||
logger.info("Unregistering Avatar Toolkit properties")
|
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
|
# Remove the property
|
||||||
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ...core.translations import t
|
|||||||
import traceback
|
import traceback
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
|
get_meshes_for_armature,
|
||||||
fix_zero_length_bones,
|
fix_zero_length_bones,
|
||||||
remove_unused_vertex_groups,
|
remove_unused_vertex_groups,
|
||||||
clear_unused_data_blocks,
|
clear_unused_data_blocks,
|
||||||
@@ -28,10 +29,32 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
return len(get_all_meshes(context)) > 1
|
# Check if we have valid armature selections for merging
|
||||||
|
base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
|
||||||
|
merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
|
||||||
|
|
||||||
|
if not base_armature_name or not merge_armature_name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
|
||||||
|
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
|
||||||
|
|
||||||
|
return (base_armature is not None and
|
||||||
|
merge_armature is not None and
|
||||||
|
base_armature.type == 'ARMATURE' and
|
||||||
|
merge_armature.type == 'ARMATURE' and
|
||||||
|
base_armature != merge_armature)
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
try:
|
try:
|
||||||
|
# Store original mode to restore later
|
||||||
|
original_mode: str = context.mode
|
||||||
|
logger.debug(f"Original mode: {original_mode}")
|
||||||
|
|
||||||
|
# Switch to object mode if not already
|
||||||
|
if context.mode != 'OBJECT':
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
wm.progress_begin(0, 100)
|
wm.progress_begin(0, 100)
|
||||||
|
|
||||||
@@ -85,6 +108,18 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
merge_armature_obj = bpy.data.objects[merge_armature_name_stored]
|
merge_armature_obj = bpy.data.objects[merge_armature_name_stored]
|
||||||
restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge)
|
restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge)
|
||||||
|
|
||||||
|
# Restore original mode if it wasn't OBJECT
|
||||||
|
try:
|
||||||
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
elif original_mode == 'POSE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
elif original_mode != 'OBJECT':
|
||||||
|
logger.debug(f"Restoring to original mode: {original_mode}")
|
||||||
|
# For other modes, stay in object mode as it's safest
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Could not restore original mode: {original_mode}")
|
||||||
|
|
||||||
self.report({'INFO'}, t('MergeArmature.success'))
|
self.report({'INFO'}, t('MergeArmature.success'))
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
@@ -92,6 +127,17 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}")
|
logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}")
|
||||||
self.report({'ERROR'}, traceback.format_exc())
|
self.report({'ERROR'}, traceback.format_exc())
|
||||||
|
|
||||||
|
# Try to restore original mode even on error
|
||||||
|
try:
|
||||||
|
if 'original_mode' in locals() and original_mode != 'OBJECT':
|
||||||
|
if original_mode == 'EDIT_ARMATURE':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
elif original_mode == 'POSE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Could not restore mode after error")
|
||||||
|
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
def delete_rigidbodies_and_joints(armature: Object) -> None:
|
def delete_rigidbodies_and_joints(armature: Object) -> None:
|
||||||
|
|||||||
@@ -119,8 +119,10 @@ class AvatarToolkit_OT_ExplodeMesh(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
|
active_obj = context.view_layer.objects.active
|
||||||
return context.view_layer.objects.active.type == "MESH" and len(context.view_layer.objects.selected) == 1
|
return (active_obj is not None and
|
||||||
|
active_obj.type == "MESH" and
|
||||||
|
len(context.view_layer.objects.selected) == 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,6 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
|
|
||||||
logger.info(f"Starting armature standardization for {armature.name}")
|
logger.info(f"Starting armature standardization for {armature.name}")
|
||||||
|
|
||||||
is_valid, _, _ = validate_armature(armature)
|
|
||||||
if is_valid:
|
|
||||||
logger.info("Armature already meets standards, no changes needed")
|
|
||||||
self.report({'INFO'}, t("Tools.standardize_already_valid"))
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
original_mode: str = context.mode
|
original_mode: str = context.mode
|
||||||
logger.debug(f"Original mode: {original_mode}")
|
logger.debug(f"Original mode: {original_mode}")
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
@@ -90,7 +84,7 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
|||||||
logger.info(f"Fixed {fixed_scale} scale issues")
|
logger.info(f"Fixed {fixed_scale} scale issues")
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
is_valid, messages, _ = validate_armature(armature)
|
is_valid, messages, _ = validate_armature(armature, override_mode='STRICT')
|
||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
logger.info("Armature successfully standardized")
|
logger.info("Armature successfully standardized")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.1)",
|
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.2)",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.1)",
|
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.2)",
|
||||||
"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.3.1)",
|
"AvatarToolkit.label": "아바타 툴킷 (알파 0.3.2)",
|
||||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
||||||
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
||||||
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
||||||
|
|||||||
Reference in New Issue
Block a user