1# <pep8-80 compliant> 2 3# ##### BEGIN GPL LICENSE BLOCK ##### 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software Foundation, 17# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18# 19# ##### END GPL LICENSE BLOCK ##### 20 21__author__ = "Nutti <nutti.metro@gmail.com>" 22__status__ = "production" 23__version__ = "6.3" 24__date__ = "10 Aug 2020" 25 26from math import fabs 27 28import bpy 29from bpy.props import ( 30 FloatProperty, 31 FloatVectorProperty, 32 BoolProperty, 33) 34import bmesh 35import mathutils 36from mathutils import Vector 37 38from ..utils.bl_class_registry import BlClassRegistry 39from ..utils.property_class_registry import PropertyClassRegistry 40from ..utils import compatibility as compat 41from .. import common 42 43 44def _is_valid_context(context): 45 obj = context.object 46 47 # only edit mode is allowed to execute 48 if obj is None: 49 return False 50 if obj.type != 'MESH': 51 return False 52 if context.object.mode != 'EDIT': 53 return False 54 55 # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. 56 # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf 57 # after the execution 58 for space in context.area.spaces: 59 if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): 60 break 61 else: 62 return False 63 64 return True 65 66 67def _sort_island_faces(kd, uvs, isl1, isl2): 68 """ 69 Sort faces in island 70 """ 71 72 sorted_faces = [] 73 for f in isl1['sorted']: 74 _, idx, _ = kd.find( 75 Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0))) 76 sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']]) 77 return sorted_faces 78 79 80def _group_island(island_info, allowable_center_deviation, 81 allowable_size_deviation): 82 """ 83 Group island 84 """ 85 86 num_group = 0 87 while True: 88 # search islands which is not parsed yet 89 isl_1 = None 90 for isl_1 in island_info: 91 if isl_1['group'] == -1: 92 break 93 else: 94 break # all faces are parsed 95 if isl_1 is None: 96 break 97 isl_1['group'] = num_group 98 isl_1['sorted'] = isl_1['faces'] 99 100 # search same island 101 for isl_2 in island_info: 102 if isl_2['group'] == -1: 103 dcx = isl_2['center'].x - isl_1['center'].x 104 dcy = isl_2['center'].y - isl_1['center'].y 105 dsx = isl_2['size'].x - isl_1['size'].x 106 dsy = isl_2['size'].y - isl_1['size'].y 107 center_x_matched = ( 108 fabs(dcx) < allowable_center_deviation[0] 109 ) 110 center_y_matched = ( 111 fabs(dcy) < allowable_center_deviation[1] 112 ) 113 size_x_matched = ( 114 fabs(dsx) < allowable_size_deviation[0] 115 ) 116 size_y_matched = ( 117 fabs(dsy) < allowable_size_deviation[1] 118 ) 119 center_matched = center_x_matched and center_y_matched 120 size_matched = size_x_matched and size_y_matched 121 num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv']) 122 # are islands have same? 123 if center_matched and size_matched and num_uv_matched: 124 isl_2['group'] = num_group 125 kd = mathutils.kdtree.KDTree(len(isl_2['faces'])) 126 uvs = [ 127 { 128 'uv': Vector( 129 (f['ave_uv'].x, f['ave_uv'].y, 0.0) 130 ), 131 'face_idx': fidx 132 } for fidx, f in enumerate(isl_2['faces']) 133 ] 134 for i, uv in enumerate(uvs): 135 kd.insert(uv['uv'], i) 136 kd.balance() 137 # sort faces for copy/paste UV 138 isl_2['sorted'] = _sort_island_faces(kd, uvs, isl_1, isl_2) 139 num_group = num_group + 1 140 141 return num_group 142 143 144@PropertyClassRegistry() 145class _Properties: 146 idname = "pack_uv" 147 148 @classmethod 149 def init_props(cls, scene): 150 scene.muv_pack_uv_enabled = BoolProperty( 151 name="Pack UV Enabled", 152 description="Pack UV is enabled", 153 default=False 154 ) 155 scene.muv_pack_uv_allowable_center_deviation = FloatVectorProperty( 156 name="Allowable Center Deviation", 157 description="Allowable center deviation to judge same UV island", 158 min=0.000001, 159 max=0.1, 160 default=(0.001, 0.001), 161 size=2 162 ) 163 scene.muv_pack_uv_allowable_size_deviation = FloatVectorProperty( 164 name="Allowable Size Deviation", 165 description="Allowable sizse deviation to judge same UV island", 166 min=0.000001, 167 max=0.1, 168 default=(0.001, 0.001), 169 size=2 170 ) 171 172 @classmethod 173 def del_props(cls, scene): 174 del scene.muv_pack_uv_enabled 175 del scene.muv_pack_uv_allowable_center_deviation 176 del scene.muv_pack_uv_allowable_size_deviation 177 178 179@BlClassRegistry() 180@compat.make_annotations 181class MUV_OT_PackUV(bpy.types.Operator): 182 """ 183 Operation class: Pack UV with same UV islands are integrated 184 Island matching algorithm 185 - Same center of UV island 186 - Same size of UV island 187 - Same number of UV 188 """ 189 190 bl_idname = "uv.muv_pack_uv" 191 bl_label = "Pack UV" 192 bl_description = "Pack UV (Same UV Islands are integrated)" 193 bl_options = {'REGISTER', 'UNDO'} 194 195 rotate = BoolProperty( 196 name="Rotate", 197 description="Rotate option used by default pack UV function", 198 default=False) 199 margin = FloatProperty( 200 name="Margin", 201 description="Margin used by default pack UV function", 202 min=0, 203 max=1, 204 default=0.001) 205 allowable_center_deviation = FloatVectorProperty( 206 name="Allowable Center Deviation", 207 description="Allowable center deviation to judge same UV island", 208 min=0.000001, 209 max=0.1, 210 default=(0.001, 0.001), 211 size=2 212 ) 213 allowable_size_deviation = FloatVectorProperty( 214 name="Allowable Size Deviation", 215 description="Allowable sizse deviation to judge same UV island", 216 min=0.000001, 217 max=0.1, 218 default=(0.001, 0.001), 219 size=2 220 ) 221 222 @classmethod 223 def poll(cls, context): 224 # we can not get area/space/region from console 225 if common.is_console_mode(): 226 return True 227 return _is_valid_context(context) 228 229 def execute(self, context): 230 obj = context.active_object 231 bm = bmesh.from_edit_mesh(obj.data) 232 if common.check_version(2, 73, 0) >= 0: 233 bm.faces.ensure_lookup_table() 234 if not bm.loops.layers.uv: 235 self.report({'WARNING'}, 236 "Object must have more than one UV map") 237 return {'CANCELLED'} 238 uv_layer = bm.loops.layers.uv.verify() 239 240 selected_faces = [f for f in bm.faces if f.select] 241 island_info = common.get_island_info(obj) 242 num_group = _group_island(island_info, 243 self.allowable_center_deviation, 244 self.allowable_size_deviation) 245 246 loop_lists = [l for f in bm.faces for l in f.loops] 247 bpy.ops.mesh.select_all(action='DESELECT') 248 249 # pack UV 250 for gidx in range(num_group): 251 group = list(filter( 252 lambda i, idx=gidx: i['group'] == idx, island_info)) 253 for f in group[0]['faces']: 254 f['face'].select = True 255 bmesh.update_edit_mesh(obj.data) 256 bpy.ops.uv.select_all(action='SELECT') 257 bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) 258 259 # copy/paste UV among same islands 260 for gidx in range(num_group): 261 group = list(filter( 262 lambda i, idx=gidx: i['group'] == idx, island_info)) 263 if len(group) <= 1: 264 continue 265 for g in group[1:]: 266 for (src_face, dest_face) in zip( 267 group[0]['sorted'], g['sorted']): 268 for (src_loop, dest_loop) in zip( 269 src_face['face'].loops, dest_face['face'].loops): 270 loop_lists[dest_loop.index][uv_layer].uv = loop_lists[ 271 src_loop.index][uv_layer].uv 272 273 # restore face/UV selection 274 bpy.ops.uv.select_all(action='DESELECT') 275 bpy.ops.mesh.select_all(action='DESELECT') 276 for f in selected_faces: 277 f.select = True 278 bpy.ops.uv.select_all(action='SELECT') 279 280 bmesh.update_edit_mesh(obj.data) 281 282 return {'FINISHED'} 283