# SPDX-License-Identifier: GPL-2.0-or-later __author__ = "Nutti " __status__ = "production" __version__ = "6.6" __date__ = "22 Apr 2022" from math import fabs import bpy from bpy.props import ( FloatProperty, FloatVectorProperty, BoolProperty, ) import bmesh import mathutils from mathutils import Vector from ..utils.bl_class_registry import BlClassRegistry from ..utils.property_class_registry import PropertyClassRegistry from ..utils.graph import graph_is_isomorphic from ..utils import compatibility as compat from .. import common def _is_valid_context(context): # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf # after the execution if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False objs = common.get_uv_editable_objects(context) if not objs: return False # only edit mode is allowed to execute if context.object.mode != 'EDIT': return False return True def _sort_island_faces(kd, uvs, isl1, isl2): """ Sort faces in island """ sorted_faces = [] for f in isl1['sorted']: _, idx, _ = kd.find( Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0))) sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']]) return sorted_faces def _group_island(island_info, allowable_center_deviation, allowable_size_deviation): """ Group island """ num_group = 0 while True: # search islands which is not parsed yet isl_1 = None for isl_1 in island_info: if isl_1['group'] == -1: break else: break # all faces are parsed if isl_1 is None: break isl_1['group'] = num_group isl_1['sorted'] = isl_1['faces'] # search same island for isl_2 in island_info: if isl_2['group'] == -1: dcx = isl_2['center'].x - isl_1['center'].x dcy = isl_2['center'].y - isl_1['center'].y dsx = isl_2['size'].x - isl_1['size'].x dsy = isl_2['size'].y - isl_1['size'].y center_x_matched = ( fabs(dcx) < allowable_center_deviation[0] ) center_y_matched = ( fabs(dcy) < allowable_center_deviation[1] ) size_x_matched = ( fabs(dsx) < allowable_size_deviation[0] ) size_y_matched = ( fabs(dsy) < allowable_size_deviation[1] ) center_matched = center_x_matched and center_y_matched size_matched = size_x_matched and size_y_matched num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv']) # are islands have same? if center_matched and size_matched and num_uv_matched: isl_2['group'] = num_group kd = mathutils.kdtree.KDTree(len(isl_2['faces'])) uvs = [ { 'uv': Vector( (f['ave_uv'].x, f['ave_uv'].y, 0.0) ), 'face_idx': fidx } for fidx, f in enumerate(isl_2['faces']) ] for i, uv in enumerate(uvs): kd.insert(uv['uv'], i) kd.balance() # sort faces for copy/paste UV isl_2['sorted'] = _sort_island_faces(kd, uvs, isl_1, isl_2) num_group = num_group + 1 return num_group @PropertyClassRegistry() class _Properties: idname = "pack_uv" @classmethod def init_props(cls, scene): scene.muv_pack_uv_enabled = BoolProperty( name="Pack UV Enabled", description="Pack UV is enabled", default=False ) scene.muv_pack_uv_allowable_center_deviation = FloatVectorProperty( name="Allowable Center Deviation", description="Allowable center deviation to judge same UV island", min=0.000001, max=10.0, default=(0.001, 0.001), size=2, subtype='XYZ' ) scene.muv_pack_uv_allowable_size_deviation = FloatVectorProperty( name="Allowable Size Deviation", description="Allowable sizes deviation to judge same UV island", min=0.000001, max=10.0, default=(0.001, 0.001), size=2, subtype='XYZ' ) scene.muv_pack_uv_accurate_island_copy = BoolProperty( name="Accurate Island Copy", description="Copy islands topologically", default=True ) scene.muv_pack_uv_stride = FloatVectorProperty( name="Stride", description="Stride UV coordinates", min=-100.0, max=100.0, default=(0.0, 0.0), size=2, subtype='XYZ' ) scene.muv_pack_uv_apply_pack_uv = BoolProperty( name="Apply Pack UV", description="Apply Pack UV operation intrinsic to Blender itself", default=True ) @classmethod def del_props(cls, scene): del scene.muv_pack_uv_enabled del scene.muv_pack_uv_allowable_center_deviation del scene.muv_pack_uv_allowable_size_deviation del scene.muv_pack_uv_accurate_island_copy del scene.muv_pack_uv_stride del scene.muv_pack_uv_apply_pack_uv @BlClassRegistry() @compat.make_annotations class MUV_OT_PackUV(bpy.types.Operator): """ Operation class: Pack UV with same UV islands are integrated Island matching algorithm - Same center of UV island - Same size of UV island - Same number of UV """ bl_idname = "uv.muv_pack_uv" bl_label = "Pack UV" bl_description = "Pack UV (Same UV Islands are integrated)" bl_options = {'REGISTER', 'UNDO'} rotate = BoolProperty( name="Rotate", description="Rotate option used by default pack UV function", default=False) margin = FloatProperty( name="Margin", description="Margin used by default pack UV function", min=0, max=1, default=0.001 ) allowable_center_deviation = FloatVectorProperty( name="Allowable Center Deviation", description="Allowable center deviation to judge same UV island", min=0.000001, max=10.0, default=(0.001, 0.001), size=2, subtype='XYZ' ) allowable_size_deviation = FloatVectorProperty( name="Allowable Size Deviation", description="Allowable sizse deviation to judge same UV island", min=0.000001, max=10.0, default=(0.001, 0.001), size=2, subtype='XYZ' ) accurate_island_copy = BoolProperty( name="Accurate Island Copy", description="Copy islands topologically", default=True ) stride = FloatVectorProperty( name="Stride", description="Stride UV coordinates", min=-100.0, max=100.0, default=(0.0, 0.0), size=2, subtype='XYZ' ) apply_pack_uv = BoolProperty( name="Apply Pack UV", description="Apply Pack UV operation intrinsic to Blender itself", default=True ) @classmethod def poll(cls, context): # we can not get area/space/region from console if common.is_console_mode(): return True return _is_valid_context(context) def execute(self, context): objs = common.get_uv_editable_objects(context) island_info = [] selected_faces = [] island_to_bm = {} island_to_uv_layer = {} bm_to_loop_lists = {} for obj in objs: bm = bmesh.from_edit_mesh(obj.data) if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() if not bm.loops.layers.uv: self.report({'WARNING'}, "Object {} must have more than one UV map" .format(obj.name)) return {'CANCELLED'} uv_layer = bm.loops.layers.uv.verify() selected_faces.extend([f for f in bm.faces if f.select]) isl = common.get_island_info(obj) for i, info in enumerate(isl): id_ = i + len(island_info) island_to_bm[id_] = bm island_to_uv_layer[id_] = uv_layer info["id"] = id_ island_info.extend(isl) bm_to_loop_lists[bm] = [l for f in bm.faces for l in f.loops] num_group = _group_island(island_info, self.allowable_center_deviation, self.allowable_size_deviation) bpy.ops.mesh.select_all(action='DESELECT') # pack UV for gidx in range(num_group): group = list(filter( lambda i, idx=gidx: i['group'] == idx, island_info)) for f in group[0]['faces']: f['face'].select = True for obj in objs: bmesh.update_edit_mesh(obj.data) bpy.ops.uv.select_all(action='SELECT') if self.apply_pack_uv: bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) # copy/paste UV among same islands for gidx in range(num_group): group = list(filter( lambda i, idx=gidx: i['group'] == idx, island_info)) if len(group) <= 1: continue src_bm = island_to_bm[group[0]["id"]] src_uv_layer = island_to_uv_layer[group[0]["id"]] src_loop_lists = bm_to_loop_lists[src_bm] src_loops = [] for f in group[0]["faces"]: for l in f["face"].loops: src_loops.append(l) src_uv_graph = common.create_uv_graph(src_loops, src_uv_layer) for stride_idx, g in enumerate(group[1:]): dst_bm = island_to_bm[g["id"]] dst_uv_layer = island_to_uv_layer[g["id"]] dst_loop_lists = bm_to_loop_lists[dst_bm] dst_loops = [] for f in g["faces"]: for l in f["face"].loops: dst_loops.append(l) dst_uv_graph = common.create_uv_graph(dst_loops, dst_uv_layer) uv_stride = Vector(((stride_idx + 1) * self.stride.x, (stride_idx + 1) * self.stride.y)) if self.accurate_island_copy: # Check if the graph is isomorphic. # If the graph is isomorphic, matching pair is returned. result, pairs = graph_is_isomorphic( src_uv_graph, dst_uv_graph) if not result: self.report( {'WARNING'}, "Island does not match. " "Disable 'Accurate Island Copy' and try again") return {'CANCELLED'} # Paste UV island. for n1, n2 in pairs.items(): uv1 = n1.value["uv_vert"][src_uv_layer].uv l2 = n2.value["loops"] for l in l2: l[dst_uv_layer].uv = uv1 + uv_stride else: for (src_face, dest_face) in zip( group[0]['sorted'], g['sorted']): for (src_loop, dest_loop) in zip( src_face['face'].loops, dest_face['face'].loops): src_lidx = src_loop.index dst_lidx = dest_loop.index dst_loop_lists[dst_lidx][dst_uv_layer].uv = \ src_loop_lists[src_lidx][src_uv_layer].uv + \ uv_stride # restore face/UV selection bpy.ops.uv.select_all(action='DESELECT') bpy.ops.mesh.select_all(action='DESELECT') for f in selected_faces: f.select = True bpy.ops.uv.select_all(action='SELECT') for obj in objs: bmesh.update_edit_mesh(obj.data) return {'FINISHED'}