- improved UI, improved EqualizeScale*
[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, 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