import bpy import os import bmesh import math import operator from mathutils import Vector from collections import defaultdict from itertools import chain # 'flattens' collection of iterables from . import utilities_uv class op(bpy.types.Operator): bl_idname = "uv.textools_island_mirror" bl_label = "Symmetry" bl_description = "Mirrors selected faces to other half or averages based on selected edge center" bl_options = {'REGISTER', 'UNDO'} is_stack : bpy.props.BoolProperty(description="Stack the halves on top of each other?", default=False) @classmethod def poll(cls, context): if not bpy.context.active_object: return False #Only in Edit mode if bpy.context.active_object.mode != 'EDIT': return False #Only in UV editor mode if bpy.context.area.type != 'IMAGE_EDITOR': return False #Requires UV map if not bpy.context.object.data.uv_layers: return False if bpy.context.scene.tool_settings.use_uv_select_sync: return False if bpy.context.scene.tool_settings.uv_select_mode != 'EDGE' and bpy.context.scene.tool_settings.uv_select_mode != 'FACE': return False # if bpy.context.scene.tool_settings.use_uv_select_sync: # return False return True def execute(self, context): main(context) return {'FINISHED'} def main(context): print("--------------------------- Executing operator_mirror") #Store selection utilities_uv.selection_store() bm = bmesh.from_edit_mesh(bpy.context.active_object.data) uv_layers = bm.loops.layers.uv.verify() if bpy.context.scene.tool_settings.uv_select_mode == 'EDGE': # 1.) Collect left and right side verts verts_middle = []; for face in bm.faces: if face.select: for loop in face.loops: if loop[uv_layers].select and loop.vert not in verts_middle: verts_middle.append(loop.vert) # 2.) Align UV shell alignToCenterLine() # Convert to Vert selection and extend edge loop in 3D space bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') bpy.ops.mesh.select_all(action='DESELECT') for vert in verts_middle: vert.select = True bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='EDGE') bpy.ops.mesh.loop_multi_select(ring=False) for vert in bm.verts: if vert.select and vert not in verts_middle: print("Append extra vert to symmetry line from xyz edge loop") verts_middle.append(vert) # Select UV shell Again bpy.ops.mesh.select_linked(delimit={'UV'}) verts_island = [] for vert in bm.verts: if vert.select: verts_island.append(vert) # 3.) Restore UV vert selection x_middle = 0 bpy.ops.uv.select_all(action='DESELECT') for face in bm.faces: if face.select: for loop in face.loops: if loop.vert in verts_middle: loop[uv_layers].select = True x_middle = loop[uv_layers].uv.x; print("Middle "+str(len(verts_middle))+"x, x pos: "+str(x_middle)) # Extend selection bpy.ops.uv.select_more() verts_A = []; verts_B = []; for face in bm.faces: if face.select: for loop in face.loops: if loop[uv_layers].select and loop.vert not in verts_middle: if loop[uv_layers].uv.x <= x_middle: # Left if loop.vert not in verts_A: verts_A.append(loop.vert) elif loop[uv_layers].uv.x > x_middle: # Right if loop.vert not in verts_B: verts_B.append(loop.vert) def remove_doubles(): verts_double = [vert for vert in verts_island if (vert in verts_A and vert in verts_B)] # print("Temp double: "+str(len(verts_double))+"x") if len(verts_double) > 0: print("TODO: Remove doubles "+str(len(verts_double))) for vert in verts_double: verts_A.remove(vert) verts_B.remove(vert) verts_middle.append(vert) def extend_half_selection(verts_middle, verts_half, verts_other): # Select initial half selection bpy.ops.uv.select_all(action='DESELECT') for face in bm.faces: if face.select: for loop in face.loops: if loop.vert in verts_half: loop[uv_layers].select = True # Extend selection bpy.ops.uv.select_more() # count_added = 0 for face in bm.faces: if face.select: for loop in face.loops: if loop.vert not in verts_half and loop.vert not in verts_middle and loop[uv_layers].select: verts_half.append(loop.vert) remove_doubles() # Limit iteration loops max_loops_extend = 200 for i in range(0, max_loops_extend): print("Now extend selection A / B") count_hash = str(len(verts_A))+"_"+str(len(verts_B)); extend_half_selection(verts_middle, verts_A, verts_B) extend_half_selection(verts_middle, verts_B, verts_A) remove_doubles() count_hash_new = str(len(verts_A))+"_"+str(len(verts_B)); if count_hash_new == count_hash: print("Break loop, same as previous loop") break; print("Edge, Sides: L:"+str(len(verts_A))+" | R:"+str(len(verts_B))) # 4.) Mirror Verts mirror_verts(verts_middle, verts_A, verts_B, False) if bpy.context.scene.tool_settings.uv_select_mode == 'FACE': # 1.) Get selected UV faces to vert faces selected_faces = [] for face in bm.faces: if face.select: # Are all UV faces selected? countSelected = 0 for loop in face.loops: if loop[uv_layers].select: countSelected+=1 # print("Vert selected "+str(face.index)) if countSelected == len(face.loops): selected_faces.append(face) # if bpy.context.scene.tool_settings.use_uv_select_sync == False: bpy.ops.uv.select_linked() verts_all = [] for face in bm.faces: if face.select: for loop in face.loops: if(loop.vert not in verts_all): verts_all.append(loop.vert) print("Verts shell: "+str(len(verts_all))) bpy.ops.mesh.select_all(action='DESELECT') for face in selected_faces: face.select = True # 2.) Select Vert shell's outer edges bpy.ops.mesh.select_linked(delimit=set()) bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') bpy.ops.mesh.region_to_loop() edges_outer_shell = [e for e in bm.edges if e.select] # 3.) Select Half's outer edges bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') bpy.ops.mesh.select_all(action='DESELECT') for face in selected_faces: face.select = True bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') bpy.ops.mesh.region_to_loop() edges_outer_selected = [e for e in bm.edges if e.select] # 4.) Mask edges exclusive to edges_outer_selected (symmetry line) edges_middle = [item for item in edges_outer_selected if item not in edges_outer_shell] # 5.) Convert to UV selection verts_middle = [] for edge in edges_middle: if edge.verts[0] not in verts_middle: verts_middle.append(edge.verts[0]) if edge.verts[1] not in verts_middle: verts_middle.append(edge.verts[1]) #Select all Vert shell faces bpy.ops.mesh.select_linked(delimit=set()) #Select UV matching vert array bpy.ops.uv.select_all(action='DESELECT') for face in bm.faces: if face.select: for loop in face.loops: if loop.vert in verts_middle: loop[uv_layers].select = True # 5.) Align UV shell alignToCenterLine() # 7.) Collect left and right side verts verts_A = []; verts_B = []; bpy.ops.uv.select_all(action='DESELECT') for face in selected_faces: for loop in face.loops: if loop.vert not in verts_middle and loop.vert not in verts_A: verts_A.append(loop.vert) for vert in verts_all: if vert not in verts_middle and vert not in verts_A and vert not in verts_B: verts_B.append(vert) # 8.) Mirror Verts mirror_verts(verts_middle, verts_A, verts_B, True) #Restore selection # utilities_uv.selection_restore() def mirror_verts(verts_middle, verts_A, verts_B, isAToB): print("--------------------------------\nMirror: C:"+str(len(verts_middle))+" ; verts: "+str(len(verts_A))+"|"+str(len(verts_B))+"x, A to B? "+str(isAToB)) bm = bmesh.from_edit_mesh(bpy.context.active_object.data) uv_layers = bm.loops.layers.uv.verify() # Get verts_island verts_island = [] for vert in verts_middle: verts_island.append(vert) for vert in verts_A: verts_island.append(vert) for vert in verts_B: verts_island.append(vert) # Select Island as Faces bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') bpy.ops.mesh.select_all(action='DESELECT') for vert in verts_island: vert.select = True bpy.ops.mesh.select_mode(use_extend=False, use_expand=True, type='FACE') # Collect Librarys of verts / UV vert_to_uv = utilities_uv.get_vert_to_uv(bm, uv_layers) uv_to_vert = utilities_uv.get_uv_to_vert(bm, uv_layers) uv_to_face = {} # UV clusters / groups (within 0.000001 distance) clusters = [] uv_to_clusters = {} vert_to_clusters = {} for face in bm.faces: if face.select: for loop in face.loops: vert = loop.vert uv = loop[uv_layers] if uv not in uv_to_face: uv_to_face[ uv ] = face; # clusters isMerged = False for cluster in clusters: d = (uv.uv - cluster.uvs[0].uv).length if d <= 0.0000001: #Merge cluster.append(uv) uv_to_clusters[uv] = cluster if vert not in vert_to_clusters: vert_to_clusters[vert] = cluster isMerged = True; break; if not isMerged: #New Group clusters.append( UVCluster(vert, [uv]) ) uv_to_clusters[uv] = clusters[-1] if vert not in vert_to_clusters: vert_to_clusters[vert] = clusters[-1] # Get Center X x_middle = vert_to_uv[ verts_middle[0] ][0].uv.x; # 3.) Grow layer by layer bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') bpy.context.scene.tool_settings.uv_select_mode = 'VERTEX' clusters_processed = [] def select_extend_filter(clusters_border, clusters_mask): # print("Extend A/B") connected_clusters = [] for cluster in clusters_border: # Select and Extend selection bpy.ops.uv.select_all(action='DESELECT') for uv in cluster.uvs: uv.select = True bpy.ops.uv.select_more() # Collect extended uv_extended = [uv for clusterMask in clusters_mask for uv in clusterMask.uvs if (uv.select and clusterMask not in clusters_processed)] clusters_extended = [] for uv in uv_extended: if uv_to_clusters[uv] not in clusters_extended: clusters_extended.append( uv_to_clusters[uv] ) # Sort by distance groups_distance = {} for i in range(0, len(clusters_extended)): sub_group = clusters_extended[i] groups_distance[i] = (cluster.uvs[0].uv - sub_group.uvs[0].uv).length # Append to connected clusters array = [] for item in sorted(groups_distance.items(), key=operator.itemgetter(1)): key = item[0] clust = clusters_extended[key] array.append( clust ) if clust not in clusters_processed: clusters_processed.append(clust) connected_clusters.append( array ) if cluster not in clusters_processed: clusters_processed.append( cluster ) bpy.ops.uv.select_all(action='DESELECT') for uv in uv_extended: uv.select = True return connected_clusters mask_A = [vert_to_clusters[vert] for vert in verts_A] mask_B = [vert_to_clusters[vert] for vert in verts_B] border_A = list([vert_to_clusters[vert] for vert in verts_middle]) border_B = list([vert_to_clusters[vert] for vert in verts_middle]) for step in range(0, 8): if len(border_A) == 0: print("{}.: Finished scanning with no growth iterations".format(step)) break; if len(border_A) != len(border_B) or len(border_A) == 0: print("Abort: non compatible border A/B: {}x {}x ".format(len(border_A), len(border_B))) break; print("{}.: border {}x|{}x, processed: {}x".format(step, len(border_A), len(border_B), len(clusters_processed))) # Collect connected pairs for each side connected_A = select_extend_filter(border_A, mask_A) connected_B = select_extend_filter(border_B, mask_B) print(" Connected: {}x|{}x".format(len(connected_A), len(connected_B))) border_A.clear() border_B.clear() # Traverse through pairs for i in range(0, min(len(connected_A), len(connected_B)) ): if len(connected_A[i]) == 0: continue if len(connected_A[i]) != len(connected_B[i]): print(". Error: Inconsistent grow mappings from {} {}x | {}x".format(i, len(connected_A[i]), len(connected_B[i]) )) continue indexA = [cluster.vertex.index for cluster in connected_A[i] ] indexB = [cluster.vertex.index for cluster in connected_B[i] ] indexA = str(indexA).replace("[","").replace("]","").replace(" ","") indexB = str(indexB).replace("[","").replace("]","").replace(" ","") print(". Map {}|{} = {}x|{}x".format(indexA, indexB, len(connected_A[i]), len(connected_B[i]) ) ) if True:#isAToB: # Copy A side to B for cluster in connected_B[i]: for uv in cluster.uvs: pos = connected_A[i][0].uvs[0].uv.copy() pos.x = x_middle - (pos.x-x_middle)# Flip cooreindate # uv.uv = pos # border_A[i] = uv_to_clusters[ connected_A[i][0] ] # border_B[i] = uv_to_clusters[ connected_B[i][0] ] for j in range(len(connected_A[i])): border_A.append( connected_A[i][j] ) border_B.append( connected_B[i][j] ) # for uv in clusters_B[idxB]: # pos = clusters_A[idxA][0].uv.copy() # # Flip cooreindate # pos.x = x_middle - (pos.x-x_middle) # uv.uv = pos # for j in range(0, len(connected_A[i])): # # Group A and B # groupA = connected_A[i][j]; # groupB = connected_B[i][j]; # # vertexA = [vert_to_clusters[key] for key in vert_to_clusters if vert_to_clusters[key] == groupA] # print("...map {} -> {}".format(groupA, groupB)) # for j in range(0, count): # if len(connected_A[j]) != len(connected_B[j]): # # print("Error: Inconsistent grow mappings from {}:{}x | {}:{}x".format(border_A[j].index,len(connected_A[j]), border_B[j].index, len(connected_B[j]) )) # print("Error: Inconsistent grow mappings from {} {}x | {}x".format(j, len(connected_A[j]), len(connected_B[j]) )) # continue # for k in range(0, len(connected_A[j])): # # Vertex A and B # vA = connected_A[j][k]; # vB = connected_B[j][k]; # uvsA = vert_to_uv[vA]; # uvsB = vert_to_uv[vB]; # clusters_A = collect_clusters(uvsA) # clusters_B = collect_clusters(uvsB) # if len(clusters_A) != len(clusters_B): # print("Error: Inconsistent vertex UV group pairs at vertex {} : {}".format(vA.index, vB.index)) # continue # message= "...Map {0} -> {1} = UVs {2}|{3}x | UV-Groups {4}x|{5}x".format( vA.index, vB.index, len(uvsA), len(uvsB), len(clusters_A), len(clusters_B) ) # if len(clusters_A) > 1: # message = ">> "+message # print(message) # if len(clusters_A) > 0: # # For each group # sortA = {} # sortB = {} # for g in range(0, len(clusters_A)): # uv_A = clusters_A[g][0].uv.copy() # uv_B = clusters_B[g][0].uv.copy() # # localize X values (from symmetry line) # uv_A.x = (uv_A.x - x_middle) # uv_B.x = (uv_B.x - x_middle) # sortA[g] = abs(uv_A.x) + uv_A.y*2.0 # sortB[g] = abs(uv_B.x) + uv_B.y*2.0 # # print(" . [{}] : {:.2f}, {:.2f} | {:.2f}, {:.2f}".format(g, uv_A.x, uv_A.y, uv_B.x, uv_B.y)) # print(" . [{}] : {:.2f} | {:.2f}".format(g, sortA[g], sortB[g])) # # Sort sortA by value # sortedA = sorted(sortA.items(), key=operator.itemgetter(1)) # sortedB = sorted(sortB.items(), key=operator.itemgetter(1)) # for g in range(0, len(clusters_A)): # # sortedA[g] # idxA = sortedA[g][0] # idxB = sortedB[g][0] # print("Map clusters_A {} -> ".format(idxA, idxB)) # for uv in clusters_B[idxB]: # pos = clusters_A[idxA][0].uv.copy() # # Flip cooreindate # pos.x = x_middle - (pos.x-x_middle) # uv.uv = pos # border_A.append(vA) # border_B.append(vB) print("--------------------------------") ''' def select_extend_filter(verts_border, verts_mask): # print("Extend A/B") connected_verts = [] for i in range(0, len(verts_border)): # Collect connected edge verts verts_connected_edges = [] for edge in verts_border[i].link_edges: if(edge.verts[0] not in verts_connected_edges): verts_connected_edges.append(edge.verts[0]) if(edge.verts[1] not in verts_connected_edges): verts_connected_edges.append(edge.verts[1]) # Select vert on border bpy.ops.mesh.select_all(action='DESELECT') verts_border[i].select = True # Extend selection bpy.ops.mesh.select_more() # Filter selected verts against mask, connected edges, processed and border verts_extended = [vert for vert in bm.verts if (vert.select and vert in verts_connected_edges and vert in verts_mask and vert and vert not in verts_border and vert not in verts_processed)] # print(" "+str(i)+". scan: "+str(verts_border[i].index)+"; ext: "+str(len(verts_extended))+"x") connected_verts.append( [] ) # Sort by distance verts_distance = {} for vert in verts_extended: verts_distance[vert] = (verts_border[i].co - vert.co).length for item in sorted(verts_distance.items(), key=operator.itemgetter(1)): connected_verts[i].append( item[0] ) if verts_border[i] not in verts_processed: verts_processed.append(verts_border[i]) return connected_verts # find UV vert blobs , see which ones are same spot def collect_clusters(uvs): groups = [] for uv in uvs: if len(groups) == 0: groups.append([uv]) else: isMerged = False for group in groups: d = (uv.uv - group[0].uv).length if d <= 0.0000001: #Merge group.append(uv) isMerged = True; break; if not isMerged: #New Group groups.append([uv]) return groups border_A = [vert for vert in verts_middle] border_B = [vert for vert in verts_middle] for i in range(0, 200): if len(border_A) == 0: print("Finished scanning at {} growth iterations".format(i)) break; if len(border_A) != len(border_B) or len(border_A) == 0: print("Abort: non compatible border A/B: {}x {}x ".format(len(border_A), len(border_B))) break; connected_A = select_extend_filter(border_A, verts_A) connected_B = select_extend_filter(border_B, verts_B) print("Map pairs: {}|{}".format(len(connected_A), len(connected_B))) border_A.clear() border_B.clear() count = min(len(connected_A), len(connected_B)) for j in range(0, count): if len(connected_A[j]) != len(connected_B[j]): # print("Error: Inconsistent grow mappings from {}:{}x | {}:{}x".format(border_A[j].index,len(connected_A[j]), border_B[j].index, len(connected_B[j]) )) print("Error: Inconsistent grow mappings from {} {}x | {}x".format(j, len(connected_A[j]), len(connected_B[j]) )) continue for k in range(0, len(connected_A[j])): # Vertex A and B vA = connected_A[j][k]; vB = connected_B[j][k]; uvsA = vert_to_uv[vA]; uvsB = vert_to_uv[vB]; clusters_A = collect_clusters(uvsA) clusters_B = collect_clusters(uvsB) if len(clusters_A) != len(clusters_B): print("Error: Inconsistent vertex UV group pairs at vertex {} : {}".format(vA.index, vB.index)) continue message= "...Map {0} -> {1} = UVs {2}|{3}x | UV-Groups {4}x|{5}x".format( vA.index, vB.index, len(uvsA), len(uvsB), len(clusters_A), len(clusters_B) ) if len(clusters_A) > 1: message = ">> "+message print(message) if len(clusters_A) > 0: # For each group sortA = {} sortB = {} for g in range(0, len(clusters_A)): uv_A = clusters_A[g][0].uv.copy() uv_B = clusters_B[g][0].uv.copy() # localize X values (from symmetry line) uv_A.x = (uv_A.x - x_middle) uv_B.x = (uv_B.x - x_middle) sortA[g] = abs(uv_A.x) + uv_A.y*2.0 sortB[g] = abs(uv_B.x) + uv_B.y*2.0 # print(" . [{}] : {:.2f}, {:.2f} | {:.2f}, {:.2f}".format(g, uv_A.x, uv_A.y, uv_B.x, uv_B.y)) print(" . [{}] : {:.2f} | {:.2f}".format(g, sortA[g], sortB[g])) # Sort sortA by value sortedA = sorted(sortA.items(), key=operator.itemgetter(1)) sortedB = sorted(sortB.items(), key=operator.itemgetter(1)) for g in range(0, len(clusters_A)): # sortedA[g] idxA = sortedA[g][0] idxB = sortedB[g][0] print("Map clusters_A {} -> ".format(idxA, idxB)) for uv in clusters_B[idxB]: pos = clusters_A[idxA][0].uv.copy() # Flip cooreindate pos.x = x_middle - (pos.x-x_middle) uv.uv = pos # print("Sorted: '"+str(sortedA)+"'") # print("Sorted: '"+str(sortedB)+"'") # for item in sorted(verts_distance.items(), key=operator.itemgetter(1)): # connected_verts[i].append( item[0] ) # TODO: Now map groups to each other # uv_avg_A = Vector([0,0]) # uv_avg_B = Vector([0,0]) # for m in range(0, len(clusters_A)): # print(" . ") # uv_avg_A+= clusters_A[m][0].uv; # uv_avg_B+= clusters_B[m][0].uv; # uv_avg_A/=len(clusters_A) # uv_avg_B/=len(clusters_B) # print(" avg: {} : {}".format(uv_avg_A, uv_avg_B)) # Done processing, add to border arrays border_A.append(vA) border_B.append(vB) ''' def alignToCenterLine(): print("align to center line") bm = bmesh.from_edit_mesh(bpy.context.active_object.data) uv_layers = bm.loops.layers.uv.verify() bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') # 1.) Get average edges rotation + center average_angle = 0 average_center = Vector((0,0)) average_count = 0 for face in bm.faces: if face.select: verts = [] for loop in face.loops: if loop[uv_layers].select: verts.append(loop[uv_layers].uv) if len(verts) == 2: diff = verts[1] - verts[0] angle = math.atan2(diff.y, diff.x)%(math.pi) average_center += verts[0] + diff/2 average_angle += angle average_count+=1 if average_count >0: average_angle/=average_count average_center/=average_count average_angle-= math.pi/2 #Rotate -90 degrees so aligned horizontally # 2.) Rotate UV Shell around edge bpy.context.tool_settings.transform_pivot_point = 'CURSOR' bpy.ops.uv.cursor_set(location=average_center) bpy.ops.uv.select_linked() bpy.ops.transform.rotate(value=average_angle, orient_axis='Z', constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False) class UVCluster: uvs = [] vertex = None def __init__(self, vertex, uvs): self.vertex = vertex self.uvs = uvs def append(self, uv): self.uvs.append(uv) bpy.utils.register_class(op)