b52a2c41c2680262963569bd91af881108981342
[blender.git] / release / scripts / op / uvcalc_smart_project.py
1 # --------------------------------------------------------------------------
2 # Smart Projection UV Projection Unwrapper v1.2 by Campbell Barton (AKA Ideasman)
3 # --------------------------------------------------------------------------
4 # ***** BEGIN GPL LICENSE BLOCK *****
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 #
20 # ***** END GPL LICENCE BLOCK *****
21 # --------------------------------------------------------------------------
22
23 # <pep8 compliant>
24
25 from mathutils import Matrix, Vector, RotationMatrix
26 import time
27 import geometry
28 import bpy
29 from math import cos, radians
30
31 DEG_TO_RAD = 0.017453292519943295 # pi/180.0
32 SMALL_NUM = 0.000000001
33 BIG_NUM = 1e15
34
35 global USER_FILL_HOLES
36 global USER_FILL_HOLES_QUALITY
37 USER_FILL_HOLES = None
38 USER_FILL_HOLES_QUALITY = None
39
40 dict_matrix = {}
41
42 def pointInTri2D(v, v1, v2, v3):
43     global dict_matrix
44
45     key = v1.x, v1.y, v2.x, v2.y, v3.x, v3.y
46
47     # Commented because its slower to do teh bounds check, we should realy cache the bounds info for each face.
48     '''
49     # BOUNDS CHECK
50     xmin= 1000000
51     ymin= 1000000
52
53     xmax= -1000000
54     ymax= -1000000
55
56     for i in (0,2,4):
57         x= key[i]
58         y= key[i+1]
59
60         if xmax<x:      xmax= x
61         if ymax<y:      ymax= y
62         if xmin>x:      xmin= x
63         if ymin>y:      ymin= y
64
65     x= v.x
66     y= v.y
67
68     if x<xmin or x>xmax or y < ymin or y > ymax:
69         return False
70     # Done with bounds check
71     '''
72     try:
73         mtx = dict_matrix[key]
74         if not mtx:
75             return False
76     except:
77         side1 = v2 - v1
78         side2 = v3 - v1
79
80         nor = side1.cross(side2)
81
82         l1 = [side1[0], side1[1], side1[2]]
83         l2 = [side2[0], side2[1], side2[2]]
84         l3 = [nor[0], nor[1], nor[2]]
85
86         mtx = Matrix(l1, l2, l3)
87
88         # Zero area 2d tri, even tho we throw away zerop area faces
89         # the projection UV can result in a zero area UV.
90         if not mtx.determinant():
91             dict_matrix[key] = None
92             return False
93
94         mtx.invert()
95
96         dict_matrix[key] = mtx
97
98     uvw = (v - v1) * mtx
99     return 0 <= uvw[0] and 0 <= uvw[1] and uvw[0] + uvw[1] <= 1
100
101
102 def boundsIsland(faces):
103     minx = maxx = faces[0].uv[0][0] # Set initial bounds.
104     miny = maxy = faces[0].uv[0][1]
105     # print len(faces), minx, maxx, miny , maxy
106     for f in faces:
107         for uv in f.uv:
108             x= uv.x
109             y= uv.y
110             if x<minx: minx= x
111             if y<miny: miny= y
112             if x>maxx: maxx= x
113             if y>maxy: maxy= y
114
115     return minx, miny, maxx, maxy
116
117 """
118 def boundsEdgeLoop(edges):
119     minx = maxx = edges[0][0] # Set initial bounds.
120     miny = maxy = edges[0][1]
121     # print len(faces), minx, maxx, miny , maxy
122     for ed in edges:
123         for pt in ed:
124             print 'ass'
125             x= pt[0]
126             y= pt[1]
127             if x<minx: x= minx
128             if y<miny: y= miny
129             if x>maxx: x= maxx
130             if y>maxy: y= maxy
131
132     return minx, miny, maxx, maxy
133 """
134
135 # Turns the islands into a list of unpordered edges (Non internal)
136 # Onlt for UV's
137 # only returns outline edges for intersection tests. and unique points.
138
139 def island2Edge(island):
140
141     # Vert index edges
142     edges = {}
143
144     unique_points= {}
145
146     for f in island:
147         f_uvkey= map(tuple, f.uv)
148
149
150         for vIdx, edkey in enumerate(f.edge_keys):
151             unique_points[f_uvkey[vIdx]] = f.uv[vIdx]
152
153             if f.v[vIdx].index > f.v[vIdx-1].index:
154                 i1= vIdx-1;     i2= vIdx
155             else:
156                 i1= vIdx;       i2= vIdx-1
157
158             try:        edges[ f_uvkey[i1], f_uvkey[i2] ] *= 0 # sets eny edge with more then 1 user to 0 are not returned.
159             except:     edges[ f_uvkey[i1], f_uvkey[i2] ] = (f.uv[i1] - f.uv[i2]).length,
160
161     # If 2 are the same then they will be together, but full [a,b] order is not correct.
162
163     # Sort by length
164
165
166     length_sorted_edges = [(Vector(key[0]), Vector(key[1]), value) for key, value in edges.items() if value != 0]
167
168     try:        length_sorted_edges.sort(key = lambda A: -A[2]) # largest first
169     except:     length_sorted_edges.sort(lambda A, B: cmp(B[2], A[2]))
170
171     # Its okay to leave the length in there.
172     #for e in length_sorted_edges:
173     #   e.pop(2)
174
175     # return edges and unique points
176     return length_sorted_edges, [v.__copy__().resize3D() for v in unique_points.values()]
177
178 # ========================= NOT WORKING????
179 # Find if a points inside an edge loop, un-orderd.
180 # pt is and x/y
181 # edges are a non ordered loop of edges.
182 # #offsets are the edge x and y offset.
183 """
184 def pointInEdges(pt, edges):
185     #
186     x1 = pt[0]
187     y1 = pt[1]
188
189     # Point to the left of this line.
190     x2 = -100000
191     y2 = -10000
192     intersectCount = 0
193     for ed in edges:
194         xi, yi = lineIntersection2D(x1,y1, x2,y2, ed[0][0], ed[0][1], ed[1][0], ed[1][1])
195         if xi != None: # Is there an intersection.
196             intersectCount+=1
197
198     return intersectCount % 2
199 """
200
201 def pointInIsland(pt, island):
202     vec1, vec2, vec3 = Vector(), Vector(), Vector()
203     for f in island:
204         vec1.x, vec1.y = f.uv[0]
205         vec2.x, vec2.y = f.uv[1]
206         vec3.x, vec3.y = f.uv[2]
207
208         if pointInTri2D(pt, vec1, vec2, vec3):
209             return True
210
211         if len(f.v) == 4:
212             vec1.x, vec1.y = f.uv[0]
213             vec2.x, vec2.y = f.uv[2]
214             vec3.x, vec3.y = f.uv[3]
215             if pointInTri2D(pt, vec1, vec2, vec3):
216                 return True
217     return False
218
219
220 # box is (left,bottom, right, top)
221 def islandIntersectUvIsland(source, target, SourceOffset):
222     # Is 1 point in the box, inside the vertLoops
223     edgeLoopsSource = source[6] # Pretend this is offset
224     edgeLoopsTarget = target[6]
225
226     # Edge intersect test
227     for ed in edgeLoopsSource:
228         for seg in edgeLoopsTarget:
229             i = geometry.LineIntersect2D(\
230             seg[0], seg[1], SourceOffset+ed[0], SourceOffset+ed[1])
231             if i:
232                 return 1 # LINE INTERSECTION
233
234     # 1 test for source being totally inside target
235     SourceOffset.resize3D()
236     for pv in source[7]:
237         if pointInIsland(pv+SourceOffset, target[0]):
238             return 2 # SOURCE INSIDE TARGET
239
240     # 2 test for a part of the target being totaly inside the source.
241     for pv in target[7]:
242         if pointInIsland(pv-SourceOffset, source[0]):
243             return 3 # PART OF TARGET INSIDE SOURCE.
244
245     return 0 # NO INTERSECTION
246
247
248
249
250 # Returns the X/y Bounds of a list of vectors.
251 def testNewVecLs2DRotIsBetter(vecs, mat=-1, bestAreaSoFar = -1):
252
253     # UV's will never extend this far.
254     minx = miny = BIG_NUM
255     maxx = maxy = -BIG_NUM
256
257     for i, v in enumerate(vecs):
258
259         # Do this allong the way
260         if mat != -1:
261             v = vecs[i] = v*mat
262             x= v.x
263             y= v.y
264             if x<minx: minx= x
265             if y<miny: miny= y
266             if x>maxx: maxx= x
267             if y>maxy: maxy= y
268
269         # Spesific to this algo, bail out if we get bigger then the current area
270         if bestAreaSoFar != -1 and (maxx-minx) * (maxy-miny) > bestAreaSoFar:
271             return (BIG_NUM, None), None
272     w = maxx-minx
273     h = maxy-miny
274     return (w*h, w,h), vecs # Area, vecs
275
276 # Takes a list of faces that make up a UV island and rotate
277 # until they optimally fit inside a square.
278 ROTMAT_2D_POS_90D = RotationMatrix( radians(90.0), 2)
279 ROTMAT_2D_POS_45D = RotationMatrix( radians(45.0), 2)
280
281 RotMatStepRotation = []
282 rot_angle = 22.5 #45.0/2
283 while rot_angle > 0.1:
284     RotMatStepRotation.append([\
285      RotationMatrix( radians(rot_angle), 2),\
286      RotationMatrix( radians(-rot_angle), 2)])
287
288     rot_angle = rot_angle/2.0
289
290
291 def optiRotateUvIsland(faces):
292     global currentArea
293
294     # Bestfit Rotation
295     def best2dRotation(uvVecs, MAT1, MAT2):
296         global currentArea
297
298         newAreaPos, newfaceProjectionGroupListPos =\
299         testNewVecLs2DRotIsBetter(uvVecs[:], MAT1, currentArea[0])
300
301
302         # Why do I use newpos here? May as well give the best area to date for an early bailout
303         # some slight speed increase in this.
304         # If the new rotation is smaller then the existing, we can
305         # avoid copying a list and overwrite the old, crappy one.
306
307         if newAreaPos[0] < currentArea[0]:
308             newAreaNeg, newfaceProjectionGroupListNeg =\
309             testNewVecLs2DRotIsBetter(uvVecs, MAT2, newAreaPos[0])  # Reuse the old bigger list.
310         else:
311             newAreaNeg, newfaceProjectionGroupListNeg =\
312             testNewVecLs2DRotIsBetter(uvVecs[:], MAT2, currentArea[0])  # Cant reuse, make a copy.
313
314
315         # Now from the 3 options we need to discover which to use
316         # we have cerrentArea/newAreaPos/newAreaNeg
317         bestArea = min(currentArea[0], newAreaPos[0], newAreaNeg[0])
318
319         if currentArea[0] == bestArea:
320             return uvVecs
321         elif newAreaPos[0] == bestArea:
322             uvVecs = newfaceProjectionGroupListPos
323             currentArea = newAreaPos
324         elif newAreaNeg[0] == bestArea:
325             uvVecs = newfaceProjectionGroupListNeg
326             currentArea = newAreaNeg
327
328         return uvVecs
329
330
331     # Serialized UV coords to Vectors
332     uvVecs = [uv for f in faces  for uv in f.uv]
333
334     # Theres a small enough number of these to hard code it
335     # rather then a loop.
336
337     # Will not modify anything
338     currentArea, dummy =\
339     testNewVecLs2DRotIsBetter(uvVecs)
340
341
342     # Try a 45d rotation
343     newAreaPos, newfaceProjectionGroupListPos = testNewVecLs2DRotIsBetter(uvVecs[:], ROTMAT_2D_POS_45D, currentArea[0])
344
345     if newAreaPos[0] < currentArea[0]:
346         uvVecs = newfaceProjectionGroupListPos
347         currentArea = newAreaPos
348     # 45d done
349
350     # Testcase different rotations and find the onfe that best fits in a square
351     for ROTMAT in RotMatStepRotation:
352         uvVecs = best2dRotation(uvVecs, ROTMAT[0], ROTMAT[1])
353
354     # Only if you want it, make faces verticle!
355     if currentArea[1] > currentArea[2]:
356         # Rotate 90d
357         # Work directly on the list, no need to return a value.
358         testNewVecLs2DRotIsBetter(uvVecs, ROTMAT_2D_POS_90D)
359
360
361     # Now write the vectors back to the face UV's
362     i = 0 # count the serialized uv/vectors
363     for f in faces:
364         #f.uv = [uv for uv in uvVecs[i:len(f)+i] ]
365         for j, k in enumerate(range(i, len(f.v)+i)):
366             f.uv[j][:] = uvVecs[k]
367         i += len(f.v)
368
369
370 # Takes an island list and tries to find concave, hollow areas to pack smaller islands into.
371 def mergeUvIslands(islandList):
372     global USER_FILL_HOLES
373     global USER_FILL_HOLES_QUALITY
374
375
376     # Pack islands to bottom LHS
377     # Sync with island
378
379     #islandTotFaceArea = [] # A list of floats, each island area
380     #islandArea = [] # a list of tuples ( area, w,h)
381
382
383     decoratedIslandList = []
384
385     islandIdx = len(islandList)
386     while islandIdx:
387         islandIdx-=1
388         minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx])
389         w, h = maxx-minx, maxy-miny
390
391         totFaceArea = 0
392         offset= Vector((minx, miny))
393         for f in islandList[islandIdx]:
394             for uv in f.uv:
395                 uv -= offset
396
397             totFaceArea += f.area
398
399         islandBoundsArea = w*h
400         efficiency = abs(islandBoundsArea - totFaceArea)
401
402         # UV Edge list used for intersections as well as unique points.
403         edges, uniqueEdgePoints = island2Edge(islandList[islandIdx])
404
405         decoratedIslandList.append([islandList[islandIdx], totFaceArea, efficiency, islandBoundsArea, w,h, edges, uniqueEdgePoints])
406
407
408     # Sort by island bounding box area, smallest face area first.
409     # no.. chance that to most simple edge loop first.
410     decoratedIslandListAreaSort =decoratedIslandList[:]
411
412     decoratedIslandListAreaSort.sort(key = lambda A: A[3])
413
414     # sort by efficiency, Least Efficient first.
415     decoratedIslandListEfficSort = decoratedIslandList[:]
416     # decoratedIslandListEfficSort.sort(lambda A, B: cmp(B[2], A[2]))
417
418     decoratedIslandListEfficSort.sort(key = lambda A: -A[2])
419
420     # ================================================== THESE CAN BE TWEAKED.
421     # This is a quality value for the number of tests.
422     # from 1 to 4, generic quality value is from 1 to 100
423     USER_STEP_QUALITY =   ((USER_FILL_HOLES_QUALITY - 1) / 25.0) + 1
424
425     # If 100 will test as long as there is enough free space.
426     # this is rarely enough, and testing takes a while, so lower quality speeds this up.
427
428     # 1 means they have the same quality
429     USER_FREE_SPACE_TO_TEST_QUALITY = 1 + (((100 - USER_FILL_HOLES_QUALITY)/100.0) *5)
430
431     #print 'USER_STEP_QUALITY', USER_STEP_QUALITY
432     #print 'USER_FREE_SPACE_TO_TEST_QUALITY', USER_FREE_SPACE_TO_TEST_QUALITY
433
434     removedCount = 0
435
436     areaIslandIdx = 0
437     ctrl = Window.Qual.CTRL
438     BREAK= False
439     while areaIslandIdx < len(decoratedIslandListAreaSort) and not BREAK:
440         sourceIsland = decoratedIslandListAreaSort[areaIslandIdx]
441         # Alredy packed?
442         if not sourceIsland[0]:
443             areaIslandIdx+=1
444         else:
445             efficIslandIdx = 0
446             while efficIslandIdx < len(decoratedIslandListEfficSort) and not BREAK:
447
448                 if Window.GetKeyQualifiers() & ctrl:
449                     BREAK= True
450                     break
451
452                 # Now we have 2 islands, is the efficience of the islands lowers theres an
453                 # increasing likely hood that we can fit merge into the bigger UV island.
454                 # this ensures a tight fit.
455
456                 # Just use figures we have about user/unused area to see if they might fit.
457
458                 targetIsland = decoratedIslandListEfficSort[efficIslandIdx]
459
460
461                 if sourceIsland[0] == targetIsland[0] or\
462                 not targetIsland[0] or\
463                 not sourceIsland[0]:
464                     pass
465                 else:
466
467                     # ([island, totFaceArea, efficiency, islandArea, w,h])
468                     # Waisted space on target is greater then UV bounding island area.
469
470
471                     # if targetIsland[3] > (sourceIsland[2]) and\ #
472                     # print USER_FREE_SPACE_TO_TEST_QUALITY, 'ass'
473                     if targetIsland[2] > (sourceIsland[1] * USER_FREE_SPACE_TO_TEST_QUALITY) and\
474                     targetIsland[4] > sourceIsland[4] and\
475                     targetIsland[5] > sourceIsland[5]:
476
477                         # DEBUG # print '%.10f  %.10f' % (targetIsland[3], sourceIsland[1])
478
479                         # These enough spare space lets move the box until it fits
480
481                         # How many times does the source fit into the target x/y
482                         blockTestXUnit = targetIsland[4]/sourceIsland[4]
483                         blockTestYUnit = targetIsland[5]/sourceIsland[5]
484
485                         boxLeft = 0
486
487
488                         # Distllllance we can move between whilst staying inside the targets bounds.
489                         testWidth = targetIsland[4] - sourceIsland[4]
490                         testHeight = targetIsland[5] - sourceIsland[5]
491
492                         # Increment we move each test. x/y
493                         xIncrement = (testWidth / (blockTestXUnit * ((USER_STEP_QUALITY/50)+0.1)))
494                         yIncrement = (testHeight / (blockTestYUnit * ((USER_STEP_QUALITY/50)+0.1)))
495
496                         # Make sure were not moving less then a 3rg of our width/height
497                         if xIncrement<sourceIsland[4]/3:
498                             xIncrement= sourceIsland[4]
499                         if yIncrement<sourceIsland[5]/3:
500                             yIncrement= sourceIsland[5]
501
502
503                         boxLeft = 0 # Start 1 back so we can jump into the loop.
504                         boxBottom= 0 #-yIncrement
505
506                         ##testcount= 0
507
508                         while boxBottom <= testHeight:
509                             # Should we use this? - not needed for now.
510                             #if Window.GetKeyQualifiers() & ctrl:
511                             #   BREAK= True
512                             #   break
513
514                             ##testcount+=1
515                             #print 'Testing intersect'
516                             Intersect = islandIntersectUvIsland(sourceIsland, targetIsland, Vector((boxLeft, boxBottom)))
517                             #print 'Done', Intersect
518                             if Intersect == 1:  # Line intersect, dont bother with this any more
519                                 pass
520
521                             if Intersect == 2:  # Source inside target
522                                 '''
523                                 We have an intersection, if we are inside the target
524                                 then move us 1 whole width accross,
525                                 Its possible this is a bad idea since 2 skinny Angular faces
526                                 could join without 1 whole move, but its a lot more optimal to speed this up
527                                 since we have already tested for it.
528
529                                 It gives about 10% speedup with minimal errors.
530                                 '''
531                                 #print 'ass'
532                                 # Move the test allong its width + SMALL_NUM
533                                 #boxLeft += sourceIsland[4] + SMALL_NUM
534                                 boxLeft += sourceIsland[4]
535                             elif Intersect == 0: # No intersection?? Place it.
536                                 # Progress
537                                 removedCount +=1
538 #XXX                                                            Window.DrawProgressBar(0.0, 'Merged: %i islands, Ctrl to finish early.' % removedCount)
539
540                                 # Move faces into new island and offset
541                                 targetIsland[0].extend(sourceIsland[0])
542                                 offset= Vector((boxLeft, boxBottom))
543
544                                 for f in sourceIsland[0]:
545                                     for uv in f.uv:
546                                         uv+= offset
547
548                                 sourceIsland[0][:] = [] # Empty
549
550
551                                 # Move edge loop into new and offset.
552                                 # targetIsland[6].extend(sourceIsland[6])
553                                 #while sourceIsland[6]:
554                                 targetIsland[6].extend( [ (\
555                                      (e[0]+offset, e[1]+offset, e[2])\
556                                 ) for e in sourceIsland[6] ] )
557
558                                 sourceIsland[6][:] = [] # Empty
559
560                                 # Sort by edge length, reverse so biggest are first.
561
562                                 try:     targetIsland[6].sort(key = lambda A: A[2])
563                                 except: targetIsland[6].sort(lambda B,A: cmp(A[2], B[2] ))
564
565
566                                 targetIsland[7].extend(sourceIsland[7])
567                                 offset= Vector((boxLeft, boxBottom, 0.0))
568                                 for p in sourceIsland[7]:
569                                     p+= offset
570
571                                 sourceIsland[7][:] = []
572
573
574                                 # Decrement the efficiency
575                                 targetIsland[1]+=sourceIsland[1] # Increment totFaceArea
576                                 targetIsland[2]-=sourceIsland[1] # Decrement efficiency
577                                 # IF we ever used these again, should set to 0, eg
578                                 sourceIsland[2] = 0 # No area if anyone wants to know
579
580                                 break
581
582
583                             # INCREMENR NEXT LOCATION
584                             if boxLeft > testWidth:
585                                 boxBottom += yIncrement
586                                 boxLeft = 0.0
587                             else:
588                                 boxLeft += xIncrement
589                         ##print testcount
590
591                 efficIslandIdx+=1
592         areaIslandIdx+=1
593
594     # Remove empty islands
595     i = len(islandList)
596     while i:
597         i-=1
598         if not islandList[i]:
599             del islandList[i] # Can increment islands removed here.
600
601 # Takes groups of faces. assumes face groups are UV groups.
602 def getUvIslands(faceGroups, me):
603
604     # Get seams so we dont cross over seams
605     edge_seams = {} # shoudl be a set
606     for ed in me.edges:
607         if ed.seam:
608             edge_seams[ed.key] = None # dummy var- use sets!
609     # Done finding seams
610
611
612     islandList = []
613
614 #XXX    Window.DrawProgressBar(0.0, 'Splitting %d projection groups into UV islands:' % len(faceGroups))
615     #print '\tSplitting %d projection groups into UV islands:' % len(faceGroups),
616     # Find grouped faces
617
618     faceGroupIdx = len(faceGroups)
619
620     while faceGroupIdx:
621         faceGroupIdx-=1
622         faces = faceGroups[faceGroupIdx]
623
624         if not faces:
625             continue
626
627         # Build edge dict
628         edge_users = {}
629
630         for i, f in enumerate(faces):
631             for ed_key in f.edge_keys:
632                 if ed_key in edge_seams: # DELIMIT SEAMS! ;)
633                     edge_users[ed_key] = [] # so as not to raise an error
634                 else:
635                     try:                edge_users[ed_key].append(i)
636                     except:             edge_users[ed_key] = [i]
637
638         # Modes
639         # 0 - face not yet touched.
640         # 1 - added to island list, and need to search
641         # 2 - touched and searched - dont touch again.
642         face_modes = [0] * len(faces) # initialize zero - untested.
643
644         face_modes[0] = 1 # start the search with face 1
645
646         newIsland = []
647
648         newIsland.append(faces[0])
649
650
651         ok = True
652         while ok:
653
654             ok = True
655             while ok:
656                 ok= False
657                 for i in range(len(faces)):
658                     if face_modes[i] == 1: # search
659                         for ed_key in faces[i].edge_keys:
660                             for ii in edge_users[ed_key]:
661                                 if i != ii and face_modes[ii] == 0:
662                                     face_modes[ii] = ok = 1 # mark as searched
663                                     newIsland.append(faces[ii])
664
665                         # mark as searched, dont look again.
666                         face_modes[i] = 2
667
668             islandList.append(newIsland)
669
670             ok = False
671             for i in range(len(faces)):
672                 if face_modes[i] == 0:
673                     newIsland = []
674                     newIsland.append(faces[i])
675
676                     face_modes[i] = ok = 1
677                     break
678             # if not ok will stop looping
679
680 #XXX    Window.DrawProgressBar(0.1, 'Optimizing Rotation for %i UV Islands' % len(islandList))
681
682     for island in islandList:
683         optiRotateUvIsland(island)
684
685     return islandList
686
687
688 def packIslands(islandList):
689     if USER_FILL_HOLES:
690 #XXX            Window.DrawProgressBar(0.1, 'Merging Islands (Ctrl: skip merge)...')
691         mergeUvIslands(islandList) # Modify in place
692
693
694     # Now we have UV islands, we need to pack them.
695
696     # Make a synchronised list with the islands
697     # so we can box pak the islands.
698     packBoxes = []
699
700     # Keep a list of X/Y offset so we can save time by writing the
701     # uv's and packed data in one pass.
702     islandOffsetList = []
703
704     islandIdx = 0
705
706     while islandIdx < len(islandList):
707         minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx])
708
709         w, h = maxx-minx, maxy-miny
710
711         if USER_ISLAND_MARGIN:
712             minx -= USER_ISLAND_MARGIN# *w
713             miny -= USER_ISLAND_MARGIN# *h
714             maxx += USER_ISLAND_MARGIN# *w
715             maxy += USER_ISLAND_MARGIN# *h
716
717             # recalc width and height
718             w, h = maxx-minx, maxy-miny
719
720         if w < 0.00001 or h < 0.00001:
721             del islandList[islandIdx]
722             islandIdx -=1
723             continue
724
725         '''Save the offset to be applied later,
726         we could apply to the UVs now and allign them to the bottom left hand area
727         of the UV coords like the box packer imagines they are
728         but, its quicker just to remember their offset and
729         apply the packing and offset in 1 pass '''
730         islandOffsetList.append((minx, miny))
731
732         # Add to boxList. use the island idx for the BOX id.
733         packBoxes.append([0, 0, w, h])
734         islandIdx+=1
735
736     # Now we have a list of boxes to pack that syncs
737     # with the islands.
738
739     #print '\tPacking UV Islands...'
740 #XXX    Window.DrawProgressBar(0.7, 'Packing %i UV Islands...' % len(packBoxes) )
741
742     time1 = time.time()
743     packWidth, packHeight = geometry.BoxPack2D(packBoxes)
744
745     # print 'Box Packing Time:', time.time() - time1
746
747     #if len(pa  ckedLs) != len(islandList):
748     #   raise "Error packed boxes differes from original length"
749
750     #print '\tWriting Packed Data to faces'
751 #XXX    Window.DrawProgressBar(0.8, 'Writing Packed Data to faces')
752
753     # Sort by ID, so there in sync again
754     islandIdx = len(islandList)
755     # Having these here avoids devide by 0
756     if islandIdx:
757
758         if USER_STRETCH_ASPECT:
759             # Maximize to uv area?? Will write a normalize function.
760             xfactor = 1.0 / packWidth
761             yfactor = 1.0 / packHeight
762         else:
763             # Keep proportions.
764             xfactor = yfactor = 1.0 / max(packWidth, packHeight)
765
766     while islandIdx:
767         islandIdx -=1
768         # Write the packed values to the UV's
769
770         xoffset = packBoxes[islandIdx][0] - islandOffsetList[islandIdx][0]
771         yoffset = packBoxes[islandIdx][1] - islandOffsetList[islandIdx][1]
772
773         for f in islandList[islandIdx]: # Offsetting the UV's so they fit in there packed box
774             for uv in f.uv:
775                 uv.x= (uv.x+xoffset) * xfactor
776                 uv.y= (uv.y+yoffset) * yfactor
777
778
779
780 def VectoMat(vec):
781     a3 = vec.__copy__().normalize()
782
783     up = Vector((0.0, 0.0, 1.0))
784     if abs(a3.dot(up)) == 1.0:
785         up = Vector((0.0, 1.0, 0.0))
786
787     a1 = a3.cross(up).normalize()
788     a2 = a3.cross(a1)
789     return Matrix([a1[0], a1[1], a1[2]], [a2[0], a2[1], a2[2]], [a3[0], a3[1], a3[2]])
790
791
792 class thickface(object):
793     __slost__= 'v', 'uv', 'no', 'area', 'edge_keys'
794     def __init__(self, face, uvface, mesh_verts):
795         self.v = [mesh_verts[i] for i in face.verts]
796         if len(self.v)==4:
797             self.uv = uvface.uv1, uvface.uv2, uvface.uv3, uvface.uv4
798         else:
799             self.uv = uvface.uv1, uvface.uv2, uvface.uv3
800
801         self.no = face.normal
802         self.area = face.area
803         self.edge_keys = face.edge_keys
804
805 global ob
806 ob = None
807 def main(context, island_margin, projection_limit):
808     global USER_FILL_HOLES
809     global USER_FILL_HOLES_QUALITY
810     global USER_STRETCH_ASPECT
811     global USER_ISLAND_MARGIN
812
813 #XXX objects= bpy.data.scenes.active.objects
814     objects = context.selected_editable_objects
815
816
817     # we can will tag them later.
818     obList =  [ob for ob in objects if ob.type == 'MESH']
819
820     # Face select object may not be selected.
821 #XXX    ob = objects.active
822     ob= objects[0]
823
824     if ob and (not ob.select) and ob.type == 'MESH':
825         # Add to the list
826         obList =[ob]
827     del objects
828
829     if not obList:
830         raise('error, no selected mesh objects')
831
832     # Create the variables.
833     USER_PROJECTION_LIMIT = projection_limit
834     USER_ONLY_SELECTED_FACES = (1)
835     USER_SHARE_SPACE = (1) # Only for hole filling.
836     USER_STRETCH_ASPECT = (1) # Only for hole filling.
837     USER_ISLAND_MARGIN = island_margin # Only for hole filling.
838     USER_FILL_HOLES = (0)
839     USER_FILL_HOLES_QUALITY = (50) # Only for hole filling.
840     USER_VIEW_INIT = (0) # Only for hole filling.
841     USER_AREA_WEIGHT = (1) # Only for hole filling.
842
843     # Reuse variable
844     if len(obList) == 1:
845         ob = "Unwrap %i Selected Mesh"
846     else:
847         ob = "Unwrap %i Selected Meshes"
848
849     # HACK, loop until mouse is lifted.
850     '''
851     while Window.GetMouseButtons() != 0:
852         time.sleep(10)
853     '''
854
855 #XXX    if not Draw.PupBlock(ob % len(obList), pup_block):
856 #XXX            return
857 #XXX    del ob
858
859     # Convert from being button types
860
861     USER_PROJECTION_LIMIT_CONVERTED = cos(USER_PROJECTION_LIMIT * DEG_TO_RAD)
862     USER_PROJECTION_LIMIT_HALF_CONVERTED = cos((USER_PROJECTION_LIMIT/2) * DEG_TO_RAD)
863
864
865     # Toggle Edit mode
866     is_editmode = (context.active_object.mode == 'EDIT')
867     if is_editmode:
868         bpy.ops.object.mode_set(mode='OBJECT')
869     # Assume face select mode! an annoying hack to toggle face select mode because Mesh dosent like faceSelectMode.
870
871     if USER_SHARE_SPACE:
872         # Sort by data name so we get consistant results
873         obList.sort(key = lambda ob: ob.data.name)
874         collected_islandList= []
875
876 #XXX    Window.WaitCursor(1)
877
878     time1 = time.time()
879
880     # Tag as False se we dont operate on teh same mesh twice.
881 #XXX    bpy.data.meshes.tag = False
882     for me in bpy.data.meshes:
883         me.tag = False
884
885
886     for ob in obList:
887         me = ob.data
888
889         if me.tag or me.library:
890             continue
891
892         # Tag as used
893         me.tag = True
894
895         if len(me.uv_textures)==0: # Mesh has no UV Coords, dont bother.
896             me.add_uv_texture()
897
898         uv_layer = me.active_uv_texture.data
899         me_verts = list(me.verts)
900
901         if USER_ONLY_SELECTED_FACES:
902             meshFaces = [thickface(f, uv_layer[i], me_verts) for i, f in enumerate(me.faces) if f.select]
903         #else:
904         #       meshFaces = map(thickface, me.faces)
905
906         if not meshFaces:
907             continue
908
909 #XXX            Window.DrawProgressBar(0.1, 'SmartProj UV Unwrapper, mapping "%s", %i faces.' % (me.name, len(meshFaces)))
910
911         # =======
912         # Generate a projection list from face normals, this is ment to be smart :)
913
914         # make a list of face props that are in sync with meshFaces
915         # Make a Face List that is sorted by area.
916         # meshFaces = []
917
918         # meshFaces.sort( lambda a, b: cmp(b.area , a.area) ) # Biggest first.
919         meshFaces.sort( key = lambda a: -a.area )
920
921         # remove all zero area faces
922         while meshFaces and meshFaces[-1].area <= SMALL_NUM:
923             # Set their UV's to 0,0
924             for uv in meshFaces[-1].uv:
925                 uv.zero()
926             meshFaces.pop()
927
928         # Smallest first is slightly more efficient, but if the user cancels early then its better we work on the larger data.
929
930         # Generate Projection Vecs
931         # 0d is   1.0
932         # 180 IS -0.59846
933
934
935         # Initialize projectVecs
936         if USER_VIEW_INIT:
937             # Generate Projection
938             projectVecs = [Vector(Window.GetViewVector()) * ob.matrix_world.copy().invert().rotation_part()] # We add to this allong the way
939         else:
940             projectVecs = []
941
942         newProjectVec = meshFaces[0].no
943         newProjectMeshFaces = []        # Popping stuffs it up.
944
945
946         # Predent that the most unique angke is ages away to start the loop off
947         mostUniqueAngle = -1.0
948
949         # This is popped
950         tempMeshFaces = meshFaces[:]
951
952
953
954         # This while only gathers projection vecs, faces are assigned later on.
955         while 1:
956             # If theres none there then start with the largest face
957
958             # add all the faces that are close.
959             for fIdx in range(len(tempMeshFaces)-1, -1, -1):
960                 # Use half the angle limit so we dont overweight faces towards this
961                 # normal and hog all the faces.
962                 if newProjectVec.dot(tempMeshFaces[fIdx].no) > USER_PROJECTION_LIMIT_HALF_CONVERTED:
963                     newProjectMeshFaces.append(tempMeshFaces.pop(fIdx))
964
965             # Add the average of all these faces normals as a projectionVec
966             averageVec = Vector((0.0, 0.0, 0.0))
967             if USER_AREA_WEIGHT:
968                 for fprop in newProjectMeshFaces:
969                     averageVec += (fprop.no * fprop.area)
970             else:
971                 for fprop in newProjectMeshFaces:
972                     averageVec += fprop.no
973
974             if averageVec.x != 0 or averageVec.y != 0 or averageVec.z != 0: # Avoid NAN
975                 projectVecs.append(averageVec.normalize())
976
977
978             # Get the next vec!
979             # Pick the face thats most different to all existing angles :)
980             mostUniqueAngle = 1.0 # 1.0 is 0d. no difference.
981             mostUniqueIndex = 0 # dummy
982
983             for fIdx in range(len(tempMeshFaces)-1, -1, -1):
984                 angleDifference = -1.0 # 180d difference.
985
986                 # Get the closest vec angle we are to.
987                 for p in projectVecs:
988                     temp_angle_diff= p.dot(tempMeshFaces[fIdx].no)
989
990                     if angleDifference < temp_angle_diff:
991                         angleDifference= temp_angle_diff
992
993                 if angleDifference < mostUniqueAngle:
994                     # We have a new most different angle
995                     mostUniqueIndex = fIdx
996                     mostUniqueAngle = angleDifference
997
998             if mostUniqueAngle < USER_PROJECTION_LIMIT_CONVERTED:
999                 #print 'adding', mostUniqueAngle, USER_PROJECTION_LIMIT, len(newProjectMeshFaces)
1000                 # Now weight the vector to all its faces, will give a more direct projection
1001                 # if the face its self was not representive of the normal from surrounding faces.
1002
1003                 newProjectVec = tempMeshFaces[mostUniqueIndex].no
1004                 newProjectMeshFaces = [tempMeshFaces.pop(mostUniqueIndex)]
1005
1006
1007             else:
1008                 if len(projectVecs) >= 1: # Must have at least 2 projections
1009                     break
1010
1011
1012         # If there are only zero area faces then its possible
1013         # there are no projectionVecs
1014         if not len(projectVecs):
1015             Draw.PupMenu('error, no projection vecs where generated, 0 area faces can cause this.')
1016             return
1017
1018         faceProjectionGroupList =[[] for i in range(len(projectVecs)) ]
1019
1020         # MAP and Arrange # We know there are 3 or 4 faces here
1021
1022         for fIdx in range(len(meshFaces)-1, -1, -1):
1023             fvec = meshFaces[fIdx].no
1024             i = len(projectVecs)
1025
1026             # Initialize first
1027             bestAng = fvec.dot(projectVecs[0])
1028             bestAngIdx = 0
1029
1030             # Cycle through the remaining, first already done
1031             while i-1:
1032                 i-=1
1033
1034                 newAng = fvec.dot(projectVecs[i])
1035                 if newAng > bestAng: # Reverse logic for dotvecs
1036                     bestAng = newAng
1037                     bestAngIdx = i
1038
1039             # Store the area for later use.
1040             faceProjectionGroupList[bestAngIdx].append(meshFaces[fIdx])
1041
1042         # Cull faceProjectionGroupList,
1043
1044
1045         # Now faceProjectionGroupList is full of faces that face match the project Vecs list
1046         for i in range(len(projectVecs)):
1047             # Account for projectVecs having no faces.
1048             if not faceProjectionGroupList[i]:
1049                 continue
1050
1051             # Make a projection matrix from a unit length vector.
1052             MatProj = VectoMat(projectVecs[i])
1053
1054             # Get the faces UV's from the projected vertex.
1055             for f in faceProjectionGroupList[i]:
1056                 f_uv = f.uv
1057                 for j, v in enumerate(f.v):
1058                     # XXX - note, between mathutils in 2.4 and 2.5 the order changed.
1059                     f_uv[j][:] = (v.co * MatProj)[:2]
1060
1061
1062         if USER_SHARE_SPACE:
1063             # Should we collect and pack later?
1064             islandList = getUvIslands(faceProjectionGroupList, me)
1065             collected_islandList.extend(islandList)
1066
1067         else:
1068             # Should we pack the islands for this 1 object?
1069             islandList = getUvIslands(faceProjectionGroupList, me)
1070             packIslands(islandList)
1071
1072
1073         # update the mesh here if we need to.
1074
1075     # We want to pack all in 1 go, so pack now
1076     if USER_SHARE_SPACE:
1077 #XXX            Window.DrawProgressBar(0.9, "Box Packing for all objects...")
1078         packIslands(collected_islandList)
1079
1080     print("Smart Projection time: %.2f" % (time.time() - time1))
1081     # Window.DrawProgressBar(0.9, "Smart Projections done, time: %.2f sec." % (time.time() - time1))
1082
1083     if is_editmode:
1084         bpy.ops.object.mode_set(mode='EDIT')
1085
1086 #XXX    Window.DrawProgressBar(1.0, "")
1087 #XXX    Window.WaitCursor(0)
1088 #XXX    Window.RedrawAll()
1089
1090 """
1091     pup_block = [\
1092     'Projection',\
1093 *       ('Angle Limit:', USER_PROJECTION_LIMIT, 1, 89, ''),\
1094     ('Selected Faces Only', USER_ONLY_SELECTED_FACES, 'Use only selected faces from all selected meshes.'),\
1095     ('Init from view', USER_VIEW_INIT, 'The first projection will be from the view vector.'),\
1096     ('Area Weight', USER_AREA_WEIGHT, 'Weight projections vector by face area.'),\
1097     '',\
1098     '',\
1099     '',\
1100     'UV Layout',\
1101     ('Share Tex Space', USER_SHARE_SPACE, 'Objects Share texture space, map all objects into 1 uvmap.'),\
1102     ('Stretch to bounds', USER_STRETCH_ASPECT, 'Stretch the final output to texture bounds.'),\
1103 *       ('Island Margin:', USER_ISLAND_MARGIN, 0.0, 0.5, ''),\
1104     'Fill in empty areas',\
1105     ('Fill Holes', USER_FILL_HOLES, 'Fill in empty areas reduced texture waistage (slow).'),\
1106     ('Fill Quality:', USER_FILL_HOLES_QUALITY, 1, 100, 'Depends on fill holes, how tightly to fill UV holes, (higher is slower)'),\
1107     ]
1108 """
1109
1110 from bpy.props import *
1111
1112
1113 class SmartProject(bpy.types.Operator):
1114     '''This script projection unwraps the selected faces of a mesh. it operates on all selected mesh objects, and can be used unwrap selected faces, or all faces.'''
1115     bl_idname = "uv.smart_project"
1116     bl_label = "Smart UV Project"
1117     bl_options = {'REGISTER', 'UNDO'}
1118
1119     angle_limit = FloatProperty(name="Angle Limit",
1120             description="lower for more projection groups, higher for less distortion.",
1121             default=66.0, min=1.0, max=89.0)
1122
1123     island_margin = FloatProperty(name="Island Margin",
1124             description="Margin to reduce bleed from adjacent islands.",
1125             default=0.0, min=0.0, max=1.0)
1126
1127     @staticmethod
1128     def poll(context):
1129         return context.active_object != None
1130
1131     def execute(self, context):
1132         main(context, self.properties.island_margin, self.properties.angle_limit)
1133         return {'FINISHED'}
1134
1135
1136 # Add to a menu
1137 menu_func = (lambda self, context: self.layout.operator(SmartProject.bl_idname,
1138                                         text="Smart Project"))
1139
1140
1141 def register():
1142     bpy.types.VIEW3D_MT_uv_map.append(menu_func)
1143
1144
1145 def unregister():
1146     bpy.types.VIEW3D_MT_uv_map.remove(menu_func)
1147
1148 if __name__ == "__main__":
1149     register()