486 lines
12 KiB
Python
486 lines
12 KiB
Python
import bpy
|
|
import bmesh
|
|
|
|
from math import hypot
|
|
from collections import defaultdict
|
|
from . import utilities_uv
|
|
|
|
|
|
precision = 3
|
|
|
|
|
|
|
|
class op(bpy.types.Operator):
|
|
bl_idname = "uv.textools_rectify"
|
|
bl_label = "Rectify"
|
|
bl_description = "Align selected UV faces or vertices to rectangular distribution"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if bpy.context.area.ui_type != 'UV':
|
|
return False
|
|
if not bpy.context.active_object:
|
|
return False
|
|
if bpy.context.active_object.mode != 'EDIT':
|
|
return False
|
|
if bpy.context.active_object.type != 'MESH':
|
|
return False
|
|
if not bpy.context.active_object.data.uv_layers:
|
|
return False
|
|
if context.scene.tool_settings.use_uv_select_sync:
|
|
return False
|
|
return True
|
|
|
|
|
|
def execute(self, context):
|
|
utilities_uv.multi_object_loop(rectify, self, context)
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
def rectify(self, context, me=None, bm=None, uv_layers=None):
|
|
if me is None:
|
|
me = bpy.context.active_object.data
|
|
bm = bmesh.from_edit_mesh(me)
|
|
uv_layers = bm.loops.layers.uv.verify()
|
|
|
|
# Store selection
|
|
faces_loops = utilities_uv.selection_store(bm, uv_layers, return_selected_faces_loops=True)
|
|
|
|
# Find selection islands
|
|
islands = utilities_uv.getSelectionIslands(bm, uv_layers, selected_faces=set(faces_loops.keys()))
|
|
|
|
for island in islands:
|
|
bpy.ops.uv.select_all(action='DESELECT')
|
|
utilities_uv.set_selected_faces(island, bm, uv_layers)
|
|
main(me, bm, uv_layers, island, faces_loops)
|
|
|
|
# Restore selection
|
|
utilities_uv.selection_restore(bm, uv_layers)
|
|
|
|
|
|
|
|
def main(me, bm, uv_layers, selFacesMix, faces_loops, return_discarded_faces=False):
|
|
|
|
filteredVerts, selFaces, vertsDict, discarded_faces = ListsOfVerts(bm, uv_layers, selFacesMix, faces_loops)
|
|
|
|
if len(filteredVerts) < 2:
|
|
if return_discarded_faces:
|
|
return set()
|
|
return
|
|
|
|
if not selFaces:
|
|
if discarded_faces:
|
|
if return_discarded_faces:
|
|
return discarded_faces
|
|
else:
|
|
# Line is selected -> align on axis
|
|
for luv in filteredVerts:
|
|
x = round(luv.uv.x, precision)
|
|
y = round(luv.uv.y, precision)
|
|
if luv not in vertsDict[(x, y)]:
|
|
vertsDict[(x, y)].append(luv)
|
|
|
|
areLinedX = True
|
|
areLinedY = True
|
|
allowedError = 0.00001
|
|
valX = filteredVerts[0].uv.x
|
|
valY = filteredVerts[0].uv.y
|
|
for v in filteredVerts:
|
|
if abs(valX - v.uv.x) > allowedError:
|
|
areLinedX = False
|
|
if abs(valY - v.uv.y) > allowedError:
|
|
areLinedY = False
|
|
|
|
if not (areLinedX or areLinedY):
|
|
verts = filteredVerts
|
|
verts.sort(key=lambda x: x.uv[0]) #sort by .x
|
|
first = verts[0]
|
|
last = verts[len(verts)-1]
|
|
|
|
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 == True:
|
|
#scale to 0 on Y
|
|
for v in verts:
|
|
x = round(v.uv.x, precision)
|
|
y = round(v.uv.y, precision)
|
|
for luv in vertsDict[(x, y)]:
|
|
luv.uv.y = first.uv.y
|
|
else:
|
|
#scale to 0 on X
|
|
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]
|
|
|
|
for v in verts:
|
|
x = round(v.uv.x, precision)
|
|
y = round(v.uv.y, precision)
|
|
for luv in vertsDict[(x, y)]:
|
|
luv.uv.x = first.uv.x
|
|
|
|
else:
|
|
# At least one face is selected -> rectify
|
|
targetFace = bm.faces.active
|
|
# Active face checks
|
|
if targetFace is None or len({loop for loop in targetFace.loops}.intersection(filteredVerts)) != len(targetFace.verts) or targetFace.select == False or len(targetFace.verts) != 4:
|
|
targetFace = selFaces[0]
|
|
|
|
ShapeFace(uv_layers, targetFace, vertsDict)
|
|
|
|
FollowActiveUV(me, targetFace, selFaces)
|
|
|
|
bmesh.update_edit_mesh(me, loop_triangles=False)
|
|
|
|
if return_discarded_faces:
|
|
return discarded_faces
|
|
|
|
|
|
|
|
def ListsOfVerts(bm, uv_layers, selFacesMix, faces_loops):
|
|
allEdgeVerts = []
|
|
filteredVerts = []
|
|
selFaces = []
|
|
discarded_faces = set()
|
|
vertsDict = defaultdict(list)
|
|
|
|
for f in selFacesMix:
|
|
isFaceSel = True
|
|
facesEdgeVerts = [l[uv_layers] for l in faces_loops[f]]
|
|
if len(faces_loops[f]) < len(f.loops):
|
|
isFaceSel = False
|
|
|
|
allEdgeVerts.extend(facesEdgeVerts)
|
|
if isFaceSel:
|
|
if len(f.verts) != 4:
|
|
filteredVerts.extend(facesEdgeVerts)
|
|
discarded_faces.add(f)
|
|
else:
|
|
selFaces.append(f)
|
|
for luv in facesEdgeVerts:
|
|
x = round(luv.uv.x, precision)
|
|
y = round(luv.uv.y, precision)
|
|
vertsDict[(x, y)].append(luv)
|
|
else:
|
|
filteredVerts.extend(facesEdgeVerts)
|
|
|
|
if len(filteredVerts) == 0:
|
|
filteredVerts.extend(allEdgeVerts)
|
|
|
|
return filteredVerts, selFaces, vertsDict, discarded_faces
|
|
|
|
|
|
|
|
def ShapeFace(uv_layers, targetFace, vertsDict):
|
|
corners = []
|
|
for l in targetFace.loops:
|
|
luv = l[uv_layers]
|
|
corners.append(luv)
|
|
|
|
if len(corners) != 4:
|
|
return
|
|
|
|
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
|
|
|
|
|
|
verts = [leftUp, leftDown, rightDown, rightUp]
|
|
|
|
ratioX, ratioY = ImageRatio()
|
|
min = float('inf')
|
|
minV = verts[0]
|
|
for v in verts:
|
|
if v is None:
|
|
continue
|
|
for area in bpy.context.screen.areas:
|
|
if area.ui_type == 'UV':
|
|
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
|
|
|
|
MakeUvFaceEqualRectangle(vertsDict, leftUp, rightUp, rightDown, leftDown, minV)
|
|
|
|
|
|
|
|
def MakeUvFaceEqualRectangle(vertsDict, leftUp, rightUp, rightDown, leftDown, startv):
|
|
ratioX, ratioY = ImageRatio()
|
|
ratio = ratioX/ratioY
|
|
|
|
if startv is None: startv = leftUp.uv
|
|
elif AreVertsQuasiEqual(startv, rightUp): startv = rightUp.uv
|
|
elif AreVertsQuasiEqual(startv, rightDown): startv = rightDown.uv
|
|
elif AreVertsQuasiEqual(startv, leftDown): startv = leftDown.uv
|
|
else: startv = leftUp.uv
|
|
|
|
leftUp = leftUp.uv
|
|
rightUp = rightUp.uv
|
|
rightDown = rightDown.uv
|
|
leftDown = leftDown.uv
|
|
|
|
if (startv == leftUp):
|
|
finalScaleX = hypotVert(leftUp, rightUp)
|
|
finalScaleY = hypotVert(leftUp, leftDown)
|
|
currRowX = leftUp.x
|
|
currRowY = leftUp.y
|
|
|
|
elif (startv == rightUp):
|
|
finalScaleX = hypotVert(rightUp, leftUp)
|
|
finalScaleY = hypotVert(rightUp, rightDown)
|
|
currRowX = rightUp.x - finalScaleX
|
|
currRowY = rightUp.y
|
|
|
|
elif (startv == rightDown):
|
|
finalScaleX = hypotVert(rightDown, leftDown)
|
|
finalScaleY = hypotVert(rightDown, rightUp)
|
|
currRowX = rightDown.x - finalScaleX
|
|
currRowY = rightDown.y + finalScaleY
|
|
|
|
else:
|
|
finalScaleX = hypotVert(leftDown, rightDown)
|
|
finalScaleY = hypotVert(leftDown, leftUp)
|
|
currRowX = leftDown.x
|
|
currRowY = leftDown.y +finalScaleY
|
|
|
|
#leftUp, rightUp
|
|
x = round(leftUp.x, precision)
|
|
y = round(leftUp.y, precision)
|
|
for v in vertsDict[(x,y)]:
|
|
v.uv.x = currRowX
|
|
v.uv.y = currRowY
|
|
|
|
x = round(rightUp.x, precision)
|
|
y = round(rightUp.y, precision)
|
|
for v in vertsDict[(x,y)]:
|
|
v.uv.x = currRowX + finalScaleX
|
|
v.uv.y = currRowY
|
|
|
|
#rightDown, leftDown
|
|
x = round(rightDown.x, precision)
|
|
y = round(rightDown.y, precision)
|
|
for v in vertsDict[(x,y)]:
|
|
v.uv.x = currRowX + finalScaleX
|
|
v.uv.y = currRowY - finalScaleY
|
|
|
|
x = round(leftDown.x, precision)
|
|
y = round(leftDown.y, precision)
|
|
for v in vertsDict[(x,y)]:
|
|
v.uv.x = currRowX
|
|
v.uv.y = currRowY - finalScaleY
|
|
|
|
|
|
|
|
def FollowActiveUV(me, f_act, faces):
|
|
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 == True and l_edge.seam == 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:
|
|
# walk 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]
|
|
|
|
try:
|
|
fac = edge_lengths[l_b[2].edge.index][0] / edge_lengths[l_a[1].edge.index][0]
|
|
except ZeroDivisionError:
|
|
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
|
|
bm.edges.index_update()
|
|
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
|
|
|
|
|
|
walk_face_init(faces, f_act)
|
|
for f_triple in walk_face(f_act):
|
|
apply_uv(*f_triple)
|
|
|
|
|
|
|
|
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 and img.size[0] != 0:
|
|
ratioX, ratioY = img.size[0], img.size[1]
|
|
break
|
|
return ratioX, ratioY
|
|
|
|
|
|
|
|
def AreVertsQuasiEqual(v1, v2, allowedError = 0.00001):
|
|
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)
|