Paste Code
Paste Blends
Paste Images
# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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; version 2
# of the License.
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
"name": "UV Align/Distribute",
"author": "Rebellion (Luca Carella)",
"version": (1, 3),
"blender": (2, 7, 3),
"location": "UV/Image editor > Tool Panel, UV/Image editor UVs > menu",
"description": "Set of tools to help UV alignment\distribution",
"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/UV/UV_Align_Distribution",
"category": "UV"}

import math
from collections import defaultdict

import bmesh
import bpy
import mathutils
from bpy.props import EnumProperty, BoolProperty, FloatProperty


# Globals:
bpy.types.Scene.relativeItems = EnumProperty(
items=[
('UV_SPACE', 'Uv Space', 'Align to UV space'),
('ACTIVE', 'Active Face', 'Align to active face\island'),
('CURSOR', 'Cursor', 'Align to cursor')],
name="Relative to")

bpy.types.Scene.selectionAsGroup = BoolProperty(
name="Selection as group",
description="Treat selection as group",
default=False)

bm = None
uvlayer = None


def InitBMesh():
global bm
global uvlayer
bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
bm.faces.ensure_lookup_table()
uvlayer = bm.loops.layers.uv.active


def update():
bmesh.update_edit_mesh(bpy.context.edit_object.data, False, False)
# bm.to_mesh(bpy.context.object.data)
# bm.free()


def GBBox(islands):
minX = minY = 1000
maxX = maxY = -1000
for island in islands:
for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
u, v = loop[uvlayer].uv
minX = min(u, minX)
minY = min(v, minY)
maxX = max(u, maxX)
maxY = max(v, maxY)

return mathutils.Vector((minX, minY)), mathutils.Vector((maxX, maxY))


def GBBoxCenter(islands):
minX = minY = 1000
maxX = maxY = -1000
for island in islands:
for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
u, v = loop[uvlayer].uv
minX = min(u, minX)
minY = min(v, minY)
maxX = max(u, maxX)
maxY = max(v, maxY)

return (mathutils.Vector((minX, minY)) +
mathutils.Vector((maxX, maxY))) / 2


def BBox(island):
minX = minY = 1000
maxX = maxY = -1000
# for island in islands:
# print(island)
for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
u, v = loop[uvlayer].uv
minX = min(u, minX)
minY = min(v, minY)
maxX = max(u, maxX)
maxY = max(v, maxY)

return mathutils.Vector((minX, minY)), mathutils.Vector((maxX, maxY))


def BBoxCenter(island):
minX = minY = 1000
maxX = maxY = -1000
# for island in islands:
for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
u, v = loop[uvlayer].uv
minX = min(u, minX)
minY = min(v, minY)
maxX = max(u, maxX)
maxY = max(v, maxY)

return (mathutils.Vector((minX, minY)) +
mathutils.Vector((maxX, maxY))) / 2


def islandAngle(island):
uvList = []
for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
uv = loop[bm.loops.layers.uv.active].uv
uvList.append(uv)

angle = math.degrees(mathutils.geometry.box_fit_2d(uvList))
return angle


def moveIslands(vector, island):
for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
loop[bm.loops.layers.uv.active].uv += vector


def rotateIsland(island, angle):
rad = math.radians(angle)
center = BBoxCenter(island)

for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
uv_act = bm.loops.layers.uv.active
x, y = loop[uv_act].uv
xt = x - center.x
yt = y - center.y
xr = (xt * math.cos(rad)) - (yt * math.sin(rad))
yr = (xt * math.sin(rad)) + (yt * math.cos(rad))
# loop[bm.loops.layers.uv.active].uv = trans
loop[bm.loops.layers.uv.active].uv.x = xr + center.x
loop[bm.loops.layers.uv.active].uv.y = yr + center.y
# print('fired')


def scaleIsland(island, scaleX, scaleY):
scale = mathutils.Vector((scaleX, scaleY))
center = BBoxCenter(island)

for face_id in island:
face = bm.faces[face_id]
for loop in face.loops:
x = loop[bm.loops.layers.uv.active].uv.x
y = loop[bm.loops.layers.uv.active].uv.y
xt = x - center.x
yt = y - center.y
xs = xt * scaleX
ys = yt * scaleY
loop[bm.loops.layers.uv.active].uv.x = xs + center.x
loop[bm.loops.layers.uv.active].uv.y = ys + center.y


def vectorDistance(vector1, vector2):
return math.sqrt(
math.pow((vector2.x - vector1.x), 2) +
math.pow((vector2.y - vector1.y), 2))


def matchIsland(active, thresold, island):
for active_face_id in active:
active_face = bm.faces[active_face_id]

for active_loop in active_face.loops:
activeUVvert = active_loop[bm.loops.layers.uv.active].uv

for face_id in island:
face = bm.faces[face_id]

for loop in face.loops:
selectedUVvert = loop[bm.loops.layers.uv.active].uv
dist = vectorDistance(selectedUVvert, activeUVvert)

if dist <= thresold:
loop[bm.loops.layers.uv.active].uv = activeUVvert


def getTargetPoint(context, islands):
if context.scene.relativeItems == 'UV_SPACE':
return mathutils.Vector((0.0, 0.0)), mathutils.Vector((1.0, 1.0))
elif context.scene.relativeItems == 'ACTIVE':
activeIsland = islands.activeIsland()
if not activeIsland:
return None
else:
return BBox(activeIsland)
elif context.scene.relativeItems == 'CURSOR':
return context.space_data.cursor_location,\
context.space_data.cursor_location


def IslandSpatialSortX(islands):
spatialSort = []
for island in islands:
spatialSort.append((BBoxCenter(island).x, island))
spatialSort.sort()
return spatialSort


def IslandSpatialSortY(islands):
spatialSort = []
for island in islands:
spatialSort.append((BBoxCenter(island).y, island))
spatialSort.sort()
return spatialSort


def averageIslandDist(islands):
distX = 0
distY = 0
counter = 0

for i in range(len(islands)):
elem1 = BBox(islands[i][1])[1]
try:
elem2 = BBox(islands[i + 1][1])[0]
counter += 1
except:
break

distX += elem2.x - elem1.x
distY += elem2.y - elem1.y

avgDistX = distX / counter
avgDistY = distY / counter
return mathutils.Vector((avgDistX, avgDistY))


def islandSize(island):
bbox = BBox(island)
sizeX = bbox[1].x - bbox[0].x
sizeY = bbox[1].y - bbox[0].y

return sizeX, sizeY


class MakeIslands():

def __init__(self):
InitBMesh()
global bm
global uvlayer

self.face_to_verts = defaultdict(set)
self.vert_to_faces = defaultdict(set)
self.selectedIsland = set()

for face in bm.faces:
for loop in face.loops:
id = loop[uvlayer].uv.to_tuple(5), loop.vert.index
self.face_to_verts[face.index].add(id)
self.vert_to_faces[id].add(face.index)
if face.select:
if loop[uvlayer].select:
self.selectedIsland.add(face.index)

def addToIsland(self, face_id):
if face_id in self.faces_left:
# add the face itself
self.current_island.append(face_id)
self.faces_left.remove(face_id)
# and add all faces that share uvs with this face
verts = self.face_to_verts[face_id]
for vert in verts:
# print('looking at vert {}'.format(vert))
connected_faces = self.vert_to_faces[vert]
if connected_faces:
for face in connected_faces:
self.addToIsland(face)

def getIslands(self):
self.islands = []
self.faces_left = set(self.face_to_verts.keys())
while len(self.faces_left) > 0:
face_id = list(self.faces_left)[0]
self.current_island = []
self.addToIsland(face_id)
self.islands.append(self.current_island)

return self.islands

def activeIsland(self):
for island in self.islands:
try:
if bm.faces.active.index in island:
return island
except:
return None

def selectedIslands(self):
_selectedIslands = []
for island in self.islands:
if not self.selectedIsland.isdisjoint(island):
_selectedIslands.append(island)
return _selectedIslands


# #####################
# OPERATOR
# #####################

class OperatorTemplate(bpy.types.Operator):

@classmethod
def poll(cls, context):
return not (context.scene.tool_settings.use_uv_select_sync)


#####################
# ALIGN
#####################

class AlignSXMargin(OperatorTemplate):

"""Align left margin"""
bl_idname = "uv.align_left_margin"
bl_label = "Align left margin"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):

makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

targetElement = getTargetPoint(context, makeIslands)
if not targetElement:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}

