From 71b22813a85d4f1a055e07ae4ff297d810c42199 Mon Sep 17 00:00:00 2001 From: 989onan Date: Wed, 2 Apr 2025 23:18:09 -0400 Subject: [PATCH] added request #112 added request #112 --- functions/tools/general_mesh_tools.py | 101 ++++++++++++++++++++++++++ resources/translations/en_US.json | 3 + ui/tools_panel.py | 8 ++ 3 files changed, 112 insertions(+) create mode 100644 functions/tools/general_mesh_tools.py 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/resources/translations/en_US.json b/resources/translations/en_US.json index fb0cc33..46771cd 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", @@ -216,6 +217,8 @@ "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.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/tools_panel.py b/ui/tools_panel.py index bc9f106..901bc9c 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -18,6 +18,7 @@ from ..functions.tools.bone_tools import ( 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 class AvatarToolKit_PT_ToolsPanel(Panel): """Panel containing various tools for avatar customization and optimization""" @@ -59,6 +60,13 @@ class AvatarToolKit_PT_ToolsPanel(Panel): 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") + # Standardization Tools standardize_box: UILayout = bone_box.box()