diff --git a/.vscode/settings.json b/.vscode/settings.json index 37a3b13..3a2609e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,11 @@ { "python.analysis.extraPaths": [ - "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\scripts\\addons", - "C:\\Users\\Onan\\AppData\\Roaming\\Blender Foundation\\Blender\\4.3\\extensions\\user_default\\",//C:/Users/Onan/AppData/Roaming/Blender Foundation/Blender/4.0/scripts/addons + "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.4\\scripts\\addons", + "C:\\Users\\Onan\\AppData\\Roaming\\Blender Foundation\\Blender\\4.4\\extensions\\user_default\\",//C:/Users/Onan/AppData/Roaming/Blender Foundation/Blender/4.0/scripts/addons "D:\\blender stuff\\blendercodestuff\\4.3", - "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\python\\lib\\site-packages", - "/Users/frankche/Documents/blendercoding/4.1/", - "/Users/frankche/Library/Application Support/Blender/4.3/extensions/user_default/" + "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.4\\python\\lib\\site-packages", + "/Users/frankche/Documents/blendercoding/4.3/", + "/Users/frankche/Library/Application Support/Blender/4.4/extensions/user_default/" ], "python.analysis.diagnosticSeverityOverrides": { "reportInvalidTypeForm": "none" diff --git a/core/common.py b/core/common.py index 4190571..e0093e9 100644 --- a/core/common.py +++ b/core/common.py @@ -15,10 +15,9 @@ from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material, from functools import lru_cache from bpy.props import PointerProperty, IntProperty, StringProperty from bpy.utils import register_class -from ..core.logging_setup import logger -from ..core.translations import t -from ..core.dictionaries import bone_names -from .dictionaries import reverse_bone_lookup, bone_names +from .logging_setup import logger +from .translations import t +from .dictionaries import reverse_bone_lookup, simplify_bonename class SceneMatClass(PropertyGroup): mat: PointerProperty(type=Material) @@ -383,7 +382,7 @@ def clear_unused_data_blocks() -> int: if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection)) return initial_count - final_count -def identify_bones(arm_data: bpy.types.Armature, context: bpy.types.Context) -> Dict[str,str]: +def identify_bones(arm_data: bpy.types.Armature) -> Dict[str,str]: """Identify bone names in an armature based on our reverse dictionary, so there is no confusion to what a bone is. Essentially makes a dictionary of keys from dictionaries.bone_names like "hips", and the corosponding value is the bone that can be mapped to that key.""" returned: Dict[str,str] = {} @@ -495,6 +494,24 @@ def fix_zero_length_bones(armature: Object) -> None: bone.length = 0.001 bpy.ops.object.mode_set(mode='OBJECT') +def remove_unused_vertex_groups(mesh: Object) -> int: + """Remove vertex groups with no weights""" + removed: int = 0 + for vg in mesh.vertex_groups: + has_weights: bool = False + for vert in mesh.data.vertices: + for group in vert.groups: + if group.group == vg.index and group.weight > 0.001: + has_weights = True + break + if has_weights: + break + if not has_weights: + mesh.vertex_groups.remove(vg) + removed = removed+1 + + return removed + def calculate_bone_orientation(mesh: Object, vertices: List[Any]) -> Tuple[Vector, float]: """Calculate optimal bone orientation based on mesh geometry""" if not vertices: @@ -518,6 +535,18 @@ def add_armature_modifier(mesh: Object, armature: Object) -> None: modifier: Modifier = mesh.modifiers.new('Armature', 'ARMATURE') modifier.object = armature +def get_modifiers(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]: + returned: List[Tuple[str, str, str]] = [] + if context.active_object == None: + return returned + if context.active_object.type != "MESH": + return returned + for mod in context.active_object.modifiers: + returned.append((mod.name,mod.name,"")) + + return returned + + def get_shapekeys(context: Context, names: List[str], is_mouth: bool, diff --git a/core/resonite_loader/resonite_animx.py b/core/resonite_loader/resonite_animx.py index ad6c4cf..4d15155 100644 --- a/core/resonite_loader/resonite_animx.py +++ b/core/resonite_loader/resonite_animx.py @@ -3,7 +3,6 @@ from os import replace from re import S from types import FrameType -import lz4.block from . import resonite_types from . import common diff --git a/core/resonite_utils.py b/core/resonite_utils.py index e5a210b..02d56ed 100644 --- a/core/resonite_utils.py +++ b/core/resonite_utils.py @@ -5,10 +5,10 @@ from numpy import double from typing import Set, Dict import re -from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker, identify_bones +from .common import get_active_armature, ProgressTracker, identify_bones from bpy.types import Context, Operator from ..core.translations import t -from ..core.dictionaries import bone_names, resonite_translations +from ..core.dictionaries import bone_names, resonite_translations, simplify_bonename from ..core.logging_setup import logger from ..core.armature_validation import validate_armature @@ -77,7 +77,7 @@ class AvatarToolkit_OT_ConvertResonite(Operator): total_bones = len(arm_data.bones) with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress: - for key_simple,bone_name in identify_bones(arm_data,context).items(): + for key_simple,bone_name in identify_bones(arm_data).items(): if key_simple in resonite_translations: new_name = resonite_translations[key_simple] diff --git a/functions/custom_tools/armature_merging.py b/functions/custom_tools/armature_merging.py index 133fe65..7cd0807 100644 --- a/functions/custom_tools/armature_merging.py +++ b/functions/custom_tools/armature_merging.py @@ -2,15 +2,16 @@ import bpy import numpy as np from typing import List, Optional, Dict, Set, Tuple, Any from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey -from ...core.dictionaries import bone_names from ...core.logging_setup import logger from ...core.translations import t from ...core.common import ( get_all_meshes, fix_zero_length_bones, + remove_unused_vertex_groups, clear_unused_data_blocks, join_mesh_objects, remove_unused_shapekeys, + identify_bones, ) from ...core.dictionaries import simplify_bonename @@ -175,44 +176,22 @@ def merge_armatures( for bone in merge_armature_data.bones: original_parents[bone.name] = bone.parent.name if bone.parent else None - #create reverse lookup - reverse_bone_lookup = {} - for preferred_name, name_list in bone_names.items(): - for name in name_list: - reverse_bone_lookup[name] = preferred_name - - # Get base bone names - base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones} - - base_armature_standards: Dict[str,Optional[str]] = {} - for bone in base_bone_names: - if simplify_bonename(bone) in reverse_bone_lookup: - base_armature_standards[reverse_bone_lookup[simplify_bonename(bone)]] = bone - # Switch to edit mode on merge armature and rename bones bpy.context.view_layer.objects.active = merge_armature bpy.ops.object.mode_set(mode='EDIT') - # Handle bone renaming/removing to target armature. - bone_names_source: list[str] = [bone.name for bone in merge_armature_data.edit_bones] - for bone in bone_names_source: - bone_name = bone - if bone_name not in base_bone_names: #not auto mergable to original - - if simplify_bonename(bone_name) in reverse_bone_lookup: #if is a standard bone through standard translation. - if reverse_bone_lookup[simplify_bonename(bone_name)] in base_armature_standards: #if this bone equals for example, "hips", does a bone that should be "hips" exist on our target armature? - #if so, rename this bone to that one - merge_armature_data.edit_bones[bone_name].name = base_armature_standards[reverse_bone_lookup[simplify_bonename(bone_name)]] - bone_name = merge_armature_data.edit_bones[bone_name].name - #adjust original parents list to point to the new name. - for child_bone in merge_armature_data.edit_bones[bone_name]: - original_parents[child_bone.name] = bone_name - #then remove so it doesn't clash when merged. - merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name]) - continue - - #if it really doesn't have a counter part, just don't bother. - else: + # Identify our bones to what their standard name is like "hips" for source and target armature bones. + identifed_base_bone_names: Dict[str,str] = identify_bones(base_armature.data) + identified_bone_names_source: Dict[str,str] = identify_bones(merge_armature_data) + + for standard,bone_name in identified_bone_names_source.items(): + if standard in identifed_base_bone_names: #if the bone we are at on our merge armature has a standard name translation for the target armature + merge_armature_data.edit_bones[bone_name].name = identifed_base_bone_names[standard] #change it's name to the one on the target merge to armature's coorisponding standard bone + bone_name = merge_armature_data.edit_bones[bone_name].name + #adjust original parents list to point to the new name. + for child_bone in merge_armature_data.edit_bones[bone_name]: + original_parents[child_bone.name] = bone_name + #then remove so it doesn't clash when merged. merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name]) # Return to object mode @@ -399,20 +378,6 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str) -> None: vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE') mesh.vertex_groups.remove(vg_from) -def remove_unused_vertex_groups(mesh: Object) -> None: - """Remove vertex groups with no weights""" - for vg in mesh.vertex_groups: - has_weights: bool = False - for vert in mesh.data.vertices: - for group in vert.groups: - if group.group == vg.index and group.weight > 0.001: - has_weights = True - break - if has_weights: - break - if not has_weights: - mesh.vertex_groups.remove(vg) - def apply_armature_to_mesh(armature: Object, mesh: Object) -> None: """Apply armature deformation to mesh""" armature_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE') diff --git a/functions/custom_tools/force_apply_modifier.py b/functions/custom_tools/force_apply_modifier.py new file mode 100644 index 0000000..2d478ab --- /dev/null +++ b/functions/custom_tools/force_apply_modifier.py @@ -0,0 +1,139 @@ +import traceback +import bpy +import re +from typing import Any, Set, Dict, List, Optional, Tuple +from bpy.types import ( + Operator, + Context, + Object, + Material, + NodeTree, + ShaderNodeTexImage +) +import mathutils +import bmesh +from ...core.logging_setup import logger +from ...core.translations import t +from ...core.common import ( + get_active_armature, + get_all_meshes, + ProgressTracker, + calculate_bone_orientation, + add_armature_modifier, + get_modifiers, + has_shapekeys +) +from ...core.armature_validation import validate_armature + +class AvatarToolkit_OT_ApplyModifierForShapkeyObj(bpy.types.Operator): + """Operator for forcing the application of a modifier. A shortened way of saying \"Apply modifier for object with shapekeys\"""" + bl_idname: str = 'avatar_toolkit.merge_armatures' + bl_label: str = t('Tools.apply_modifier_on_shapekey_obj') + bl_description: str = t('Tools.apply_modifier_on_shapekey_obj_desc') + bl_options: Set[str] = {'REGISTER', 'UNDO'} + + modifier: bpy.props.EnumProperty(items=get_modifiers,name="Modifier To Apply") + + + def draw(self, context: Context) -> None: + """Draw the operator's UI""" + layout = self.layout + layout.prop(self, "modifier") + + def invoke(self, context: Context, event: bpy.types.Event) -> set[str]: + """Initialize the operator""" + return context.window_manager.invoke_props_dialog(self) + + @classmethod + def poll(cls, context: Context) -> bool: + if context.active_object != None: + return context.active_object.type == "MESH" + return False + + def execute(self, context: Context) -> Set[str]: + + obj: bpy.types.Object = context.active_object + mesh: bpy.types.Mesh = obj.data + + shapes: list[bpy.types.Object] = [] + + bpy.ops.object.mode_set(mode="OBJECT") + + if has_shapekeys(obj): + #reset shapekeys + for idx,key in enumerate(mesh.shape_keys.key_blocks): + obj.active_shape_key_index = idx + obj.active_shape_key.value = 0 + + for idx,key in enumerate(mesh.shape_keys.key_blocks): + # duplicate object for shapekey + bpy.ops.object.select_all(action="DESELECT") + context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.duplicate() + + # name new object after shapekey + new_obj = context.view_layer.objects.active + new_obj.select_set(True) + new_obj.active_shape_key_index = idx + new_obj.name = new_obj.active_shape_key.name + + #add to cleanup list + shapes.append(new_obj) + + #make basis the same shape as shapekey + for idx,point in enumerate(new_obj.active_shape_key.points): + new_obj.data.vertices[idx].co.xyz = point.co.xyz + + #remove all shaoekeys on new object and then apply modifier + bpy.ops.object.shape_key_remove(all=True,apply_mix=False) + try: + bpy.ops.object.modifier_apply(modifier=self.modifier) + except Exception as e: + self.report({'ERROR'}, f"Shapekey modifier apply for shapekey \"{new_obj.name}\" failed!!") + print(f"Shapekey modifier apply for shapekey \"{new_obj.name}\" failed!!") + print(traceback.format_exc(e)) + #clean up after critical failure + for shape in shapes: + bpy.data.objects.remove(shape)#faster than ops delete + bpy.ops.object.select_all(action="DESELECT") + + + + try: + #remove shapekeys on original object + bpy.ops.object.select_all(action="DESELECT") + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.shape_key_remove(all=True,apply_mix=False) + bpy.ops.object.modifier_apply(modifier=self.modifier) + bpy.ops.object.select_all(action="DESELECT") + #delete first shapekey object aka basis + bpy.data.objects.remove(shapes.pop(0)) + + #join all objects with applied modifiers back together as shapes + for shape in shapes: + shape.select_set(True) + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.join_shapes() + except Exception as e: + + self.report({'ERROR'}, f"Shapekey joining failed!!") + print(f"Shapekey joining failed!!") + print(traceback.format_exc(e)) + #clean up after critical failure + for shape in shapes: + bpy.data.objects.remove(shape)#faster than ops delete + + #final clean up + for shape in shapes: + bpy.data.objects.remove(shape)#faster than ops delete + + else: + #mesh has no shapekeys, just apply normally. + bpy.ops.object.modifier_apply(modifier=self.modifier) + + + + return {'FINISHED'} \ No newline at end of file diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index 78e6c72..9df48ce 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -7,7 +7,9 @@ from ...core.common import ( get_active_armature, get_all_meshes, ProgressTracker, - restore_bone_transforms + restore_bone_transforms, + remove_unused_vertex_groups, + identify_bones, ) from ...core.armature_validation import validate_armature, validate_bone_hierarchy @@ -262,6 +264,23 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count)) return {'FINISHED'} +class AvatarToolKit_OT_RemoveZeroWeightVertexGroups(Operator): + """Operator to remove vertex groups with no weights""" + bl_idname = "avatar_toolkit.clean_vertex_groups" + bl_label = t("Tools.clean_vertex_groups") + bl_description = t("Tools.clean_vertex_groups_desc") + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context: Context) -> set[str]: + meshes: list[bpy.types.Object] = get_all_meshes(context) + removed: int = 0 + for mesh_obj in meshes: + removed = removed+remove_unused_vertex_groups(mesh_obj) + + self.report({'INFO'}, t("Tools.vertex_groups_removed", count=removed)) + 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" @@ -285,4 +304,111 @@ class AvatarToolKit_OT_RemoveSelectedBones(Operator): toolkit.zero_weight_bones.clear() self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones))) - return {'FINISHED'} \ No newline at end of file + return {'FINISHED'} + + +class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator): + """Operator to flip the selected bone keyframes using blender's flip pose.""" + bl_idname = "avatar_toolkit.flip_pose_frames" + bl_label = t("Tools.flip_pose_frames") + bl_description = t("Tools.flip_pose_frames_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 + if context.mode != 'POSE': + return False + if not armature.animation_data: + return False + valid, _, _ = validate_armature(armature) + return valid + + def execute(self, context: Context) -> set[str]: + armature = get_active_armature(context) + + + + armature_data: bpy.types.Armature = armature.data + + standard_mappings: Dict[str,str] = identify_bones(armature_data) + + + + + # Do we need this? If flipping in the future has issues, then uncommenting this may help - @989onan + #To make sure our flip pose is extremely reliable, we're gonna temp rename all bones to standard names to make the posing work. + #for standard,bone_name in standard_mappings.items(): + # armature_data.bones[bone_name].name = standard + + #save our selection + selected: list[bool] = [False] * len(armature_data.bones) + armature_data.bones.foreach_get("select", selected) + #select everything + armature_data.bones.foreach_set("select", [False] * len(armature_data.bones)) + + + + #create a set for every frame time where we need to key a keyframe for the flipped pose + times: Dict[float,list[bpy.types.FCurve]] = {} + for curve in armature.animation_data.action.fcurves: + if not curve.data_path.startswith("pose"): + continue + for point in curve.keyframe_points: + if point.select_control_point: + if point.co.x not in times: + times[point.co.x] = [] + + times[point.co.x].append(curve) + + for time,curves in times.items(): + context.scene.frame_set(frame=int(time), subframe=float(time-float(int(time)))) + armature_data.bones.foreach_set("select", [True] * len(armature_data.bones)) + bpy.ops.pose.copy() + armature_data.bones.foreach_set("select", [False] * len(armature_data.bones)) + bpy.ops.pose.paste(flipped=True,selected_mask=False) + + + + + for curve in curves: + + bone_name: str = curve.data_path.replace("pose.bones[\"","") + bone_name = bone_name[:bone_name.index("\"")] + + armature_data.bones[bone_name].select = True + + bpy.ops.pose.select_mirror(extend=False) + + #this can get the opposite side bone's data path and key it, if it is ever needed - @989onan + #for bone in armature_data.bones: + # if bone.select == True: + # bone_name = bone.name + # break + #new_path = curve.data_path[:curve.data_path.index("[")+1]+"\""+bone_name+"\""+curve.data_path[curve.data_path.index("]"):] + + if armature.keyframe_insert(data_path=curve.data_path, index=curve.array_index, frame=time): + #if armature.keyframe_insert(data_path=new_path, index=curve.array_index, frame=time): + continue + self.report({'ERROR'}, f"Keyframe insertion for key with data path \"{curve.data_path}\" and frame {time} failed!") + return {'FINISHED'} + + + + + + + + + + # Do we need this? If flipping in the future has issues, then uncommenting this may help - @989onan + #bring our names back as to not break their model. + #for standard,bone_name in standard_mappings.items(): + # armature_data.bones[standard].name = bone_name + + # restore selection + armature_data.bones.foreach_set("select", selected) + return {'FINISHED'} diff --git a/functions/tools/general_mesh_tools.py b/functions/tools/general_mesh_tools.py new file mode 100644 index 0000000..0ac6d3c --- /dev/null +++ b/functions/tools/general_mesh_tools.py @@ -0,0 +1,101 @@ +import bpy +import numpy as np +from bpy.types import Operator, Context +from typing import Set +from ...core.translations import t +from ...core.logging_setup import logger +from ...core.common import get_active_armature, get_all_meshes +from ...core.armature_validation import validate_armature + +import bmesh + + +class MapItem(): + length: int + current_node: bmesh.types.BMVert + marched_paths: list[bmesh.types.BMEdge] + +class AvatarToolkit_OT_SelectShortestSeamPath(Operator): + """Find the shortest seam path between two vertices.""" + bl_idname = "avatar_toolkit.find_shortest_seam_path" + bl_label = t("Tools.find_shortest_seam_path") + bl_description = t("Tools.find_shortest_seam_path_desc") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + if context.mode != "EDIT_MESH": + return False + mesh_data: bpy.types.Mesh = context.active_object.data + mesh = bmesh.from_edit_mesh(mesh_data) + selected: int = 0 + for vert in mesh.verts: + if vert.select == True: + selected = selected+1 + if selected > 2: + return False + found_seam: bool = False + for edge in vert.link_edges: + if edge.seam: + found_seam = True + if not found_seam: + return False + if selected < 2: + return False + armature = get_active_armature(context) + if not armature: + return False + valid, _, _ = validate_armature(armature) + return valid + + def execute(self, context: Context) -> Set[str]: + mesh_data: bpy.types.Mesh = context.active_object.data + mesh = bmesh.from_edit_mesh(mesh_data) + vert1: bmesh.types.BMVert = None + vert2: bmesh.types.BMVert = None + for vert in mesh.verts: + if vert.select == True: + if vert1 == None: + vert1 = vert + else: + vert2 = vert + + current_verts: list[MapItem] = [] + + first_item: MapItem = MapItem() + first_item.current_node = vert1 + first_item.length = 0 + first_item.marched_paths = [] + current_verts.append(first_item) + + def find_next_edge() -> list[bmesh.types.BMEdge]: + if len(current_verts) == 0: #all paths have been exausted. + return [] + for mapeditem in current_verts: + current_verts.remove(mapeditem) + for edge in mapeditem.current_node.link_edges: + if edge.seam and (edge not in mapeditem.marched_paths): + for vert_new in edge.verts: + if vert_new != mapeditem.current_node: + if vert_new == vert2: + mapeditem.marched_paths.append(edge) + return mapeditem.marched_paths + first_item: MapItem = MapItem() + first_item.current_node = vert_new + first_item.length = mapeditem.length+1 + first_item.marched_paths = [] + first_item.marched_paths.extend(mapeditem.marched_paths) + first_item.marched_paths.append(edge) + current_verts.append(first_item) + return find_next_edge() + + mesh.select_flush(False) + path: list[bmesh.types.BMEdge] = find_next_edge() + for edge in path: + edge.select = True + for vert in edge.verts: + vert.select = True + bpy.ops.mesh.select_mode(type='EDGE') + + return {'FINISHED'} + diff --git a/functions/tools/uv_tools.py b/functions/tools/uv_tools.py index e7dcc5d..6002d73 100644 --- a/functions/tools/uv_tools.py +++ b/functions/tools/uv_tools.py @@ -31,6 +31,8 @@ class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator): return False if not context.space_data: return False + if not hasattr(context.space_data, "show_uvedit"): + return False if not context.space_data.show_uvedit: return False if context.scene.tool_settings.use_uv_select_sync: diff --git a/functions/visemes.py b/functions/visemes.py index da332bc..889306a 100644 --- a/functions/visemes.py +++ b/functions/visemes.py @@ -124,7 +124,7 @@ class VisemePreview: cls._preview_shapes = None cls._mesh_name = "" -class ATOOLKIT_OT_preview_visemes(Operator): +class AvatarToolkit_OT_PreviewVisemes(Operator): """Operator for previewing viseme shapes in real-time""" bl_idname: str = "avatar_toolkit.preview_visemes" bl_label: str = t("Visemes.preview_label") @@ -181,7 +181,7 @@ def validate_deformation(mesh, mix_data): mesh_size = max(mesh.dimensions) return max_deform < (mesh_size * 0.4) -class ATOOLKIT_OT_create_visemes(Operator): +class AvatarToolkit_OT_CreateVisemes(Operator): """Operator for generating VRChat-compatible viseme shape keys""" bl_idname: str = "avatar_toolkit.create_visemes" bl_label: str = t("Visemes.create_label") diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 39b1183..2c642d6 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -167,6 +167,7 @@ "Optimization.remove_doubles_completed": "Remove doubles completed successfully", "Tools.label": "Tools", + "Tools.mesh_title": "Mesh Tools", "Tools.general_title": "General Tools", "Tools.select_armature": "Select an Armature", "Tools.convert_resonite": "Convert to Resonite", @@ -191,6 +192,8 @@ "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_vertex_groups": "Remove Unused Vertex Groups", + "Tools.clean_vertex_groups_desc": "Remove vertex groups on meshes assigned to no vertices.", "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", @@ -203,7 +206,10 @@ "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.flip_pose_frames": "Flip Selected Armature Key Frames", + "Tools.flip_pose_frames_desc": "Takes the selected keyframes and sets them to a mirrored pose, gotten from the opposite side of the armature on that frame.\nSelecting the entire animation's keyframes will flip the entire animation.", "Tools.bones_removed": "Removed {count} bones", + "Tools.vertex_groups_removed": "Removed {count} vertex groups.", "Tools.clean_constraints": "Delete Bone Constraints", "Tools.clean_constraints_desc": "Remove all bone constraints from armature", "Tools.clean_constraints_success": "Removed {count} bone constraints", @@ -211,6 +217,10 @@ "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.find_shortest_seam_path": "Find Shortest Seam Path", + "Tools.find_shortest_seam_path_desc": "Find shortest path of seams between two selected vertices connected to seams.", + "Tools.apply_modifier_on_shapekey_obj":"Apply Modifier on Shapekey Object", + "Tools.apply_modifier_on_shapekey_obj_desc":"Applies a modifier on an object regardless of it having shapekeys.", "Tools.merge_title": "Merge Tools", "Tools.merge_to_active": "Merge to Active", "Tools.merge_to_active_desc": "Merge selected bones to active bone", diff --git a/ui/atlas_materials_panel.py b/ui/atlas_materials_panel.py index 8f8c056..35e6316 100644 --- a/ui/atlas_materials_panel.py +++ b/ui/atlas_materials_panel.py @@ -97,14 +97,14 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList): row = layout.row(align=True) row.scale_y = 1.2 - row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT', + row.operator(AvatarToolKit_OT_SelectAllMaterials.bl_idname, text="", icon='CHECKBOX_HLT', emboss=True).tooltip = t("TextureAtlas.select_all_tooltip") - row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT', + row.operator(AvatarToolKit_OT_SelectNoneMaterials.bl_idname, text="", icon='CHECKBOX_DEHLT', emboss=True).tooltip = t("TextureAtlas.select_none_tooltip") row.separator(factor=0.5) - row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN', + row.operator(AvatarToolKit_OT_ExpandAllMaterials.bl_idname, text="", icon='DISCLOSURE_TRI_DOWN', emboss=True).tooltip = t("TextureAtlas.expand_all_tooltip") - row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT', + row.operator(AvatarToolKit_OT_CollapseAllMaterials.bl_idname, text="", icon='DISCLOSURE_TRI_RIGHT', emboss=True).tooltip = t("TextureAtlas.collapse_all_tooltip") row.separator(factor=1.0) diff --git a/ui/custom_avatar_panel.py b/ui/custom_avatar_panel.py index 3ebf160..451c88d 100644 --- a/ui/custom_avatar_panel.py +++ b/ui/custom_avatar_panel.py @@ -175,12 +175,12 @@ class AvatarToolKit_PT_CustomPanel(Panel): # Armature selection with better alignment row: UILayout = col.row(align=True) row.label(text=t('MergeArmature.into'), icon='ARMATURE_DATA') - row.operator("avatar_toolkit.search_merge_armature_into", + row.operator(AvatarToolkit_OT_SearchMergeArmatureInto.bl_idname, text=toolkit.merge_armature_into) row: UILayout = col.row(align=True) row.label(text=t('MergeArmature.from'), icon='ARMATURE_DATA') - row.operator("avatar_toolkit.search_merge_armature", + row.operator(AvatarToolkit_OT_SearchMergeArmature.bl_idname, text=toolkit.merge_armature) # Merge button with emphasis @@ -188,7 +188,7 @@ class AvatarToolKit_PT_CustomPanel(Panel): col: UILayout = merge_box.column(align=True) row: UILayout = col.row(align=True) row.scale_y = 1.5 - row.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA') + row.operator(AvatarToolkit_OT_MergeArmature.bl_idname, icon='ARMATURE_DATA') def draw_mesh_tools(self, layout: UILayout, context: Context) -> None: """Draw the mesh attachment tools section""" @@ -213,17 +213,17 @@ class AvatarToolKit_PT_CustomPanel(Panel): # Selection rows with icons and better alignment row: UILayout = col.row(align=True) row.label(text=t('CustomPanel.select_armature'), icon='ARMATURE_DATA') - row.operator("avatar_toolkit.search_merge_armature_into", + row.operator(AvatarToolkit_OT_SearchMergeArmatureInto.bl_idname, text=toolkit.merge_armature_into) row: UILayout = col.row(align=True) row.label(text=t('CustomPanel.select_mesh'), icon='MESH_DATA') - row.operator("avatar_toolkit.search_attach_mesh", + row.operator(AvatarToolkit_OT_SearchAttachMesh.bl_idname, text=toolkit.attach_mesh) row: UILayout = col.row(align=True) row.label(text=t('CustomPanel.select_bone'), icon='BONE_DATA') - row.operator("avatar_toolkit.search_attach_bone", + row.operator(AvatarToolkit_OT_SearchAttachBone.bl_idname, text=toolkit.attach_bone) # Attach button with emphasis @@ -231,4 +231,4 @@ class AvatarToolKit_PT_CustomPanel(Panel): col: UILayout = attach_box.column(align=True) row: UILayout = col.row(align=True) row.scale_y = 1.5 - row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA') + row.operator(AvatarToolkit_OT_AttachMesh.bl_idname, icon='ARMATURE_DATA') diff --git a/ui/optimization_panel.py b/ui/optimization_panel.py index 57e9802..04eb8dc 100644 --- a/ui/optimization_panel.py +++ b/ui/optimization_panel.py @@ -3,6 +3,9 @@ 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 +from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials +from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles,AvatarToolkit_OT_RemoveDoublesAdvanced +from ..functions.optimization.mesh_tools import AvatarToolkit_OT_JoinAllMeshes, AvatarToolkit_OT_JoinSelectedMeshes class AvatarToolKit_PT_OptimizationPanel(Panel): """Panel containing mesh and material optimization tools for avatar optimization""" @@ -26,7 +29,7 @@ class AvatarToolKit_PT_OptimizationPanel(Panel): col.separator(factor=0.5) # Material Operations - col.operator("avatar_toolkit.combine_materials", icon='MATERIAL') + col.operator(AvatarToolkit_OT_CombineMaterials.bl_idname, icon='MATERIAL') # Mesh Cleanup Box cleanup_box: UILayout = layout.box() @@ -36,8 +39,8 @@ class AvatarToolKit_PT_OptimizationPanel(Panel): # Remove Doubles Row row: UILayout = col.row(align=True) - row.operator("avatar_toolkit.remove_doubles", icon='MESH_DATA') - row.operator("avatar_toolkit.remove_doubles_advanced", icon='PREFERENCES') + row.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA') + row.operator(AvatarToolkit_OT_RemoveDoublesAdvanced.bl_idname, icon='PREFERENCES') # Join Meshes Box join_box: UILayout = layout.box() @@ -47,5 +50,5 @@ class AvatarToolKit_PT_OptimizationPanel(Panel): # Join Meshes Row row: UILayout = col.row(align=True) - row.operator("avatar_toolkit.join_all_meshes", icon='OBJECT_DATA') - row.operator("avatar_toolkit.join_selected_meshes", icon='RESTRICT_SELECT_OFF') + row.operator(AvatarToolkit_OT_JoinAllMeshes.bl_idname, icon='OBJECT_DATA') + row.operator(AvatarToolkit_OT_JoinSelectedMeshes.bl_idname, icon='RESTRICT_SELECT_OFF') diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index 59fbd6d..d0d6755 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -23,7 +23,10 @@ from ..functions.pose_mode import ( AvatarToolkit_OT_ApplyPoseAsShapekey, AvatarToolkit_OT_ApplyPoseAsRest ) -from ..core.armature_validation import validate_armature +from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose +from ..core.importers.importer import AvatarToolKit_OT_Import +from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite +from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature class AvatarToolKit_OT_ExportFBX(Operator): """Export selected objects as FBX""" @@ -41,8 +44,8 @@ class AvatarToolKit_MT_ExportMenu(Menu): def draw(self, context: Context) -> None: layout: UILayout = self.layout - layout.operator("avatar_toolkit.export_fbx", text=t("QuickAccess.export_fbx")) - layout.operator("avatar_toolkit.export_resonite", text=t("QuickAccess.export_resonite")) + layout.operator(AvatarToolKit_OT_ExportFBX.bl_idname, text=t("QuickAccess.export_fbx")) + layout.operator(AvatarToolKit_OT_ExportResonite.bl_idname, text=t("QuickAccess.export_resonite")) class AvatarToolKit_OT_ExportMenu(Operator): """Open the export menu""" @@ -170,7 +173,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): col = pose_box.column(align=True) col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA') col.separator(factor=0.5) - col.operator("avatar_toolkit.validate_tpose", icon='CHECKMARK') + col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, icon='CHECKMARK') if props.show_tpose_validation: validation_box = col.box() @@ -207,7 +210,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): # Add standardize button standardize_box = info_box.box() - standardize_box.operator("avatar_toolkit.standardize_armature", + standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, text=t("QuickAccess.standardize_armature"), icon='MODIFIER') @@ -247,5 +250,5 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): # Import/Export Buttons button_row: UILayout = col.row(align=True) button_row.scale_y = 1.5 - button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT') - button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT') + button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT') + button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT') diff --git a/ui/settings_panel.py b/ui/settings_panel.py index 6e7c322..0036ef6 100644 --- a/ui/settings_panel.py +++ b/ui/settings_panel.py @@ -10,6 +10,7 @@ from bpy.types import ( ) from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.translations import t, get_languages_list +from ..core.armature_validation import AvatarToolkit_OT_HighlightProblemBones, AvatarToolkit_OT_ClearBoneHighlighting class AvatarToolkit_OT_TranslationRestartPopup(Operator): """Popup dialog shown after language change to inform about restart requirement""" @@ -71,9 +72,9 @@ class AvatarToolKit_PT_SettingsPanel(Panel): col.separator() col.prop(props, "highlight_problem_bones") if props.highlight_problem_bones: - col.operator("avatar_toolkit.highlight_problem_bones", icon='COLOR') + col.operator(AvatarToolkit_OT_HighlightProblemBones.bl_idname, icon='COLOR') else: - col.operator("avatar_toolkit.clear_bone_highlighting", icon='X') + col.operator(AvatarToolkit_OT_ClearBoneHighlighting.bl_idname, icon='X') # Debug Settings debug_box = layout.box() diff --git a/ui/tools_panel.py b/ui/tools_panel.py index 7fbfe71..b8aa933 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -4,17 +4,22 @@ 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') +from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite +from ..functions.tools.mesh_separation import AvatarToolKit_OT_SeparateByLooseParts, AvatarToolKit_OT_SeparateByMaterials +from ..functions.tools.additional_tools import AvatarToolkit_OT_ApplyTransforms, AvatarToolkit_OT_CleanShapekeys +from ..functions.tools.bone_tools import ( + AvatarToolKit_OT_CreateDigitigradeLegs, + AvatarToolKit_OT_DeleteBoneConstraints, + AvatarToolKit_OT_RemoveSelectedBones, + AvatarToolKit_OT_RemoveZeroWeightBones, + AvatarToolKit_OT_RemoveZeroWeightVertexGroups, + AvatarToolKit_OT_FlipCurrentKeyFrames +) +from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature +from ..functions.tools.merge_tools import AvatarToolkit_OT_MergeToActive, AvatarToolkit_OT_MergeToParent, AvatarToolkit_OT_ConnectBones +from ..functions.tools.rigify_converter import AvatarToolkit_OT_ConvertRigifyToUnity +from ..functions.tools.general_mesh_tools import AvatarToolkit_OT_SelectShortestSeamPath +from ..functions.custom_tools.force_apply_modifier import AvatarToolkit_OT_ApplyModifierForShapkeyObj class AvatarToolKit_PT_ToolsPanel(Panel): """Panel containing various tools for avatar customization and optimization""" @@ -37,7 +42,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel): 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') + col.operator(AvatarToolkit_OT_ConvertResonite.bl_idname, text=t("Tools.convert_resonite"), icon='EXPORT') # Separation Tools sep_box: UILayout = layout.box() @@ -45,22 +50,32 @@ class AvatarToolKit_PT_ToolsPanel(Panel): 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') + row.operator(AvatarToolKit_OT_SeparateByMaterials.bl_idname, text=t("Tools.separate_materials"), icon='MATERIAL') + row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, 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') + col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), icon='BONE_DATA') + col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname,text=t("Tools.flip_pose_frames"),icon="ACTION") + + # Mesh Tools + mesh_box: UILayout = layout.box() + col = mesh_box.column(align=True) + col.label(text=t("Tools.mesh_title"), icon='MESH_DATA') + col.separator(factor=0.5) + col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname,text=t("Tools.find_shortest_seam_path"),icon="MESH_DATA") + col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname,text=t("Tools.apply_modifier_on_shapekey_obj"),icon="SHAPEKEY_DATA") + # Standardization Tools standardize_box: UILayout = bone_box.box() col = standardize_box.column(align=True) col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE') col.separator(factor=0.5) - col.operator("avatar_toolkit.standardize_armature", icon='CHECKMARK') + col.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname, icon='CHECKMARK') # Weight Tools weight_box: UILayout = bone_box.box() @@ -78,12 +93,14 @@ class AvatarToolKit_PT_ToolsPanel(Panel): toolkit, "zero_weight_bones_index") col = box.column(align=True) - col.operator("avatar_toolkit.remove_selected_bones", + col.operator(AvatarToolKit_OT_RemoveSelectedBones.bl_idname, 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') + row.operator(AvatarToolKit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.clean_weights"), icon='GROUP_BONE') + row.operator(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE') + row = col.row(align=True) + row.operator(AvatarToolKit_OT_RemoveZeroWeightVertexGroups.bl_idname, text=t("Tools.clean_vertex_groups"), icon='CONSTRAINT_BONE') # Merge Tools merge_box: UILayout = layout.box() @@ -91,22 +108,35 @@ class AvatarToolKit_PT_ToolsPanel(Panel): 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') + row.operator(AvatarToolkit_OT_MergeToActive.bl_idname, text=t("Tools.merge_to_active"), icon='BONE_DATA') + row.operator(AvatarToolkit_OT_MergeToParent.bl_idname, text=t("Tools.merge_to_parent"), icon='BONE_DATA') + col.operator(AvatarToolkit_OT_ConnectBones.bl_idname, 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') + col.operator(AvatarToolkit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms"), icon='OBJECT_DATA') + col.operator(AvatarToolkit_OT_CleanShapekeys.bl_idname, 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.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA') col.prop(context.scene.avatar_toolkit, "merge_twist_bones") + + +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') \ No newline at end of file diff --git a/ui/uv_panel.py b/ui/uv_panel.py index d5499b6..b7d5596 100644 --- a/ui/uv_panel.py +++ b/ui/uv_panel.py @@ -1,6 +1,7 @@ import bpy from bpy.types import Panel, Context, UILayout from ..core.translations import t +from .main_panel import CATEGORY_NAME class AvatarToolKit_PT_UVPanel(Panel): """Main UV Tools panel for Avatar Toolkit""" @@ -8,7 +9,7 @@ class AvatarToolKit_PT_UVPanel(Panel): bl_idname = "OBJECT_PT_avatar_toolkit_uv_main" bl_space_type = 'IMAGE_EDITOR' bl_region_type = 'UI' - bl_category = "Avatar Toolkit" + bl_category = CATEGORY_NAME def draw(self, context: Context) -> None: layout: UILayout = self.layout diff --git a/ui/uv_tools.py b/ui/uv_tools.py index ecb258e..d72e303 100644 --- a/ui/uv_tools.py +++ b/ui/uv_tools.py @@ -1,6 +1,8 @@ import bpy from bpy.types import Panel, Context, UILayout from ..core.translations import t +from ..functions.tools.uv_tools import AvatarToolkit_OT_AlignUVEdgesToTarget +from .uv_panel import AvatarToolKit_PT_UVPanel class AvatarToolKit_PT_UVTools(Panel): """UV Tools panel containing UV manipulation operators""" @@ -8,9 +10,9 @@ class AvatarToolKit_PT_UVTools(Panel): 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_category = "UV Tools" + bl_parent_id = AvatarToolKit_PT_UVPanel.bl_idname + bl_order = 0 bl_options = {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: @@ -22,6 +24,6 @@ class AvatarToolKit_PT_UVTools(Panel): col.separator(factor=0.5) row: UILayout = col.row(align=True) - row.operator("avatar_toolkit.align_uv_edges_to_target", + row.operator(AvatarToolkit_OT_AlignUVEdgesToTarget.bl_idname, text=t("UVTools.align_edges"), icon='GP_MULTIFRAME_EDITING') diff --git a/ui/visemes_panel.py b/ui/visemes_panel.py index 0ebb472..11d2a30 100644 --- a/ui/visemes_panel.py +++ b/ui/visemes_panel.py @@ -3,6 +3,7 @@ from bpy.types import Panel, Context, UILayout, Object, ShapeKey from ..core.translations import t from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.common import get_active_armature +from ..functions.visemes import AvatarToolkit_OT_PreviewVisemes, AvatarToolkit_OT_CreateVisemes class AvatarToolKit_PT_VisemesPanel(Panel): """Panel containing viseme creation and preview tools""" @@ -65,11 +66,11 @@ class AvatarToolKit_PT_VisemesPanel(Panel): col.separator() preview_text: str = t("Visemes.stop_preview") if props.viseme_preview_mode else t("Visemes.start_preview") - col.operator("avatar_toolkit.preview_visemes", text=preview_text, icon='HIDE_OFF') + col.operator(AvatarToolkit_OT_PreviewVisemes.bl_idname, text=preview_text, icon='HIDE_OFF') # Create Box create_box: UILayout = layout.box() col: UILayout = create_box.column(align=True) col.label(text=t("Visemes.create_label"), icon='ADD') col.separator(factor=0.5) - col.operator("avatar_toolkit.create_visemes", icon='ADD') + col.operator(AvatarToolkit_OT_CreateVisemes.bl_idname, icon='ADD')