mirror of
https://github.com/drewcassidy/TexTools-Blender
synced 2024-09-01 14:54:44 +00:00
637 lines
15 KiB
Python
637 lines
15 KiB
Python
import bpy
|
|
import bmesh
|
|
import operator
|
|
from mathutils import Vector
|
|
from collections import defaultdict
|
|
from math import pi
|
|
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'}
|
|
|
|
|
|
|
|
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()
|
|
edge_lengths = [None] * len(bm.edges) #NoneType times the length of edges list
|
|
|
|
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) |