fix for lookup table
[blender-addons-contrib.git] / uv_align_distribute.py
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, 2),
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     def execute(self, context):
605         makeIslands = MakeIslands()
606         islands = makeIslands.getIslands()
607         selectedIslands = makeIslands.selectedIslands()
608         activeIsland = makeIslands.activeIsland()
609
610         if not activeIsland:
611             self.report({"ERROR"}, "No active face")
612             return {"CANCELLED"}
613
614         activeSize = islandSize(activeIsland)
615         selectedIslands.remove(activeIsland)
616
617         for island in selectedIslands:
618             size = islandSize(island)
619             scaleX = activeSize[0] / size[0]
620             scaleY = activeSize[1] / size[1]
621             scaleIsland(island, scaleX, scaleY)
622
623         update()
624         return {"FINISHED"}
625
626
627 ############################
628 # DISTRIBUTION
629 ############################
630 class DistributeLEdgesH(OperatorTemplate):
631
632     """Distribute left edges equidistantly horizontally"""
633     bl_idname = "uv.distribute_ledges_horizontally"
634     bl_label = "Distribute Left Edges Horizontally"
635     bl_options = {'REGISTER', 'UNDO'}
636
637     def execute(self, context):
638         makeIslands = MakeIslands()
639         islands = makeIslands.getIslands()
640         selectedIslands = makeIslands.selectedIslands()
641
642         if len(selectedIslands) < 3:
643             return {'CANCELLED'}
644
645         islandSpatialSort = IslandSpatialSortX(selectedIslands)
646         uvFirstX = BBox(islandSpatialSort[0][1])[0].x
647         uvLastX = BBox(islandSpatialSort[-1][1])[0].x
648
649         distX = uvLastX - uvFirstX
650
651         deltaDist = distX / (len(selectedIslands) - 1)
652
653         islandSpatialSort.pop(0)
654         islandSpatialSort.pop(-1)
655
656         pos = uvFirstX + deltaDist
657
658         for island in islandSpatialSort:
659             vec = mathutils.Vector((pos - BBox(island[1])[0].x, 0.0))
660             pos += deltaDist
661             moveIslands(vec, island[1])
662         update()
663         return {"FINISHED"}
664
665
666 class DistributeCentersH(OperatorTemplate):
667
668     """Distribute centers equidistantly horizontally"""
669     bl_idname = "uv.distribute_center_horizontally"
670     bl_label = "Distribute Centers Horizontally"
671     bl_options = {'REGISTER', 'UNDO'}
672
673     def execute(self, context):
674         makeIslands = MakeIslands()
675         islands = makeIslands.getIslands()
676         selectedIslands = makeIslands.selectedIslands()
677
678         if len(selectedIslands) < 3:
679             return {'CANCELLED'}
680
681         islandSpatialSort = IslandSpatialSortX(selectedIslands)
682         uvFirstX = min(islandSpatialSort)
683         uvLastX = max(islandSpatialSort)
684
685         distX = uvLastX[0] - uvFirstX[0]
686
687         deltaDist = distX / (len(selectedIslands) - 1)
688
689         islandSpatialSort.pop(0)
690         islandSpatialSort.pop(-1)
691
692         pos = uvFirstX[0] + deltaDist
693
694         for island in islandSpatialSort:
695             vec = mathutils.Vector((pos - BBoxCenter(island[1]).x, 0.0))
696             pos += deltaDist
697             moveIslands(vec, island[1])
698         update()
699         return {"FINISHED"}
700
701
702 class DistributeREdgesH(OperatorTemplate):
703
704     """Distribute right edges equidistantly horizontally"""
705     bl_idname = "uv.distribute_redges_horizontally"
706     bl_label = "Distribute Right Edges Horizontally"
707     bl_options = {'REGISTER', 'UNDO'}
708
709     def execute(self, context):
710         makeIslands = MakeIslands()
711         islands = makeIslands.getIslands()
712         selectedIslands = makeIslands.selectedIslands()
713
714         if len(selectedIslands) < 3:
715             return {'CANCELLED'}
716
717         islandSpatialSort = IslandSpatialSortX(selectedIslands)
718         uvFirstX = BBox(islandSpatialSort[0][1])[1].x
719         uvLastX = BBox(islandSpatialSort[-1][1])[1].x
720
721         distX = uvLastX - uvFirstX
722
723         deltaDist = distX / (len(selectedIslands) - 1)
724
725         islandSpatialSort.pop(0)
726         islandSpatialSort.pop(-1)
727
728         pos = uvFirstX + deltaDist
729
730         for island in islandSpatialSort:
731             vec = mathutils.Vector((pos - BBox(island[1])[1].x, 0.0))
732             pos += deltaDist
733             moveIslands(vec, island[1])
734         update()
735         return {"FINISHED"}
736
737
738 class DistributeTEdgesV(OperatorTemplate):
739
740     """Distribute top edges equidistantly vertically"""
741     bl_idname = "uv.distribute_tedges_vertically"
742     bl_label = "Distribute Top Edges Vertically"
743     bl_options = {'REGISTER', 'UNDO'}
744
745     def execute(self, context):
746         makeIslands = MakeIslands()
747         islands = makeIslands.getIslands()
748         selectedIslands = makeIslands.selectedIslands()
749
750         if len(selectedIslands) < 3:
751             return {'CANCELLED'}
752
753         islandSpatialSort = IslandSpatialSortY(selectedIslands)
754         uvFirstX = BBox(islandSpatialSort[0][1])[1].y
755         uvLastX = BBox(islandSpatialSort[-1][1])[1].y
756
757         distX = uvLastX - uvFirstX
758
759         deltaDist = distX / (len(selectedIslands) - 1)
760
761         islandSpatialSort.pop(0)
762         islandSpatialSort.pop(-1)
763
764         pos = uvFirstX + deltaDist
765
766         for island in islandSpatialSort:
767             vec = mathutils.Vector((0.0, pos - BBox(island[1])[1].y))
768             pos += deltaDist
769             moveIslands(vec, island[1])
770         update()
771         return {"FINISHED"}
772
773
774 class DistributeCentersV(OperatorTemplate):
775
776     """Distribute centers equidistantly vertically"""
777     bl_idname = "uv.distribute_center_vertically"
778     bl_label = "Distribute Centers Vertically"
779     bl_options = {'REGISTER', 'UNDO'}
780
781     def execute(self, context):
782         makeIslands = MakeIslands()
783         islands = makeIslands.getIslands()
784         selectedIslands = makeIslands.selectedIslands()
785
786         if len(selectedIslands) < 3:
787             return {'CANCELLED'}
788
789         islandSpatialSort = IslandSpatialSortY(selectedIslands)
790         uvFirst = BBoxCenter(islandSpatialSort[0][1]).y
791         uvLast = BBoxCenter(islandSpatialSort[-1][1]).y
792
793         dist = uvLast - uvFirst
794
795         deltaDist = dist / (len(selectedIslands) - 1)
796
797         islandSpatialSort.pop(0)
798         islandSpatialSort.pop(-1)
799
800         pos = uvFirst + deltaDist
801
802         for island in islandSpatialSort:
803             vec = mathutils.Vector((0.0, pos - BBoxCenter(island[1]).y))
804             pos += deltaDist
805             moveIslands(vec, island[1])
806         update()
807         return {"FINISHED"}
808
809
810 class DistributeBEdgesV(OperatorTemplate):
811
812     """Distribute bottom edges equidistantly vertically"""
813     bl_idname = "uv.distribute_bedges_vertically"
814     bl_label = "Distribute Bottom Edges Vertically"
815     bl_options = {'REGISTER', 'UNDO'}
816
817     def execute(self, context):
818         makeIslands = MakeIslands()
819         islands = makeIslands.getIslands()
820         selectedIslands = makeIslands.selectedIslands()
821
822         if len(selectedIslands) < 3:
823             return {'CANCELLED'}
824
825         islandSpatialSort = IslandSpatialSortY(selectedIslands)
826         uvFirst = BBox(islandSpatialSort[0][1])[0].y
827         uvLast = BBox(islandSpatialSort[-1][1])[0].y
828
829         dist = uvLast - uvFirst
830
831         deltaDist = dist / (len(selectedIslands) - 1)
832
833         islandSpatialSort.pop(0)
834         islandSpatialSort.pop(-1)
835
836         pos = uvFirst + deltaDist
837
838         for island in islandSpatialSort:
839             vec = mathutils.Vector((0.0, pos - BBox(island[1])[0].y))
840             pos += deltaDist
841             moveIslands(vec, island[1])
842         update()
843         return {"FINISHED"}
844
845
846 class EqualizeHGap(OperatorTemplate):
847
848     """Equalize horizontal gap between island"""
849     bl_idname = "uv.equalize_horizontal_gap"
850     bl_label = "Equalize Horizontal Gap"
851     bl_options = {'REGISTER', 'UNDO'}
852
853     def execute(self, context):
854         makeIslands = MakeIslands()
855         islands = makeIslands.getIslands()
856         selectedIslands = makeIslands.selectedIslands()
857
858         if len(selectedIslands) < 3:
859             return {'CANCELLED'}
860
861         islandSpatialSort = IslandSpatialSortX(selectedIslands)
862
863         averageDist = averageIslandDist(islandSpatialSort)
864
865         for i in range(len(islandSpatialSort)):
866             if islandSpatialSort.index(islandSpatialSort[i + 1]) == \
867                     islandSpatialSort.index(islandSpatialSort[-1]):
868                 break
869             elem1 = BBox(islandSpatialSort[i][1])[1].x
870             elem2 = BBox(islandSpatialSort[i + 1][1])[0].x
871
872             dist = elem2 - elem1
873             increment = averageDist.x - dist
874
875             vec = mathutils.Vector((increment, 0.0))
876             island = islandSpatialSort[i + 1][1]
877             moveIslands(vec, islandSpatialSort[i + 1][1])
878         update()
879         return {"FINISHED"}
880
881
882 class EqualizeVGap(OperatorTemplate):
883
884     """Equalize vertical gap between island"""
885     bl_idname = "uv.equalize_vertical_gap"
886     bl_label = "Equalize Vertical Gap"
887     bl_options = {'REGISTER', 'UNDO'}
888
889     def execute(self, context):
890         makeIslands = MakeIslands()
891         islands = makeIslands.getIslands()
892         selectedIslands = makeIslands.selectedIslands()
893
894         if len(selectedIslands) < 3:
895             return {'CANCELLED'}
896
897         islandSpatialSort = IslandSpatialSortY(selectedIslands)
898
899         averageDist = averageIslandDist(islandSpatialSort)
900
901         for i in range(len(islandSpatialSort)):
902             if islandSpatialSort.index(islandSpatialSort[i + 1]) ==\
903                     islandSpatialSort.index(islandSpatialSort[-1]):
904                 break
905             elem1 = BBox(islandSpatialSort[i][1])[1].y
906             elem2 = BBox(islandSpatialSort[i + 1][1])[0].y
907
908             dist = elem2 - elem1
909
910             increment = averageDist.y - dist
911
912             vec = mathutils.Vector((0.0, increment))
913             island = islandSpatialSort[i + 1][1]
914
915             moveIslands(vec, islandSpatialSort[i + 1][1])
916         update()
917         return {"FINISHED"}
918
919 ##############
920 # SPECIALS
921 ##############
922
923
924 class MatchIsland(OperatorTemplate):
925
926     """Match UV Island by moving their vertex"""
927     bl_idname = "uv.match_island"
928     bl_label = "Match Island"
929     bl_options = {'REGISTER', 'UNDO'}
930
931     threshold = FloatProperty(
932         name="Threshold",
933         description="Threshold for island matching",
934         default=0.1,
935         min=0,
936         max=1,
937         soft_min=0.01,
938         soft_max=1,
939         step=1,
940         precision=2)
941
942     def execute(self, context):
943         makeIslands = MakeIslands()
944         islands = makeIslands.getIslands()
945         selectedIslands = makeIslands.selectedIslands()
946         activeIsland = makeIslands.activeIsland()
947
948         if len(selectedIslands) < 2:
949             return {'CANCELLED'}
950
951         selectedIslands.remove(activeIsland)
952
953         for island in selectedIslands:
954             matchIsland(activeIsland, self.threshold, island)
955
956         update()
957         return{'FINISHED'}
958
959
960 ##############
961 #   UI
962 ##############
963 class IMAGE_PT_align_distribute(bpy.types.Panel):
964     bl_label = "Align\Distribute"
965     bl_space_type = 'IMAGE_EDITOR'
966     bl_region_type = 'TOOLS'
967     bl_category = "Tools"
968
969     @classmethod
970     def poll(cls, context):
971         sima = context.space_data
972         return sima.show_uvedit and \
973             not (context.tool_settings.use_uv_sculpt
974                  or context.scene.tool_settings.use_uv_select_sync)
975
976     def draw(self, context):
977         scn = context.scene
978         layout = self.layout
979         layout.prop(scn, "relativeItems")
980         layout.prop(scn, "selectionAsGroup")
981
982         layout.separator()
983         layout.label(text="Align:")
984
985         box = layout.box()
986         row = box.row(True)
987         row.operator("uv.align_left_margin", "Align Left")
988         row.operator("uv.align_vertical_axis", "Align VAxis")
989         row.operator("uv.align_right_margin", "Align Right")
990         row = box.row(True)
991         row.operator("uv.align_top_margin", "Align Top")
992         row.operator("uv.align_horizontal_axis", "Align HAxis")
993         row.operator("uv.align_low_margin", "Align Low")
994
995         row = layout.row()
996         row.operator("uv.align_rotation", "Align rotation")
997         row.operator("uv.equalize_scale", "Equalize scale")
998
999         layout.separator()
1000         # Another Panel??
1001         layout.label(text="Distribute:")
1002
1003         box = layout.box()
1004
1005         row = box.row(True)
1006         row.operator("uv.distribute_ledges_horizontally", "Distribute LEdges")
1007
1008         row.operator("uv.distribute_center_horizontally",
1009                      "Distribute HCenters")
1010
1011         row.operator("uv.distribute_redges_horizontally",
1012                      "Distribute RCenters")
1013
1014         row = box.row(True)
1015         row.operator("uv.distribute_tedges_vertically", "Distribute TEdges")
1016         row.operator("uv.distribute_center_vertically", "Distribute VCenters")
1017         row.operator("uv.distribute_bedges_vertically", "Distribute BEdges")
1018
1019         row = layout.row(True)
1020         row.operator("uv.equalize_horizontal_gap", "Equalize HGap")
1021         row.operator("uv.equalize_vertical_gap", "Equalize VGap")
1022
1023         layout.separator()
1024         layout.label("Others:")
1025         row = layout.row()
1026         layout.operator("uv.match_island")
1027
1028
1029 # Registration
1030 classes = (
1031     IMAGE_PT_align_distribute,
1032     AlignSXMargin,
1033     AlignRxMargin,
1034     AlignVAxis,
1035     AlignTopMargin,
1036     AlignLowMargin,
1037     AlignHAxis,
1038     AlignRotation,
1039     DistributeLEdgesH,
1040     DistributeCentersH,
1041     DistributeREdgesH,
1042     DistributeTEdgesV,
1043     DistributeCentersV,
1044     DistributeBEdgesV,
1045     EqualizeHGap,
1046     EqualizeVGap,
1047     EqualizeScale,
1048     MatchIsland)
1049
1050
1051 def register():
1052     for item in classes:
1053         bpy.utils.register_class(item)
1054     # bpy.utils.register_manual_map(add_object_manual_map)
1055     # bpy.types.INFO_MT_mesh_add.append(add_object_button)
1056
1057
1058 def unregister():
1059     for item in classes:
1060         bpy.utils.unregister_class(item)
1061     # bpy.utils.unregister_manual_map(add_object_manual_map)
1062     # bpy.types.INFO_MT_mesh_add.remove(add_object_button)
1063
1064
1065 if __name__ == "__main__":
1066     register()
1067