# SPDX-License-Identifier: GPL-2.0-or-later __author__ = "imdjs, Nutti " __status__ = "production" __version__ = "6.6" __date__ = "22 Apr 2022" import math from math import atan2, sin, cos import bpy import bmesh from mathutils import Vector from bpy.props import BoolProperty from .. import common 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 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 # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: return False # only edit mode is allowed to execute if context.object.mode != 'EDIT': return False return True @PropertyClassRegistry() class _Properties: idname = "copy_paste_uv_uvedit" @classmethod def init_props(cls, scene): class CopyPastUVProps(): src_uvs = None class CopyPasteUVIslandProps(): # [ # { # "bmesh": BMesh, # "uv_layer": UV Layer, # "island": UV Island, # } # ] src_data = [] src_objects = [] scene.muv_props.copy_paste_uv_uvedit = CopyPastUVProps() scene.muv_props.copy_paste_uv_island = CopyPasteUVIslandProps() scene.muv_copy_paste_uv_uvedit_unique_target = BoolProperty( name="Unique Target", description="Paste to the target uniquely", default=False ) @classmethod def del_props(cls, scene): del scene.muv_props.copy_paste_uv_uvedit del scene.muv_props.copy_paste_uv_island @BlClassRegistry() class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate on UV/Image Editor """ bl_idname = "uv.muv_copy_paste_uv_uvedit_copy_uv" bl_label = "Copy UV (UV/Image Editor)" bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} @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): props = context.scene.muv_props.copy_paste_uv_uvedit objs = common.get_uv_editable_objects(context) # poll() method ensures that only one object is selected. obj = objs[0] bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() props.src_uvs = [] for face in bm.faces: if not face.select: continue skip = False for l in face.loops: if not l[uv_layer].select: skip = True break if skip: continue props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops]) return {'FINISHED'} @BlClassRegistry() class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate on UV/Image Editor """ bl_idname = "uv.muv_copy_paste_uv_uvedit_paste_uv" bl_label = "Paste UV (UV/Image Editor)" bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): # we can not get area/space/region from console if common.is_console_mode(): return True sc = context.scene props = sc.muv_props.copy_paste_uv_uvedit if not props.src_uvs: return False return _is_valid_context(context) def execute(self, context): props = context.scene.muv_props.copy_paste_uv_uvedit objs = common.get_uv_editable_objects(context) # poll() method ensures that only one object is selected. obj = objs[0] bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() dest_uvs = [] dest_face_indices = [] for face in bm.faces: if not face.select: continue skip = False for l in face.loops: if not l[uv_layer].select: skip = True break if skip: continue dest_face_indices.append(face.index) uvs = [l[uv_layer].uv.copy() for l in face.loops] dest_uvs.append(uvs) for suvs, duvs in zip(props.src_uvs, dest_uvs): src_diff = suvs[1] - suvs[0] dest_diff = duvs[1] - duvs[0] src_base = suvs[0] dest_base = duvs[0] src_rad = atan2(src_diff.y, src_diff.x) dest_rad = atan2(dest_diff.y, dest_diff.x) if src_rad < dest_rad: radian = dest_rad - src_rad elif src_rad > dest_rad: radian = math.pi * 2 - (src_rad - dest_rad) else: # src_rad == dest_rad radian = 0.0 ratio = dest_diff.length / src_diff.length break for suvs, fidx in zip(props.src_uvs, dest_face_indices): for l, suv in zip(bm.faces[fidx].loops, suvs): base = suv - src_base radian_ref = atan2(base.y, base.x) radian_fin = (radian + radian_ref) length = base.length turn = Vector((length * cos(radian_fin), length * sin(radian_fin))) target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \ dest_base l[uv_layer].uv = target_uv bmesh.update_edit_mesh(obj.data) return {'FINISHED'} # Return selected/all count. # If context.tool_settings.use_uv_select_sync is enabled: # Return selected/all face count. # If context.tool_settings.use_uv_select_sync is disabled: # Return selected/all loop count. def get_counts(context, island, uv_layer): selected_count = 0 all_count = 0 if context.tool_settings.use_uv_select_sync: for f in island["faces"]: all_count += 1 if f["face"].select: selected_count += 1 else: for f in island["faces"]: for l in f["face"].loops: all_count += 1 if l[uv_layer].select: selected_count += 1 return selected_count, all_count @BlClassRegistry() class MUV_OT_CopyPasteUVUVEdit_CopyUVIsland(bpy.types.Operator): """ Operation class: Copy UV island on UV/Image Editor """ bl_idname = "uv.muv_copy_paste_uv_uvedit_copy_uv_island" bl_label = "Copy UV Island (UV/Image Editor)" bl_description = "Copy UV island (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} @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): sc = context.scene props = sc.muv_props.copy_paste_uv_island props.src_data = [] props.src_objects = [] objs = common.get_uv_editable_objects(context) for obj in objs: bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() if common.check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() if context.tool_settings.use_uv_select_sync: islands = common.get_island_info_from_bmesh( bm, only_selected=False) else: islands = common.get_island_info_from_bmesh( bm, only_selected=True) for isl in islands: # Check if all UVs belonging to the island is selected. selected_count, all_count = get_counts(context, isl, uv_layer) if selected_count == 0: continue if selected_count != all_count: self.report( {'WARNING'}, "All UVs belonging to the island must be selected") return {'CANCELLED'} data = { "bmesh": bm, "uv_layer": uv_layer, "island": isl } props.src_data.append(data) props.src_objects.append(obj) return {'FINISHED'} @BlClassRegistry() @compat.make_annotations class MUV_OT_CopyPasteUVUVEdit_PasteUVIsland(bpy.types.Operator): """ Operation class: Paste UV island on UV/Image Editor """ bl_idname = "uv.muv_copy_paste_uv_uvedit_paste_uv_island" bl_label = "Paste UV Island (UV/Image Editor)" bl_description = "Paste UV island (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} unique_target = BoolProperty( name="Unique Target", description="Paste to the target uniquely", default=False ) @classmethod def poll(cls, context): # we can not get area/space/region from console if common.is_console_mode(): return True sc = context.scene props = sc.muv_props.copy_paste_uv_island if not props.src_data: return False return _is_valid_context(context) def execute(self, context): sc = context.scene props = sc.muv_props.copy_paste_uv_island src_data = props.src_data src_objs = props.src_objects bms_and_uv_layers = {} for d in src_data: bms_and_uv_layers[d["bmesh"]] = d["uv_layer"] dst_data = [] for bm, uv_layer in bms_and_uv_layers.items(): if context.tool_settings.use_uv_select_sync: islands = common.get_island_info_from_bmesh( bm, only_selected=False) else: islands = common.get_island_info_from_bmesh( bm, only_selected=True) for isl in islands: # Check if all UVs belonging to the island is selected. selected_count, all_count = get_counts(context, isl, uv_layer) if selected_count == 0: continue if selected_count != all_count: self.report( {'WARNING'}, "All UVs belonging to the island must be selected") return {'CANCELLED'} dst_data.append( { "bm": bm, "uv_layer": uv_layer, "island": isl, } ) used = [] for ddata in dst_data: dst_loops = [] for f in ddata["island"]["faces"]: for l in f["face"].loops: dst_loops.append(l) dst_uv_layer = ddata["uv_layer"] # Find a suitable island. for sdata in src_data: if self.unique_target and sdata in used: continue src_loops = [] for f in sdata["island"]["faces"]: for l in f["face"].loops: src_loops.append(l) src_uv_layer = sdata["uv_layer"] # Create UV graph. src_uv_graph = common.create_uv_graph(src_loops, src_uv_layer) dst_uv_graph = common.create_uv_graph(dst_loops, dst_uv_layer) # 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 result: # 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 used.append(sdata) break else: self.report({'WARNING'}, "Island does not match") return {'CANCELLED'} for obj in src_objs: bmesh.update_edit_mesh(obj.data) return {'FINISHED'}