Moved Armature Validation to it's own file
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
import bpy
|
||||||
|
from typing import Tuple, List, Dict, Set
|
||||||
|
from bpy.types import Object, Bone
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.dictionaries import bone_names
|
||||||
|
|
||||||
|
def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
|
||||||
|
"""Enhanced armature validation with multiple validation modes"""
|
||||||
|
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
|
||||||
|
messages: List[str] = []
|
||||||
|
|
||||||
|
if validation_mode == 'NONE':
|
||||||
|
return True, []
|
||||||
|
|
||||||
|
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
|
||||||
|
return False, [t("Armature.validation.basic_check_failed")]
|
||||||
|
|
||||||
|
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
|
||||||
|
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
|
||||||
|
|
||||||
|
missing_bones: List[str] = []
|
||||||
|
for bone in essential_bones:
|
||||||
|
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
|
||||||
|
missing_bones.append(bone)
|
||||||
|
|
||||||
|
if missing_bones:
|
||||||
|
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
|
||||||
|
|
||||||
|
if validation_mode == 'STRICT':
|
||||||
|
hierarchy: List[Tuple[str, str]] = [
|
||||||
|
('hips', 'spine'), ('spine', 'chest'),
|
||||||
|
('chest', 'neck'), ('neck', 'head')
|
||||||
|
]
|
||||||
|
for parent, child in hierarchy:
|
||||||
|
if not validate_bone_hierarchy(found_bones, parent, child):
|
||||||
|
messages.append(t("Armature.validation.invalid_hierarchy",
|
||||||
|
parent=parent, child=child))
|
||||||
|
|
||||||
|
symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
|
||||||
|
for base, left, right in symmetry_pairs:
|
||||||
|
if not validate_symmetry(found_bones, base, left, right):
|
||||||
|
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
|
||||||
|
|
||||||
|
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
|
||||||
|
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
|
||||||
|
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
|
||||||
|
|
||||||
|
is_valid: bool = len(messages) == 0
|
||||||
|
return is_valid, messages
|
||||||
|
|
||||||
|
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
|
||||||
|
"""Validate if there is a valid parent-child relationship between bones"""
|
||||||
|
parent_bone: Optional[Bone] = None
|
||||||
|
child_bone: Optional[Bone] = None
|
||||||
|
|
||||||
|
for alt_name in bone_names[parent_name]:
|
||||||
|
if alt_name in bones:
|
||||||
|
parent_bone = bones[alt_name]
|
||||||
|
break
|
||||||
|
|
||||||
|
for alt_name in bone_names[child_name]:
|
||||||
|
if alt_name in bones:
|
||||||
|
child_bone = bones[alt_name]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not parent_bone or not child_bone:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return child_bone.parent == parent_bone
|
||||||
|
|
||||||
|
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
|
||||||
|
"""Validate if matching left and right bones exist for a given base bone name"""
|
||||||
|
left_patterns: List[str] = [
|
||||||
|
f"{base}.{left}",
|
||||||
|
f"{base}_{left}",
|
||||||
|
f"{left}_{base}"
|
||||||
|
]
|
||||||
|
|
||||||
|
right_patterns: List[str] = [
|
||||||
|
f"{base}.{right}",
|
||||||
|
f"{base}_{right}",
|
||||||
|
f"{right}_{base}"
|
||||||
|
]
|
||||||
|
|
||||||
|
left_exists: bool = any(pattern in bones for pattern in left_patterns)
|
||||||
|
right_exists: bool = any(pattern in bones for pattern in right_patterns)
|
||||||
|
|
||||||
|
return left_exists and right_exists
|
||||||
@@ -111,89 +111,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
|
|||||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
return [('NONE', t("Armature.validation.no_armature"), '')]
|
||||||
return armatures
|
return armatures
|
||||||
|
|
||||||
def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
|
|
||||||
"""Enhanced armature validation with multiple validation modes"""
|
|
||||||
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
|
|
||||||
messages: List[str] = []
|
|
||||||
|
|
||||||
if validation_mode == 'NONE':
|
|
||||||
return True, []
|
|
||||||
|
|
||||||
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
|
|
||||||
return False, [t("Armature.validation.basic_check_failed")]
|
|
||||||
|
|
||||||
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
|
|
||||||
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
|
|
||||||
|
|
||||||
missing_bones: List[str] = []
|
|
||||||
for bone in essential_bones:
|
|
||||||
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
|
|
||||||
missing_bones.append(bone)
|
|
||||||
|
|
||||||
if missing_bones:
|
|
||||||
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
|
|
||||||
|
|
||||||
if validation_mode == 'STRICT':
|
|
||||||
hierarchy: List[Tuple[str, str]] = [
|
|
||||||
('hips', 'spine'), ('spine', 'chest'),
|
|
||||||
('chest', 'neck'), ('neck', 'head')
|
|
||||||
]
|
|
||||||
for parent, child in hierarchy:
|
|
||||||
if not validate_bone_hierarchy(found_bones, parent, child):
|
|
||||||
messages.append(t("Armature.validation.invalid_hierarchy",
|
|
||||||
parent=parent, child=child))
|
|
||||||
|
|
||||||
symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
|
|
||||||
for base, left, right in symmetry_pairs:
|
|
||||||
if not validate_symmetry(found_bones, base, left, right):
|
|
||||||
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
|
|
||||||
|
|
||||||
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
|
|
||||||
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
|
|
||||||
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
|
|
||||||
|
|
||||||
is_valid: bool = len(messages) == 0
|
|
||||||
return is_valid, messages
|
|
||||||
|
|
||||||
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
|
|
||||||
"""Validate if there is a valid parent-child relationship between bones"""
|
|
||||||
parent_bone: Optional[Bone] = None
|
|
||||||
child_bone: Optional[Bone] = None
|
|
||||||
|
|
||||||
for alt_name in bone_names[parent_name]:
|
|
||||||
if alt_name in bones:
|
|
||||||
parent_bone = bones[alt_name]
|
|
||||||
break
|
|
||||||
|
|
||||||
for alt_name in bone_names[child_name]:
|
|
||||||
if alt_name in bones:
|
|
||||||
child_bone = bones[alt_name]
|
|
||||||
break
|
|
||||||
|
|
||||||
if not parent_bone or not child_bone:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return child_bone.parent == parent_bone
|
|
||||||
|
|
||||||
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
|
|
||||||
"""Validate if matching left and right bones exist for a given base bone name"""
|
|
||||||
left_patterns: List[str] = [
|
|
||||||
f"{base}.{left}",
|
|
||||||
f"{base}_{left}",
|
|
||||||
f"{left}_{base}"
|
|
||||||
]
|
|
||||||
|
|
||||||
right_patterns: List[str] = [
|
|
||||||
f"{base}.{right}",
|
|
||||||
f"{base}_{right}",
|
|
||||||
f"{right}_{base}"
|
|
||||||
]
|
|
||||||
|
|
||||||
left_exists: bool = any(pattern in bones for pattern in left_patterns)
|
|
||||||
right_exists: bool = any(pattern in bones for pattern in right_patterns)
|
|
||||||
|
|
||||||
return left_exists and right_exists
|
|
||||||
|
|
||||||
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"""
|
||||||
armatures: List[Tuple[str, str, str]] = get_armature_list(context)
|
armatures: List[Tuple[str, str, str]] = get_armature_list(context)
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import bpy_extras
|
|||||||
from numpy import double
|
from numpy import double
|
||||||
from typing import Set, Dict
|
from typing import Set, Dict
|
||||||
|
|
||||||
from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
|
from .common import get_active_armature, simplify_bonename, ProgressTracker
|
||||||
from bpy.types import Context, Operator
|
from bpy.types import Context, Operator
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.dictionaries import bone_names, resonite_translations
|
from ..core.dictionaries import bone_names, resonite_translations
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from .resonite_loader import resonite_animx, resonite_types
|
from .resonite_loader import resonite_animx, resonite_types
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ from ...core.logging_setup import logger
|
|||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
validate_armature,
|
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
ProgressTracker,
|
ProgressTracker,
|
||||||
calculate_bone_orientation,
|
calculate_bone_orientation,
|
||||||
add_armature_modifier
|
add_armature_modifier
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_AttachMesh(Operator):
|
class AvatarToolkit_OT_AttachMesh(Operator):
|
||||||
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
|
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ from ..core.common import (
|
|||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
get_armature_list,
|
get_armature_list,
|
||||||
validate_armature,
|
|
||||||
validate_mesh_for_pose,
|
validate_mesh_for_pose,
|
||||||
cache_vertex_positions,
|
cache_vertex_positions,
|
||||||
apply_vertex_positions
|
apply_vertex_positions
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
VALID_EYE_NAMES: Dict[str, List[str]] = {
|
VALID_EYE_NAMES: Dict[str, List[str]] = {
|
||||||
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
|
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from ..core.logging_setup import logger
|
|||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
ProgressTracker,
|
ProgressTracker,
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
validate_armature,
|
|
||||||
get_vertex_weights,
|
get_vertex_weights,
|
||||||
transfer_vertex_weights,
|
transfer_vertex_weights,
|
||||||
get_all_meshes
|
get_all_meshes
|
||||||
)
|
)
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.dictionaries import bone_names, dont_delete_these_main_bones
|
from ..core.dictionaries import bone_names, dont_delete_these_main_bones
|
||||||
|
from ..core.armature_validation import validate_armature, validate_bone_hierarchy
|
||||||
|
|
||||||
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
|
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
|
||||||
"""MMD Bone standardization system"""
|
"""MMD Bone standardization system"""
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ from ...core.translations import t
|
|||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature,
|
|
||||||
clear_unused_data_blocks,
|
clear_unused_data_blocks,
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
|
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
|
||||||
"""Compare two texture nodes for matching properties and image data"""
|
"""Compare two texture nodes for matching properties and image data"""
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ from ...core.translations import t
|
|||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature,
|
|
||||||
validate_meshes,
|
validate_meshes,
|
||||||
join_mesh_objects,
|
join_mesh_objects,
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_JoinAllMeshes(Operator):
|
class AvatarToolkit_OT_JoinAllMeshes(Operator):
|
||||||
"""Operator to join all meshes in the scene"""
|
"""Operator to join all meshes in the scene"""
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from ...core.translations import t
|
|||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature
|
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
MERGE_ITERATION_COUNT = 20
|
MERGE_ITERATION_COUNT = 20
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ from ..core.common import (
|
|||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
apply_pose_as_rest,
|
apply_pose_as_rest,
|
||||||
validate_armature,
|
|
||||||
cache_vertex_positions,
|
cache_vertex_positions,
|
||||||
apply_vertex_positions,
|
apply_vertex_positions,
|
||||||
validate_mesh_for_pose,
|
validate_mesh_for_pose,
|
||||||
process_armature_modifiers,
|
process_armature_modifiers,
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class BatchPoseOperationMixin:
|
class BatchPoseOperationMixin:
|
||||||
"""Base class for batch pose operations"""
|
"""Base class for batch pose operations"""
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from bpy.types import Operator, Context
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.common import get_active_armature, get_all_meshes, validate_armature, remove_unused_shapekeys
|
from ...core.common import get_active_armature, get_all_meshes, remove_unused_shapekeys
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyTransforms(Operator):
|
class AvatarToolkit_OT_ApplyTransforms(Operator):
|
||||||
"""Apply all transformations to armature and associated meshes"""
|
"""Apply all transformations to armature and associated meshes"""
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ from typing import Optional, Dict, Any, List, Tuple
|
|||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import (
|
from ...core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
validate_armature,
|
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
ProgressTracker,
|
ProgressTracker,
|
||||||
validate_bone_hierarchy,
|
|
||||||
restore_bone_transforms
|
restore_bone_transforms
|
||||||
)
|
)
|
||||||
|
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
|
||||||
|
|
||||||
def duplicate_bone(bone: EditBone) -> EditBone:
|
def duplicate_bone(bone: EditBone) -> EditBone:
|
||||||
"""Create a duplicate of the given bone"""
|
"""Create a duplicate of the given bone"""
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ from typing import Set, Dict, Optional
|
|||||||
from bpy.types import Operator, Context
|
from bpy.types import Operator, Context
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
|
from ...core.common import get_active_armature, simplify_bonename, ProgressTracker
|
||||||
from ...core.dictionaries import bone_names, resonite_translations
|
from ...core.dictionaries import bone_names, resonite_translations
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_ConvertResonite(Operator):
|
class AvatarToolkit_OT_ConvertResonite(Operator):
|
||||||
"""Convert armature bone names to Resonite format with progress tracking and validation"""
|
"""Convert armature bone names to Resonite format with progress tracking and validation"""
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from typing import Set, List
|
|||||||
from bpy.types import Operator, Context, Armature, EditBone
|
from bpy.types import Operator, Context, Armature, EditBone
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.logging_setup import logger
|
from ...core.logging_setup import logger
|
||||||
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, validate_armature
|
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_ConnectBones(Operator):
|
class AvatarToolkit_OT_ConnectBones(Operator):
|
||||||
"""Connect disconnected bones in chain"""
|
"""Connect disconnected bones in chain"""
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator, Context
|
from bpy.types import Operator, Context
|
||||||
from ...core.translations import t
|
from ...core.translations import t
|
||||||
from ...core.common import get_active_armature, validate_armature
|
from ...core.common import get_active_armature
|
||||||
|
from ...core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
||||||
"""Operator to separate mesh by materials"""
|
"""Operator to separate mesh by materials"""
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ from ..core.logging_setup import logger
|
|||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
validate_armature,
|
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_mesh_for_pose
|
validate_mesh_for_pose
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class VisemeCache:
|
class VisemeCache:
|
||||||
"""Manages caching of generated viseme shape data for performance optimization"""
|
"""Manages caching of generated viseme shape data for performance optimization"""
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from ..core.translations import t
|
|||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
get_all_meshes,
|
get_all_meshes,
|
||||||
validate_armature,
|
|
||||||
get_armature_list
|
get_armature_list
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
|
||||||
"""Search operator for selecting target armature to merge into"""
|
"""Search operator for selecting target armature to merge into"""
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from ..core.translations import t
|
|||||||
from ..core.common import (
|
from ..core.common import (
|
||||||
get_active_armature,
|
get_active_armature,
|
||||||
clear_default_objects,
|
clear_default_objects,
|
||||||
validate_armature,
|
|
||||||
get_armature_list,
|
get_armature_list,
|
||||||
get_armature_stats
|
get_armature_stats
|
||||||
)
|
)
|
||||||
@@ -24,6 +23,7 @@ from ..functions.pose_mode import (
|
|||||||
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
AvatarToolkit_OT_ApplyPoseAsShapekey,
|
||||||
AvatarToolkit_OT_ApplyPoseAsRest
|
AvatarToolkit_OT_ApplyPoseAsRest
|
||||||
)
|
)
|
||||||
|
from ..core.armature_validation import validate_armature
|
||||||
|
|
||||||
class AvatarToolKit_OT_ExportFBX(Operator):
|
class AvatarToolKit_OT_ExportFBX(Operator):
|
||||||
"""Export selected objects as FBX"""
|
"""Export selected objects as FBX"""
|
||||||
|
|||||||
Reference in New Issue
Block a user