Cleanup: style
[blender.git] / release / scripts / startup / bl_operators / uvcalc_smart_project.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; either version 2
6 #  of the License, or (at your option) any later version.
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 # TODO <pep8 compliant>
20
21 from mathutils import (
22     Matrix,
23     Vector,
24     geometry,
25 )
26 import bpy
27 from bpy.types import Operator
28
29 DEG_TO_RAD = 0.017453292519943295  # pi/180.0
30 # see bugs:
31 # - T31598 (when too small).
32 # - T48086 (when too big).
33 SMALL_NUM = 1e-12
34
35
36 global USER_FILL_HOLES
37 global USER_FILL_HOLES_QUALITY
38 USER_FILL_HOLES = None
39 USER_FILL_HOLES_QUALITY = None
40
41
42 def pointInTri2D(v, v1, v2, v3):
43     key = v1.x, v1.y, v2.x, v2.y, v3.x, v3.y
44
45     # Commented because its slower to do the bounds check, we should really cache the bounds info for each face.
46     '''
47     # BOUNDS CHECK
48     xmin= 1000000
49     ymin= 1000000
50
51     xmax= -1000000
52     ymax= -1000000
53
54     for i in (0,2,4):
55         x= key[i]
56         y= key[i+1]
57
58         if xmax<x:      xmax= x
59         if ymax<y:      ymax= y
60         if xmin>x:      xmin= x
61         if ymin>y:      ymin= y
62
63     x= v.x
64     y= v.y
65
66     if x<xmin or x>xmax or y < ymin or y > ymax:
67         return False
68     # Done with bounds check
69     '''
70     try:
71         mtx = dict_matrix[key]
72         if not mtx:
73             return False
74     except:
75         side1 = v2 - v1
76         side2 = v3 - v1
77
78         nor = side1.cross(side2)
79
80         mtx = Matrix((side1, side2, nor))
81
82         # Zero area 2d tri, even tho we throw away zero area faces
83         # the projection UV can result in a zero area UV.
84         if not mtx.determinant():
85             dict_matrix[key] = None
86             return False
87
88         mtx.invert()
89
90         dict_matrix[key] = mtx
91
92     uvw = (v - v1) @ mtx
93     return 0 <= uvw[0] and 0 <= uvw[1] and uvw[0] + uvw[1] <= 1
94
95
96 def boundsIsland(faces):
97     minx = maxx = faces[0].uv[0][0]  # Set initial bounds.
98     miny = maxy = faces[0].uv[0][1]
99     # print len(faces), minx, maxx, miny , maxy
100     for f in faces:
101         for uv in f.uv:
102             x = uv.x
103             y = uv.y
104             if x < minx:
105                 minx = x
106             if y < miny:
107                 miny = y
108             if x > maxx:
109                 maxx = x
110             if y > maxy:
111                 maxy = y
112
113     return minx, miny, maxx, maxy
114
115
116 """
117 def boundsEdgeLoop(edges):
118     minx = maxx = edges[0][0] # Set initial bounds.
119     miny = maxy = edges[0][1]
120     # print len(faces), minx, maxx, miny , maxy
121     for ed in edges:
122         for pt in ed:
123             x= pt[0]
124             y= pt[1]
125             if x<minx: x= minx
126             if y<miny: y= miny
127             if x>maxx: x= maxx
128             if y>maxy: y= maxy
129
130     return minx, miny, maxx, maxy
131 """
132
133 # Turns the islands into a list of unpordered edges (Non internal)
134 # Only for UV's
135 # only returns outline edges for intersection tests. and unique points.
136
137
138 def island2Edge(island):
139
140     # Vert index edges
141     edges = {}
142
143     unique_points = {}
144
145     for f in island:
146         f_uvkey = list(map(tuple, f.uv))
147
148         for vIdx in range(len(f_uvkey)):
149             unique_points[f_uvkey[vIdx]] = f.uv[vIdx]
150
151             if f.v[vIdx].index > f.v[vIdx - 1].index:
152                 i1 = vIdx - 1
153                 i2 = vIdx
154             else:
155                 i1 = vIdx
156                 i2 = vIdx - 1
157
158             try:
159                 edges[f_uvkey[i1], f_uvkey[i2]] *= 0  # sets any edge with more than 1 user to 0 are not returned.
160             except:
161                 edges[f_uvkey[i1], f_uvkey[i2]] = (f.uv[i1] - f.uv[i2]).length
162
163     # If 2 are the same then they will be together, but full [a,b] order is not correct.
164
165     # Sort by length
166     length_sorted_edges = [(Vector(key[0]), Vector(key[1]), value) for key, value in edges.items() if value != 0]
167
168     length_sorted_edges.sort(key=lambda a: -a[2])  # largest first
169
170     # Its okay to leave the length in there.
171     # for e in length_sorted_edges:
172     #   e.pop(2)
173
174     # return edges and unique points
175     return length_sorted_edges, [v.to_3d() for v in unique_points.values()]
176
177
178 def pointInIsland(pt, island):
179     vec1, vec2, vec3 = Vector(), Vector(), Vector()
180     for f in island:
181         vec1.x, vec1.y = f.uv[0]
182         vec2.x, vec2.y = f.uv[1]
183         vec3.x, vec3.y = f.uv[2]
184
185         if pointInTri2D(pt, vec1, vec2, vec3):
186             return True
187
188         if len(f.v) == 4:
189             vec1.x, vec1.y = f.uv[0]
190             vec2.x, vec2.y = f.uv[2]
191             vec3.x, vec3.y = f.uv[3]
192             if pointInTri2D(pt, vec1, vec2, vec3):
193                 return True
194     return False
195
196
197 # box is (left,bottom, right, top)
198 def islandIntersectUvIsland(source, target, SourceOffset):
199     # Is 1 point in the box, inside the vertLoops
200     edgeLoopsSource = source[6]  # Pretend this is offset
201     edgeLoopsTarget = target[6]
202
203     # Edge intersect test
204     for ed in edgeLoopsSource:
205         for seg in edgeLoopsTarget:
206             i = geometry.intersect_line_line_2d(seg[0],
207                                                 seg[1],
208                                                 SourceOffset + ed[0],
209                                                 SourceOffset + ed[1],
210                                                 )
211             if i:
212                 return 1  # LINE INTERSECTION
213
214     # 1 test for source being totally inside target
215     SourceOffset.resize_3d()
216     for pv in source[7]:
217         if pointInIsland(pv + SourceOffset, target[0]):
218             return 2  # SOURCE INSIDE TARGET
219
220     # 2 test for a part of the target being totally inside the source.
221     for pv in target[7]:
222         if pointInIsland(pv - SourceOffset, source[0]):
223             return 3  # PART OF TARGET INSIDE SOURCE.
224
225     return 0  # NO INTERSECTION
226
227
228 def rotate_uvs(uv_points, angle):
229
230     if angle != 0.0:
231         mat = Matrix.Rotation(angle, 2)
232         for uv in uv_points:
233             uv[:] = mat @ uv
234
235
236 def optiRotateUvIsland(faces):
237     uv_points = [uv for f in faces for uv in f.uv]
238     angle = geometry.box_fit_2d(uv_points)
239
240     if angle != 0.0:
241         rotate_uvs(uv_points, angle)
242
243     # orient them vertically (could be an option)
244     minx, miny, maxx, maxy = boundsIsland(faces)
245     w, h = maxx - minx, maxy - miny
246     # use epsilon so we don't randomly rotate (almost) perfect squares.
247     if h + 0.00001 < w:
248         from math import pi
249         angle = pi / 2.0
250         rotate_uvs(uv_points, angle)
251
252
253 # Takes an island list and tries to find concave, hollow areas to pack smaller islands into.
254 def mergeUvIslands(islandList):
255     global USER_FILL_HOLES
256     global USER_FILL_HOLES_QUALITY
257
258     # Pack islands to bottom LHS
259     # Sync with island
260
261     # islandTotFaceArea = [] # A list of floats, each island area
262     # islandArea = [] # a list of tuples ( area, w,h)
263
264     decoratedIslandList = []
265
266     islandIdx = len(islandList)
267     while islandIdx:
268         islandIdx -= 1
269         minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx])
270         w, h = maxx - minx, maxy - miny
271
272         totFaceArea = 0
273         offset = Vector((minx, miny))
274         for f in islandList[islandIdx]:
275             for uv in f.uv:
276                 uv -= offset
277
278             totFaceArea += f.area
279
280         islandBoundsArea = w * h
281         efficiency = abs(islandBoundsArea - totFaceArea)
282
283         # UV Edge list used for intersections as well as unique points.
284         edges, uniqueEdgePoints = island2Edge(islandList[islandIdx])
285
286         decoratedIslandList.append([islandList[islandIdx], totFaceArea, efficiency, islandBoundsArea, w, h, edges, uniqueEdgePoints])
287
288     # Sort by island bounding box area, smallest face area first.
289     # no.. chance that to most simple edge loop first.
290     decoratedIslandListAreaSort = decoratedIslandList[:]
291
292     decoratedIslandListAreaSort.sort(key=lambda A: A[3])
293
294     # sort by efficiency, Least Efficient first.
295     decoratedIslandListEfficSort = decoratedIslandList[:]
296     # decoratedIslandListEfficSort.sort(lambda A, B: cmp(B[2], A[2]))
297
298     decoratedIslandListEfficSort.sort(key=lambda A: -A[2])
299
300     # ================================================== THESE CAN BE TWEAKED.
301     # This is a quality value for the number of tests.
302     # from 1 to 4, generic quality value is from 1 to 100
303     USER_STEP_QUALITY = ((USER_FILL_HOLES_QUALITY - 1) / 25.0) + 1
304
305     # If 100 will test as long as there is enough free space.
306     # this is rarely enough, and testing takes a while, so lower quality speeds this up.
307
308     # 1 means they have the same quality
309     USER_FREE_SPACE_TO_TEST_QUALITY = 1 + (((100 - USER_FILL_HOLES_QUALITY) / 100.0) * 5)
310
311     # print 'USER_STEP_QUALITY', USER_STEP_QUALITY
312     # print 'USER_FREE_SPACE_TO_TEST_QUALITY', USER_FREE_SPACE_TO_TEST_QUALITY
313
314     removedCount = 0
315
316     areaIslandIdx = 0
317     ctrl = Window.Qual.CTRL
318     BREAK = False
319     while areaIslandIdx < len(decoratedIslandListAreaSort) and not BREAK:
320         sourceIsland = decoratedIslandListAreaSort[areaIslandIdx]
321         # Already packed?
322         if not sourceIsland[0]:
323             areaIslandIdx += 1
324         else:
325             efficIslandIdx = 0
326             while efficIslandIdx < len(decoratedIslandListEfficSort) and not BREAK:
327
328                 if Window.GetKeyQualifiers() & ctrl:
329                     BREAK = True
330                     break
331
332                 # Now we have 2 islands, if the efficiency of the islands lowers there's an
333                 # increasing likely hood that we can fit merge into the bigger UV island.
334                 # this ensures a tight fit.
335
336                 # Just use figures we have about user/unused area to see if they might fit.
337
338                 targetIsland = decoratedIslandListEfficSort[efficIslandIdx]
339
340                 if sourceIsland[0] == targetIsland[0] or\
341                         not targetIsland[0] or\
342                         not sourceIsland[0]:
343                     pass
344                 else:
345
346                     #~ ([island, totFaceArea, efficiency, islandArea, w,h])
347                     # Wasted space on target is greater then UV bounding island area.
348
349                     #~ if targetIsland[3] > (sourceIsland[2]) and\ #
350                     # ~ print USER_FREE_SPACE_TO_TEST_QUALITY
351                     if targetIsland[2] > (sourceIsland[1] * USER_FREE_SPACE_TO_TEST_QUALITY) and\
352                             targetIsland[4] > sourceIsland[4] and\
353                             targetIsland[5] > sourceIsland[5]:
354
355                         # DEBUG # print '%.10f  %.10f' % (targetIsland[3], sourceIsland[1])
356
357                         # These enough spare space lets move the box until it fits
358
359                         # How many times does the source fit into the target x/y
360                         blockTestXUnit = targetIsland[4] / sourceIsland[4]
361                         blockTestYUnit = targetIsland[5] / sourceIsland[5]
362
363                         boxLeft = 0
364
365                         # Distance we can move between whilst staying inside the targets bounds.
366                         testWidth = targetIsland[4] - sourceIsland[4]
367                         testHeight = targetIsland[5] - sourceIsland[5]
368
369                         # Increment we move each test. x/y
370                         xIncrement = (testWidth / (blockTestXUnit * ((USER_STEP_QUALITY / 50) + 0.1)))
371                         yIncrement = (testHeight / (blockTestYUnit * ((USER_STEP_QUALITY / 50) + 0.1)))
372
373                         # Make sure were not moving less then a 3rg of our width/height
374                         if xIncrement < sourceIsland[4] / 3:
375                             xIncrement = sourceIsland[4]
376                         if yIncrement < sourceIsland[5] / 3:
377                             yIncrement = sourceIsland[5]
378
379                         boxLeft = 0  # Start 1 back so we can jump into the loop.
380                         boxBottom = 0  # -yIncrement
381
382                         # ~ testcount= 0
383
384                         while boxBottom <= testHeight:
385                             # Should we use this? - not needed for now.
386                             # ~ if Window.GetKeyQualifiers() & ctrl:
387                             # ~     BREAK= True
388                             # ~     break
389
390                             # testcount+=1
391                             # print 'Testing intersect'
392                             Intersect = islandIntersectUvIsland(sourceIsland, targetIsland, Vector((boxLeft, boxBottom)))
393                             # print 'Done', Intersect
394                             if Intersect == 1:  # Line intersect, don't bother with this any more
395                                 pass
396
397                             if Intersect == 2:  # Source inside target
398                                 """
399                                 We have an intersection, if we are inside the target
400                                 then move us 1 whole width across,
401                                 Its possible this is a bad idea since 2 skinny Angular faces
402                                 could join without 1 whole move, but its a lot more optimal to speed this up
403                                 since we have already tested for it.
404
405                                 It gives about 10% speedup with minimal errors.
406                                 """
407                                 # Move the test along its width + SMALL_NUM
408                                 #boxLeft += sourceIsland[4] + SMALL_NUM
409                                 boxLeft += sourceIsland[4]
410                             elif Intersect == 0:  # No intersection?? Place it.
411                                 # Progress
412                                 removedCount += 1
413 # XXX                                                           Window.DrawProgressBar(0.0, 'Merged: %i islands, Ctrl to finish early.' % removedCount)
414
415                                 # Move faces into new island and offset
416                                 targetIsland[0].extend(sourceIsland[0])
417                                 offset = Vector((boxLeft, boxBottom))
418
419                                 for f in sourceIsland[0]:
420                                     for uv in f.uv:
421                                         uv += offset
422
423                                 del sourceIsland[0][:]  # Empty
424
425                                 # Move edge loop into new and offset.
426                                 # targetIsland[6].extend(sourceIsland[6])
427                                 # while sourceIsland[6]:
428                                 targetIsland[6].extend([(
429                                     (e[0] + offset, e[1] + offset, e[2])
430                                 ) for e in sourceIsland[6]])
431
432                                 del sourceIsland[6][:]  # Empty
433
434                                 # Sort by edge length, reverse so biggest are first.
435
436                                 try:
437                                     targetIsland[6].sort(key=lambda A: A[2])
438                                 except:
439                                     targetIsland[6].sort(lambda B, A: cmp(A[2], B[2]))
440
441                                 targetIsland[7].extend(sourceIsland[7])
442                                 offset = Vector((boxLeft, boxBottom, 0.0))
443                                 for p in sourceIsland[7]:
444                                     p += offset
445
446                                 del sourceIsland[7][:]
447
448                                 # Decrement the efficiency
449                                 targetIsland[1] += sourceIsland[1]  # Increment totFaceArea
450                                 targetIsland[2] -= sourceIsland[1]  # Decrement efficiency
451                                 # IF we ever used these again, should set to 0, eg
452                                 sourceIsland[2] = 0  # No area if anyone wants to know
453
454                                 break
455
456                             # INCREMENT NEXT LOCATION
457                             if boxLeft > testWidth:
458                                 boxBottom += yIncrement
459                                 boxLeft = 0.0
460                             else:
461                                 boxLeft += xIncrement
462                         # print testcount
463
464                 efficIslandIdx += 1
465         areaIslandIdx += 1
466
467     # Remove empty islands
468     i = len(islandList)
469     while i:
470         i -= 1
471         if not islandList[i]:
472             del islandList[i]  # Can increment islands removed here.
473
474 # Takes groups of faces. assumes face groups are UV groups.
475
476
477 def getUvIslands(faceGroups, me):
478
479     # Get seams so we don't cross over seams
480     edge_seams = {}  # should be a set
481     for ed in me.edges:
482         if ed.use_seam:
483             edge_seams[ed.key] = None  # dummy var- use sets!
484     # Done finding seams
485
486     islandList = []
487
488 # XXX   Window.DrawProgressBar(0.0, 'Splitting %d projection groups into UV islands:' % len(faceGroups))
489     # print '\tSplitting %d projection groups into UV islands:' % len(faceGroups),
490     # Find grouped faces
491
492     faceGroupIdx = len(faceGroups)
493
494     while faceGroupIdx:
495         faceGroupIdx -= 1
496         faces = faceGroups[faceGroupIdx]
497
498         if not faces:
499             continue
500
501         # Build edge dict
502         edge_users = {}
503
504         for i, f in enumerate(faces):
505             for ed_key in f.edge_keys:
506                 if ed_key in edge_seams:  # DELIMIT SEAMS! ;)
507                     edge_users[ed_key] = []  # so as not to raise an error
508                 else:
509                     try:
510                         edge_users[ed_key].append(i)
511                     except:
512                         edge_users[ed_key] = [i]
513
514         # Modes
515         # 0 - face not yet touched.
516         # 1 - added to island list, and need to search
517         # 2 - touched and searched - don't touch again.
518         face_modes = [0] * len(faces)  # initialize zero - untested.
519
520         face_modes[0] = 1  # start the search with face 1
521
522         newIsland = []
523
524         newIsland.append(faces[0])
525
526         ok = True
527         while ok:
528
529             ok = True
530             while ok:
531                 ok = False
532                 for i in range(len(faces)):
533                     if face_modes[i] == 1:  # search
534                         for ed_key in faces[i].edge_keys:
535                             for ii in edge_users[ed_key]:
536                                 if i != ii and face_modes[ii] == 0:
537                                     face_modes[ii] = ok = 1  # mark as searched
538                                     newIsland.append(faces[ii])
539
540                         # mark as searched, don't look again.
541                         face_modes[i] = 2
542
543             islandList.append(newIsland)
544
545             ok = False
546             for i in range(len(faces)):
547                 if face_modes[i] == 0:
548                     newIsland = []
549                     newIsland.append(faces[i])
550
551                     face_modes[i] = ok = 1
552                     break
553             # if not ok will stop looping
554
555 # XXX   Window.DrawProgressBar(0.1, 'Optimizing Rotation for %i UV Islands' % len(islandList))
556
557     for island in islandList:
558         optiRotateUvIsland(island)
559
560     return islandList
561
562
563 def packIslands(islandList):
564     if USER_FILL_HOLES:
565         # XXX           Window.DrawProgressBar(0.1, 'Merging Islands (Ctrl: skip merge)...')
566         mergeUvIslands(islandList)  # Modify in place
567
568     # Now we have UV islands, we need to pack them.
569
570     # Make a synchronized list with the islands
571     # so we can box pack the islands.
572     packBoxes = []
573
574     # Keep a list of X/Y offset so we can save time by writing the
575     # uv's and packed data in one pass.
576     islandOffsetList = []
577
578     islandIdx = 0
579
580     while islandIdx < len(islandList):
581         minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx])
582
583         w, h = maxx - minx, maxy - miny
584
585         if USER_ISLAND_MARGIN:
586             minx -= USER_ISLAND_MARGIN * w / 2
587             miny -= USER_ISLAND_MARGIN * h / 2
588             maxx += USER_ISLAND_MARGIN * w / 2
589             maxy += USER_ISLAND_MARGIN * h / 2
590
591             # recalc width and height
592             w, h = maxx - minx, maxy - miny
593
594         if w < SMALL_NUM:
595             w = SMALL_NUM
596         if h < SMALL_NUM:
597             h = SMALL_NUM
598
599         """Save the offset to be applied later,
600         we could apply to the UVs now and align them to the bottom left hand area
601         of the UV coords like the box packer imagines they are
602         but, its quicker just to remember their offset and
603         apply the packing and offset in 1 pass """
604         islandOffsetList.append((minx, miny))
605
606         # Add to boxList. use the island idx for the BOX id.
607         packBoxes.append([0, 0, w, h])
608         islandIdx += 1
609
610     # Now we have a list of boxes to pack that syncs
611     # with the islands.
612
613     # print '\tPacking UV Islands...'
614 # XXX   Window.DrawProgressBar(0.7, "Packing %i UV Islands..." % len(packBoxes) )
615
616     # time1 = time.time()
617     packWidth, packHeight = geometry.box_pack_2d(packBoxes)
618
619     # print 'Box Packing Time:', time.time() - time1
620
621     # if len(pa ckedLs) != len(islandList):
622     #    raise ValueError("Packed boxes differs from original length")
623
624     # print '\tWriting Packed Data to faces'
625 # XXX   Window.DrawProgressBar(0.8, "Writing Packed Data to faces")
626
627     # Sort by ID, so there in sync again
628     islandIdx = len(islandList)
629     # Having these here avoids divide by 0
630     if islandIdx:
631
632         if USER_STRETCH_ASPECT:
633             # Maximize to uv area?? Will write a normalize function.
634             xfactor = 1.0 / packWidth
635             yfactor = 1.0 / packHeight
636         else:
637             # Keep proportions.
638             xfactor = yfactor = 1.0 / max(packWidth, packHeight)
639
640     while islandIdx:
641         islandIdx -= 1
642         # Write the packed values to the UV's
643
644         xoffset = packBoxes[islandIdx][0] - islandOffsetList[islandIdx][0]
645         yoffset = packBoxes[islandIdx][1] - islandOffsetList[islandIdx][1]
646
647         for f in islandList[islandIdx]:  # Offsetting the UV's so they fit in there packed box
648             for uv in f.uv:
649                 uv.x = (uv.x + xoffset) * xfactor
650                 uv.y = (uv.y + yoffset) * yfactor
651
652
653 def VectoQuat(vec):
654     vec = vec.normalized()
655     return vec.to_track_quat('Z', 'X' if abs(vec.x) > 0.5 else 'Y').inverted()
656
657
658 class thickface:
659     __slost__ = "v", "uv", "no", "area", "edge_keys"
660
661     def __init__(self, face, uv_layer, mesh_verts):
662         self.v = [mesh_verts[i] for i in face.vertices]
663         self.uv = [uv_layer[i].uv for i in face.loop_indices]
664
665         self.no = face.normal.copy()
666         self.area = face.area
667         self.edge_keys = face.edge_keys
668
669
670 def main_consts():
671     from math import radians
672
673     global ROTMAT_2D_POS_90D
674     global ROTMAT_2D_POS_45D
675     global RotMatStepRotation
676
677     ROTMAT_2D_POS_90D = Matrix.Rotation(radians(90.0), 2)
678     ROTMAT_2D_POS_45D = Matrix.Rotation(radians(45.0), 2)
679
680     RotMatStepRotation = []
681     rot_angle = 22.5  # 45.0/2
682     while rot_angle > 0.1:
683         RotMatStepRotation.append([
684             Matrix.Rotation(radians(+rot_angle), 2),
685             Matrix.Rotation(radians(-rot_angle), 2),
686         ])
687
688         rot_angle = rot_angle / 2.0
689
690
691 global ob
692 ob = None
693
694
695 def main(context,
696          island_margin,
697          projection_limit,
698          user_area_weight,
699          use_aspect,
700          stretch_to_bounds,
701          ):
702     global USER_FILL_HOLES
703     global USER_FILL_HOLES_QUALITY
704     global USER_STRETCH_ASPECT
705     global USER_ISLAND_MARGIN
706
707     from math import cos
708     import time
709
710     global dict_matrix
711     dict_matrix = {}
712
713     # Constants:
714     # Takes a list of faces that make up a UV island and rotate
715     # until they optimally fit inside a square.
716     global ROTMAT_2D_POS_90D
717     global ROTMAT_2D_POS_45D
718     global RotMatStepRotation
719     main_consts()
720
721     # Create the variables.
722     USER_PROJECTION_LIMIT = projection_limit
723     USER_ONLY_SELECTED_FACES = True
724     USER_SHARE_SPACE = 1  # Only for hole filling.
725     USER_STRETCH_ASPECT = stretch_to_bounds
726     USER_ISLAND_MARGIN = island_margin  # Only for hole filling.
727     USER_FILL_HOLES = 0
728     USER_FILL_HOLES_QUALITY = 50  # Only for hole filling.
729     USER_VIEW_INIT = 0  # Only for hole filling.
730
731     is_editmode = (context.mode == 'EDIT_MESH')
732     if is_editmode:
733         obList = context.objects_in_mode_unique_data
734     else:
735         obList = [
736             ob for ob in context.selected_editable_objects
737             if ob.type == 'MESH' and ob.data.library is None
738         ]
739
740     if not is_editmode:
741         USER_ONLY_SELECTED_FACES = False
742
743     if not obList:
744         raise Exception("error, no selected mesh objects")
745
746     # Convert from being button types
747     USER_PROJECTION_LIMIT_CONVERTED = cos(USER_PROJECTION_LIMIT * DEG_TO_RAD)
748     USER_PROJECTION_LIMIT_HALF_CONVERTED = cos((USER_PROJECTION_LIMIT / 2) * DEG_TO_RAD)
749
750     # Toggle Edit mode
751     if is_editmode:
752         bpy.ops.object.mode_set(mode='OBJECT')
753     # Assume face select mode! an annoying hack to toggle face select mode because Mesh doesn't like faceSelectMode.
754
755     if USER_SHARE_SPACE:
756         # Sort by data name so we get consistent results
757         obList.sort(key=lambda ob: ob.data.name)
758         collected_islandList = []
759
760     time1 = time.time()
761
762     # Tag as False so we don't operate on the same mesh twice.
763     for me in bpy.data.meshes:
764         me.tag = False
765
766     for ob in obList:
767         me = ob.data
768
769         if me.tag or me.library:
770             continue
771
772         # Tag as used
773         me.tag = True
774
775         if not me.uv_layers:  # Mesh has no UV Coords, don't bother.
776             me.uv_layers.new()
777
778         uv_layer = me.uv_layers.active.data
779         me_verts = list(me.vertices)
780
781         if USER_ONLY_SELECTED_FACES:
782             meshFaces = [thickface(f, uv_layer, me_verts) for i, f in enumerate(me.polygons) if f.select]
783         else:
784             meshFaces = [thickface(f, uv_layer, me_verts) for i, f in enumerate(me.polygons)]
785
786         # =======
787         # Generate a projection list from face normals, this is meant to be smart :)
788
789         # make a list of face props that are in sync with meshFaces
790         # Make a Face List that is sorted by area.
791         # meshFaces = []
792
793         # meshFaces.sort( lambda a, b: cmp(b.area , a.area) ) # Biggest first.
794         meshFaces.sort(key=lambda a: -a.area)
795
796         # remove all zero area faces
797         while meshFaces and meshFaces[-1].area <= SMALL_NUM:
798             # Set their UV's to 0,0
799             for uv in meshFaces[-1].uv:
800                 uv.zero()
801             meshFaces.pop()
802
803         if not meshFaces:
804             continue
805
806         # Smallest first is slightly more efficient,
807         # but if the user cancels early then its better we work on the larger data.
808
809         # Generate Projection Vecs
810         # 0d is   1.0
811         # 180 IS -0.59846
812
813         # Initialize projectVecs
814         if USER_VIEW_INIT:
815             # Generate Projection
816
817             # We add to this along the way
818             projectVecs = [Vector(Window.GetViewVector()) @ ob.matrix_world.inverted().to_3x3()]
819         else:
820             projectVecs = []
821
822         newProjectVec = meshFaces[0].no
823         newProjectMeshFaces = []  # Popping stuffs it up.
824
825         # Pretend that the most unique angle is ages away to start the loop off
826         mostUniqueAngle = -1.0
827
828         # This is popped
829         tempMeshFaces = meshFaces[:]
830
831         # This while only gathers projection vecs, faces are assigned later on.
832         while 1:
833             # If there's none there then start with the largest face
834
835             # add all the faces that are close.
836             for fIdx in range(len(tempMeshFaces) - 1, -1, -1):
837                 # Use half the angle limit so we don't overweight faces towards this
838                 # normal and hog all the faces.
839                 if newProjectVec.dot(tempMeshFaces[fIdx].no) > USER_PROJECTION_LIMIT_HALF_CONVERTED:
840                     newProjectMeshFaces.append(tempMeshFaces.pop(fIdx))
841
842             # Add the average of all these faces normals as a projectionVec
843             averageVec = Vector((0.0, 0.0, 0.0))
844             if user_area_weight == 0.0:
845                 for fprop in newProjectMeshFaces:
846                     averageVec += fprop.no
847             elif user_area_weight == 1.0:
848                 for fprop in newProjectMeshFaces:
849                     averageVec += fprop.no * fprop.area
850             else:
851                 for fprop in newProjectMeshFaces:
852                     averageVec += fprop.no * ((fprop.area * user_area_weight) + (1.0 - user_area_weight))
853
854             if averageVec.x != 0 or averageVec.y != 0 or averageVec.z != 0:  # Avoid NAN
855                 projectVecs.append(averageVec.normalized())
856
857             # Get the next vec!
858             # Pick the face that's most different to all existing angles :)
859             mostUniqueAngle = 1.0  # 1.0 is 0d. no difference.
860             mostUniqueIndex = 0  # dummy
861
862             for fIdx in range(len(tempMeshFaces) - 1, -1, -1):
863                 angleDifference = -1.0  # 180d difference.
864
865                 # Get the closest vec angle we are to.
866                 for p in projectVecs:
867                     temp_angle_diff = p.dot(tempMeshFaces[fIdx].no)
868
869                     if angleDifference < temp_angle_diff:
870                         angleDifference = temp_angle_diff
871
872                 if angleDifference < mostUniqueAngle:
873                     # We have a new most different angle
874                     mostUniqueIndex = fIdx
875                     mostUniqueAngle = angleDifference
876
877             if mostUniqueAngle < USER_PROJECTION_LIMIT_CONVERTED:
878                 # print 'adding', mostUniqueAngle, USER_PROJECTION_LIMIT, len(newProjectMeshFaces)
879                 # Now weight the vector to all its faces, will give a more direct projection
880                 # if the face its self was not representative of the normal from surrounding faces.
881
882                 newProjectVec = tempMeshFaces[mostUniqueIndex].no
883                 newProjectMeshFaces = [tempMeshFaces.pop(mostUniqueIndex)]
884
885             else:
886                 if len(projectVecs) >= 1:  # Must have at least 2 projections
887                     break
888
889         # If there are only zero area faces then its possible
890         # there are no projectionVecs
891         if not len(projectVecs):
892             Draw.PupMenu('error, no projection vecs where generated, 0 area faces can cause this.')
893             return
894
895         faceProjectionGroupList = [[] for i in range(len(projectVecs))]
896
897         # MAP and Arrange # We know there are 3 or 4 faces here
898
899         for fIdx in range(len(meshFaces) - 1, -1, -1):
900             fvec = meshFaces[fIdx].no
901             i = len(projectVecs)
902
903             # Initialize first
904             bestAng = fvec.dot(projectVecs[0])
905             bestAngIdx = 0
906
907             # Cycle through the remaining, first already done
908             while i - 1:
909                 i -= 1
910
911                 newAng = fvec.dot(projectVecs[i])
912                 if newAng > bestAng:  # Reverse logic for dotvecs
913                     bestAng = newAng
914                     bestAngIdx = i
915
916             # Store the area for later use.
917             faceProjectionGroupList[bestAngIdx].append(meshFaces[fIdx])
918
919         # Cull faceProjectionGroupList,
920
921         # Now faceProjectionGroupList is full of faces that face match the project Vecs list
922         for i in range(len(projectVecs)):
923             # Account for projectVecs having no faces.
924             if not faceProjectionGroupList[i]:
925                 continue
926
927             # Make a projection matrix from a unit length vector.
928             MatQuat = VectoQuat(projectVecs[i])
929
930             # Get the faces UV's from the projected vertex.
931             for f in faceProjectionGroupList[i]:
932                 f_uv = f.uv
933                 for j, v in enumerate(f.v):
934                     f_uv[j][:] = (MatQuat @ v.co).xy
935
936         if USER_SHARE_SPACE:
937             # Should we collect and pack later?
938             islandList = getUvIslands(faceProjectionGroupList, me)
939             collected_islandList.extend(islandList)
940
941         else:
942             # Should we pack the islands for this 1 object?
943             islandList = getUvIslands(faceProjectionGroupList, me)
944             packIslands(islandList)
945
946         # update the mesh here if we need to.
947
948     # We want to pack all in 1 go, so pack now
949     if USER_SHARE_SPACE:
950         packIslands(collected_islandList)
951
952     print("Smart Projection time: %.2f" % (time.time() - time1))
953
954     # aspect correction is only done in edit mode - and only smart unwrap supports currently
955     if is_editmode:
956         bpy.ops.object.mode_set(mode='EDIT')
957
958         if use_aspect:
959             import bmesh
960             aspect = context.scene.uvedit_aspect(context.active_object)
961             if aspect[0] > aspect[1]:
962                 aspect[0] = aspect[1] / aspect[0]
963                 aspect[1] = 1.0
964             else:
965                 aspect[1] = aspect[0] / aspect[1]
966                 aspect[0] = 1.0
967
968             bm = bmesh.from_edit_mesh(me)
969
970             uv_act = bm.loops.layers.uv.active
971
972             faces = [f for f in bm.faces if f.select]
973
974             for f in faces:
975                 for l in f.loops:
976                     l[uv_act].uv[0] *= aspect[0]
977                     l[uv_act].uv[1] *= aspect[1]
978
979     dict_matrix.clear()
980
981
982 from bpy.props import FloatProperty, BoolProperty
983
984
985 class SmartProject(Operator):
986     """This script projection unwraps the selected faces of a mesh """ \
987         """(it operates on all selected mesh objects, and can be used """ \
988         """to unwrap selected faces, or all faces)"""
989     bl_idname = "uv.smart_project"
990     bl_label = "Smart UV Project"
991     bl_options = {'REGISTER', 'UNDO'}
992
993     angle_limit: FloatProperty(
994         name="Angle Limit",
995         description="Lower for more projection groups, higher for less distortion",
996         min=1.0, max=89.0,
997         default=66.0,
998     )
999     island_margin: FloatProperty(
1000         name="Island Margin",
1001         description="Margin to reduce bleed from adjacent islands",
1002         min=0.0, max=1.0,
1003         default=0.0,
1004     )
1005     user_area_weight: FloatProperty(
1006         name="Area Weight",
1007         description="Weight projections vector by faces with larger areas",
1008         min=0.0, max=1.0,
1009         default=0.0,
1010     )
1011     use_aspect: BoolProperty(
1012         name="Correct Aspect",
1013         description="Map UVs taking image aspect ratio into account",
1014         default=True
1015     )
1016     stretch_to_bounds: BoolProperty(
1017         name="Stretch to UV Bounds",
1018         description="Stretch the final output to texture bounds",
1019         default=True,
1020     )
1021
1022     @classmethod
1023     def poll(cls, context):
1024         return context.active_object is not None
1025
1026     def execute(self, context):
1027         main(context,
1028              self.island_margin,
1029              self.angle_limit,
1030              self.user_area_weight,
1031              self.use_aspect,
1032              self.stretch_to_bounds
1033              )
1034         return {'FINISHED'}
1035
1036     def invoke(self, context, event):
1037         wm = context.window_manager
1038         return wm.invoke_props_dialog(self)
1039
1040
1041 classes = (
1042     SmartProject,
1043 )