if context.scene.selectionAsGroup:
groupBox = GBBox(selectedIslands)
if context.scene.relativeItems == 'ACTIVE':
selectedIslands.remove(makeIslands.activeIsland())
for island in selectedIslands:
vector = mathutils.Vector((targetElement[0].x - groupBox[0].x,
0.0))
moveIslands(vector, island)

else:
for island in selectedIslands:
vector = mathutils.Vector(
(targetElement[0].x - BBox(island)[0].x, 0.0))
moveIslands(vector, island)

update()
return {'FINISHED'}


class AlignRxMargin(OperatorTemplate):

"""Align right margin"""
bl_idname = "uv.align_right_margin"
bl_label = "Align right margin"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

targetElement = getTargetPoint(context, makeIslands)
if not targetElement:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}

if context.scene.selectionAsGroup:
groupBox = GBBox(selectedIslands)
if context.scene.relativeItems == 'ACTIVE':
selectedIslands.remove(makeIslands.activeIsland())
for island in selectedIslands:
vector = mathutils.Vector((targetElement[1].x - groupBox[1].x,
0.0))
moveIslands(vector, island)

else:
for island in selectedIslands:
vector = mathutils.Vector(
(targetElement[1].x - BBox(island)[1].x, 0.0))
moveIslands(vector, island)

update()
return {'FINISHED'}


class AlignVAxis(OperatorTemplate):

"""Align vertical axis"""
bl_idname = "uv.align_vertical_axis"
bl_label = "Align vertical axis"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

targetElement = getTargetPoint(context, makeIslands)
if not targetElement:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}
targetCenter = (targetElement[0] + targetElement[1]) / 2
if context.scene.selectionAsGroup:
groupBoxCenter = GBBoxCenter(selectedIslands)
if context.scene.relativeItems == 'ACTIVE':
selectedIslands.remove(makeIslands.activeIsland())
for island in selectedIslands:
vector = mathutils.Vector(
(targetCenter.x - groupBoxCenter.x, 0.0))
moveIslands(vector, island)

else:
for island in selectedIslands:
vector = mathutils.Vector(
(targetCenter.x - BBoxCenter(island).x, 0.0))
moveIslands(vector, island)

update()
return {'FINISHED'}


##################################################
class AlignTopMargin(OperatorTemplate):

"""Align top margin"""
bl_idname = "uv.align_top_margin"
bl_label = "Align top margin"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):

makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

targetElement = getTargetPoint(context, makeIslands)
if not targetElement:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}
if context.scene.selectionAsGroup:
groupBox = GBBox(selectedIslands)
if context.scene.relativeItems == 'ACTIVE':
selectedIslands.remove(makeIslands.activeIsland())
for island in selectedIslands:
vector = mathutils.Vector(
(0.0, targetElement[1].y - groupBox[1].y))
moveIslands(vector, island)

else:
for island in selectedIslands:
vector = mathutils.Vector(
(0.0, targetElement[1].y - BBox(island)[1].y))
moveIslands(vector, island)

update()
return {'FINISHED'}


class AlignLowMargin(OperatorTemplate):

"""Align low margin"""
bl_idname = "uv.align_low_margin"
bl_label = "Align low margin"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

targetElement = getTargetPoint(context, makeIslands)
if not targetElement:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}
if context.scene.selectionAsGroup:
groupBox = GBBox(selectedIslands)
if context.scene.relativeItems == 'ACTIVE':
selectedIslands.remove(makeIslands.activeIsland())
for island in selectedIslands:
vector = mathutils.Vector(
(0.0, targetElement[0].y - groupBox[0].y))
moveIslands(vector, island)

else:
for island in selectedIslands:
vector = mathutils.Vector(
(0.0, targetElement[0].y - BBox(island)[0].y))
moveIslands(vector, island)

update()
return {'FINISHED'}


class AlignHAxis(OperatorTemplate):

"""Align horizontal axis"""
bl_idname = "uv.align_horizontal_axis"
bl_label = "Align horizontal axis"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

targetElement = getTargetPoint(context, makeIslands)
if not targetElement:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}
targetCenter = (targetElement[0] + targetElement[1]) / 2

if context.scene.selectionAsGroup:
groupBoxCenter = GBBoxCenter(selectedIslands)
if context.scene.relativeItems == 'ACTIVE':
selectedIslands.remove(makeIslands.activeIsland())
for island in selectedIslands:
vector = mathutils.Vector(
(0.0, targetCenter.y - groupBoxCenter.y))
moveIslands(vector, island)

else:
for island in selectedIslands:
vector = mathutils.Vector(
(0.0, targetCenter.y - BBoxCenter(island).y))
moveIslands(vector, island)

update()
return {'FINISHED'}


#########################################
class AlignRotation(OperatorTemplate):

"""Align island rotation """
bl_idname = "uv.align_rotation"
bl_label = "Align island rotation"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()
activeIsland = makeIslands.activeIsland()
if not activeIsland:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}
activeAngle = islandAngle(activeIsland)

for island in selectedIslands:
uvAngle = islandAngle(island)
deltaAngle = activeAngle - uvAngle
deltaAngle = round(-deltaAngle, 5)
rotateIsland(island, deltaAngle)

update()
return {'FINISHED'}


class EqualizeScale(OperatorTemplate):

"""Equalize the islands scale to the active one"""
bl_idname = "uv.equalize_scale"
bl_label = "Equalize Scale"
bl_options = {'REGISTER', 'UNDO'}

keepProportions = BoolProperty(
name="Keep Proportions",
description="Mantain proportions during scaling",
default=False)

useYaxis = BoolProperty(
name="Use Y axis",
description="Use y axis as scale reference, default is x",
default=False)

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()
activeIsland = makeIslands.activeIsland()

if not activeIsland:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}

activeSize = islandSize(activeIsland)
selectedIslands.remove(activeIsland)

for island in selectedIslands:
size = islandSize(island)
scaleX = activeSize[0] / size[0]
scaleY = activeSize[1] / size[1]

if self.keepProportions:
if self.useYaxis:
scaleX = scaleY
else:
scaleY = scaleX

scaleIsland(island, scaleX, scaleY)

update()
return {"FINISHED"}

def draw(self,context):
layout = self.layout
layout.prop(self, "keepProportions")
if self.keepProportions:
layout.prop(self,"useYaxis")


############################
# DISTRIBUTION
############################
class DistributeLEdgesH(OperatorTemplate):

"""Distribute left edges equidistantly horizontally"""
bl_idname = "uv.distribute_ledges_horizontally"
bl_label = "Distribute Left Edges Horizontally"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortX(selectedIslands)
uvFirstX = BBox(islandSpatialSort[0][1])[0].x
uvLastX = BBox(islandSpatialSort[-1][1])[0].x

distX = uvLastX - uvFirstX

deltaDist = distX / (len(selectedIslands) - 1)

islandSpatialSort.pop(0)
islandSpatialSort.pop(-1)

pos = uvFirstX + deltaDist

for island in islandSpatialSort:
vec = mathutils.Vector((pos - BBox(island[1])[0].x, 0.0))
pos += deltaDist
moveIslands(vec, island[1])
update()
return {"FINISHED"}


class DistributeCentersH(OperatorTemplate):

"""Distribute centers equidistantly horizontally"""
bl_idname = "uv.distribute_center_horizontally"
bl_label = "Distribute Centers Horizontally"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortX(selectedIslands)
uvFirstX = min(islandSpatialSort)
uvLastX = max(islandSpatialSort)

distX = uvLastX[0] - uvFirstX[0]

deltaDist = distX / (len(selectedIslands) - 1)

islandSpatialSort.pop(0)
islandSpatialSort.pop(-1)

pos = uvFirstX[0] + deltaDist

for island in islandSpatialSort:
vec = mathutils.Vector((pos - BBoxCenter(island[1]).x, 0.0))
pos += deltaDist
moveIslands(vec, island[1])
update()
return {"FINISHED"}


class DistributeREdgesH(OperatorTemplate):

"""Distribute right edges equidistantly horizontally"""
bl_idname = "uv.distribute_redges_horizontally"
bl_label = "Distribute Right Edges Horizontally"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortX(selectedIslands)
uvFirstX = BBox(islandSpatialSort[0][1])[1].x
uvLastX = BBox(islandSpatialSort[-1][1])[1].x

distX = uvLastX - uvFirstX

deltaDist = distX / (len(selectedIslands) - 1)

islandSpatialSort.pop(0)
islandSpatialSort.pop(-1)

