#!BPY
"""
Name: 'UV Tool'
Blender: 248
Group: 'UV'
Tooltip: 'Some tools to manage UVs (super weld, distribute, rectify)'
"""
__author__ = "Guillaume Englert"
__version__ = "0.3 2009/07/15"
__url__ = "Online doc , http://www.hybird.org/~guiea_7/"
__email__ = "GuieA_7, genglert:hybird*org"
__bpydoc__ = """\
Some tools to manage UVs:
- Super weld : weld the selected UV by group, like 'remove doubles'.
- Distribute : distribute regularly UV points which are (nearly) aligned.
- Rectify : rotate UV groups in order to have vertical and horizontal edges.
"""
################################################################################
# #
# GNU GPL LICENSE #
# --------------- #
# #
# Copyright (C) 2006-2009: Guillaume Englert #
# #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the #
# Free Software Foundation; either version 2 of the License, or (at your #
# option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program; if not, write to the Free Software Foundation, #
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #
# #
################################################################################
################################################################################
# Importing modules
################################################################################
from Blender import Mesh, Redraw
from Blender.Object import GetSelected
from Blender.Window import EditMode
from Blender.Draw import PupMenu
from exceptions import Exception
from itertools import islice
################################################################################
# EXCEPTION #
################################################################################
class UVToolError(Exception):
def __init__(self, msg):
Exception.__init__(self)
self.msg = msg
def __str__(self):
return self.msg
################################################################################
# CLASSES #
################################################################################
class UVToolAction(object):
"""Action that can do this module, like projection or intersection."""
def __init__(self, function, label, **kw):
"""Constructor.
function: function object that do the action
label: description of the action (for menu)
"""
self.__function = function
self.__label = label
for key, val in kw.iteritems():
setattr(self, key, val)
def __call__(self, *args):
return self.__function(self, *args)
@property
def label(self):
return self.__label
#-------------------------------------------------------------------------------
class FaceUV(object):
"""Owner of an UV coordinate."""
__slots__ = ('faceid', 'uvid')
def __init__(self, faceid, uvid):
"""Constructor.
faceid: index of the face.
uvid: index of the UV coord for this face.
"""
self.faceid = faceid
self.uvid = uvid
#-------------------------------------------------------------------------------
class UVPoint(object):
"""An UV coordinate, which is shared by some faces."""
def __init__(self, uv, faceuv):
"""Constructor.
uv: UV coordinates (Blender.Mathutils.Vector).
faceuv: a FaceUV object which corresponds to this UV point.
"""
self.uv = uv
self.facelst = [faceuv] #list of FaceUV objects which share this UV coordinate
################################################################################
# CONSTANTS #
################################################################################
EPSILON = 0.001
################################################################################
# GLOBALS #
################################################################################
actions = [] #list of UVToolAction objects
################################################################################
# FUNCTIONS #
################################################################################
##############
# DECORATORS #
##############
def actionfunc(label, **kw):
"""Decorator that builds a UVToolAction which have a method that corresponds
to the decoated function, and adds the new action to the global actions list.
label: label that describe the function (used in the popup menu).
kw: keyword parameter will be added as members to the UVToolAction object.
Notice that decorated function are on the model:
def foobar(self, mesh):
...
where self is the UVToolAction object, and mesh the Blender.Mesh.Mesh concerned.
"""
def _deco(func):
actions.append(UVToolAction(func, label, **kw))
return func
return _deco
##################
# USER INTERFACE #
##################
def get_action():
"""Ask to the user the action he wants.
return: None or a UVToolAction object
"""
ret = PupMenu('UV tool%t|' + '|'.join(action.label for action in actions))
if ret == -1: #Cancel
return None
else:
return actions[ret-1]
#-------------------------------------------------------------------------------
def display_error(string):
PupMenu("ERROR: " + string)
#-------------------------------------------------------------------------------
#########
# UTILS #
#########
def build_uvpoints(mfaces, uvptclass=UVPoint):
"""Build the list of UVPoint objects.
mfaces: faces list of the mesh.
uvptclass: class of UV point to use (UVPoint or a child class)
return: a list of uvptclass objects.
"""
uvpts = [] #UV Points
for face in mfaces:
if not face.sel:
continue
for i, flag in enumerate(face.uvSel):
if not flag: continue
uv = face.uv[i]
for uvpt in uvpts:
if (uv - uvpt.uv).length < EPSILON:
uvpt.facelst.append(FaceUV(face.index, i))
break
else:
uvpts.append(uvptclass(uv, FaceUV(face.index, i)))
return uvpts
#-------------------------------------------------------------------------------
#######
# UV #
#######
@actionfunc("Super weld")
def superweld(self, mesh):
"""Find groups of UV coordinates that are close, and weld them.
mesh: selected mesh (Blender.Mesh.Mesh object).
"""
from Blender.Mathutils import Vector
from Blender.Draw import PupFloatInput
#get the limit for the weld
epsilon = PupFloatInput("Limit", 0.001, 0.0001, 1.0, 0.1, 4)
if not epsilon: return
class _TaggedUV(object):
__slots__ = ('uv', 'flag', 'faceid', 'uvid')
def __init__(self, uv, faceid, uvid):
self.uv = uv #UV coord (Vector)
self.flag = False #if the object belongs to a group
self.faceid = faceid #index of the face
self.uvid = uvid #index of the UV coord for this face
uvlist = [_TaggedUV(face.uv[i], face.index, i)
for face in mesh.faces
for i, flag in enumerate(face.uvSel) if flag]
uvlist.sort(key=lambda v: v.uv.x) #X coord sort
groups = []
for i1, uv1 in enumerate(islice(uvlist, len(uvlist)-1)):
if uv1.flag: continue
group = [uv1]
x = uv1.uv.x
for uv2 in islice(uvlist, i1, len(uvlist)):
if abs(x - uv2.uv.x) > epsilon: break
if uv2.flag: continue
if (uv1.uv - uv2.uv).length < epsilon:
#if UVs are close --> group them
uv2.flag = True
group.append(uv2)
groups.append(group)
mfaces = mesh.faces
nullv = Vector(0.0, 0.0)
for group in groups:
#final UV = average of UV coordinates of the group
finaluv = sum((tuv.uv for tuv in group), nullv) * (1.0/len(group))
for tuv in group:
uv = mfaces[tuv.faceid].uv[tuv.uvid]
uv.x = finaluv.x
uv.y = finaluv.y
mesh.update()
#-------------------------------------------------------------------------------
@actionfunc("Distribute")
def distribute(self, mesh):
"""Distribute regularly UV coordinates beetween the 2 extremities.
mesh: selected mesh (Blender.Mesh.Mesh object).
"""
mfaces = mesh.faces
uvpts = build_uvpoints(mfaces)
if len(uvpts) < 3: return
#sort the UV points to get a list of aligned point (normally :)
uvpts.sort(key=lambda pt: pt.uv.x) #X coord sort
uvptstmp = list(uvpts)
uvptstmp.sort(key=lambda pt: pt.uv.y) #Y coord sort
#compare the x diff and the y diff
if abs((uvpts[0].uv.x - uvpts[-1].uv.x)) < abs((uvptstmp[0].uv.y - uvptstmp[-1].uv.y)):
uvpts = uvptstmp
del uvptstmp
#UV points are regularly aligned
point = uvpts[0].uv
vect = (uvpts[-1].uv - point) * (1.0/(len(uvpts)-1))
for mult, pt in enumerate(uvpts):
finaluv = mult * vect + point
for face in pt.facelst:
uv = mfaces[face.faceid].uv[face.uvid]
uv.x = finaluv.x
uv.y = finaluv.y
mesh.update()
#-------------------------------------------------------------------------------
@actionfunc("Rectify")
def rectify(self, mesh):
from Blender.Mathutils import Vector, AngleBetweenVecs, RotationMatrix, Matrix
from itertools import islice
class _UVPoint(UVPoint):
def __init__(self, uv, faceuv):
UVPoint.__init__(self, uv, faceuv)
self.faceset = None #set of the faces indices
def compute_faceset(self):
self.faceset = set(faceuv.faceid for faceuv in self.facelst)
#---------------------------------------------------------------------------
class _Forest:
"""A group of connexity trees, in order to know the connex group"""
def __init__(self, links_lst):
"""Constructor.
links_lst: list of links ((int, int) tuples).
"""
# key: value --> value is the parent of key (for roots, key is None).
self._links = dict((v, None) for link in links_lst for v in link)
#key: value --> value is the size of the tree whose root is key.
self._sizes = dict((key, 1) for key in self._links.iterkeys())
for v1, v2 in links:
self._union(v1, v2)
def _root(self, key):
links = self._links
tmpkey = links[key]
while tmpkey is not None:
key = tmpkey
tmpkey = links[key]
return key
def _union(self, key1, key2):
root1 = self._root(key1)
root2 = self._root(key2)
if root1 != root2:
size1 = self._sizes[root1]
size2 = self._sizes[root2]
if size1 < size2:
self._links[root1] = root2
self._sizes[root2] += size1
else:
self._links[root2] = root1
self._sizes[root1] += size2
def get_groups(self):
"""Get the connexity groups.
return: a list of groups. A group is a list of ints.
"""
trees = dict((k, []) for k, v in self._links.iteritems() if v is None)
for k in self._links.iterkeys():
trees[self._root(k)].append(k)
return trees.values()
#---------------------------------------------------------------------------
def _build_links(uvpts):
"""Build the links between UV points.
uvpts: list of UVPoint objects.
return: list of links. A link is a (int, int) tuple, where integers are
indices of UV points in uvpts."""
def _find_uvid(uvpt, faceid):
for uvface in uvpt.facelst:
if faceid == uvface.faceid:
return uvface.uvid
def _link_gen():
for i, uvpt1 in enumerate(islice(uvpts, len(uvpts)-1)):
faceset1 = uvpt1.faceset
for j, uvpt2 in enumerate(islice(uvpts, i+1, len(uvpts))):
for faceid in faceset1 & uvpt2.faceset:
diff = abs(_find_uvid(uvpt1, faceid) - _find_uvid(uvpt2, faceid))
if (diff == 1) or (diff == len(mfaces[faceid].uv)-1):
yield (i, j+i+1)
break
for uvpt in uvpts:
uvpt.compute_faceset()
return [link for link in _link_gen()]
#---------------------------------------------------------------------------
def _angle(vect):
"""Angle of a vector ; angle is beetween -45. and 45 degrees.
vect: vector (Blender.Mathutils.Vector object).
return: the angle.
"""
if vect.x < 0.:
vect = -vect
angle = AngleBetweenVecs(unitv, vect)
if angle >= 45.: #-45 =< angle < 45
angle -= 90.
if vect.y >= 0.:
return -angle
return angle
def _compute_bestangle(vects):
"""Get the best angle with a list of vector.
vects: a list of Blender.Mathutils.Vector objects.
return: the best angle
"""
angles = [_angle(vect) for vect in vects]
angles.sort()
bestangle = None #0.0
maxcount = 1
curcount = 1 #current count
epsilon = 0.001
it = iter(angles)
oldangle = it.next()
for angle in it:
if (angle-oldangle) < epsilon:
curcount += 1
if curcount > maxcount:
maxcount = curcount
bestangle = angle
else:
curcount = 1
oldangle = angle
if bestangle is None: #take the longest vector
maxlen = -1.0
for vect in vects:
vlen = vect.length
if vlen > maxlen:
maxlen = vlen
bestvect = vect
bestangle = _angle(bestvect)
return bestangle
#---------------------------------------------------------------------------
mfaces = mesh.faces
uvpts = build_uvpoints(mfaces, _UVPoint)
links = _build_links(uvpts)
clusters = _Forest(links).get_groups()
clusterdic = dict((i, j) for j, cluster in enumerate(clusters) for i in cluster)
vectlst = [[] for i in clusters]
for l1, l2 in links:
vectlst[clusterdic[l1]].append(uvpts[l2].uv - uvpts[l1].uv)
nullv = Vector(0.0, 0.0) #null vector
unitv = Vector(1.0, 0.0) #x axis unit vector
for clustind, cluster in enumerate(clusters):
center = sum((uvpts[i].uv for i in cluster), nullv) * (1.0/len(cluster))
bestangle = _compute_bestangle(vectlst[clustind])
mat = Matrix([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-center.x, -center.y, 1.0]) #translation matrix
mat *= RotationMatrix(bestangle, 3, 'z')
mat *= Matrix([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [center.x, center.y, 1.0]) #translation matrix
for i in cluster:
uvpt = uvpts[i]
uvvect = uvpt.uv
finaluv = Vector(uvvect.x, uvvect.y, 1.0)
finaluv *= mat
finaluv.resize2D()
for face in uvpt.facelst:
uv = mfaces[face.faceid].uv[face.uvid]
uv.x = finaluv.x
uv.y = finaluv.y
mesh.update()
################################################################################
# MAIN FUNCTION #
################################################################################
def main():
#init
is_editmode = EditMode()
if is_editmode:
EditMode(0)
try:
#get selected object (or quit)
objs = GetSelected()
if not objs:
raise UVToolError("none selected object")
if len(objs) > 1:
raise UVToolError("only one object must be selected")
obj = objs[0]
if obj.getType() != "Mesh":
raise UVToolError("active object must be a mesh")
mesh = obj.getData(mesh=True)
if not mesh.faceUV:
raise UVToolError("active object must have UV")
#do the action
action = get_action()
if action:
action(mesh)
except UVToolError, e:
print e
display_error(str(e))
except:
import sys
sys.excepthook(*sys.exc_info())
display_error("An exception occured | (look at the terminal)")
#finish
if is_editmode :
EditMode(1)
################################################################################
# MAIN PROGRAM #
################################################################################
main()