Merge branch 'Alpha-2' into texture-atlas

This commit is contained in:
Yusarina
2025-02-07 18:38:12 +00:00
committed by GitHub
14 changed files with 896 additions and 45 deletions
+3 -3
View File
@@ -28,7 +28,7 @@ class MaterialListBool:
#For the love that is holy do not ever touch these. If this was java I would make these private
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
old_list: dict[str,list[Material]] = {}
bool_material_list_expand: dict[str,bool] = {}
@@ -46,7 +46,7 @@ class MaterialListBool:
if mat_slot.material:
if mat_slot.material not in newlist:
newlist.append(mat_slot.material)
still_the_same: bool = True
if bpy.context.scene.name in MaterialListBool.old_list:
for item in newlist:
@@ -60,7 +60,7 @@ class MaterialListBool:
else:
still_the_same = False
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
class ProgressTracker:
+69
View File
@@ -354,3 +354,72 @@ resonite_translations = {
'thumb_2_r': "thumb2.R",
'thumb_3_r': "thumb3.R"
}
rigify_unity_names = {
"DEF-spine": "Hips",
"DEF-spine.001": "Spine",
"DEF-spine.002": "Chest",
"DEF-spine.003": "UpperChest",
"DEF-neck": "Neck",
"DEF-head": "Head",
"DEF-shoulder.L": "LeftShoulder",
"DEF-upper_arm.L": "LeftUpperArm",
"DEF-forearm.L": "LeftLowerArm",
"DEF-hand.L": "LeftHand",
"DEF-shoulder.R": "RightShoulder",
"DEF-upper_arm.R": "RightUpperArm",
"DEF-forearm.R": "RightLowerArm",
"DEF-hand.R": "RightHand",
"DEF-thigh.L": "LeftUpperLeg",
"DEF-shin.L": "LeftLowerLeg",
"DEF-foot.L": "LeftFoot",
"DEF-toe.L": "LeftToes",
"DEF-thigh.R": "RightUpperLeg",
"DEF-shin.R": "RightLowerLeg",
"DEF-foot.R": "RightFoot",
"DEF-toe.R": "RightToes"
}
rigify_basic_unity_names = {
"spine": "Hips",
"spine.001": "Spine",
"spine.002": "Chest",
"spine.003": "UpperChest",
"neck": "Neck",
"head": "Head",
"shoulder.L": "LeftShoulder",
"upper_arm.L": "LeftUpperArm",
"forearm.L": "LeftLowerArm",
"hand.L": "LeftHand",
"shoulder.R": "RightShoulder",
"upper_arm.R": "RightUpperArm",
"forearm.R": "RightLowerArm",
"hand.R": "RightHand",
"thigh.L": "LeftUpperLeg",
"shin.L": "LeftLowerLeg",
"foot.L": "LeftFoot",
"toe.L": "LeftToes",
"thigh.R": "RightUpperLeg",
"shin.R": "RightLowerLeg",
"foot.R": "RightFoot",
"toe.R": "RightToes"
}
rigify_unnecessary_bones = [
'face',
'ear.l', 'ear.r',
'forehead',
'cheek.t.l', 'cheek.t.r',
'cheek.b.l', 'cheek.b.r',
'brow.t.l', 'brow.t.r',
'brow.b.l', 'brow.b.r',
'jaw',
'chin',
'nose',
'temple.l', 'temple.r',
'teeth',
'lip',
'lid',
'heel',
'pelvis.'
]
+8 -6
View File
@@ -4,6 +4,7 @@ from typing import Optional, Any
from bpy.types import Context
logger = logging.getLogger('avatar_toolkit')
_original_error = logger.error
def configure_logging(enabled: bool = False) -> None:
"""Configure logging for Avatar Toolkit"""
@@ -16,15 +17,16 @@ def configure_logging(enabled: bool = False) -> None:
if enabled:
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s\n%(exc_info)s' if enabled else '%(message)s')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
# Override error logging to include traceback
def error_with_traceback(msg, *args, **kwargs):
if kwargs.get('exc_info', False):
msg = f"{msg}\n{traceback.format_exc()}"
logger.error(msg, *args, **kwargs)
if kwargs.get('exc_info', False) or isinstance(msg, Exception):
full_msg = f"{msg}\n{traceback.format_exc()}"
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
else:
_original_error(msg, *args, **kwargs)
logger.error = error_with_traceback
@@ -33,4 +35,4 @@ def update_logging_state(self: Any, context: Context) -> None:
from .addon_preferences import save_preference
enabled = self.enable_logging
save_preference("enable_logging", enabled)
configure_logging(enabled)
configure_logging(enabled)
+48 -2
View File
@@ -18,6 +18,13 @@ from .common import get_armature_list, get_active_armature, get_all_meshes, Scen
from ..functions.visemes import VisemePreview
from ..functions.eye_tracking import set_rotation
class ZeroWeightBoneItem(PropertyGroup):
"""Property group for zero weight bone list items"""
name: StringProperty(name="Bone Name")
selected: BoolProperty(name="Selected", default=True)
has_children: BoolProperty(name="Has Children", default=False)
is_deform: BoolProperty(name="Is Deform Bone", default=False)
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
"""Updates validation mode and saves preference"""
logger.info(f"Updating validation mode to: {self.validation_mode}")
@@ -361,6 +368,46 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=True
)
preserve_parent_bones: BoolProperty(
name=t("Tools.preserve_parent_bones"),
description=t("Tools.preserve_parent_bones_desc"),
default=True
)
target_bone_type: EnumProperty(
name=t("Tools.target_bone_type"),
description=t("Tools.target_bone_type_desc"),
items=[
('ALL', t("Tools.target_all_bones"), ""),
('DEFORM', t("Tools.target_deform_bones"), ""),
('NON_DEFORM', t("Tools.target_non_deform_bones"), "")
],
default='ALL'
)
zero_weight_bones: CollectionProperty(
type=ZeroWeightBoneItem,
name="Zero Weight Bones",
description="List of bones with zero weights"
)
zero_weight_bones_index: IntProperty(
name="Zero Weight Bone Index",
default=0
)
merge_twist_bones: BoolProperty(
name=t("Tools.merge_twist_bones"),
description=t("Tools.merge_twist_bones_desc"),
default=True
)
list_only_mode: BoolProperty(
name=t("Tools.list_only_mode"),
description=t("Tools.list_only_mode_desc"),
default=False
)
cleanup_shape_keys: BoolProperty(
name=t('MergeArmature.cleanup_shape_keys'),
description=t('MergeArmature.cleanup_shape_keys_desc'),
@@ -463,8 +510,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
type=SceneMatClass
)
def register() -> None:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")
+52 -19
View File
@@ -54,6 +54,28 @@ def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[in
return merged_vertices
def vertex_moves(mesh_data: bpy.types.Mesh, vertex: int) -> bool:
for shapekey in mesh_data.shape_keys.key_blocks:
data: bpy.types.ShapeKey = shapekey
if data.points[vertex].co.xyz != mesh_data.vertices[vertex].co.xyz:
return True
return False
def merge_vertex_at_index(mesh_data: bpy.types.Mesh, index: int, distance: float):
select_target_vertex = [False]*len(mesh_data.vertices)
select_target_vertex[index] = True
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",select_target_vertex)
bpy.ops.object.mode_set(mode='EDIT')
for _ in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it.
bpy.ops.mesh.remove_doubles(threshold=distance, use_unselected=True, use_sharp_edge_from_normals=False)
bpy.ops.object.mode_set(mode='OBJECT')
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
bl_idname = "avatar_toolkit.remove_doubles_advanced"
bl_label = t("Optimization.remove_doubles_advanced")
@@ -168,7 +190,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
except Exception as e:
logger.error(f"Error in modify_mesh: {str(e)}")
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> bool:
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> int:
"""Advanced mesh modification with shape key handling"""
try:
final_merged_vertex_group = []
@@ -179,26 +201,28 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)}
merge_vertex_at_index(duplicate.data, mesh_entry["cur_vertex_pass"], merge_distance) #merge the vertex at our pass to find vertices that would merge to our vertex at this shapekey.
# Process merging
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"])
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"]) # find what vertices actually merged.
if not initialized_final:
final_merged_vertex_group = merged_vertices.copy()
initialized_final = True
else:
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices]
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] # remove vertices that merged from the list if they didn't merge during this shapkey.
bpy.ops.object.delete()
# Apply final merging
if final_merged_vertex_group:
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance)
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance) # merge all vertices that merged on every shapekey no matter the shapekey during the loop.
return not (len(final_merged_vertex_group) > 1)
return len(final_merged_vertex_group)
except Exception as e:
logger.error(f"Error in modify_mesh_advanced: {str(e)}")
return True
return 1
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
"""Apply final vertex merging operations"""
@@ -232,16 +256,14 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
"""Complete the mesh processing by performing final merge operations"""
logger.debug("Finishing mesh processing")
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
if not advanced:
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
"""Modal operator execution"""
@@ -266,10 +288,21 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
self.process_simple_mesh(context, mesh, merge_distance)
self.objects_to_do.pop(0)
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced:
if self.modify_mesh_advanced(context, mesh):
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced: #advanced merging vertex by vertex
if(mesh["cur_vertex_pass"] < 0): #make sure it doesn't go below 0 and explode when advancing backwards from a previous step
mesh["cur_vertex_pass"] = 0
if vertex_moves(mesh["mesh"].data, mesh["cur_vertex_pass"]): # do not do advanced merging for vertices that don't move
mesh["cur_vertex_pass"] -= self.modify_mesh_advanced(context, mesh)-2 #advance forward or backwards based on how many vertices actually got merged, changing the list size.
#if above returns 1 (no vertices other than this one being merged to ourselves), advance by 1. else don't advance or go backwards. Makes sure all vertices get merged in the end.
else:
mesh["cur_vertex_pass"] += 1
elif (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced and len(mesh['shapekeys']) > 0: #after advanced merging has gone past all the moving vertices, now we need to merge non moving vertices.
shapekeyname = mesh['shapekeys'].pop(0)
mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname)
logger.debug(f"Processing shapekey {shapekeyname}")
self.modify_mesh(context, mesh)
else:
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
self.objects_to_do.pop(0)
+65 -13
View File
@@ -134,17 +134,11 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
def execute(self, context: Context) -> set[str]:
"""Execute the constraint removal operation"""
# Make sure we are in Object mode first or it will error
bpy.ops.object.mode_set(mode='OBJECT')
armature = get_active_armature(context)
# Select armature and make it active before changing mode
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
constraints_removed = 0
@@ -157,7 +151,6 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
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"
@@ -167,10 +160,37 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
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()
toolkit = context.scene.avatar_toolkit
bone = context.active_object.data.bones.get(bone_name)
if not bone:
return False
if toolkit.preserve_parent_bones and bone.children:
return True
if toolkit.target_bone_type == 'DEFORM' and not bone.use_deform:
return True
if toolkit.target_bone_type == 'NON_DEFORM' and bone.use_deform:
return True
return False
def populate_bone_list(self, context: Context, zero_weight_bones: List[str]) -> None:
"""Populate the zero weight bones list"""
toolkit = context.scene.avatar_toolkit
toolkit.zero_weight_bones.clear()
armature = get_active_armature(context)
for bone_name in zero_weight_bones:
bone = armature.data.bones.get(bone_name)
if bone:
item = toolkit.zero_weight_bones.add()
item.name = bone_name
item.has_children = len(bone.children) > 0
item.is_deform = bone.use_deform
def execute(self, context: Context) -> set[str]:
"""Execute the zero weight bone removal operation"""
armature = get_active_armature(context)
@@ -192,6 +212,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
# Get weighted bones
weighted_bones: List[str] = []
meshes = get_all_meshes(context)
zero_weight_bones: List[str] = []
for mesh in meshes:
mesh_data: Mesh = mesh.data
@@ -209,6 +230,10 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
if (bone.name not in weighted_bones and
not self.should_preserve_bone(bone.name, context)):
if context.scene.avatar_toolkit.list_only_mode:
zero_weight_bones.append(bone.name)
continue
# Store children data
children = bone.children
children_data = {child.name: initial_transforms[child.name] for child in children}
@@ -227,11 +252,38 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
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']
restore_bone_transforms(child, data)
bpy.ops.object.mode_set(mode='OBJECT')
if context.scene.avatar_toolkit.list_only_mode:
self.populate_bone_list(context, zero_weight_bones)
return {'FINISHED'}
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
return {'FINISHED'}
class AvatarToolKit_OT_RemoveSelectedBones(Operator):
"""Operator to remove selected bones from the zero weight bones list"""
bl_idname = "avatar_toolkit.remove_selected_bones"
bl_label = t("Tools.remove_selected_bones")
bl_description = t("Tools.remove_selected_bones_desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
armature = get_active_armature(context)
toolkit = context.scene.avatar_toolkit
selected_bones = [item.name for item in toolkit.zero_weight_bones
if item.selected]
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in selected_bones:
if bone_name in armature.data.edit_bones:
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
bpy.ops.object.mode_set(mode='OBJECT')
toolkit.zero_weight_bones.clear()
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
return {'FINISHED'}
+255
View File
@@ -0,0 +1,255 @@
import bpy
from typing import Dict, List, Set, Optional, Tuple, Any
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
from ...core.common import get_active_armature, validate_armature
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
"""Convert Rigify armature to Unity-compatible format"""
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
bl_label = t("Tools.convert_rigify_to_unity")
bl_description = t("Tools.convert_rigify_to_unity_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return ("DEF-spine" in armature.data.bones or
"spine" in armature.data.bones and "metarig" in armature.name.lower())
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting Rigify to Unity conversion")
armature = get_active_armature(context)
if not armature:
logger.error("No armature found")
self.report({'ERROR'}, t("Tools.no_armature"))
return {'CANCELLED'}
logger.debug(f"Converting armature: {armature.name}")
armature.name = "Armature"
armature.data.name = "Armature"
logger.debug("Renamed armature to 'Armature'")
if "DEF-spine" in armature.data.bones:
logger.info("Processing DEF bones")
self.move_def_bones(armature)
self.rename_bones_for_unity(armature)
else:
logger.info("Processing basic bones")
self.cleanup_extra_bones(armature)
self.rename_basic_bones_for_unity(armature)
logger.debug("Cleaning up bone collections")
self.cleanup_bone_collections(armature)
if context.scene.avatar_toolkit.merge_twist_bones:
logger.info("Merging twist bones")
self.handle_twist_bones(armature)
logger.info("Successfully converted Rigify armature to Unity format")
self.report({'INFO'}, t("Tools.rigify_converted"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to convert Rigify: {str(e)}", exc_info=True)
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def cleanup_extra_bones(self, armature: Object) -> None:
"""Remove unnecessary bones and merge neck bones"""
logger.debug("Starting cleanup of extra bones")
# Set armature as active object before mode switch
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bones_to_remove: List[str] = []
for bone in armature.data.edit_bones:
if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones):
bones_to_remove.append(bone.name)
for bone_name in bones_to_remove:
if bone_name in armature.data.edit_bones:
logger.debug(f"Removing bone: {bone_name}")
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones:
logger.debug("Merging neck bones")
neck_start = armature.data.edit_bones['spine.004']
neck_end = armature.data.edit_bones['spine.005']
neck_start.tail = neck_end.tail
armature.data.edit_bones.remove(neck_end)
neck_start.name = "Neck"
if 'spine.006' in armature.data.edit_bones:
logger.debug("Renaming head bone")
head_bone = armature.data.edit_bones['spine.006']
head_bone.name = "Head"
def move_def_bones(self, armature: Object) -> None:
"""Move DEF bones to their correct positions"""
logger.debug("Moving DEF bones to correct positions")
# Set armature as active object
bpy.context.view_layer.objects.active = armature
remap: Dict[str, str] = self.get_org_remap(armature)
remap.update(self.get_special_remap())
remove_bones_in_chain: List[str] = [
'DEF-upper_arm.L.001', 'DEF-forearm.L.001',
'DEF-upper_arm.R.001', 'DEF-forearm.R.001',
'DEF-thigh.L.001', 'DEF-shin.L.001',
'DEF-thigh.R.001', 'DEF-shin.R.001'
]
transform_copies: List[str] = self.get_transform_copies(armature)
logger.debug("Setting up transform copies")
bpy.ops.object.mode_set(mode='POSE')
for bone_name in transform_copies:
bone = armature.pose.bones[bone_name]
org_name = 'ORG-' + self.get_proto_name(bone_name)
if org_name in armature.pose.bones:
constraint = bone.constraints.new('COPY_TRANSFORMS')
constraint.target = armature
constraint.subtarget = org_name
constr_count = len(bone.constraints)
if constr_count > 1:
bone.constraints.move(constr_count-1, 0)
logger.debug("Remapping bone parents")
bpy.ops.object.mode_set(mode='EDIT')
for remap_key in remap:
if remap_key in armature.data.edit_bones and remap[remap_key] in armature.data.edit_bones:
armature.data.edit_bones[remap_key].parent = armature.data.edit_bones[remap[remap_key]]
logger.debug("Processing bone chain removal")
bpy.ops.object.mode_set(mode='OBJECT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
armature.data.bones[bone_name].use_deform = False
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
remove_bone = armature.data.edit_bones[bone_name]
parent_bone = remove_bone.parent
parent_bone.tail = remove_bone.tail
retarget_bones = list(remove_bone.children)
for bone in retarget_bones:
bone.parent = parent_bone
armature.data.edit_bones.remove(remove_bone)
def rename_bones_for_unity(self, armature: Object) -> None:
"""Rename bones to Unity-compatible names"""
logger.debug("Renaming bones to Unity format")
for old_name, new_name in rigify_unity_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
logger.debug(f"Renaming bone: {old_name} -> {new_name}")
bone.name = new_name
def rename_basic_bones_for_unity(self, armature: Object) -> None:
"""Rename basic metarig bones to Unity-compatible names"""
logger.debug("Renaming basic metarig bones")
for old_name, new_name in rigify_basic_unity_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
logger.debug(f"Renaming basic bone: {old_name} -> {new_name}")
bone.name = new_name
def cleanup_bone_collections(self, armature: Object) -> None:
"""Remove all bone collections since they're not needed for Unity"""
logger.debug("Cleaning up bone collections")
if hasattr(armature.data, 'collections') and armature.data.collections:
while len(armature.data.collections) > 0:
collection = armature.data.collections[0]
armature.data.collections.remove(collection)
while len(armature.data.collections) > 1:
collection = armature.data.collections[1]
armature.data.collections.remove(collection)
def handle_twist_bones(self, armature: Object) -> None:
"""Handle twist bones during conversion"""
logger.debug("Processing twist bones")
twist_bones: List[Tuple[str, str]] = [
("DEF-upper_arm_twist.L", "DEF-upper_arm.L"),
("DEF-upper_arm_twist.R", "DEF-upper_arm.R"),
("DEF-forearm_twist.L", "DEF-forearm.L"),
("DEF-forearm_twist.R", "DEF-forearm.R"),
("DEF-thigh_twist.L", "DEF-thigh.L"),
("DEF-thigh_twist.R", "DEF-thigh.R")
]
bpy.ops.object.mode_set(mode='EDIT')
for twist_bone, parent_bone in twist_bones:
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
logger.debug(f"Merging twist bone: {twist_bone} into {parent_bone}")
twist = armature.data.edit_bones[twist_bone]
parent = armature.data.edit_bones[parent_bone]
parent.tail = twist.tail
for child in twist.children:
child.parent = parent
armature.data.edit_bones.remove(twist)
bpy.ops.object.mode_set(mode='OBJECT')
def get_org_remap(self, armature: Object) -> Dict[str, str]:
"""Get original bone remapping"""
logger.debug("Getting original bone remapping")
remap: Dict[str, str] = {}
for bone in armature.data.bones:
if self.is_def_bone(bone.name):
name = self.get_proto_name(bone.name)
parent = bone.parent
while parent:
parent_name = self.get_proto_name(parent.name)
if parent_name != name:
if ('DEF-' + parent_name) in armature.data.bones:
remap[bone.name] = 'DEF-' + parent_name
break
parent = parent.parent
return remap
def get_special_remap(self) -> Dict[str, str]:
"""Get special bone remapping cases"""
logger.debug("Getting special bone remapping")
return {
'DEF-thigh.L': 'DEF-pelvis.L',
'DEF-thigh.R': 'DEF-pelvis.R',
'DEF-upper_arm.L': 'DEF-shoulder.L',
'DEF-upper_arm.R': 'DEF-shoulder.R',
}
def get_transform_copies(self, armature: Object) -> List[str]:
"""Get bones that need transform copies"""
logger.debug("Getting transform copy bones")
result: List[str] = []
for bone in armature.pose.bones:
if self.is_def_bone(bone.name) and not self.has_transform_copies(bone):
result.append(bone.name)
return result
def has_transform_copies(self, bone: PoseBone) -> bool:
"""Check if bone has transform copy constraints"""
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
def is_def_bone(self, bone_name: str) -> bool:
"""Check if bone is a DEF bone"""
return bone_name.startswith('DEF-')
def is_org_bone(self, bone_name: str) -> bool:
"""Check if bone is an ORG bone"""
return bone_name.startswith('ORG-')
def get_proto_name(self, bone_name: str) -> str:
"""Get the prototype name of a bone"""
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
return bone_name[4:]
return bone_name
+253
View File
@@ -0,0 +1,253 @@
from typing import TypedDict, Set, Dict, List, Optional, Any, Tuple
import bpy
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
import bmesh
import numpy as np
import math
from ...core.translations import t
from ...core.logging_setup import logger
class GenerateLoopTreeResult(TypedDict):
tree: Dict[str, Set[str]]
selected_loops: Dict[str, List[int]]
selected_verts: Dict[str, int]
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
"""Operator to align selected UV edges to target edge"""
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
bl_label = t("UVTools.align_edges")
bl_description = t("UVTools.align_edges_desc")
bl_options = {'REGISTER', 'UNDO'}
#all selected objects need to be meshes for this to work - @989onan
@classmethod
def poll(cls, context: Context) -> bool:
if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)):
return False
if context.mode != "EDIT_MESH":
return False
for obj in context.view_layer.objects.selected:
if obj.type != "MESH":
return False
if not context.space_data:
return False
if not context.space_data.show_uvedit:
return False
if context.scene.tool_settings.use_uv_select_sync:
return False
return True
def execute(self, context: Context) -> Set[str]:
target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to
sources: List[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line
prev_mode: str = bpy.context.object.mode
bpy.ops.object.mode_set(mode='OBJECT')
def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult:
logger.debug(f"Finding selected line for: {obj_name}")
vert_target_loops: Dict[str, List[int]] = {}
vert_target_verts: Dict[str, int] = {}
me: Mesh = bpy.data.objects[obj_name].data
uv_lay: MeshUVLoopLayer = me.uv_layers.active
bm: bmesh.types.BMesh = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
# To explain:
# So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order)
#
# For some preknowledge:
# When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in
# the same position on the UV map, then it considers it one point and the user can move it
# (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying)
#
# The problem:
# The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist
# Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI,
# allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with.
#
# The solution
# We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected.
# that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain
# that two vertices share the same face loop, and therefore are connected.
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
for k,i in enumerate(uv_lay.vertex_selection):
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
key = np.array(uv_lay.uv[k].vector[:])
key = key.round(decimals=5)
if str(key) not in vert_target_loops:
vert_target_loops[str(key)] = []
vert_target_loops[str(key)].append(k)
vert_target_verts[str(key)] = me.loops[k].vertex_index
if len(vert_target_loops) > 4000:
self.report({'WARNING'}, t("UVTools.too_many_vertices"))
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
logger.debug(f"Finding connections on line for {obj_name}")
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
tree: Dict[str, Set[str]] = {}
selected_verts = np.hstack(list(vert_target_loops.values()))
bm.verts.ensure_lookup_table()
for uvcoordsstr in vert_target_loops:
uv_lay = me.uv_layers.active
#before this section, each vert_target_loops is just groupings of vertices that share coordinates.
# Using the data that determines UV face corners (uvloops) that are associated with the real vertex,
# and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in
# vert_target_loops, we can now identify them
#TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected)
# Someone explain this better than me if you can please - @989onan
extension_loops = []
loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops
loops_indexes = [i.index for i in loops]
for loop in vert_target_loops[uvcoordsstr]:
if loop in loops_indexes:
loop_obj = loops[loops_indexes.index(loop)]
extension_loops.append(loop_obj.link_loop_next.index)
extension_loops.append(loop_obj.link_loop_prev.index)
#make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary.
#the order of this dictionary is unknown.
# Someone explain this better than me if you can please - @989onan
tree[uvcoordsstr] = set()
for i in extension_loops:
if i in selected_verts:
key = np.array(uv_lay.uv[i].vector[:])
key = key.round(decimals=5)
tree[uvcoordsstr].add(str(key))
if uvcoordsstr in tree:
if len(tree[uvcoordsstr]) > 2:
self.report({'WARNING'}, t("UVTools.need_line", obj=obj_name))
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
uv_lay = me.uv_layers.active
for uvcoordstr in vert_target_loops:
for loop in vert_target_loops[uvcoordstr]:
uv_lay.vertex_selection[loop].value = True
bm.free()
me.validate()
logger.debug(f"Found UV line connections for {obj_name}")
return {"tree": tree, "selected_loops": vert_target_loops, "selected_verts": vert_target_verts}
def sort_uv_tree(originaltree: Dict[str, Set[str]], obj_name: str) -> List[str]:
sortedtree: Dict[str, Set[str]] = originaltree.copy()
startpoints: List[str] = []
for i in sortedtree:
if len(sortedtree[i]) < 2:
startpoints.append(i)
if len(startpoints) != 2:
self.report({'WARNING'}, t("UVTools.need_line", obj=obj_name))
return []
uvcoords1 = [float(x) for x in startpoints[0].replace("[","").replace("]","").split()]
uvcoords2 = [float(x) for x in startpoints[1].replace("[","").replace("]","").split()]
cursor = context.space_data.cursor_location
startpoint = startpoints[0] if math.sqrt((uvcoords1[0] - cursor[0])**2 + (uvcoords1[1] - cursor[1])**2) > math.sqrt((uvcoords2[0] - cursor[0])**2 + (uvcoords2[1] - cursor[1])**2) else startpoints[1]
#Wew my first actual recursive sort! - @989onan
def recursive_sort_uv_tree(point: str, sortedfinal: List[str]) -> List[str]:
#print("appending "+point)
sortedfinal.append(point)
new_point: str = ""
for i in sortedtree:
if point in sortedtree[i]:
new_point = i
removed_value = sortedtree.pop(i)
#print(removed_value)
break
if new_point == "":
logger.debug("Sorting complete, remaining tree:")
logger.debug(sortedtree)
return sortedfinal
return recursive_sort_uv_tree(new_point, sortedfinal)
sortedtree.pop(startpoint)
return recursive_sort_uv_tree(startpoint, [])
def lerp(v0: float, v1: float, t: float) -> float:
return v0 + t * (v1 - v0)
target_data = generate_loop_tree(target)
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
logger.debug("Sorted target tree")
for source in sources:
if source == target:
continue
try:
source_data = generate_loop_tree(source)
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
logger.debug(f"Sorted source {source}")
vertex_factor = float(len(sorted_target_tree)-1) / float(len(sorted_source_tree)-1)
logger.debug(f"Vertex factor: {vertex_factor}")
for k, i in enumerate(sorted_source_tree):
try:
#find where we are on the target edges, to interpolate the current point we're placing along the target point's line.
progress_along_edge = float(k) * vertex_factor
previous_vertex_index = math.floor(progress_along_edge)
next_vertex_index = math.ceil(progress_along_edge)
#find the uv coordinates of the previous and next points on the target uv line.
previous_point = [float(x) for x in sorted_target_tree[previous_vertex_index].replace("[","").replace("]","").split()]
next_point = [float(x) for x in sorted_target_tree[next_vertex_index].replace("[","").replace("]","").split()]
#create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with.
progress_between_points = progress_along_edge - int(progress_along_edge)
lerped_point = [
lerp(previous_point[0], next_point[0], progress_between_points),
lerp(previous_point[1], next_point[1], progress_between_points)
]
#grab our uv face corners for each uv coord that we saved.
#Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes.
#basically pretend they are split apart.
uv_face_corners = source_data["selected_loops"][i]
me = bpy.data.objects[source].data
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
uv_lay = me.uv_layers.active
bm.verts.ensure_lookup_table()
for corner in uv_face_corners:
uv_lay.uv[corner].vector = lerped_point
except:
#This is probably fine? - @989onan
#TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan
pass
logger.info(f"Finished mesh {source} for UV's")
except Exception as e:
logger.error(f"Error processing source {source}: {str(e)}")
return {'CANCELLED'}
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
+23
View File
@@ -149,6 +149,19 @@
"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.preserve_parent_bones": "Preserve Parent Bones",
"Tools.preserve_parent_bones_desc": "Keep bones that have children even if they have no weights",
"Tools.target_bone_type": "Target Bone Type",
"Tools.target_bone_type_desc": "Filter which types of bones to process",
"Tools.target_all_bones": "All Bones",
"Tools.target_deform_bones": "Deform Bones Only",
"Tools.target_non_deform_bones": "Non-Deform Bones Only",
"Tools.list_only_mode": "List Mode Only",
"Tools.list_only_mode_desc": "List zero weight bones instead of removing them",
"Tools.zero_weight_bones_found": "Zero weight bones found: {bones}",
"Tools.remove_selected_bones": "Remove Selected Bones",
"Tools.remove_selected_bones_desc": "Remove selected zero weight bones from armature",
"Tools.bones_removed": "Removed {count} bones",
"Tools.clean_constraints": "Delete Bone Constraints",
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
"Tools.clean_constraints_success": "Removed {count} bone constraints",
@@ -187,6 +200,16 @@
"Tools.shapekey_tolerance": "Shape Key Tolerance",
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
"Tools.rigify_title": "Rigify Tools",
"Tools.convert_rigify_to_unity": "Convert Rigify to Unity",
"Tools.convert_rigify_to_unity_desc": "Convert Rigify armature to Unity-compatible format",
"Tools.rigify_converted": "Rigify armature converted successfully",
"Tools.no_armature": "No armature selected",
"UVTools.too_many_vertices": "Error! You have too much stuff selected. Are you sure you're selecting two edges?",
"UVTools.need_line": "You need one line of selected UV points per selected object. Object \"{obj}\" does not meet this requirement!",
"UVTools.align_edges": "Align UV Edges to Target",
"UVTools.align_edges_desc": "Aligns a selected line of UV points on each selected mesh to the line of selected UV points on the active mesh. Useful for kitbashing textures of one model onto another. Uses distance from the 2D cursor to identify the start of the line of UV points on each mesh.",
"MMD.label": "MMD Tools",
"MMD.bone_standardization": "Bone Standardization",
+17
View File
@@ -149,6 +149,19 @@
"Tools.merge_twist_bones_desc": "チェックすると、重みが0でもツイストボーンを保持します",
"Tools.clean_weights": "重みなしボーンを削除",
"Tools.clean_weights_desc": "頂点の重みがないボーンを削除",
"Tools.preserve_parent_bones": "親ボーンを保持",
"Tools.preserve_parent_bones_desc": "ウェイトがなくても子ボーンを持つボーンを保持",
"Tools.target_bone_type": "対象ボーンタイプ",
"Tools.target_bone_type_desc": "処理するボーンタイプを選択",
"Tools.target_all_bones": "全てのボーン",
"Tools.target_deform_bones": "変形ボーンのみ",
"Tools.target_non_deform_bones": "非変形ボーンのみ",
"Tools.list_only_mode": "リストモードのみ",
"Tools.list_only_mode_desc": "ゼロウェイトボーンを削除せずにリスト表示",
"Tools.zero_weight_bones_found": "ゼロウェイトボーンが見つかりました: {bones}",
"Tools.remove_selected_bones": "選択したボーンを削除",
"Tools.remove_selected_bones_desc": "選択したゼロウェイトボーンをアーマチュアから削除",
"Tools.bones_removed": "{count}個のボーンを削除しました",
"Tools.clean_constraints": "ボーンのコンストレイントを削除",
"Tools.clean_constraints_desc": "アーマチュアからすべてのボーンコンストレイントを削除",
"Tools.clean_constraints_success": "{count}個のボーンコンストレイントを削除しました",
@@ -187,6 +200,10 @@
"Tools.shapekey_tolerance": "シェイプキーの許容値",
"Tools.shapekey_tolerance_desc": "シェイプキーを使用済みと判断する最小差分",
"Tools.shapekeys_removed": "{count}個の未使用シェイプキーを削除しました",
"Tools.convert_rigify_to_unity": "RigifyをUnityに変換",
"Tools.convert_rigify_to_unity_desc": "RigifyアーマチュアをUnity互換フォーマットに変換",
"Tools.rigify_converted": "Rigifyアーマチュアの変換が完了しました",
"Tools.no_armature": "アーマチュアが選択されていません",
"MMD.label": "MMDツール",
"MMD.bone_standardization": "ボーン標準化",
+17
View File
@@ -149,6 +149,19 @@
"Tools.merge_twist_bones_desc": "체크하면 가중치가 0이어도 트위스트 본 유지",
"Tools.clean_weights": "0 가중치 본 제거",
"Tools.clean_weights_desc": "버텍스 가중치가 없는 본 제거",
"Tools.preserve_parent_bones": "부모 본 보존",
"Tools.preserve_parent_bones_desc": "가중치가 없어도 자식 본이 있는 본 유지",
"Tools.target_bone_type": "대상 본 유형",
"Tools.target_bone_type_desc": "처리할 본 유형 필터링",
"Tools.target_all_bones": "모든 본",
"Tools.target_deform_bones": "변형 본만",
"Tools.target_non_deform_bones": "비변형 본만",
"Tools.list_only_mode": "목록 모드만",
"Tools.list_only_mode_desc": "제로 가중치 본을 제거하지 않고 목록으로 표시",
"Tools.zero_weight_bones_found": "제로 가중치 본 발견: {bones}",
"Tools.remove_selected_bones": "선택한 본 제거",
"Tools.remove_selected_bones_desc": "선택한 제로 가중치 본을 아마추어에서 제거",
"Tools.bones_removed": "{count}개의 본이 제거되었습니다",
"Tools.clean_constraints": "본 제약 조건 삭제",
"Tools.clean_constraints_desc": "아마추어에서 모든 본 제약 조건 제거",
"Tools.clean_constraints_success": "{count}개의 본 제약 조건 제거됨",
@@ -187,6 +200,10 @@
"Tools.shapekey_tolerance": "쉐이프 키 허용 오차",
"Tools.shapekey_tolerance_desc": "쉐이프 키를 사용된 것으로 간주할 최소 차이",
"Tools.shapekeys_removed": "{count}개의 미사용 쉐이프 키 제거됨",
"Tools.convert_rigify_to_unity": "Rigify를 Unity로 변환",
"Tools.convert_rigify_to_unity_desc": "Rigify 아마추어를 Unity 호환 형식으로 변환",
"Tools.rigify_converted": "Rigify 아마추어 변환 완료",
"Tools.no_armature": "아마추어가 선택되지 않았습니다",
"MMD.label": "MMD 도구",
"MMD.bone_standardization": "본 표준화",
+38 -2
View File
@@ -1,9 +1,21 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from bpy.types import Panel, Context, UILayout, Operator, UIList
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
class AVATAR_TOOLKIT_UL_ZeroWeightBones(UIList):
"""UI List for displaying zero weight bones with selection options"""
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row(align=True)
row.prop(item, "selected", text="")
row.label(text=item.name)
if item.has_children:
row.label(text="", icon='OUTLINER_OB_ARMATURE')
if item.is_deform:
row.label(text="", icon='MOD_ARMATURE')
class AvatarToolKit_PT_ToolsPanel(Panel):
"""Panel containing various tools for avatar customization and optimization"""
bl_label: str = t("Tools.label")
@@ -18,6 +30,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
def draw(self, context: Context) -> None:
"""Draw the tools panel interface"""
layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit
# General Tools
tools_box: UILayout = layout.box()
@@ -45,7 +58,22 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
# 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"))
col.prop(toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
col.prop(toolkit, "preserve_parent_bones")
col.prop(toolkit, "target_bone_type")
col.prop(toolkit, "list_only_mode")
if toolkit.list_only_mode and len(toolkit.zero_weight_bones) > 0:
box = weight_box.box()
row = box.row()
row.template_list("AVATAR_TOOLKIT_UL_ZeroWeightBones", "",
toolkit, "zero_weight_bones",
toolkit, "zero_weight_bones_index")
col = box.column(align=True)
col.operator("avatar_toolkit.remove_selected_bones",
text=t("Tools.remove_selected_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')
@@ -67,3 +95,11 @@ class AvatarToolKit_PT_ToolsPanel(Panel):
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')
# Rigify Tools
rigify_box: UILayout = layout.box()
col = rigify_box.column(align=True)
col.label(text=t("Tools.rigify_title"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
col.operator("avatar_toolkit.convert_rigify_to_unity", icon='ARMATURE_DATA')
col.prop(context.scene.avatar_toolkit, "merge_twist_bones")
+21
View File
@@ -0,0 +1,21 @@
import bpy
from bpy.types import Panel, Context, UILayout
from ..core.translations import t
class AvatarToolKit_PT_UVPanel(Panel):
"""Main UV Tools panel for Avatar Toolkit"""
bl_label = t("AvatarToolkit.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv_main"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
# Add title section
box: UILayout = layout.box()
col: UILayout = box.column(align=True)
row: UILayout = col.row()
row.scale_y = 1.2
row.label(text=t("AvatarToolkit.label"), icon='UV')
+27
View File
@@ -0,0 +1,27 @@
import bpy
from bpy.types import Panel, Context, UILayout
from ..core.translations import t
class AvatarToolKit_PT_UVTools(Panel):
"""UV Tools panel containing UV manipulation operators"""
bl_label = t("Tools.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv_tools"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
bl_parent_id = "OBJECT_PT_avatar_toolkit_uv_main"
bl_order = 3
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
tools_box: UILayout = layout.box()
col: UILayout = tools_box.column(align=True)
col.label(text=t("Tools.uv_title"), icon='UV')
col.separator(factor=0.5)
row: UILayout = col.row(align=True)
row.operator("avatar_toolkit.align_uv_edges_to_target",
text=t("UVTools.align_edges"),
icon='GP_MULTIFRAME_EDITING')