You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Blender-TexTools/op_island_mirror.py

783 lines
29 KiB
Python

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)