import bpy import bmesh import operator from mathutils import Vector from collections import defaultdict from math import pi import sys import time from math import radians, hypot from . import utilities_uv class op(bpy.types.Operator): bl_idname = "uv.textools_rectify" bl_label = "Rectify" bl_description = "Align selected faces or verts to rectangular distribution." bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): if not bpy.context.active_object: return False if bpy.context.active_object.type != 'MESH': return False if bpy.context.active_object.mode != 'EDIT': return False # No Sync mode if context.scene.tool_settings.use_uv_select_sync: return False return True def execute(self, context): rectify(self, context) return {'FINISHED'} def time_clock(): if sys.version_info >= (3, 3): return time.process_time() else: return time.clock() precision = 3 def rectify(self, context): obj = bpy.context.active_object bm = bmesh.from_edit_mesh(obj.data) uv_layers = bm.loops.layers.uv.verify() # Store selection utilities_uv.selection_store() main(False) # Restore selection utilities_uv.selection_restore() def main(square=False, snapToClosest=False): startTime = time_clock() obj = bpy.context.active_object me = obj.data bm = bmesh.from_edit_mesh(me) uv_layers = bm.loops.layers.uv.verify() # bm.faces.layers.tex.verify() # currently blender needs both layers. face_act = bm.faces.active targetFace = face_act # if len(bm.faces) > allowedFaces: # operator.report({'ERROR'}, "selected more than " +str(allowedFaces) +" allowed faces.") # return edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge = ListsOfVerts( uv_layers, bm) if len(filteredVerts) is 0: return if len(filteredVerts) is 1: SnapCursorToClosestSelected(filteredVerts) return cursorClosestTo = CursorClosestTo(filteredVerts) #line is selected if len(selFaces) is 0: if snapToClosest is True: SnapCursorToClosestSelected(filteredVerts) return VertsDictForLine(uv_layers, bm, filteredVerts, vertsDict) if AreVectsLinedOnAxis(filteredVerts) is False: ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, cursorClosestTo) return SuccessFinished(me, startTime) MakeEqualDistanceBetweenVertsInLine( filteredVerts, vertsDict, cursorClosestTo) return SuccessFinished(me, startTime) # else: # active face checks if targetFace is None or targetFace.select is False or len(targetFace.verts) is not 4: targetFace = selFaces[0] else: for l in targetFace.loops: if l[uv_layers].select is False: targetFace = selFaces[0] break ShapeFace(uv_layers, operator, targetFace, vertsDict, square) for nf in nonQuadFaces: for l in nf.loops: luv = l[uv_layers] luv.select = False if square: FollowActiveUV(operator, me, targetFace, selFaces, 'EVEN') else: FollowActiveUV(operator, me, targetFace, selFaces) if noEdge is False: # edge has ripped so we connect it back for ev in edgeVerts: key = (round(ev.uv.x, precision), round(ev.uv.y, precision)) if key in vertsDict: ev.uv = vertsDict[key][0].uv ev.select = True return SuccessFinished(me, startTime) def ListsOfVerts(uv_layers, bm): edgeVerts = [] allEdgeVerts = [] filteredVerts = [] selFaces = [] nonQuadFaces = [] vertsDict = defaultdict(list) # dict for f in bm.faces: isFaceSel = True facesEdgeVerts = [] if (f.select == False): continue # collect edge verts if any for l in f.loops: luv = l[uv_layers] if luv.select is True: facesEdgeVerts.append(luv) else: isFaceSel = False allEdgeVerts.extend(facesEdgeVerts) if isFaceSel: if len(f.verts) is not 4: nonQuadFaces.append(f) edgeVerts.extend(facesEdgeVerts) else: selFaces.append(f) for l in f.loops: luv = l[uv_layers] x = round(luv.uv.x, precision) y = round(luv.uv.y, precision) vertsDict[(x, y)].append(luv) else: edgeVerts.extend(facesEdgeVerts) noEdge = False if len(edgeVerts) is 0: noEdge = True edgeVerts.extend(allEdgeVerts) if len(selFaces) is 0: for ev in edgeVerts: if ListQuasiContainsVect(filteredVerts, ev) is False: filteredVerts.append(ev) else: filteredVerts = edgeVerts return edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge def ListQuasiContainsVect(list, vect): for v in list: if AreVertsQuasiEqual(v, vect): return True return False def SnapCursorToClosestSelected(filteredVerts): # TODO: snap to closest selected if len(filteredVerts) is 1: SetAll2dCursorsTo(filteredVerts[0].uv.x, filteredVerts[0].uv.y) return def VertsDictForLine(uv_layers, bm, selVerts, vertsDict): for f in bm.faces: for l in f.loops: luv = l[uv_layers] if luv.select is True: x = round(luv.uv.x, precision) y = round(luv.uv.y, precision) vertsDict[(x, y)].append(luv) def AreVectsLinedOnAxis(verts): areLinedX = True areLinedY = True allowedError = 0.0009 valX = verts[0].uv.x valY = verts[0].uv.y for v in verts: if abs(valX - v.uv.x) > allowedError: areLinedX = False if abs(valY - v.uv.y) > allowedError: areLinedY = False return areLinedX or areLinedY def ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, startv=None, horizontal=None): verts = filteredVerts verts.sort(key=lambda x: x.uv[0]) # sort by .x first = verts[0] last = verts[len(verts)-1] if horizontal is None: horizontal = True if ((last.uv.x - first.uv.x) > 0.0009): slope = (last.uv.y - first.uv.y)/(last.uv.x - first.uv.x) if (slope > 1) or (slope < -1): horizontal = False else: horizontal = False if horizontal is True: if startv is None: startv = first SetAll2dCursorsTo(startv.uv.x, startv.uv.y) # scale to 0 on Y ScaleTo0('Y') return else: verts.sort(key=lambda x: x.uv[1]) # sort by .y verts.reverse() # reverse because y values drop from up to down first = verts[0] last = verts[len(verts)-1] if startv is None: startv = first SetAll2dCursorsTo(startv.uv.x, startv.uv.y) # scale to 0 on X ScaleTo0('X') return def SetAll2dCursorsTo(x, y): last_area = bpy.context.area.type bpy.context.area.type = 'IMAGE_EDITOR' bpy.ops.uv.cursor_set(location=(x, y)) bpy.context.area.type = last_area return def CursorClosestTo(verts, allowedError=0.025): ratioX, ratioY = ImageRatio() # any length that is certantly not smaller than distance of the closest min = 1000 minV = verts[0] for v in verts: if v is None: continue for area in bpy.context.screen.areas: if area.type == 'IMAGE_EDITOR': loc = area.spaces[0].cursor_location hyp = hypot(loc.x/ratioX - v.uv.x, loc.y/ratioY - v.uv.y) if (hyp < min): min = hyp minV = v if min is not 1000: return minV return None def SuccessFinished(me, startTime): # use for backtrack of steps # bpy.ops.ed.undo_push() bmesh.update_edit_mesh(me) #elapsed = round(time_clock()-startTime, 2) #if (elapsed >= 0.05): operator.report({'INFO'}, "UvSquares finished, elapsed:", elapsed, "s.") return def ShapeFace(uv_layers, operator, targetFace, vertsDict, square): corners = [] for l in targetFace.loops: luv = l[uv_layers] corners.append(luv) if len(corners) is not 4: #operator.report({'ERROR'}, "bla") return lucv, ldcv, rucv, rdcv = Corners(corners) cct = CursorClosestTo([lucv, ldcv, rdcv, rucv]) if cct is None: cct = lucv MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, cct, square) return def MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, startv, square=False): ratioX, ratioY = ImageRatio() ratio = ratioX/ratioY if startv is None: startv = lucv.uv elif AreVertsQuasiEqual(startv, rucv): startv = rucv.uv elif AreVertsQuasiEqual(startv, rdcv): startv = rdcv.uv elif AreVertsQuasiEqual(startv, ldcv): startv = ldcv.uv else: startv = lucv.uv lucv = lucv.uv rucv = rucv.uv rdcv = rdcv.uv ldcv = ldcv.uv if (startv == lucv): finalScaleX = hypotVert(lucv, rucv) finalScaleY = hypotVert(lucv, ldcv) currRowX = lucv.x currRowY = lucv.y elif (startv == rucv): finalScaleX = hypotVert(rucv, lucv) finalScaleY = hypotVert(rucv, rdcv) currRowX = rucv.x - finalScaleX currRowY = rucv.y elif (startv == rdcv): finalScaleX = hypotVert(rdcv, ldcv) finalScaleY = hypotVert(rdcv, rucv) currRowX = rdcv.x - finalScaleX currRowY = rdcv.y + finalScaleY else: finalScaleX = hypotVert(ldcv, rdcv) finalScaleY = hypotVert(ldcv, lucv) currRowX = ldcv.x currRowY = ldcv.y + finalScaleY if square: finalScaleY = finalScaleX*ratio #lucv, rucv x = round(lucv.x, precision) y = round(lucv.y, precision) for v in vertsDict[(x, y)]: v.uv.x = currRowX v.uv.y = currRowY x = round(rucv.x, precision) y = round(rucv.y, precision) for v in vertsDict[(x, y)]: v.uv.x = currRowX + finalScaleX v.uv.y = currRowY #rdcv, ldcv x = round(rdcv.x, precision) y = round(rdcv.y, precision) for v in vertsDict[(x, y)]: v.uv.x = currRowX + finalScaleX v.uv.y = currRowY - finalScaleY x = round(ldcv.x, precision) y = round(ldcv.y, precision) for v in vertsDict[(x, y)]: v.uv.x = currRowX v.uv.y = currRowY - finalScaleY return def FollowActiveUV(operator, me, f_act, faces, EXTEND_MODE='LENGTH_AVERAGE'): bm = bmesh.from_edit_mesh(me) uv_act = bm.loops.layers.uv.active # our own local walker def walk_face_init(faces, f_act): # first tag all faces True (so we dont uvmap them) for f in bm.faces: f.tag = True # then tag faces arg False for f in faces: f.tag = False # tag the active face True since we begin there f_act.tag = True def walk_face(f): # all faces in this list must be tagged f.tag = True faces_a = [f] faces_b = [] while faces_a: for f in faces_a: for l in f.loops: l_edge = l.edge if (l_edge.is_manifold is True) and (l_edge.seam is False): l_other = l.link_loop_radial_next f_other = l_other.face if not f_other.tag: yield (f, l, f_other) f_other.tag = True faces_b.append(f_other) # swap faces_a, faces_b = faces_b, faces_a faces_b.clear() def walk_edgeloop(l): """ Could make this a generic function """ e_first = l.edge e = None while True: e = l.edge yield e # don't step past non-manifold edges if e.is_manifold: # welk around the quad and then onto the next face l = l.link_loop_radial_next if len(l.face.verts) == 4: l = l.link_loop_next.link_loop_next if l.edge is e_first: break else: break else: break def extrapolate_uv(fac, l_a_outer, l_a_inner, l_b_outer, l_b_inner): l_b_inner[:] = l_a_inner l_b_outer[:] = l_a_inner + ((l_a_inner - l_a_outer) * fac) def apply_uv(f_prev, l_prev, f_next): l_a = [None, None, None, None] l_b = [None, None, None, None] l_a[0] = l_prev l_a[1] = l_a[0].link_loop_next l_a[2] = l_a[1].link_loop_next l_a[3] = l_a[2].link_loop_next # l_b # +-----------+ # |(3) |(2) # | | # |l_next(0) |(1) # +-----------+ # ^ # l_a | # +-----------+ # |l_prev(0) |(1) # | (f) | # |(3) |(2) # +-----------+ # copy from this face to the one above. # get the other loops l_next = l_prev.link_loop_radial_next if l_next.vert != l_prev.vert: l_b[1] = l_next l_b[0] = l_b[1].link_loop_next l_b[3] = l_b[0].link_loop_next l_b[2] = l_b[3].link_loop_next else: l_b[0] = l_next l_b[1] = l_b[0].link_loop_next l_b[2] = l_b[1].link_loop_next l_b[3] = l_b[2].link_loop_next l_a_uv = [l[uv_act].uv for l in l_a] l_b_uv = [l[uv_act].uv for l in l_b] if EXTEND_MODE == 'LENGTH_AVERAGE': fac = edge_lengths[l_b[2].edge.index][0] / \ edge_lengths[l_a[1].edge.index][0] elif EXTEND_MODE == 'LENGTH': a0, b0, c0 = l_a[3].vert.co, l_a[0].vert.co, l_b[3].vert.co a1, b1, c1 = l_a[2].vert.co, l_a[1].vert.co, l_b[2].vert.co d1 = (a0 - b0).length + (a1 - b1).length d2 = (b0 - c0).length + (b1 - c1).length try: fac = d2 / d1 except ZeroDivisionError: fac = 1.0 else: fac = 1.0 extrapolate_uv(fac, l_a_uv[3], l_a_uv[0], l_b_uv[3], l_b_uv[0]) extrapolate_uv(fac, l_a_uv[2], l_a_uv[1], l_b_uv[2], l_b_uv[1]) # ------------------------------------------- # Calculate average length per loop if needed if EXTEND_MODE == 'LENGTH_AVERAGE': bm.edges.index_update() # NoneType times the length of edges list edge_lengths = [None] * len(bm.edges) for f in faces: # we know its a quad l_quad = f.loops[:] l_pair_a = (l_quad[0], l_quad[2]) l_pair_b = (l_quad[1], l_quad[3]) for l_pair in (l_pair_a, l_pair_b): if edge_lengths[l_pair[0].edge.index] is None: edge_length_store = [-1.0] edge_length_accum = 0.0 edge_length_total = 0 for l in l_pair: if edge_lengths[l.edge.index] is None: for e in walk_edgeloop(l): if edge_lengths[e.index] is None: edge_lengths[e.index] = edge_length_store edge_length_accum += e.calc_length() edge_length_total += 1 edge_length_store[0] = edge_length_accum / \ edge_length_total # done with average length # ------------------------ walk_face_init(faces, f_act) for f_triple in walk_face(f_act): apply_uv(*f_triple) bmesh.update_edit_mesh(me, False) def ImageRatio(): ratioX, ratioY = 256, 256 for a in bpy.context.screen.areas: if a.type == 'IMAGE_EDITOR': img = a.spaces[0].image if img is not None and img.size[0] is not 0: ratioX, ratioY = img.size[0], img.size[1] break return ratioX, ratioY def Corners(corners): firstHighest = corners[0] for c in corners: if c.uv.y > firstHighest.uv.y: firstHighest = c corners.remove(firstHighest) secondHighest = corners[0] for c in corners: if (c.uv.y > secondHighest.uv.y): secondHighest = c if firstHighest.uv.x < secondHighest.uv.x: leftUp = firstHighest rightUp = secondHighest else: leftUp = secondHighest rightUp = firstHighest corners.remove(secondHighest) firstLowest = corners[0] secondLowest = corners[1] if firstLowest.uv.x < secondLowest.uv.x: leftDown = firstLowest rightDown = secondLowest else: leftDown = secondLowest rightDown = firstLowest return leftUp, leftDown, rightUp, rightDown def AreVertsQuasiEqual(v1, v2, allowedError=0.0009): if abs(v1.uv.x - v2.uv.x) < allowedError and abs(v1.uv.y - v2.uv.y) < allowedError: return True return False def hypotVert(v1, v2): hyp = hypot(v1.x - v2.x, v1.y - v2.y) return hyp bpy.utils.register_class(op)