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