import bpy import numpy as np from bpy.types import Operator, Context from typing import Set, Literal 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'} class AvatarToolkit_OT_ExplodeMesh(Operator): """Explodes the mesh for use with painting programs, or painting inside blender.""" bl_idname = "avatar_toolkit.explode_mesh" bl_label = t("Tools.explode_mesh") bl_description = t("Tools.explode_mesh_desc") bl_options = {'REGISTER', 'UNDO'} distance: bpy.props.FloatProperty(default=2.0,name=t("Tools.explode_mesh.distance"),description=t("Tools.explode_mesh.distance_desc")) split_on_seams: bpy.props.BoolProperty(default=True,name=t("Tools.explode_mesh.split_on_seams"),description=t("Tools.explode_mesh.split_on_seams_desc")) def draw(self, context: Context) -> None: """Draw the operator's UI""" layout = self.layout layout.prop(self, "distance") 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: return context.view_layer.objects.active.type == "MESH" and len(context.view_layer.objects.selected) == 1 def execute(self, context: Context) -> Set[str]: mesh_obj: bpy.types.Object = context.view_layer.objects.active.type mesh: bpy.types.Mesh = context.view_layer.objects.active.data if(self.split_on_seams): #set to correct mode bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='EDGE') #mark seams by islands bpy.ops.mesh.select_all(action="SELECT") bpy.ops.uv.select_all(action="SELECT") bpy.ops.uv.seams_from_islands(mark_seams=True,mark_sharp=False) #clear selection bpy.ops.mesh.select_all(action="DESELECT") bpy.ops.object.mode_set(mode='OBJECT') bm = bmesh.new() # create an empty BMesh bm.from_mesh(mesh) # fill it in from active mesh #select seam edges for idx,edge in enumerate(bm.edges): edge.select = edge.seam bm.to_mesh(mesh) bm.free() #split edges. bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.edge_split() #separate by loose. bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='FACE') bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.separate(type='LOOSE') distance: float = self.distance #set origins to geometry bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY",center="BOUNDS") #store original settings origin_only_orig: bool = context.scene.tool_settings.use_transform_data_origin pos_only_orig: bool = context.scene.tool_settings.use_transform_pivot_point_align parents_only_orig: bool = context.scene.tool_settings.use_transform_skip_children original_pivot: Literal['BOUNDING_BOX_CENTER', 'CURSOR', 'INDIVIDUAL_ORIGINS', 'MEDIAN_POINT', 'ACTIVE_ELEMENT'] = context.scene.tool_settings.transform_pivot_point #set scene settings correctly. context.scene.tool_settings.use_transform_data_origin = False context.scene.tool_settings.use_transform_pivot_point_align = True context.scene.tool_settings.use_transform_skip_children = False context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT' #spread out separated objects bpy.ops.transform.resize(value=(self.distance, self.distance, self.distance), orient_type='GLOBAL') #restore settings. context.scene.tool_settings.use_transform_data_origin = origin_only_orig context.scene.tool_settings.use_transform_pivot_point_align = pos_only_orig context.scene.tool_settings.use_transform_skip_children = parents_only_orig context.scene.tool_settings.transform_pivot_point = original_pivot return {'FINISHED'}