pos = uvFirstX + deltaDist

for island in islandSpatialSort:
vec = mathutils.Vector((pos - BBox(island[1])[1].x, 0.0))
pos += deltaDist
moveIslands(vec, island[1])
update()
return {"FINISHED"}


class DistributeTEdgesV(OperatorTemplate):

"""Distribute top edges equidistantly vertically"""
bl_idname = "uv.distribute_tedges_vertically"
bl_label = "Distribute Top Edges Vertically"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortY(selectedIslands)
uvFirstX = BBox(islandSpatialSort[0][1])[1].y
uvLastX = BBox(islandSpatialSort[-1][1])[1].y

distX = uvLastX - uvFirstX

deltaDist = distX / (len(selectedIslands) - 1)

islandSpatialSort.pop(0)
islandSpatialSort.pop(-1)

pos = uvFirstX + deltaDist

for island in islandSpatialSort:
vec = mathutils.Vector((0.0, pos - BBox(island[1])[1].y))
pos += deltaDist
moveIslands(vec, island[1])
update()
return {"FINISHED"}


class DistributeCentersV(OperatorTemplate):

"""Distribute centers equidistantly vertically"""
bl_idname = "uv.distribute_center_vertically"
bl_label = "Distribute Centers Vertically"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortY(selectedIslands)
uvFirst = BBoxCenter(islandSpatialSort[0][1]).y
uvLast = BBoxCenter(islandSpatialSort[-1][1]).y

dist = uvLast - uvFirst

deltaDist = dist / (len(selectedIslands) - 1)

islandSpatialSort.pop(0)
islandSpatialSort.pop(-1)

pos = uvFirst + deltaDist

for island in islandSpatialSort:
vec = mathutils.Vector((0.0, pos - BBoxCenter(island[1]).y))
pos += deltaDist
moveIslands(vec, island[1])
update()
return {"FINISHED"}


class DistributeBEdgesV(OperatorTemplate):

"""Distribute bottom edges equidistantly vertically"""
bl_idname = "uv.distribute_bedges_vertically"
bl_label = "Distribute Bottom Edges Vertically"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortY(selectedIslands)
uvFirst = BBox(islandSpatialSort[0][1])[0].y
uvLast = BBox(islandSpatialSort[-1][1])[0].y

dist = uvLast - uvFirst

deltaDist = dist / (len(selectedIslands) - 1)

islandSpatialSort.pop(0)
islandSpatialSort.pop(-1)

pos = uvFirst + deltaDist

for island in islandSpatialSort:
vec = mathutils.Vector((0.0, pos - BBox(island[1])[0].y))
pos += deltaDist
moveIslands(vec, island[1])
update()
return {"FINISHED"}


class EqualizeHGap(OperatorTemplate):

"""Equalize horizontal gap between island"""
bl_idname = "uv.equalize_horizontal_gap"
bl_label = "Equalize Horizontal Gap"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortX(selectedIslands)

averageDist = averageIslandDist(islandSpatialSort)

for i in range(len(islandSpatialSort)):
if islandSpatialSort.index(islandSpatialSort[i + 1]) == \
islandSpatialSort.index(islandSpatialSort[-1]):
break
elem1 = BBox(islandSpatialSort[i][1])[1].x
elem2 = BBox(islandSpatialSort[i + 1][1])[0].x

dist = elem2 - elem1
increment = averageDist.x - dist

vec = mathutils.Vector((increment, 0.0))
island = islandSpatialSort[i + 1][1]
moveIslands(vec, islandSpatialSort[i + 1][1])
update()
return {"FINISHED"}


class EqualizeVGap(OperatorTemplate):

"""Equalize vertical gap between island"""
bl_idname = "uv.equalize_vertical_gap"
bl_label = "Equalize Vertical Gap"
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()

if len(selectedIslands) < 3:
return {'CANCELLED'}

islandSpatialSort = IslandSpatialSortY(selectedIslands)

averageDist = averageIslandDist(islandSpatialSort)

for i in range(len(islandSpatialSort)):
if islandSpatialSort.index(islandSpatialSort[i + 1]) ==\
islandSpatialSort.index(islandSpatialSort[-1]):
break
elem1 = BBox(islandSpatialSort[i][1])[1].y
elem2 = BBox(islandSpatialSort[i + 1][1])[0].y

dist = elem2 - elem1

increment = averageDist.y - dist

vec = mathutils.Vector((0.0, increment))
island = islandSpatialSort[i + 1][1]

moveIslands(vec, islandSpatialSort[i + 1][1])
update()
return {"FINISHED"}

##############
# SPECIALS
##############


class MatchIsland(OperatorTemplate):

"""Match UV Island by moving their vertex"""
bl_idname = "uv.match_island"
bl_label = "Match Island"
bl_options = {'REGISTER', 'UNDO'}

threshold = FloatProperty(
name="Threshold",
description="Threshold for island matching",
default=0.1,
min=0,
max=1,
soft_min=0.01,
soft_max=1,
step=1,
precision=2)

def execute(self, context):
makeIslands = MakeIslands()
islands = makeIslands.getIslands()
selectedIslands = makeIslands.selectedIslands()
activeIsland = makeIslands.activeIsland()

if not activeIsland:
self.report({"ERROR"}, "No active face")
return {"CANCELLED"}

if len(selectedIslands) < 2:
return {'CANCELLED'}

selectedIslands.remove(activeIsland)

for island in selectedIslands:
matchIsland(activeIsland, self.threshold, island)

update()
return{'FINISHED'}


##############
# UI
##############
class IMAGE_PT_align_distribute(bpy.types.Panel):
bl_label = "Align\Distribute"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'TOOLS'
bl_category = "Tools"

@classmethod
def poll(cls, context):
sima = context.space_data
return sima.show_uvedit and \
not (context.tool_settings.use_uv_sculpt
or context.scene.tool_settings.use_uv_select_sync)

def draw(self, context):
scn = context.scene
layout = self.layout
layout.prop(scn, "relativeItems")
layout.prop(scn, "selectionAsGroup")

layout.separator()
layout.label(text="Align:")

box = layout.box()
row = box.row(True)
row.operator("uv.align_left_margin", "Left")
row.operator("uv.align_vertical_axis", "VAxis")
row.operator("uv.align_right_margin", "Right")
row = box.row(True)
row.operator("uv.align_top_margin", "Top")
row.operator("uv.align_horizontal_axis", "HAxis")
row.operator("uv.align_low_margin", "Low")

row = layout.row()
row.operator("uv.align_rotation", "Rotation")
row.operator("uv.equalize_scale", "Eq. Scale")

layout.separator()
# Another Panel??
layout.label(text="Distribute:")

box = layout.box()

row = box.row(True)
row.operator("uv.distribute_ledges_horizontally", "LEdges")

row.operator("uv.distribute_center_horizontally",
"HCenters")

row.operator("uv.distribute_redges_horizontally",
"RCenters")

row = box.row(True)
row.operator("uv.distribute_tedges_vertically", "TEdges")
row.operator("uv.distribute_center_vertically", "VCenters")
row.operator("uv.distribute_bedges_vertically", "BEdges")

row = layout.row(True)
row.operator("uv.equalize_horizontal_gap", "Eq. HGap")
row.operator("uv.equalize_vertical_gap", "Eq. VGap")

layout.separator()
layout.label("Others:")
row = layout.row()
layout.operator("uv.match_island")


# Registration
classes = (
IMAGE_PT_align_distribute,
AlignSXMargin,
AlignRxMargin,
AlignVAxis,
AlignTopMargin,
AlignLowMargin,
AlignHAxis,
AlignRotation,
DistributeLEdgesH,
DistributeCentersH,
DistributeREdgesH,
DistributeTEdgesV,
DistributeCentersV,
DistributeBEdgesV,
EqualizeHGap,
EqualizeVGap,
EqualizeScale,
MatchIsland)


def register():
for item in classes:
bpy.utils.register_class(item)
# bpy.utils.register_manual_map(add_object_manual_map)
# bpy.types.INFO_MT_mesh_add.append(add_object_button)


def unregister():
for item in classes:
bpy.utils.unregister_class(item)
# bpy.utils.unregister_manual_map(add_object_manual_map)
# bpy.types.INFO_MT_mesh_add.remove(add_object_button)


