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_rectify.py

639 lines
18 KiB
Python

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):
4 years ago
bl_idname = "uv.textools_rectify"
bl_label = "Rectify"
bl_description = "Align selected faces or verts to rectangular distribution."
bl_options = {'REGISTER', 'UNDO'}
4 years ago
4 years ago
@classmethod
def poll(cls, context):
if not bpy.context.active_object:
return False
4 years ago
if bpy.context.active_object.type != 'MESH':
return False
4 years ago
if bpy.context.active_object.mode != 'EDIT':
return False
4 years ago
# No Sync mode
if context.scene.tool_settings.use_uv_select_sync:
return False
4 years ago
return True
4 years ago
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):
4 years ago
obj = bpy.context.active_object
4 years ago
bm = bmesh.from_edit_mesh(obj.data)
uv_layers = bm.loops.layers.uv.verify()
4 years ago
# Store selection
4 years ago
utilities_uv.selection_store()
4 years ago
main(False)
4 years ago
# Restore selection
4 years ago
utilities_uv.selection_restore()
4 years ago
def main(square=False, snapToClosest=False):
startTime = time_clock()
4 years ago
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.
4 years ago
face_act = bm.faces.active
4 years ago
targetFace = face_act
4 years ago
# if len(bm.faces) > allowedFaces:
4 years ago
# operator.report({'ERROR'}, "selected more than " +str(allowedFaces) +" allowed faces.")
4 years ago
# return
edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge = ListsOfVerts(
uv_layers, bm)
4 years ago
4 years ago
if len(filteredVerts) is 0:
return
if len(filteredVerts) is 1:
4 years ago
SnapCursorToClosestSelected(filteredVerts)
4 years ago
return
4 years ago
cursorClosestTo = CursorClosestTo(filteredVerts)
#line is selected
4 years ago
4 years ago
if len(selFaces) is 0:
if snapToClosest is True:
SnapCursorToClosestSelected(filteredVerts)
return
4 years ago
4 years ago
VertsDictForLine(uv_layers, bm, filteredVerts, vertsDict)
4 years ago
4 years ago
if AreVectsLinedOnAxis(filteredVerts) is False:
ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, cursorClosestTo)
return SuccessFinished(me, startTime)
4 years ago
MakeEqualDistanceBetweenVertsInLine(
filteredVerts, vertsDict, cursorClosestTo)
4 years ago
return SuccessFinished(me, startTime)
4 years ago
# else:
# active face checks
4 years ago
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:
4 years ago
if l[uv_layers].select is False:
4 years ago
targetFace = selFaces[0]
4 years ago
break
4 years ago
ShapeFace(uv_layers, operator, targetFace, vertsDict, square)
4 years ago
4 years ago
for nf in nonQuadFaces:
for l in nf.loops:
luv = l[uv_layers]
luv.select = False
4 years ago
if square:
FollowActiveUV(operator, me, targetFace, selFaces, 'EVEN')
else:
FollowActiveUV(operator, me, targetFace, selFaces)
4 years ago
if noEdge is False:
4 years ago
# edge has ripped so we connect it back
4 years ago
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
4 years ago
return SuccessFinished(me, startTime)
def ListsOfVerts(uv_layers, bm):
4 years ago
edgeVerts = []
allEdgeVerts = []
filteredVerts = []
selFaces = []
nonQuadFaces = []
4 years ago
vertsDict = defaultdict(list) # dict
4 years ago
for f in bm.faces:
isFaceSel = True
facesEdgeVerts = []
if (f.select == False):
continue
4 years ago
# collect edge verts if any
4 years ago
for l in f.loops:
luv = l[uv_layers]
if luv.select is True:
facesEdgeVerts.append(luv)
4 years ago
else:
isFaceSel = False
4 years ago
allEdgeVerts.extend(facesEdgeVerts)
4 years ago
if isFaceSel:
4 years ago
if len(f.verts) is not 4:
nonQuadFaces.append(f)
edgeVerts.extend(facesEdgeVerts)
4 years ago
else:
4 years ago
selFaces.append(f)
4 years ago
4 years ago
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)
4 years ago
else:
edgeVerts.extend(facesEdgeVerts)
4 years ago
noEdge = False
if len(edgeVerts) is 0:
noEdge = True
edgeVerts.extend(allEdgeVerts)
4 years ago
4 years ago
if len(selFaces) is 0:
for ev in edgeVerts:
if ListQuasiContainsVect(filteredVerts, ev) is False:
filteredVerts.append(ev)
4 years ago
else:
filteredVerts = edgeVerts
4 years ago
return edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge
def ListQuasiContainsVect(list, vect):
4 years ago
for v in list:
if AreVertsQuasiEqual(v, vect):
return True
return False
def SnapCursorToClosestSelected(filteredVerts):
4 years ago
# TODO: snap to closest selected
if len(filteredVerts) is 1:
4 years ago
SetAll2dCursorsTo(filteredVerts[0].uv.x, filteredVerts[0].uv.y)
4 years ago
return
def VertsDictForLine(uv_layers, bm, selVerts, vertsDict):
4 years ago
for f in bm.faces:
for l in f.loops:
4 years ago
luv = l[uv_layers]
if luv.select is True:
x = round(luv.uv.x, precision)
y = round(luv.uv.y, precision)
4 years ago
vertsDict[(x, y)].append(luv)
def AreVectsLinedOnAxis(verts):
4 years ago
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
4 years ago
return areLinedX or areLinedY
4 years ago
def ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, startv=None, horizontal=None):
4 years ago
verts = filteredVerts
4 years ago
verts.sort(key=lambda x: x.uv[0]) # sort by .x
4 years ago
first = verts[0]
last = verts[len(verts)-1]
4 years ago
4 years ago
if horizontal is None:
horizontal = True
4 years ago
if ((last.uv.x - first.uv.x) > 0.0009):
4 years ago
slope = (last.uv.y - first.uv.y)/(last.uv.x - first.uv.x)
4 years ago
if (slope > 1) or (slope < -1):
horizontal = False
else:
4 years ago
horizontal = False
4 years ago
4 years ago
if horizontal is True:
if startv is None:
4 years ago
startv = first
4 years ago
SetAll2dCursorsTo(startv.uv.x, startv.uv.y)
4 years ago
# scale to 0 on Y
4 years ago
ScaleTo0('Y')
return
4 years ago
4 years ago
else:
4 years ago
verts.sort(key=lambda x: x.uv[1]) # sort by .y
verts.reverse() # reverse because y values drop from up to down
4 years ago
first = verts[0]
last = verts[len(verts)-1]
if startv is None:
4 years ago
startv = first
4 years ago
SetAll2dCursorsTo(startv.uv.x, startv.uv.y)
4 years ago
# scale to 0 on X
4 years ago
ScaleTo0('X')
return
4 years ago
def SetAll2dCursorsTo(x, y):
4 years ago
last_area = bpy.context.area.type
bpy.context.area.type = 'IMAGE_EDITOR'
4 years ago
4 years ago
bpy.ops.uv.cursor_set(location=(x, y))
4 years ago
bpy.context.area.type = last_area
return
4 years ago
def CursorClosestTo(verts, allowedError=0.025):
4 years ago
ratioX, ratioY = ImageRatio()
4 years ago
# any length that is certantly not smaller than distance of the closest
4 years ago
min = 1000
minV = verts[0]
for v in verts:
4 years ago
if v is None:
continue
4 years ago
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
loc = area.spaces[0].cursor_location
4 years ago
hyp = hypot(loc.x/ratioX - v.uv.x, loc.y/ratioY - v.uv.y)
4 years ago
if (hyp < min):
min = hyp
minV = v
4 years ago
if min is not 1000:
4 years ago
return minV
return None
def SuccessFinished(me, startTime):
4 years ago
# use for backtrack of steps
# bpy.ops.ed.undo_push()
4 years ago
bmesh.update_edit_mesh(me)
#elapsed = round(time_clock()-startTime, 2)
4 years ago
#if (elapsed >= 0.05): operator.report({'INFO'}, "UvSquares finished, elapsed:", elapsed, "s.")
return
def ShapeFace(uv_layers, operator, targetFace, vertsDict, square):
4 years ago
corners = []
for l in targetFace.loops:
luv = l[uv_layers]
corners.append(luv)
4 years ago
if len(corners) is not 4:
4 years ago
#operator.report({'ERROR'}, "bla")
return
4 years ago
4 years ago
lucv, ldcv, rucv, rdcv = Corners(corners)
4 years ago
4 years ago
cct = CursorClosestTo([lucv, ldcv, rdcv, rucv])
4 years ago
if cct is None:
4 years ago
cct = lucv
4 years ago
4 years ago
MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, cct, square)
return
4 years ago
def MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, startv, square=False):
4 years ago
ratioX, ratioY = ImageRatio()
ratio = ratioX/ratioY
4 years ago
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
4 years ago
lucv = lucv.uv
rucv = rucv.uv
rdcv = rdcv.uv
4 years ago
ldcv = ldcv.uv
if (startv == lucv):
4 years ago
finalScaleX = hypotVert(lucv, rucv)
finalScaleY = hypotVert(lucv, ldcv)
currRowX = lucv.x
currRowY = lucv.y
4 years ago
4 years ago
elif (startv == rucv):
finalScaleX = hypotVert(rucv, lucv)
finalScaleY = hypotVert(rucv, rdcv)
currRowX = rucv.x - finalScaleX
currRowY = rucv.y
4 years ago
4 years ago
elif (startv == rdcv):
finalScaleX = hypotVert(rdcv, ldcv)
finalScaleY = hypotVert(rdcv, rucv)
currRowX = rdcv.x - finalScaleX
currRowY = rdcv.y + finalScaleY
4 years ago
4 years ago
else:
finalScaleX = hypotVert(ldcv, rdcv)
finalScaleY = hypotVert(ldcv, lucv)
currRowX = ldcv.x
4 years ago
currRowY = ldcv.y + finalScaleY
if square:
finalScaleY = finalScaleX*ratio
4 years ago
#lucv, rucv
x = round(lucv.x, precision)
y = round(lucv.y, precision)
4 years ago
for v in vertsDict[(x, y)]:
4 years ago
v.uv.x = currRowX
v.uv.y = currRowY
4 years ago
4 years ago
x = round(rucv.x, precision)
y = round(rucv.y, precision)
4 years ago
for v in vertsDict[(x, y)]:
4 years ago
v.uv.x = currRowX + finalScaleX
v.uv.y = currRowY
4 years ago
4 years ago
#rdcv, ldcv
x = round(rdcv.x, precision)
4 years ago
y = round(rdcv.y, precision)
for v in vertsDict[(x, y)]:
4 years ago
v.uv.x = currRowX + finalScaleX
v.uv.y = currRowY - finalScaleY
4 years ago
4 years ago
x = round(ldcv.x, precision)
4 years ago
y = round(ldcv.y, precision)
for v in vertsDict[(x, y)]:
4 years ago
v.uv.x = currRowX
v.uv.y = currRowY - finalScaleY
return
4 years ago
def FollowActiveUV(operator, me, f_act, faces, EXTEND_MODE='LENGTH_AVERAGE'):
4 years ago
bm = bmesh.from_edit_mesh(me)
uv_act = bm.loops.layers.uv.active
4 years ago
4 years ago
# 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':
4 years ago
fac = edge_lengths[l_b[2].edge.index][0] / \
edge_lengths[l_a[1].edge.index][0]
4 years ago
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()
4 years ago
# NoneType times the length of edges list
edge_lengths = [None] * len(bm.edges)
4 years ago
for f in faces:
# we know its a quad
4 years ago
l_quad = f.loops[:]
4 years ago
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
4 years ago
edge_length_store[0] = edge_length_accum / \
edge_length_total
4 years ago
# 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():
4 years ago
ratioX, ratioY = 256, 256
4 years ago
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):
4 years ago
firstHighest = corners[0]
for c in corners:
if c.uv.y > firstHighest.uv.y:
4 years ago
firstHighest = c
4 years ago
corners.remove(firstHighest)
4 years ago
4 years ago
secondHighest = corners[0]
for c in corners:
if (c.uv.y > secondHighest.uv.y):
secondHighest = c
4 years ago
4 years ago
if firstHighest.uv.x < secondHighest.uv.x:
leftUp = firstHighest
rightUp = secondHighest
else:
leftUp = secondHighest
rightUp = firstHighest
corners.remove(secondHighest)
4 years ago
4 years ago
firstLowest = corners[0]
secondLowest = corners[1]
4 years ago
4 years ago
if firstLowest.uv.x < secondLowest.uv.x:
leftDown = firstLowest
rightDown = secondLowest
else:
leftDown = secondLowest
rightDown = firstLowest
4 years ago
return leftUp, leftDown, rightUp, rightDown
4 years ago
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:
4 years ago
return True
return False
def hypotVert(v1, v2):
hyp = hypot(v1.x - v2.x, v1.y - v2.y)
return hyp
4 years ago
bpy.utils.register_class(op)