Start of Tools Panel
Several Improvements and etc. Still need to do the other half of the functions but getting there.
This commit is contained in:
+26
-1
@@ -1,6 +1,6 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy.types import Context, Object, Modifier
|
from bpy.types import Context, Object, Modifier, EditBone
|
||||||
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
|
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
|
||||||
from ..core.logging_setup import logger
|
from ..core.logging_setup import logger
|
||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
@@ -385,3 +385,28 @@ def clear_unused_data_blocks(self) -> int:
|
|||||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||||
final_count: int = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
|
final_count: int = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
|
||||||
return initial_count - final_count
|
return initial_count - final_count
|
||||||
|
|
||||||
|
def simplify_bonename(name: str) -> str:
|
||||||
|
"""Simplify bone name by removing spaces, underscores, dots and converting to lowercase"""
|
||||||
|
return name.lower().translate(dict.fromkeys(map(ord, u" _.")))
|
||||||
|
|
||||||
|
def duplicate_bone_chain(bones: List[EditBone]) -> List[EditBone]:
|
||||||
|
"""Duplicate a chain of bones while preserving hierarchy"""
|
||||||
|
new_bones = []
|
||||||
|
parent_map = {}
|
||||||
|
|
||||||
|
for bone in bones:
|
||||||
|
new_bone = duplicate_bone(bone)
|
||||||
|
if bone.parent and bone.parent in parent_map:
|
||||||
|
new_bone.parent = parent_map[bone.parent]
|
||||||
|
parent_map[bone] = new_bone
|
||||||
|
new_bones.append(new_bone)
|
||||||
|
|
||||||
|
return new_bones
|
||||||
|
|
||||||
|
def restore_bone_transforms(bone: EditBone, transforms: Dict[str, Any]) -> None:
|
||||||
|
"""Restore bone transforms from stored data"""
|
||||||
|
bone.head = transforms['head']
|
||||||
|
bone.tail = transforms['tail']
|
||||||
|
bone.roll = transforms['roll']
|
||||||
|
bone.matrix = transforms['matrix']
|
||||||
|
|||||||
@@ -110,3 +110,59 @@ dont_delete_these_main_bones = [
|
|||||||
'RingFinger1_R', 'RingFinger2_R', 'RingFinger3_R',
|
'RingFinger1_R', 'RingFinger2_R', 'RingFinger3_R',
|
||||||
'LittleFinger1_R', 'LittleFinger2_R', 'LittleFinger3_R',
|
'LittleFinger1_R', 'LittleFinger2_R', 'LittleFinger3_R',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
resonite_translations = {
|
||||||
|
'hips': "Hips",
|
||||||
|
'spine': "Spine",
|
||||||
|
'chest': "Chest",
|
||||||
|
'neck': "Neck",
|
||||||
|
'head': "Head",
|
||||||
|
'left_eye': "Eye.L",
|
||||||
|
'right_eye': "Eye.R",
|
||||||
|
'right_leg': "UpperLeg.R",
|
||||||
|
'right_knee': "Calf.R",
|
||||||
|
'right_ankle': "Foot.R",
|
||||||
|
'right_toe': 'Toes.R',
|
||||||
|
'right_shoulder': "Shoulder.R",
|
||||||
|
'right_arm': "UpperArm.R",
|
||||||
|
'right_elbow': "ForeArm.R",
|
||||||
|
'right_wrist': "Hand.R",
|
||||||
|
'left_leg': "UpperLeg.L",
|
||||||
|
'left_knee': "Calf.L",
|
||||||
|
'left_ankle': "Foot.L",
|
||||||
|
'left_toe': "Toes.L",
|
||||||
|
'left_shoulder': "Shoulder.L",
|
||||||
|
'left_arm': "UpperArm.L",
|
||||||
|
'left_elbow': "ForeArm.L",
|
||||||
|
'left_wrist': "Hand.L",
|
||||||
|
'pinkie_1_l': "pinkie1.L",
|
||||||
|
'pinkie_2_l': "pinkie2.L",
|
||||||
|
'pinkie_3_l': "pinkie3.L",
|
||||||
|
'ring_1_l': "ring1.L",
|
||||||
|
'ring_2_l': "ring2.L",
|
||||||
|
'ring_3_l': "ring3.L",
|
||||||
|
'middle_1_l': "middle1.L",
|
||||||
|
'middle_2_l': "middle2.L",
|
||||||
|
'middle_3_l': "middle3.L",
|
||||||
|
'index_1_l': "index1.L",
|
||||||
|
'index_2_l': "index2.L",
|
||||||
|
'index_3_l': "index3.L",
|
||||||
|
'thumb_1_l': "thumb1.L",
|
||||||
|
'thumb_2_l': "thumb2.L",
|
||||||
|
'thumb_3_l': "thumb3.L",
|
||||||
|
'pinkie_1_r': "pinkie1.R",
|
||||||
|
'pinkie_2_r': "pinkie2.R",
|
||||||
|
'pinkie_3_r': "pinkie3.R",
|
||||||
|
'ring_1_r': "ring1.R",
|
||||||
|
'ring_2_r': "ring2.R",
|
||||||
|
'ring_3_r': "ring3.R",
|
||||||
|
'middle_1_r': "middle1.R",
|
||||||
|
'middle_2_r': "middle2.R",
|
||||||
|
'middle_3_r': "middle3.R",
|
||||||
|
'index_1_r': "index1.R",
|
||||||
|
'index_2_r': "index2.R",
|
||||||
|
'index_3_r': "index3.R",
|
||||||
|
'thumb_1_r': "thumb1.R",
|
||||||
|
'thumb_2_r': "thumb2.R",
|
||||||
|
'thumb_3_r': "thumb3.R"
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,6 +86,20 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
merge_twist_bones: BoolProperty(
|
||||||
|
name=t("Tools.merge_twist_bones"),
|
||||||
|
description=t("Tools.merge_twist_bones_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
clean_weights_threshold: FloatProperty(
|
||||||
|
name=t("Tools.clean_weights_threshold"),
|
||||||
|
description=t("Tools.clean_weights_threshold_desc"),
|
||||||
|
default=0.01,
|
||||||
|
min=0.0000001,
|
||||||
|
max=0.9999999
|
||||||
|
)
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import bpy
|
||||||
|
import re
|
||||||
|
from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh
|
||||||
|
from typing import Optional, Dict, Any, List, Tuple
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.common import (
|
||||||
|
get_active_armature,
|
||||||
|
validate_armature,
|
||||||
|
get_all_meshes,
|
||||||
|
ProgressTracker,
|
||||||
|
validate_bone_hierarchy,
|
||||||
|
restore_bone_transforms
|
||||||
|
)
|
||||||
|
|
||||||
|
def duplicate_bone(bone: EditBone) -> EditBone:
|
||||||
|
"""Create a duplicate of the given bone"""
|
||||||
|
arm = bone.id_data
|
||||||
|
new_bone = arm.edit_bones.new(bone.name + "_copy")
|
||||||
|
new_bone.head = bone.head
|
||||||
|
new_bone.tail = bone.tail
|
||||||
|
new_bone.roll = bone.roll
|
||||||
|
new_bone.parent = bone.parent
|
||||||
|
return new_bone
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
|
||||||
|
"""Operator to convert standard legs to digitigrade setup"""
|
||||||
|
bl_idname = "avatar_toolkit.create_digitigrade"
|
||||||
|
bl_label = t("Tools.create_digitigrade")
|
||||||
|
bl_description = t("Tools.create_digitigrade_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
"""Check if operator can be executed"""
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return (is_valid and
|
||||||
|
context.mode == 'EDIT_ARMATURE' and
|
||||||
|
context.selected_editable_bones is not None and
|
||||||
|
len(context.selected_editable_bones) == 2)
|
||||||
|
|
||||||
|
def store_bone_chain_data(self, digi0: EditBone) -> Dict[str, Any]:
|
||||||
|
"""Store initial bone chain data"""
|
||||||
|
chain_data = {}
|
||||||
|
current = digi0
|
||||||
|
while current:
|
||||||
|
chain_data[current.name] = {
|
||||||
|
'head': current.head.copy(),
|
||||||
|
'tail': current.tail.copy(),
|
||||||
|
'roll': current.roll,
|
||||||
|
'matrix': current.matrix.copy(),
|
||||||
|
'parent': current.parent.name if current.parent else None
|
||||||
|
}
|
||||||
|
if current.children:
|
||||||
|
current = current.children[0]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return chain_data
|
||||||
|
|
||||||
|
def process_leg_chain(self, digi0: EditBone) -> bool:
|
||||||
|
"""Process a single leg bone chain"""
|
||||||
|
try:
|
||||||
|
# Get bone chain
|
||||||
|
digi1: EditBone = digi0.children[0]
|
||||||
|
digi2: EditBone = digi1.children[0]
|
||||||
|
digi3: EditBone = digi2.children[0]
|
||||||
|
digi4: Optional[EditBone] = digi3.children[0] if digi3.children else None
|
||||||
|
|
||||||
|
# Clear roll for all bones
|
||||||
|
for bone in [digi0, digi1, digi2, digi3] + ([digi4] if digi4 else []):
|
||||||
|
bone.select = True
|
||||||
|
bpy.ops.armature.roll_clear()
|
||||||
|
bpy.ops.armature.select_all(action='DESELECT')
|
||||||
|
|
||||||
|
# Create thigh bone
|
||||||
|
thigh = duplicate_bone(digi0)
|
||||||
|
base_name = digi0.name.split('.')[0]
|
||||||
|
thigh.name = base_name
|
||||||
|
|
||||||
|
# Create and position calf bone
|
||||||
|
calf = duplicate_bone(digi1)
|
||||||
|
calf.name = digi1.name.split('.')[0]
|
||||||
|
calf.parent = thigh
|
||||||
|
|
||||||
|
# Calculate new positions
|
||||||
|
midpoint = (digi1.tail + digi2.tail) * 0.5
|
||||||
|
calf.head = thigh.tail
|
||||||
|
calf.tail = midpoint
|
||||||
|
|
||||||
|
# Reparent foot to new calf
|
||||||
|
digi3.parent = calf
|
||||||
|
|
||||||
|
# Mark original bones as non-IK
|
||||||
|
for bone in [digi0, digi1, digi2]:
|
||||||
|
if "<noik>" not in bone.name:
|
||||||
|
bone.name = bone.name.split('.')[0] + "<noik>"
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.report({'ERROR'}, t("Tools.digitigrade_error", error=str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
"""Execute the digitigrade conversion"""
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
with ProgressTracker(context, len(context.selected_editable_bones), t("Tools.digitigrade")) as progress:
|
||||||
|
for digi0 in context.selected_editable_bones:
|
||||||
|
progress.step(t("Tools.processing_leg", bone=digi0.name))
|
||||||
|
if not self.process_leg_chain(digi0):
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
self.report({'INFO'}, t("Tools.digitigrade_success"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
|
||||||
|
"""Operator to remove all bone constraints from armature"""
|
||||||
|
bl_idname = "avatar_toolkit.clean_constraints"
|
||||||
|
bl_label = t("Tools.clean_constraints")
|
||||||
|
bl_description = t("Tools.clean_constraints_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
"""Check if operator can be executed"""
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
"""Execute the constraint removal operation"""
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
|
constraints_removed = 0
|
||||||
|
for bone in armature.pose.bones:
|
||||||
|
while bone.constraints:
|
||||||
|
bone.constraints.remove(bone.constraints[0])
|
||||||
|
constraints_removed += 1
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||||
|
"""Operator to remove bones with no vertex weights"""
|
||||||
|
bl_idname = "avatar_toolkit.clean_weights"
|
||||||
|
bl_label = t("Tools.clean_weights")
|
||||||
|
bl_description = t("Tools.clean_weights_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
|
||||||
|
"""Check if bone should be preserved based on settings"""
|
||||||
|
if context.scene.avatar_toolkit.merge_twist_bones:
|
||||||
|
return "twist" in bone_name.lower()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
"""Execute the zero weight bone removal operation"""
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Store initial transforms
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
||||||
|
for bone in armature.data.edit_bones:
|
||||||
|
initial_transforms[bone.name] = {
|
||||||
|
'head': bone.head.copy(),
|
||||||
|
'tail': bone.tail.copy(),
|
||||||
|
'roll': bone.roll,
|
||||||
|
'matrix': bone.matrix.copy(),
|
||||||
|
'parent': bone.parent.name if bone.parent else None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get weighted bones
|
||||||
|
weighted_bones: List[str] = []
|
||||||
|
meshes = get_all_meshes(context)
|
||||||
|
|
||||||
|
for mesh in meshes:
|
||||||
|
mesh_data: Mesh = mesh.data
|
||||||
|
for vertex in mesh_data.vertices:
|
||||||
|
for group in vertex.groups:
|
||||||
|
if group.weight > context.scene.avatar_toolkit.clean_weights_threshold:
|
||||||
|
weighted_bones.append(mesh.vertex_groups[group.group].name)
|
||||||
|
|
||||||
|
# Process bone removal
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
armature_data: Armature = armature.data
|
||||||
|
removed_count = 0
|
||||||
|
|
||||||
|
for bone in armature_data.edit_bones[:]: # Create a copy of the list
|
||||||
|
if (bone.name not in weighted_bones and
|
||||||
|
not self.should_preserve_bone(bone.name, context)):
|
||||||
|
|
||||||
|
# Store children data
|
||||||
|
children = bone.children
|
||||||
|
children_data = {child.name: initial_transforms[child.name] for child in children}
|
||||||
|
|
||||||
|
# Reparent children
|
||||||
|
for child in children:
|
||||||
|
child.use_connect = False
|
||||||
|
if bone.parent:
|
||||||
|
child.parent = bone.parent
|
||||||
|
|
||||||
|
# Remove bone
|
||||||
|
armature_data.edit_bones.remove(bone)
|
||||||
|
removed_count += 1
|
||||||
|
|
||||||
|
# Restore children positions
|
||||||
|
for child_name, data in children_data.items():
|
||||||
|
if child_name in armature_data.edit_bones:
|
||||||
|
child = armature_data.edit_bones[child_name]
|
||||||
|
child.head = data['head']
|
||||||
|
child.tail = data['tail']
|
||||||
|
child.roll = data['roll']
|
||||||
|
child.matrix = data['matrix']
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||||
|
return {'FINISHED'}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import bpy
|
||||||
|
import re
|
||||||
|
from typing import Set, Dict, Optional
|
||||||
|
from bpy.types import Operator, Context
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.logging_setup import logger
|
||||||
|
from ...core.common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
|
||||||
|
from ...core.dictionaries import bone_names, resonite_translations
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ConvertResonite(Operator):
|
||||||
|
"""Convert armature bone names to Resonite format with progress tracking and validation"""
|
||||||
|
bl_idname = "avatar_toolkit.convert_resonite"
|
||||||
|
bl_label = t("Tools.convert_resonite")
|
||||||
|
bl_description = t("Tools.convert_resonite_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
logger.warning("No armature selected for Resonite conversion")
|
||||||
|
self.report({'WARNING'}, t("Armature.validation.no_armature"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
translate_bone_fails: int = 0
|
||||||
|
untranslated_bones: Set[str] = set()
|
||||||
|
simplified_names: Dict[str, str] = {}
|
||||||
|
|
||||||
|
# Create reverse lookup dictionary
|
||||||
|
reverse_bone_lookup = {}
|
||||||
|
for preferred_name, name_list in bone_names.items():
|
||||||
|
for name in name_list:
|
||||||
|
reverse_bone_lookup[name] = preferred_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
# Cache simplified bone names
|
||||||
|
for bone in armature.data.bones:
|
||||||
|
simplified_names[bone.name] = simplify_bonename(bone.name)
|
||||||
|
|
||||||
|
total_bones = len(armature.data.bones)
|
||||||
|
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
|
||||||
|
for bone in armature.data.bones:
|
||||||
|
# Remove any existing "<noik>" tags
|
||||||
|
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
|
||||||
|
simplified_name = simplified_names[bone.name]
|
||||||
|
|
||||||
|
if simplified_name in reverse_bone_lookup and reverse_bone_lookup[simplified_name] in resonite_translations:
|
||||||
|
new_name = resonite_translations[reverse_bone_lookup[simplified_name]]
|
||||||
|
logger.debug(f"Translating bone: {bone.name} -> {new_name}")
|
||||||
|
bone.name = new_name
|
||||||
|
else:
|
||||||
|
untranslated_bones.add(bone.name)
|
||||||
|
bone.name = bone.name + "<noik>"
|
||||||
|
translate_bone_fails += 1
|
||||||
|
logger.debug(f"Failed to translate bone: {bone.name}")
|
||||||
|
|
||||||
|
progress.step(t("Tools.convert_resonite.processing", name=bone.name))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during Resonite conversion: {str(e)}")
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error returning to object mode: {str(e)}")
|
||||||
|
|
||||||
|
if translate_bone_fails > 0:
|
||||||
|
logger.info(f"Conversion completed with {translate_bone_fails} untranslated bones")
|
||||||
|
logger.debug(f"Untranslated bones: {untranslated_bones}")
|
||||||
|
self.report({'INFO'}, t("Tools.bones_translated_with_fails", translate_bone_fails=translate_bone_fails))
|
||||||
|
else:
|
||||||
|
logger.info("All bones translated successfully")
|
||||||
|
self.report({'INFO'}, t("Tools.bones_translated_success"))
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import Operator, Context
|
||||||
|
from ...core.translations import t
|
||||||
|
from ...core.common import get_active_armature, validate_armature
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_SeparateByMaterials(Operator):
|
||||||
|
"""Operator to separate mesh by materials"""
|
||||||
|
bl_idname = "avatar_toolkit.separate_materials"
|
||||||
|
bl_label = t("Tools.separate_materials")
|
||||||
|
bl_description = t("Tools.separate_materials_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
"""Check if operator can be executed"""
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return (context.active_object and
|
||||||
|
context.active_object.type == 'MESH' and
|
||||||
|
is_valid)
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
"""Execute the separation operation"""
|
||||||
|
try:
|
||||||
|
obj = context.active_object
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
bpy.ops.mesh.separate(type='MATERIAL')
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
self.report({'INFO'}, t("Tools.separate_materials_success"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
except Exception as e:
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
|
||||||
|
"""Operator to separate mesh by loose parts"""
|
||||||
|
bl_idname = "avatar_toolkit.separate_loose"
|
||||||
|
bl_label = t("Tools.separate_loose")
|
||||||
|
bl_description = t("Tools.separate_loose_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
"""Check if operator can be executed"""
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
if not armature:
|
||||||
|
return False
|
||||||
|
is_valid, _ = validate_armature(armature)
|
||||||
|
return (context.active_object and
|
||||||
|
context.active_object.type == 'MESH' and
|
||||||
|
is_valid)
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set[str]:
|
||||||
|
"""Execute the separation operation"""
|
||||||
|
try:
|
||||||
|
obj = context.active_object
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
bpy.ops.mesh.separate(type='LOOSE')
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
self.report({'INFO'}, t("Tools.separate_loose_success"))
|
||||||
|
return {'FINISHED'}
|
||||||
|
except Exception as e:
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
@@ -125,6 +125,56 @@
|
|||||||
"Optimization.processing_shapekey": "Processing shape key: {name}",
|
"Optimization.processing_shapekey": "Processing shape key: {name}",
|
||||||
"Optimization.remove_doubles_completed": "Remove doubles completed successfully",
|
"Optimization.remove_doubles_completed": "Remove doubles completed successfully",
|
||||||
|
|
||||||
|
"Tools.label": "Tools",
|
||||||
|
"Tools.general_title": "General Tools",
|
||||||
|
"Tools.convert_resonite": "Convert to Resonite",
|
||||||
|
"Tools.convert_resonite_desc": "Convert model for use in Resonite",
|
||||||
|
"Tools.convert_resonite.operation": "Converting to Resonite",
|
||||||
|
"Tools.separate_title": "Separation Tools",
|
||||||
|
"Tools.separate_materials": "By Materials",
|
||||||
|
"Tools.separate_materials_desc": "Separate mesh by materials",
|
||||||
|
"Tools.separate_loose": "Loose Parts",
|
||||||
|
"Tools.separate_loose_desc": "Separate mesh into loose parts",
|
||||||
|
"Tools.separate_materials_success": "Mesh separated by materials successfully",
|
||||||
|
"Tools.separate_loose_success": "Mesh separated into loose parts successfully",
|
||||||
|
"Tools.bone_title": "Bone Tools",
|
||||||
|
"Tools.create_digitigrade": "Create Digitigrade Legs",
|
||||||
|
"Tools.create_digitigrade_desc": "Convert legs to digitigrade setup",
|
||||||
|
"Tools.digitigrade": "Create Digitigrade Legs",
|
||||||
|
"Tools.digitigrade_desc": "Convert selected leg bones to digitigrade setup",
|
||||||
|
"Tools.digitigrade_error": "Failed to create digitigrade legs: {error}",
|
||||||
|
"Tools.digitigrade_success": "Successfully created digitigrade leg setup",
|
||||||
|
"Tools.processing_leg": "Processing leg bone: {bone}",
|
||||||
|
"Tools.merge_twist_bones": "Keep Twist Bones",
|
||||||
|
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
|
||||||
|
"Tools.clean_weights": "Remove Zero Weight Bones",
|
||||||
|
"Tools.clean_weights_desc": "Remove bones with no vertex weights",
|
||||||
|
"Tools.clean_constraints": "Delete Bone Constraints",
|
||||||
|
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
|
||||||
|
"Tools.clean_constraints_success": "Removed {count} bone constraints",
|
||||||
|
"Tools.processing_bone_constraints": "Removing constraints from bone: {bone}",
|
||||||
|
"Tools.clean_weights_success": "Removed {count} zero-weight bones",
|
||||||
|
"Tools.clean_weights_threshold": "Weight Threshold",
|
||||||
|
"Tools.clean_weights_threshold_desc": "Minimum weight value to consider a bone as weighted",
|
||||||
|
"Tools.merge_title": "Merge Tools",
|
||||||
|
"Tools.merge_to_active": "Merge to Active",
|
||||||
|
"Tools.merge_to_active_desc": "Merge selected bones to active bone",
|
||||||
|
"Tools.merge_to_parent": "Merge to Parent",
|
||||||
|
"Tools.merge_to_parent_desc": "Merge bones to their respective parents",
|
||||||
|
"Tools.connect_bones": "Connect Bones",
|
||||||
|
"Tools.connect_bones_desc": "Connect disconnected bones in chain",
|
||||||
|
"Tools.additional_title": "Additional Tools",
|
||||||
|
"Tools.apply_transforms": "Apply Transforms",
|
||||||
|
"Tools.apply_transforms_desc": "Apply all transformations to objects",
|
||||||
|
"Tools.clean_shapekeys": "Remove Unused Shapekeys",
|
||||||
|
"Tools.clean_shapekeys_desc": "Remove unused shape keys from meshes",
|
||||||
|
"Tools.bones_translated_success": "All bones translated successfully",
|
||||||
|
"Tools.bones_translated_with_fails": "Translation completed with {translate_bone_fails} untranslated bones",
|
||||||
|
"Tools.storing_transforms": "Storing bone transforms...",
|
||||||
|
"Tools.analyzing_weights": "Analyzing vertex weights...",
|
||||||
|
"Tools.removing_bones": "Removing unweighted bones...",
|
||||||
|
"Tools.verifying_hierarchy": "Verifying bone hierarchy...",
|
||||||
|
|
||||||
"Settings.label": "Settings",
|
"Settings.label": "Settings",
|
||||||
"Settings.language": "Language",
|
"Settings.language": "Language",
|
||||||
"Settings.language_desc": "Select interface language",
|
"Settings.language_desc": "Select interface language",
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import bpy
|
||||||
|
from typing import Set
|
||||||
|
from bpy.types import Panel, Context, UILayout, Operator
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from ..core.translations import t
|
||||||
|
|
||||||
|
# Temporary Operator Classes for UI Preview
|
||||||
|
class AvatarToolkit_OT_MergeToActive(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.merge_to_active"
|
||||||
|
bl_label = "Merge to Active"
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_MergeToParent(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.merge_to_parent"
|
||||||
|
bl_label = "Merge to Parent"
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ConnectBones(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.connect_bones"
|
||||||
|
bl_label = "Connect Bones"
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_ApplyTransforms(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.apply_transforms"
|
||||||
|
bl_label = "Apply Transforms"
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolkit_OT_CleanShapekeys(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.clean_shapekeys"
|
||||||
|
bl_label = "Remove Unused Shapekeys"
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_ToolsPanel(Panel):
|
||||||
|
"""Panel containing various tools for avatar customization and optimization"""
|
||||||
|
bl_label: str = t("Tools.label")
|
||||||
|
bl_idname: str = "OBJECT_PT_avatar_toolkit_tools"
|
||||||
|
bl_space_type: str = 'VIEW_3D'
|
||||||
|
bl_region_type: str = 'UI'
|
||||||
|
bl_category: str = CATEGORY_NAME
|
||||||
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
bl_order: int = 2
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the tools panel interface"""
|
||||||
|
layout: UILayout = self.layout
|
||||||
|
|
||||||
|
# General Tools
|
||||||
|
tools_box: UILayout = layout.box()
|
||||||
|
col: UILayout = tools_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.general_title"), icon='TOOL_SETTINGS')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.convert_resonite", text=t("Tools.convert_resonite"), icon='EXPORT')
|
||||||
|
|
||||||
|
# Separation Tools
|
||||||
|
sep_box: UILayout = layout.box()
|
||||||
|
col = sep_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.separate_title"), icon='MOD_EXPLODE')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
row: UILayout = col.row(align=True)
|
||||||
|
row.operator("avatar_toolkit.separate_materials", text=t("Tools.separate_materials"), icon='MATERIAL')
|
||||||
|
row.operator("avatar_toolkit.separate_loose", text=t("Tools.separate_loose"), icon='MESH_DATA')
|
||||||
|
|
||||||
|
# Bone Tools
|
||||||
|
bone_box: UILayout = layout.box()
|
||||||
|
col = bone_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.bone_title"), icon='BONE_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.create_digitigrade", text=t("Tools.create_digitigrade"), icon='BONE_DATA')
|
||||||
|
|
||||||
|
# Weight Tools
|
||||||
|
weight_box: UILayout = bone_box.box()
|
||||||
|
col = weight_box.column(align=True)
|
||||||
|
col.prop(context.scene.avatar_toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE')
|
||||||
|
row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
|
||||||
|
|
||||||
|
# Merge Tools
|
||||||
|
merge_box: UILayout = layout.box()
|
||||||
|
col = merge_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.merge_title"), icon='AUTOMERGE_ON')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.operator("avatar_toolkit.merge_to_active", text=t("Tools.merge_to_active"), icon='BONE_DATA')
|
||||||
|
row.operator("avatar_toolkit.merge_to_parent", text=t("Tools.merge_to_parent"), icon='BONE_DATA')
|
||||||
|
col.operator("avatar_toolkit.connect_bones", text=t("Tools.connect_bones"), icon='BONE_DATA')
|
||||||
|
|
||||||
|
# Additional Tools
|
||||||
|
extra_box: UILayout = layout.box()
|
||||||
|
col = extra_box.column(align=True)
|
||||||
|
col.label(text=t("Tools.additional_title"), icon='TOOL_SETTINGS')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.apply_transforms", text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
|
||||||
|
col.operator("avatar_toolkit.clean_shapekeys", text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
|
||||||
Reference in New Issue
Block a user