if __name__ == "__main__":
register()
  1. # ##### BEGIN GPL LICENSE BLOCK #####
  2. #
  3. #  This program is free software; you can redistribute it and/or
  4. #  modify it under the terms of the GNU General Public License
  5. #  as published by the Free Software Foundation; version 2
  6. #  of the License.
  7. #
  8. #  This program is distributed in the hope that it will be useful,
  9. #  but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  11. #  GNU General Public License for more details.
  12. #
  13. #  You should have received a copy of the GNU General Public License
  14. #  along with this program; if not, write to the Free Software Foundation,
  15. #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # ##### END GPL LICENSE BLOCK #####
  18.  
  19. bl_info = {
  20.     "name": "UV Align/Distribute",
  21.     "author": "Rebellion (Luca Carella)",
  22.     "version": (1, 3),
  23.     "blender": (2, 7, 3),
  24.     "location": "UV/Image editor > Tool Panel, UV/Image editor UVs > menu",
  25.     "description": "Set of tools to help UV alignment\distribution",
  26.     "warning": "",
  27.     "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/UV/UV_Align_Distribution",
  28.     "category": "UV"}
  29.  
  30. import math
  31. from collections import defaultdict
  32.  
  33. import bmesh
  34. import bpy
  35. import mathutils
  36. from bpy.props import EnumProperty, BoolProperty, FloatProperty
  37.  
  38.  
  39. # Globals:
  40. bpy.types.Scene.relativeItems = EnumProperty(
  41.     items=[
  42.         ('UV_SPACE', 'Uv Space', 'Align to UV space'),
  43.         ('ACTIVE', 'Active Face', 'Align to active face\island'),
  44.         ('CURSOR', 'Cursor', 'Align to cursor')],
  45.     name="Relative to")
  46.  
  47. bpy.types.Scene.selectionAsGroup = BoolProperty(
  48.     name="Selection as group",
  49.     description="Treat selection as group",
  50.     default=False)
  51.  
  52. bm = None
  53. uvlayer = None
  54.  
  55.  
  56. def InitBMesh():
  57.     global bm
  58.     global uvlayer
  59.     bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
  60.     bm.faces.ensure_lookup_table()
  61.     uvlayer = bm.loops.layers.uv.active
  62.  
  63.  
  64. def update():
  65.     bmesh.update_edit_mesh(bpy.context.edit_object.data, False, False)
  66.     # bm.to_mesh(bpy.context.object.data)
  67.     # bm.free()
  68.  
  69.  
  70. def GBBox(islands):
  71.     minX = minY = 1000
  72.     maxX = maxY = -1000
  73.     for island in islands:
  74.         for face_id in island:
  75.             face = bm.faces[face_id]
  76.             for loop in face.loops:
  77.                 u, v = loop[uvlayer].uv
  78.                 minX = min(u, minX)
  79.                 minY = min(v, minY)
  80.                 maxX = max(u, maxX)
  81.                 maxY = max(v, maxY)
  82.  
  83.     return mathutils.Vector((minX, minY)), mathutils.Vector((maxX, maxY))
  84.  
  85.  
  86. def GBBoxCenter(islands):
  87.     minX = minY = 1000
  88.     maxX = maxY = -1000
  89.     for island in islands:
  90.         for face_id in island:
  91.             face = bm.faces[face_id]
  92.             for loop in face.loops:
  93.                 u, v = loop[uvlayer].uv
  94.                 minX = min(u, minX)
  95.                 minY = min(v, minY)
  96.                 maxX = max(u, maxX)
  97.                 maxY = max(v, maxY)
  98.  
  99.     return (mathutils.Vector((minX, minY)) +
  100.             mathutils.Vector((maxX, maxY))) / 2
  101.  
  102.  
  103. def BBox(island):
  104.     minX = minY = 1000
  105.     maxX = maxY = -1000
  106.     # for island in islands:
  107.     # print(island)
  108.     for face_id in island:
  109.         face = bm.faces[face_id]
  110.         for loop in face.loops:
  111.             u, v = loop[uvlayer].uv
  112.             minX = min(u, minX)
  113.             minY = min(v, minY)
  114.             maxX = max(u, maxX)
  115.             maxY = max(v, maxY)
  116.  
  117.     return mathutils.Vector((minX, minY)), mathutils.Vector((maxX, maxY))
  118.  
  119.  
  120. def BBoxCenter(island):
  121.     minX = minY = 1000
  122.     maxX = maxY = -1000
  123.     # for island in islands:
  124.     for face_id in island:
  125.         face = bm.faces[face_id]
  126.         for loop in face.loops:
  127.             u, v = loop[uvlayer].uv
  128.             minX = min(u, minX)
  129.             minY = min(v, minY)
  130.             maxX = max(u, maxX)
  131.             maxY = max(v, maxY)
  132.  
  133.     return (mathutils.Vector((minX, minY)) +
  134.             mathutils.Vector((maxX, maxY))) / 2
  135.  
  136.  
  137. def islandAngle(island):
  138.     uvList = []
  139.     for face_id in island:
  140.         face = bm.faces[face_id]
  141.         for loop in face.loops:
  142.             uv = loop[bm.loops.layers.uv.active].uv
  143.             uvList.append(uv)
  144.  
  145.     angle = math.degrees(mathutils.geometry.box_fit_2d(uvList))
  146.     return angle
  147.  
  148.  
  149. def moveIslands(vector, island):
  150.     for face_id in island:
  151.         face = bm.faces[face_id]
  152.         for loop in face.loops:
  153.             loop[bm.loops.layers.uv.active].uv += vector
  154.  
  155.  
  156. def rotateIsland(island, angle):
  157.     rad = math.radians(angle)
  158.     center = BBoxCenter(island)
  159.  
  160.     for face_id in island:
  161.         face = bm.faces[face_id]
  162.         for loop in face.loops:
  163.             uv_act = bm.loops.layers.uv.active
  164.             x, y = loop[uv_act].uv
  165.             xt = x - center.x
  166.             yt = y - center.y
  167.             xr = (xt * math.cos(rad)) - (yt * math.sin(rad))
  168.             yr = (xt * math.sin(rad)) + (yt * math.cos(rad))
  169.             # loop[bm.loops.layers.uv.active].uv = trans
  170.             loop[bm.loops.layers.uv.active].uv.x = xr + center.x
  171.             loop[bm.loops.layers.uv.active].uv.y = yr + center.y
  172.             # print('fired')
  173.  
  174.  
  175. def scaleIsland(island, scaleX, scaleY):
  176.     scale = mathutils.Vector((scaleX, scaleY))
  177.     center = BBoxCenter(island)
  178.  
  179.     for face_id in island:
  180.         face = bm.faces[face_id]
  181.         for loop in face.loops:
  182.             x = loop[bm.loops.layers.uv.active].uv.x
  183.             y = loop[bm.loops.layers.uv.active].uv.y
  184.             xt = x - center.x
  185.             yt = y - center.y
  186.             xs = xt * scaleX
  187.             ys = yt * scaleY
  188.             loop[bm.loops.layers.uv.active].uv.x = xs + center.x
  189.             loop[bm.loops.layers.uv.active].uv.y = ys + center.y
  190.  
  191.  
  192. def vectorDistance(vector1, vector2):
  193.     return math.sqrt(
  194.         math.pow((vector2.x - vector1.x), 2) +
  195.         math.pow((vector2.y - vector1.y), 2))
  196.  
  197.  
  198. def matchIsland(active, thresold, island):
  199.     for active_face_id in active:
  200.         active_face = bm.faces[active_face_id]
  201.  
  202.         for active_loop in active_face.loops:
  203.             activeUVvert = active_loop[bm.loops.layers.uv.active].uv
  204.  
  205.             for face_id in island:
  206.                 face = bm.faces[face_id]
  207.  
  208.                 for loop in face.loops:
  209.                     selectedUVvert = loop[bm.loops.layers.uv.active].uv
  210.                     dist = vectorDistance(selectedUVvert, activeUVvert)
  211.  
  212.                     if dist <= thresold:
  213.                         loop[bm.loops.layers.uv.active].uv = activeUVvert
  214.  
  215.  
  216. def getTargetPoint(context, islands):
  217.     if context.scene.relativeItems == 'UV_SPACE':
  218.         return mathutils.Vector((0.0, 0.0)), mathutils.Vector((1.0, 1.0))
  219.     elif context.scene.relativeItems == 'ACTIVE':
  220.         activeIsland = islands.activeIsland()
  221.         if not activeIsland:
  222.             return None
  223.         else:
  224.             return BBox(activeIsland)
  225.     elif context.scene.relativeItems == 'CURSOR':
  226.         return context.space_data.cursor_location,\
  227.             context.space_data.cursor_location
  228.  
  229.  
  230. def IslandSpatialSortX(islands):
  231.     spatialSort = []
  232.     for island in islands:
  233.         spatialSort.append((BBoxCenter(island).x, island))
  234.     spatialSort.sort()
  235.     return spatialSort
  236.  
  237.  
  238. def IslandSpatialSortY(islands):
  239.     spatialSort = []
  240.     for island in islands:
  241.         spatialSort.append((BBoxCenter(island).y, island))
  242.     spatialSort.sort()
  243.     return spatialSort
  244.  
  245.  
  246. def averageIslandDist(islands):
  247.     distX = 0
  248.     distY = 0
  249.     counter = 0
  250.  
  251.     for i in range(len(islands)):
  252.         elem1 = BBox(islands[i][1])[1]
  253.         try:
  254.             elem2 = BBox(islands[i + 1][1])[0]
  255.             counter += 1
  256.         except:
  257.             break
  258.  
  259.         distX += elem2.x - elem1.x
  260.         distY += elem2.y - elem1.y
  261.  
  262.     avgDistX = distX / counter
  263.     avgDistY = distY / counter
  264.     return mathutils.Vector((avgDistX, avgDistY))
  265.  
  266.  
  267. def islandSize(island):
  268.     bbox = BBox(island)
  269.     sizeX = bbox[1].x - bbox[0].x
  270.     sizeY = bbox[1].y - bbox[0].y
  271.  
  272.     return sizeX, sizeY
  273.  
  274.  
  275. class MakeIslands():
  276.  
  277.     def __init__(self):
  278.         InitBMesh()
  279.         global bm
  280.         global uvlayer
  281.  
  282.         self.face_to_verts = defaultdict(set)
  283.         self.vert_to_faces = defaultdict(set)
  284.         self.selectedIsland = set()
  285.  
  286.         for face in bm.faces:
  287.             for loop in face.loops:
  288.                 id = loop[uvlayer].uv.to_tuple(5), loop.vert.index
  289.                 self.face_to_verts[face.index].add(id)
  290.                 self.vert_to_faces[id].add(face.index)
  291.                 if face.select:
  292.                     if loop[uvlayer].select:
  293.                         self.selectedIsland.add(face.index)
  294.  
  295.     def addToIsland(self, face_id):
  296.         if face_id in self.faces_left:
  297.             # add the face itself
  298.             self.current_island.append(face_id)
  299.             self.faces_left.remove(face_id)
  300.             # and add all faces that share uvs with this face
  301.             verts = self.face_to_verts[face_id]
  302.             for vert in verts:
  303.                 # print('looking at vert {}'.format(vert))
  304.                 connected_faces = self.vert_to_faces[vert]
  305.                 if connected_faces:
  306.                     for face in connected_faces:
  307.                         self.addToIsland(face)
  308.  
  309.     def getIslands(self):
  310.         self.islands = []
  311.         self.faces_left = set(self.face_to_verts.keys())
  312.         while len(self.faces_left) > 0:
  313.             face_id = list(self.faces_left)[0]
  314.             self.current_island = []
  315.             self.addToIsland(face_id)
  316.             self.islands.append(self.current_island)
  317.  
  318.         return self.islands
  319.  
  320.     def activeIsland(self):
  321.         for island in self.islands:
  322.             try:
  323.                 if bm.faces.active.index in island:
  324.                     return island
  325.             except:
  326.                 return None
  327.  
  328.     def selectedIslands(self):
  329.         _selectedIslands = []
  330.         for island in self.islands:
  331.             if not self.selectedIsland.isdisjoint(island):
  332.                 _selectedIslands.append(island)
  333.         return _selectedIslands
  334.  
  335.  
  336. # #####################
  337. # OPERATOR
  338. # #####################
  339.  
  340. class OperatorTemplate(bpy.types.Operator):
  341.  
  342.     @classmethod
  343.     def poll(cls, context):
  344.         return not (context.scene.tool_settings.use_uv_select_sync)
  345.  
  346.  
  347. #####################
  348. # ALIGN
  349. #####################
  350.  
  351. class AlignSXMargin(OperatorTemplate):
  352.  
  353.     """Align left margin"""
  354.     bl_idname = "uv.align_left_margin"
  355.     bl_label = "Align left margin"
  356.     bl_options = {'REGISTER', 'UNDO'}
  357.  
  358.     def execute(self, context):
  359.  
  360.         makeIslands = MakeIslands()
  361.         islands = makeIslands.getIslands()
  362.         selectedIslands = makeIslands.selectedIslands()
  363.  
  364.         targetElement = getTargetPoint(context, makeIslands)
  365.         if not targetElement:
  366.             self.report({"ERROR"}, "No active face")
  367.             return {"CANCELLED"}
  368.  
  369.         if context.scene.selectionAsGroup:
  370.             groupBox = GBBox(selectedIslands)
  371.             if context.scene.relativeItems == 'ACTIVE':
  372.                 selectedIslands.remove(makeIslands.activeIsland())
  373.             for island in selectedIslands:
  374.                 vector = mathutils.Vector((targetElement[0].x - groupBox[0].x,
  375.                                            0.0))
  376.                 moveIslands(vector, island)
  377.  
  378.         else:
  379.             for island in selectedIslands:
  380.                 vector = mathutils.Vector(
  381.                     (targetElement[0].x - BBox(island)[0].x, 0.0))
  382.                 moveIslands(vector, island)
  383.  
  384.         update()
  385.         return {'FINISHED'}
  386.  
  387.  
  388. class AlignRxMargin(OperatorTemplate):
  389.  
  390.     """Align right margin"""
  391.     bl_idname = "uv.align_right_margin"
  392.     bl_label = "Align right margin"
  393.     bl_options = {'REGISTER', 'UNDO'}
  394.  
  395.     def execute(self, context):
  396.         makeIslands = MakeIslands()
  397.         islands = makeIslands.getIslands()
  398.         selectedIslands = makeIslands.selectedIslands()
  399.  
  400.         targetElement = getTargetPoint(context, makeIslands)
  401.         if not targetElement:
  402.             self.report({"ERROR"}, "No active face")
  403.             return {"CANCELLED"}
  404.  
  405.         if context.scene.selectionAsGroup:
  406.             groupBox = GBBox(selectedIslands)
  407.             if context.scene.relativeItems == 'ACTIVE':
  408.                 selectedIslands.remove(makeIslands.activeIsland())
  409.             for island in selectedIslands:
  410.                 vector = mathutils.Vector((targetElement[1].x - groupBox[1].x,
  411.                                            0.0))
  412.                 moveIslands(vector, island)
  413.  
  414.         else:
  415.             for island in selectedIslands:
  416.                 vector = mathutils.Vector(
  417.                     (targetElement[1].x - BBox(island)[1].x, 0.0))
  418.                 moveIslands(vector, island)
  419.  
  420.         update()
  421.         return {'FINISHED'}
  422.  
  423.  
  424. class AlignVAxis(OperatorTemplate):
  425.  
  426.     """Align vertical axis"""
  427.     bl_idname = "uv.align_vertical_axis"
  428.     bl_label = "Align vertical axis"
  429.     bl_options = {'REGISTER', 'UNDO'}
  430.  
  431.     def execute(self, context):
  432.         makeIslands = MakeIslands()
  433.         islands = makeIslands.getIslands()
  434.         selectedIslands = makeIslands.selectedIslands()
  435.  
  436.         targetElement = getTargetPoint(context, makeIslands)
  437.         if not targetElement:
  438.             self.report({"ERROR"}, "No active face")
  439.             return {"CANCELLED"}
  440.         targetCenter = (targetElement[0] + targetElement[1]) / 2
  441.         if context.scene.selectionAsGroup:
  442.             groupBoxCenter = GBBoxCenter(selectedIslands)
  443.             if context.scene.relativeItems == 'ACTIVE':
  444.                 selectedIslands.remove(makeIslands.activeIsland())
  445.             for island in selectedIslands:
  446.                 vector = mathutils.Vector(
  447.                     (targetCenter.x - groupBoxCenter.x, 0.0))
  448.                 moveIslands(vector, island)
  449.  
  450.         else:
  451.             for island in selectedIslands:
  452.                 vector = mathutils.Vector(
  453.                     (targetCenter.x - BBoxCenter(island).x, 0.0))
  454.                 moveIslands(vector, island)
  455.  
  456.         update()
  457.         return {'FINISHED'}
  458.  
  459.  
  460. ##################################################
  461. class AlignTopMargin(OperatorTemplate):
  462.  
  463.     """Align top margin"""
  464.     bl_idname = "uv.align_top_margin"
  465.     bl_label = "Align top margin"
  466.     bl_options = {'REGISTER', 'UNDO'}
  467.  
  468.     def execute(self, context):
  469.  
  470.         makeIslands = MakeIslands()
  471.         islands = makeIslands.getIslands()
  472.         selectedIslands = makeIslands.selectedIslands()
  473.  
  474.         targetElement = getTargetPoint(context, makeIslands)
  475.         if not targetElement:
  476.             self.report({"ERROR"}, "No active face")
  477.             return {"CANCELLED"}
  478.         if context.scene.selectionAsGroup:
  479.             groupBox = GBBox(selectedIslands)
  480.             if context.scene.relativeItems == 'ACTIVE':
  481.                 selectedIslands.remove(makeIslands.activeIsland())
  482.             for island in selectedIslands:
  483.                 vector = mathutils.Vector(
  484.                     (0.0, targetElement[1].y - groupBox[1].y))
  485.                 moveIslands(vector, island)
  486.  
  487.         else:
  488.             for island in selectedIslands:
  489.                 vector = mathutils.Vector(
  490.                     (0.0, targetElement[1].y - BBox(island)[1].y))
  491.                 moveIslands(vector, island)
  492.  
  493.         update()
  494.         return {'FINISHED'}
  495.  
  496.  
  497. class AlignLowMargin(OperatorTemplate):
  498.  
  499.     """Align low margin"""
  500.     bl_idname = "uv.align_low_margin"
  501.     bl_label = "Align low margin"
  502.     bl_options = {'REGISTER', 'UNDO'}
  503.  
  504.     def execute(self, context):
  505.         makeIslands = MakeIslands()
  506.         islands = makeIslands.getIslands()
  507.         selectedIslands = makeIslands.selectedIslands()
  508.  
  509.         targetElement = getTargetPoint(context, makeIslands)
  510.         if not targetElement:
  511.             self.report({"ERROR"}, "No active face")
  512.             return {"CANCELLED"}
  513.         if context.scene.selectionAsGroup:
  514.             groupBox = GBBox(selectedIslands)
  515.             if context.scene.relativeItems == 'ACTIVE':
  516.                 selectedIslands.remove(makeIslands.activeIsland())
  517.             for island in selectedIslands:
  518.                 vector = mathutils.Vector(
  519.                     (0.0, targetElement[0].y - groupBox[0].y))
  520.                 moveIslands(vector, island)
  521.  
  522.         else:
  523.             for island in selectedIslands:
  524.                 vector = mathutils.Vector(
  525.                     (0.0, targetElement[0].y - BBox(island)[0].y))
  526.                 moveIslands(vector, island)
  527.  
  528.         update()
  529.         return {'FINISHED'}
  530.  
  531.  
  532. class AlignHAxis(OperatorTemplate):
  533.  
  534.     """Align horizontal axis"""
  535.     bl_idname = "uv.align_horizontal_axis"
  536.     bl_label = "Align horizontal axis"
  537.     bl_options = {'REGISTER', 'UNDO'}
  538.  
  539.     def execute(self, context):
  540.         makeIslands = MakeIslands()
  541.         islands = makeIslands.getIslands()
  542.         selectedIslands = makeIslands.selectedIslands()
  543.  
  544.         targetElement = getTargetPoint(context, makeIslands)
  545.         if not targetElement:
  546.             self.report({"ERROR"}, "No active face")
  547.             return {"CANCELLED"}
  548.         targetCenter = (targetElement[0] + targetElement[1]) / 2
  549.  
  550.         if context.scene.selectionAsGroup:
  551.             groupBoxCenter = GBBoxCenter(selectedIslands)
  552.             if context.scene.relativeItems == 'ACTIVE':
  553.                 selectedIslands.remove(makeIslands.activeIsland())
  554.             for island in selectedIslands:
  555.                 vector = mathutils.Vector(
  556.                     (0.0, targetCenter.y - groupBoxCenter.y))
  557.                 moveIslands(vector, island)
  558.  
  559.         else:
  560.             for island in selectedIslands:
  561.                 vector = mathutils.Vector(
  562.                     (0.0, targetCenter.y - BBoxCenter(island).y))
  563.                 moveIslands(vector, island)
  564.  
  565.         update()
  566.         return {'FINISHED'}
  567.  
  568.  
  569. #########################################
  570. class AlignRotation(OperatorTemplate):
  571.  
  572.     """Align island rotation """
  573.     bl_idname = "uv.align_rotation"
  574.     bl_label = "Align island rotation"
  575.     bl_options = {'REGISTER', 'UNDO'}
  576.  
  577.     def execute(self, context):
  578.         makeIslands = MakeIslands()
  579.         islands = makeIslands.getIslands()
  580.         selectedIslands = makeIslands.selectedIslands()
  581.         activeIsland = makeIslands.activeIsland()
  582.         if not activeIsland:
  583.             self.report({"ERROR"}, "No active face")
  584.             return {"CANCELLED"}
  585.         activeAngle = islandAngle(activeIsland)
  586.  
  587.         for island in selectedIslands:
  588.             uvAngle = islandAngle(island)
  589.             deltaAngle = activeAngle - uvAngle
  590.             deltaAngle = round(-deltaAngle, 5)
  591.             rotateIsland(island, deltaAngle)
  592.  
  593.         update()
  594.         return {'FINISHED'}
  595.  
  596.  
  597. class EqualizeScale(OperatorTemplate):
  598.  
  599.     """Equalize the islands scale to the active one"""
  600.     bl_idname = "uv.equalize_scale"
  601.     bl_label = "Equalize Scale"
  602.     bl_options = {'REGISTER', 'UNDO'}
  603.    
  604.     keepProportions = BoolProperty(
  605.     name="Keep Proportions",
  606.     description="Mantain proportions during scaling",
  607.     default=False)
  608.    
  609.     useYaxis = BoolProperty(
  610.     name="Use Y axis",
  611.     description="Use y axis as scale reference, default is x",
  612.     default=False)
  613.    
  614.     def execute(self, context):
  615.         makeIslands = MakeIslands()
  616.         islands = makeIslands.getIslands()
  617.         selectedIslands = makeIslands.selectedIslands()
  618.         activeIsland = makeIslands.activeIsland()
  619.  
  620.         if not activeIsland:
  621.             self.report({"ERROR"}, "No active face")
  622.             return {"CANCELLED"}
  623.  
  624.         activeSize = islandSize(activeIsland)
  625.         selectedIslands.remove(activeIsland)
  626.  
  627.         for island in selectedIslands:
  628.             size = islandSize(island)
  629.             scaleX = activeSize[0] / size[0]
  630.             scaleY = activeSize[1] / size[1]
  631.            
  632.             if self.keepProportions:
  633.                 if self.useYaxis:
  634.                     scaleX = scaleY
  635.                 else:
  636.                     scaleY = scaleX
  637.                                  
  638.             scaleIsland(island, scaleX, scaleY)
  639.  
  640.         update()
  641.         return {"FINISHED"}
  642.    
  643.     def draw(self,context):
  644.         layout = self.layout      
  645.         layout.prop(self, "keepProportions")        
  646.         if self.keepProportions:
  647.             layout.prop(self,"useYaxis")
  648.  
  649.  
  650. ############################
  651. # DISTRIBUTION
  652. ############################
  653. class DistributeLEdgesH(OperatorTemplate):
  654.  
  655.     """Distribute left edges equidistantly horizontally"""
  656.     bl_idname = "uv.distribute_ledges_horizontally"
  657.     bl_label = "Distribute Left Edges Horizontally"
  658.     bl_options = {'REGISTER', 'UNDO'}
  659.  
  660.     def execute(self, context):
  661.         makeIslands = MakeIslands()
  662.         islands = makeIslands.getIslands()
  663.         selectedIslands = makeIslands.selectedIslands()
  664.  
  665.         if len(selectedIslands) < 3:
  666.             return {'CANCELLED'}
  667.  
  668.         islandSpatialSort = IslandSpatialSortX(selectedIslands)
  669.         uvFirstX = BBox(islandSpatialSort[0][1])[0].x
  670.         uvLastX = BBox(islandSpatialSort[-1][1])[0].x
  671.  
  672.         distX = uvLastX - uvFirstX
  673.  
  674.         deltaDist = distX / (len(selectedIslands) - 1)
  675.  
  676.         islandSpatialSort.pop(0)
  677.         islandSpatialSort.pop(-1)
  678.  
  679.         pos = uvFirstX + deltaDist
  680.  
  681.         for island in islandSpatialSort:
  682.             vec = mathutils.Vector((pos - BBox(island[1])[0].x, 0.0))
  683.             pos += deltaDist
  684.             moveIslands(vec, island[1])
  685.         update()
  686.         return {"FINISHED"}
  687.  
  688.  
  689. class DistributeCentersH(OperatorTemplate):
  690.  
  691.     """Distribute centers equidistantly horizontally"""
  692.     bl_idname = "uv.distribute_center_horizontally"
  693.     bl_label = "Distribute Centers Horizontally"
  694.     bl_options = {'REGISTER', 'UNDO'}
  695.  
  696.     def execute(self, context):
  697.         makeIslands = MakeIslands()
  698.         islands = makeIslands.getIslands()
  699.         selectedIslands = makeIslands.selectedIslands()
  700.  
  701.         if len(selectedIslands) < 3:
  702.             return {'CANCELLED'}
  703.  
  704.         islandSpatialSort = IslandSpatialSortX(selectedIslands)
  705.         uvFirstX = min(islandSpatialSort)
  706.         uvLastX = max(islandSpatialSort)
  707.  
  708.         distX = uvLastX[0] - uvFirstX[0]
  709.  
  710.         deltaDist = distX / (len(selectedIslands) - 1)
  711.  
  712.         islandSpatialSort.pop(0)
  713.         islandSpatialSort.pop(-1)
  714.  
  715.         pos = uvFirstX[0] + deltaDist
  716.  
  717.         for island in islandSpatialSort:
  718.             vec = mathutils.Vector((pos - BBoxCenter(island[1]).x, 0.0))
  719.             pos += deltaDist
  720.             moveIslands(vec, island[1])
  721.         update()
  722.         return {"FINISHED"}
  723.  
  724.  
  725. class DistributeREdgesH(OperatorTemplate):
  726.  
  727.     """Distribute right edges equidistantly horizontally"""
  728.     bl_idname = "uv.distribute_redges_horizontally"
  729.     bl_label = "Distribute Right Edges Horizontally"
  730.     bl_options = {'REGISTER', 'UNDO'}
  731.  
  732.     def execute(self, context):
  733.         makeIslands = MakeIslands()
  734.         islands = makeIslands.getIslands()
  735.         selectedIslands = makeIslands.selectedIslands()
  736.  
  737.         if len(selectedIslands) < 3:
  738.             return {'CANCELLED'}
  739.  
  740.         islandSpatialSort = IslandSpatialSortX(selectedIslands)
  741.         uvFirstX = BBox(islandSpatialSort[0][1])[1].x
  742.         uvLastX = BBox(islandSpatialSort[-1][1])[1].x
  743.  
  744.         distX = uvLastX - uvFirstX
  745.  
  746.         deltaDist = distX / (len(selectedIslands) - 1)
  747.  
  748.         islandSpatialSort.pop(0)
  749.         islandSpatialSort.pop(-1)
  750.  
  751.         pos = uvFirstX + deltaDist
  752.  
  753.         for island in islandSpatialSort:
  754.             vec = mathutils.Vector((pos - BBox(island[1])[1].x, 0.0))
  755.             pos += deltaDist
  756.             moveIslands(vec, island[1])
  757.         update()
  758.         return {"FINISHED"}
  759.  
  760.  
  761. class DistributeTEdgesV(OperatorTemplate):
  762.  
  763.     """Distribute top edges equidistantly vertically"""
  764.     bl_idname = "uv.distribute_tedges_vertically"
  765.     bl_label = "Distribute Top Edges Vertically"
  766.     bl_options = {'REGISTER', 'UNDO'}
  767.  
  768.     def execute(self, context):
  769.         makeIslands = MakeIslands()
  770.         islands = makeIslands.getIslands()
  771.         selectedIslands = makeIslands.selectedIslands()
  772.  
  773.         if len(selectedIslands) < 3:
  774.             return {'CANCELLED'}
  775.  
  776.         islandSpatialSort = IslandSpatialSortY(selectedIslands)
  777.         uvFirstX = BBox(islandSpatialSort[0][1])[1].y
  778.         uvLastX = BBox(islandSpatialSort[-1][1])[1].y
  779.  
  780.         distX = uvLastX - uvFirstX
  781.  
  782.         deltaDist = distX / (len(selectedIslands) - 1)
  783.  
  784.         islandSpatialSort.pop(0)
  785.         islandSpatialSort.pop(-1)
  786.  
  787.         pos = uvFirstX + deltaDist
  788.  
  789.         for island in islandSpatialSort:
  790.             vec = mathutils.Vector((0.0, pos - BBox(island[1])[1].y))
  791.             pos += deltaDist
  792.             moveIslands(vec, island[1])
  793.         update()
  794.         return {"FINISHED"}
  795.  
  796.  
  797. class DistributeCentersV(OperatorTemplate):
  798.  
  799.     """Distribute centers equidistantly vertically"""
  800.     bl_idname = "uv.distribute_center_vertically"
  801.     bl_label = "Distribute Centers Vertically"
  802.     bl_options = {'REGISTER', 'UNDO'}
  803.  
  804.     def execute(self, context):
  805.         makeIslands = MakeIslands()
  806.         islands = makeIslands.getIslands()
  807.         selectedIslands = makeIslands.selectedIslands()
  808.  
  809.         if len(selectedIslands) < 3:
  810.             return {'CANCELLED'}
  811.  
  812.         islandSpatialSort = IslandSpatialSortY(selectedIslands)
  813.         uvFirst = BBoxCenter(islandSpatialSort[0][1]).y
  814.         uvLast = BBoxCenter(islandSpatialSort[-1][1]).y
  815.  
  816.         dist = uvLast - uvFirst
  817.  
  818.         deltaDist = dist / (len(selectedIslands) - 1)
  819.  
  820.         islandSpatialSort.pop(0)
  821.         islandSpatialSort.pop(-1)
  822.  
  823.         pos = uvFirst + deltaDist
  824.  
  825.         for island in islandSpatialSort:
  826.             vec = mathutils.Vector((0.0, pos - BBoxCenter(island[1]).y))
  827.             pos += deltaDist
  828.             moveIslands(vec, island[1])
  829.         update()
  830.         return {"FINISHED"}
  831.  
  832.  
  833. class DistributeBEdgesV(OperatorTemplate):
  834.  
  835.     """Distribute bottom edges equidistantly vertically"""
  836.     bl_idname = "uv.distribute_bedges_vertically"
  837.     bl_label = "Distribute Bottom Edges Vertically"
  838.     bl_options = {'REGISTER', 'UNDO'}
  839.  
  840.     def execute(self, context):
  841.         makeIslands = MakeIslands()
  842.         islands = makeIslands.getIslands()
  843.         selectedIslands = makeIslands.selectedIslands()
  844.  
  845.         if len(selectedIslands) < 3:
  846.             return {'CANCELLED'}
  847.  
  848.         islandSpatialSort = IslandSpatialSortY(selectedIslands)
  849.         uvFirst = BBox(islandSpatialSort[0][1])[0].y
  850.         uvLast = BBox(islandSpatialSort[-1][1])[0].y
  851.  
  852.         dist = uvLast - uvFirst
  853.  
  854.         deltaDist = dist / (len(selectedIslands) - 1)
  855.  
  856.         islandSpatialSort.pop(0)
  857.         islandSpatialSort.pop(-1)
  858.  
  859.         pos = uvFirst + deltaDist
  860.  
  861.         for island in islandSpatialSort:
  862.             vec = mathutils.Vector((0.0, pos - BBox(island[1])[0].y))
  863.             pos += deltaDist
  864.             moveIslands(vec, island[1])
  865.         update()
  866.         return {"FINISHED"}
  867.  
  868.  
  869. class EqualizeHGap(OperatorTemplate):
  870.  
  871.     """Equalize horizontal gap between island"""
  872.     bl_idname = "uv.equalize_horizontal_gap"
  873.     bl_label = "Equalize Horizontal Gap"
  874.     bl_options = {'REGISTER', 'UNDO'}
  875.  
  876.     def execute(self, context):
  877.         makeIslands = MakeIslands()
  878.         islands = makeIslands.getIslands()
  879.         selectedIslands = makeIslands.selectedIslands()
  880.  
  881.         if len(selectedIslands) < 3:
  882.             return {'CANCELLED'}
  883.  
  884.         islandSpatialSort = IslandSpatialSortX(selectedIslands)
  885.  
  886.         averageDist = averageIslandDist(islandSpatialSort)
  887.  
  888.         for i in range(len(islandSpatialSort)):
  889.             if islandSpatialSort.index(islandSpatialSort[i + 1]) == \
  890.                     islandSpatialSort.index(islandSpatialSort[-1]):
  891.                 break
  892.             elem1 = BBox(islandSpatialSort[i][1])[1].x
  893.             elem2 = BBox(islandSpatialSort[i + 1][1])[0].x
  894.  
  895.             dist = elem2 - elem1
  896.             increment = averageDist.x - dist
  897.  
  898.             vec = mathutils.Vector((increment, 0.0))
  899.             island = islandSpatialSort[i + 1][1]
  900.             moveIslands(vec, islandSpatialSort[i + 1][1])
  901.         update()
  902.         return {"FINISHED"}
  903.  
  904.  
  905. class EqualizeVGap(OperatorTemplate):
  906.  
  907.     """Equalize vertical gap between island"""
  908.     bl_idname = "uv.equalize_vertical_gap"
  909.     bl_label = "Equalize Vertical Gap"
  910.     bl_options = {'REGISTER', 'UNDO'}
  911.  
  912.     def execute(self, context):
  913.         makeIslands = MakeIslands()
  914.         islands = makeIslands.getIslands()
  915.         selectedIslands = makeIslands.selectedIslands()
  916.  
  917.         if len(selectedIslands) < 3:
  918.             return {'CANCELLED'}
  919.  
  920.         islandSpatialSort = IslandSpatialSortY(selectedIslands)
  921.  
  922.         averageDist = averageIslandDist(islandSpatialSort)
  923.  
  924.         for i in range(len(islandSpatialSort)):
  925.             if islandSpatialSort.index(islandSpatialSort[i + 1]) ==\
  926.                     islandSpatialSort.index(islandSpatialSort[-1]):
  927.                 break
  928.             elem1 = BBox(islandSpatialSort[i][1])[1].y
  929.             elem2 = BBox(islandSpatialSort[i + 1][1])[0].y
  930.  
  931.             dist = elem2 - elem1
  932.  
  933.             increment = averageDist.y - dist
  934.  
  935.             vec = mathutils.Vector((0.0, increment))
  936.             island = islandSpatialSort[i + 1][1]
  937.  
  938.             moveIslands(vec, islandSpatialSort[i + 1][1])
  939.         update()
  940.         return {"FINISHED"}
  941.  
  942. ##############
  943. # SPECIALS
  944. ##############
  945.  
  946.  
  947. class MatchIsland(OperatorTemplate):
  948.  
  949.     """Match UV Island by moving their vertex"""
  950.     bl_idname = "uv.match_island"
  951.     bl_label = "Match Island"
  952.     bl_options = {'REGISTER', 'UNDO'}
  953.  
  954.     threshold = FloatProperty(
  955.         name="Threshold",
  956.         description="Threshold for island matching",
  957.         default=0.1,
  958.         min=0,
  959.         max=1,
  960.         soft_min=0.01,
  961.         soft_max=1,
  962.         step=1,
  963.         precision=2)
  964.  
  965.     def execute(self, context):
  966.         makeIslands = MakeIslands()
  967.         islands = makeIslands.getIslands()
  968.         selectedIslands = makeIslands.selectedIslands()
  969.         activeIsland = makeIslands.activeIsland()
  970.        
  971.         if not activeIsland:
  972.             self.report({"ERROR"}, "No active face")
  973.             return {"CANCELLED"}
  974.        
  975.         if len(selectedIslands) < 2:
  976.             return {'CANCELLED'}
  977.  
  978.         selectedIslands.remove(activeIsland)
  979.  
  980.         for island in selectedIslands:
  981.             matchIsland(activeIsland, self.threshold, island)
  982.  
  983.         update()
  984.         return{'FINISHED'}
  985.  
  986.  
  987. ##############
  988. #   UI
  989. ##############
  990. class IMAGE_PT_align_distribute(bpy.types.Panel):
  991.     bl_label = "Align\Distribute"
  992.     bl_space_type = 'IMAGE_EDITOR'
  993.     bl_region_type = 'TOOLS'
  994.     bl_category = "Tools"
  995.  
  996.     @classmethod
  997.     def poll(cls, context):
  998.         sima = context.space_data
  999.         return sima.show_uvedit and \
  1000.             not (context.tool_settings.use_uv_sculpt
  1001.                  or context.scene.tool_settings.use_uv_select_sync)
  1002.  
  1003.     def draw(self, context):
  1004.         scn = context.scene
  1005.         layout = self.layout
  1006.         layout.prop(scn, "relativeItems")
  1007.         layout.prop(scn, "selectionAsGroup")
  1008.  
  1009.         layout.separator()
  1010.         layout.label(text="Align:")
  1011.  
  1012.         box = layout.box()
  1013.         row = box.row(True)
  1014.         row.operator("uv.align_left_margin", "Left")
  1015.         row.operator("uv.align_vertical_axis", "VAxis")
  1016.         row.operator("uv.align_right_margin", "Right")
  1017.         row = box.row(True)
  1018.         row.operator("uv.align_top_margin", "Top")
  1019.         row.operator("uv.align_horizontal_axis", "HAxis")
  1020.         row.operator("uv.align_low_margin", "Low")
  1021.  
  1022.         row = layout.row()
  1023.         row.operator("uv.align_rotation", "Rotation")
  1024.         row.operator("uv.equalize_scale", "Eq. Scale")
  1025.  
  1026.         layout.separator()
  1027.         # Another Panel??
  1028.         layout.label(text="Distribute:")
  1029.  
  1030.         box = layout.box()
  1031.  
  1032.         row = box.row(True)
  1033.         row.operator("uv.distribute_ledges_horizontally", "LEdges")
  1034.  
  1035.         row.operator("uv.distribute_center_horizontally",
  1036.                      "HCenters")
  1037.  
  1038.         row.operator("uv.distribute_redges_horizontally",
  1039.                      "RCenters")
  1040.  
  1041.         row = box.row(True)
  1042.         row.operator("uv.distribute_tedges_vertically", "TEdges")
  1043.         row.operator("uv.distribute_center_vertically", "VCenters")
  1044.         row.operator("uv.distribute_bedges_vertically", "BEdges")
  1045.  
  1046.         row = layout.row(True)
  1047.         row.operator("uv.equalize_horizontal_gap", "Eq. HGap")
  1048.         row.operator("uv.equalize_vertical_gap", "Eq. VGap")
  1049.  
  1050.         layout.separator()
  1051.         layout.label("Others:")
  1052.         row = layout.row()
  1053.         layout.operator("uv.match_island")
  1054.  
  1055.  
  1056. # Registration
  1057. classes = (
  1058.     IMAGE_PT_align_distribute,
  1059.     AlignSXMargin,
  1060.     AlignRxMargin,
  1061.     AlignVAxis,
  1062.     AlignTopMargin,
  1063.     AlignLowMargin,
  1064.     AlignHAxis,
  1065.     AlignRotation,
  1066.     DistributeLEdgesH,
  1067.     DistributeCentersH,
  1068.     DistributeREdgesH,
  1069.     DistributeTEdgesV,
  1070.     DistributeCentersV,
  1071.     DistributeBEdgesV,
  1072.     EqualizeHGap,
  1073.     EqualizeVGap,
  1074.     EqualizeScale,
  1075.     MatchIsland)
  1076.  
  1077.  
  1078. def register():
  1079.     for item in classes:
  1080.         bpy.utils.register_class(item)
  1081.     # bpy.utils.register_manual_map(add_object_manual_map)
  1082.     # bpy.types.INFO_MT_mesh_add.append(add_object_button)
  1083.  
  1084.  
  1085. def unregister():
  1086.     for item in classes:
  1087.         bpy.utils.unregister_class(item)
  1088.     # bpy.utils.unregister_manual_map(add_object_manual_map)
  1089.     # bpy.types.INFO_MT_mesh_add.remove(add_object_button)
  1090.  
  1091.  
  1092. if __name__ == "__main__":
  1093.     register()
  1094.  
go to heaven